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 { FormField } from './FormField/FormField'
|
||||
export { InfoCallout } from './InfoCallout/InfoCallout'
|
||||
export { InlineEdit } from './InlineEdit/InlineEdit'
|
||||
export type { InlineEditProps } from './InlineEdit/InlineEdit'
|
||||
export { Input } from './Input/Input'
|
||||
export { KeyboardHint } from './KeyboardHint/KeyboardHint'
|
||||
export { Label } from './Label/Label'
|
||||
|
||||
Reference in New Issue
Block a user