--- phase: 11-catalog-database-view-ux-legacy-consolidation plan: 03 type: execute wave: 3 depends_on: ["11-02"] files_modified: - src/components/ui/editable-cell.tsx - src/components/ui/tag-multi-select.tsx autonomous: true requirements: [OFFER-07, OFFER-08] must_haves: truths: - "Clicking a cell in display mode transitions it to an editable input/textarea/checkbox with a visible focus ring" - "Pressing Enter (non-textarea) or blurring the field saves the value via the onSave callback" - "Pressing Escape reverts to the previous value without calling onSave" - "Tag badges display with deterministic colors derived from tag name (same name = same color, no stored color column)" - "Typing a new tag name and pressing Enter in the TagMultiSelect dropdown calls addTagToService and the badge appears without a full page reload (via revalidation)" artifacts: - path: "src/components/ui/editable-cell.tsx" provides: "EditableCell component — click-to-edit text/number/textarea/toggle cell" contains: "export function EditableCell" - path: "src/components/ui/tag-multi-select.tsx" provides: "TagMultiSelect component — tag badges + add/remove dropdown with deterministic color hashing" contains: "export function TagMultiSelect" key_links: - from: "src/components/ui/tag-multi-select.tsx" to: "src/app/admin/catalog/actions.ts" via: "addTagToService / removeTagFromService server action calls" pattern: "addTagToService|removeTagFromService" - from: "src/components/ui/editable-cell.tsx" to: "src/components/ui/input.tsx, src/components/ui/textarea.tsx" via: "renders Input/Textarea in edit mode with ring-1 ring-primary" pattern: "ring-1 ring-primary" --- Build the two new shared UI primitives the database-view table (Plan 04) depends on: `EditableCell` (generic click-to-edit cell for text/number/textarea/toggle, per D-11/D-14 and DESIGN-SYSTEM.md inline-edit pattern) and `TagMultiSelect` (tag badges with deterministic color hashing per D-07, inline "+" dropdown to add/remove tags per D-06/D-08). Purpose: Interface-first — these are pure, reusable components with no ServiceTable-specific logic. Plan 04 imports and wires them into `ServiceRow`/`ServiceTable` without needing to touch their internals. Output: - `src/components/ui/editable-cell.tsx` — `EditableCell` component + `EditableCellProps` type - `src/components/ui/tag-multi-select.tsx` — `TagMultiSelect` component + color-hash utility @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/11-catalog-database-view-ux-legacy-consolidation/11-CONTEXT.md @.planning/DESIGN-SYSTEM.md ```typescript export async function addTagToService(serviceId: string, tagName: string): Promise; export async function removeTagFromService(serviceId: string, tagName: string): Promise; ``` ```typescript import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } ``` ```typescript export interface BadgeProps extends React.HTMLAttributes, VariantProps {} function Badge({ className, variant, ...props }: BadgeProps): JSX.Element; ``` Task 1: Build EditableCell component (text/number/textarea/toggle) src/components/ui/editable-cell.tsx src/components/admin/catalog/ServiceTable.tsx (current ServiceRow inline-edit state pattern, lines 11-35, for the useState/useTransition conventions to mirror — NOT the form-based editing, but the local state + save/cancel idiom) src/components/ui/input.tsx (Input forwardRef signature) src/components/ui/textarea.tsx (Textarea forwardRef signature) .planning/DESIGN-SYSTEM.md (inline edit pattern: "click su cella -> diventa input/select borderless con ring-1 ring-primary on focus -> Enter salva, Esc annulla, blur salva") - Test 1 (display mode): renders `value` as plain text (or "—" if empty/falsy for text/textarea; "✓ Attivo"/"✗ Disattivato" for toggle) - Test 2 (click to edit): clicking the display div sets `isEditing=true` and renders the appropriate input (`Input` for text/number, `Textarea` for textarea, `` for toggle), auto-focused - Test 3 (Enter saves, non-textarea): pressing Enter in a text/number input calls `onSave(tempValue)` and exits edit mode - Test 4 (Escape cancels): pressing Escape reverts `tempValue` to the original `value` and exits edit mode WITHOUT calling `onSave` - Test 5 (blur saves): blurring any input calls `onSave(tempValue)` and exits edit mode - Test 6 (required validation): if `required=true` and `tempValue.trim() === ""`, `onSave` is NOT called and an inline error message renders - Test 7 (disabled): if `disabled=true`, clicking the display div does NOT enter edit mode (no `cursor-pointer`, has `cursor-not-allowed opacity-50`) - Test 8 (toggle type): for `type="toggle"`, `onSave` receives `"true"` or `"false"` as a string based on checkbox state Create `src/components/ui/editable-cell.tsx`: ```typescript "use client"; import { useState, useRef, useEffect } from "react"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; export interface EditableCellProps { value: string | number | boolean; type?: "text" | "number" | "textarea" | "toggle"; onSave: (value: string) => void; required?: boolean; placeholder?: string; disabled?: boolean; formatDisplay?: (value: string) => string; } export function EditableCell({ value, type = "text", onSave, required, placeholder, disabled, formatDisplay, }: EditableCellProps) { const [isEditing, setIsEditing] = useState(false); const [tempValue, setTempValue] = useState(String(value)); const [error, setError] = useState(null); const inputRef = useRef(null); useEffect(() => { setTempValue(String(value)); }, [value]); useEffect(() => { if (isEditing && inputRef.current) { inputRef.current.focus(); if ("select" in inputRef.current) { inputRef.current.select(); } } }, [isEditing]); function startEdit() { if (disabled) return; setError(null); setTempValue(String(value)); setIsEditing(true); } function commit() { if (required && tempValue.trim().length === 0) { setError("Campo richiesto"); return; } setError(null); onSave(tempValue); setIsEditing(false); } function cancel() { setTempValue(String(value)); setError(null); setIsEditing(false); } function handleKeyDown(e: React.KeyboardEvent) { if (e.key === "Enter" && type !== "textarea") { e.preventDefault(); commit(); } else if (e.key === "Escape") { e.preventDefault(); cancel(); } } if (!isEditing) { let display: string; if (type === "toggle") { display = tempValue === "true" ? "✓ Attivo" : "✗ Disattivato"; } else { const raw = String(value); display = formatDisplay ? formatDisplay(raw) : raw || "—"; } return (
{display}
); } return (
{type === "textarea" ? (