feat: add InlineEdit primitive component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
78
src/design-system/primitives/InlineEdit/InlineEdit.tsx
Normal file
78
src/design-system/primitives/InlineEdit/InlineEdit.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user