From 20a5d2030ee5ed7028e6ccb0cd98d6bed8b57020 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:02:44 +0100 Subject: [PATCH] feat: add InlineEdit primitive component Co-Authored-By: Claude Sonnet 4.6 --- .../InlineEdit/InlineEdit.module.css | 59 ++++++++++++++ .../primitives/InlineEdit/InlineEdit.tsx | 78 +++++++++++++++++++ src/design-system/primitives/index.ts | 2 + 3 files changed, 139 insertions(+) create mode 100644 src/design-system/primitives/InlineEdit/InlineEdit.module.css create mode 100644 src/design-system/primitives/InlineEdit/InlineEdit.tsx diff --git a/src/design-system/primitives/InlineEdit/InlineEdit.module.css b/src/design-system/primitives/InlineEdit/InlineEdit.module.css new file mode 100644 index 0000000..cde427a --- /dev/null +++ b/src/design-system/primitives/InlineEdit/InlineEdit.module.css @@ -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); +} diff --git a/src/design-system/primitives/InlineEdit/InlineEdit.tsx b/src/design-system/primitives/InlineEdit/InlineEdit.tsx new file mode 100644 index 0000000..8dc197b --- /dev/null +++ b/src/design-system/primitives/InlineEdit/InlineEdit.tsx @@ -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(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 ( + setDraft(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={handleBlur} + /> + ) + } + + const isEmpty = !value + return ( + + + {isEmpty ? placeholder : value} + + {!disabled && ( + + )} + + ) +} diff --git a/src/design-system/primitives/index.ts b/src/design-system/primitives/index.ts index 5c3e4c7..7f5a77d 100644 --- a/src/design-system/primitives/index.ts +++ b/src/design-system/primitives/index.ts @@ -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'