feat: add InlineEdit primitive component

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-18 23:02:44 +01:00
parent c76ae79d7a
commit 20a5d2030e
3 changed files with 139 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
.display {
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
}
.display:hover .editBtn {
opacity: 1;
}
.disabled {
cursor: default;
}
.value {
font-family: var(--font-body);
font-size: 14px;
color: var(--text-primary);
}
.placeholder {
font-family: var(--font-body);
font-size: 14px;
color: var(--text-faint);
font-style: italic;
}
.editBtn {
border: none;
background: none;
color: var(--text-faint);
cursor: pointer;
font-size: 13px;
padding: 0 2px;
opacity: 0;
transition: opacity 0.15s, color 0.15s;
line-height: 1;
}
.editBtn:hover {
color: var(--text-primary);
}
.disabled .editBtn {
display: none;
}
.input {
font-family: var(--font-body);
font-size: 14px;
color: var(--text-primary);
background: var(--bg-raised);
border: 1px solid var(--amber);
border-radius: var(--radius-sm);
padding: 2px 8px;
outline: none;
box-shadow: 0 0 0 3px var(--amber-bg);
}

View File

@@ -0,0 +1,78 @@
import { useState, useRef, useEffect } from 'react'
import styles from './InlineEdit.module.css'
export interface InlineEditProps {
value: string
onSave: (value: string) => void
placeholder?: string
disabled?: boolean
className?: string
}
export function InlineEdit({ value, onSave, placeholder, disabled, className }: InlineEditProps) {
const [editing, setEditing] = useState(false)
const [draft, setDraft] = useState(value)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (editing) {
inputRef.current?.focus()
inputRef.current?.select()
}
}, [editing])
function startEdit() {
if (disabled) return
setDraft(value)
setEditing(true)
}
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault()
setEditing(false)
onSave(draft)
} else if (e.key === 'Escape') {
setEditing(false)
}
}
function handleBlur() {
setEditing(false)
}
if (editing) {
return (
<input
ref={inputRef}
className={`${styles.input} ${className ?? ''}`}
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
/>
)
}
const isEmpty = !value
return (
<span className={`${styles.display} ${disabled ? styles.disabled : ''} ${className ?? ''}`}>
<span
className={isEmpty ? styles.placeholder : styles.value}
onClick={startEdit}
>
{isEmpty ? placeholder : value}
</span>
{!disabled && (
<button
className={styles.editBtn}
onClick={startEdit}
aria-label="Edit"
type="button"
>
</button>
)}
</span>
)
}

View File

@@ -13,6 +13,8 @@ export { EmptyState } from './EmptyState/EmptyState'
export { FilterPill } from './FilterPill/FilterPill' export { FilterPill } from './FilterPill/FilterPill'
export { FormField } from './FormField/FormField' export { FormField } from './FormField/FormField'
export { InfoCallout } from './InfoCallout/InfoCallout' export { InfoCallout } from './InfoCallout/InfoCallout'
export { InlineEdit } from './InlineEdit/InlineEdit'
export type { InlineEditProps } from './InlineEdit/InlineEdit'
export { Input } from './Input/Input' export { Input } from './Input/Input'
export { KeyboardHint } from './KeyboardHint/KeyboardHint' export { KeyboardHint } from './KeyboardHint/KeyboardHint'
export { Label } from './Label/Label' export { Label } from './Label/Label'