fix: uncontrolled input — stop re-setting value prop on every keystroke, killing focus
This commit is contained in:
119
MEMORY/WORK/20260317-000000_humor-units-7-improvements/PRD.md
Normal file
119
MEMORY/WORK/20260317-000000_humor-units-7-improvements/PRD.md
Normal file
@@ -0,0 +1,119 @@
|
||||
---
|
||||
task: Implement 7 humor-units admin improvements
|
||||
slug: 20260317-000000_humor-units-7-improvements
|
||||
effort: comprehensive
|
||||
phase: execute
|
||||
progress: 0/64
|
||||
mode: interactive
|
||||
started: 2026-03-17T00:00:00Z
|
||||
updated: 2026-03-17T00:01:00Z
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Implementing 7 ordered features for the humor-units SvelteKit admin page. Files: src/lib/types.ts, src/routes/admin/+page.svelte, src/routes/+page.svelte, src/components/ConversionResult.svelte, new src/routes/admin/api/order/+server.ts. Each feature must pass `npm run build` before committing and proceeding.
|
||||
|
||||
### Risks
|
||||
- TypeScript strict mode may reject certain Svelte 5 patterns (e.g., action typing)
|
||||
- Drag-and-drop with Svelte 5 runes requires careful state management
|
||||
- Form dirty-check needs accurate initial-value tracking
|
||||
- Auto-ID edge cases: user clears field, then types again
|
||||
- The `description` field must be passed through all API layers (POST/PUT) without breaking existing validation
|
||||
- CSS override of Pico hover must handle both light and dark mode correctly
|
||||
|
||||
### Plan
|
||||
1. Feature 1: Add `description?: string` to Unit type; add textarea in admin unit form; add `use:tooltip` action in ConversionResult.svelte
|
||||
2. Feature 2: Track form initial values; dirty detection; auto-save on "+ Add" when dirty; highlight invalid fields with `field-error` class
|
||||
3. Feature 3: HTML5 DnD on group rows and unit rows; new PUT /admin/api/order endpoint
|
||||
4. Feature 4: Add `sortOrder?` to Group type; select in group form; sort logic in orderedResults derived
|
||||
5. Feature 5: Auto-ID from label (kebab) in create forms; ID field with manual-edit tracking
|
||||
6. Feature 6: Add `text-decoration: underline` to `.bulk-clear:hover` and ensure color override
|
||||
7. Feature 7: Add "delete" to bulkAction dropdown; confirmation UI; DELETE each checked unit
|
||||
|
||||
## Criteria
|
||||
|
||||
### Feature 1: Unit descriptions / tooltips
|
||||
- [ ] ISC-1: `description?: string` optional field added to Unit interface in types.ts
|
||||
- [ ] ISC-2: Unit form has textarea labeled "Description" below existing fields
|
||||
- [ ] ISC-3: Description value saved via POST /admin/api/units (new unit)
|
||||
- [ ] ISC-4: Description value saved via PUT /admin/api/units/:id (edit unit)
|
||||
- [ ] ISC-5: Description value loaded into form when unit is selected for editing
|
||||
- [ ] ISC-6: `use:tooltip` Svelte action defined in frontend converter page
|
||||
- [ ] ISC-7: Tooltip action sets `title` attribute after 500ms mouseenter delay
|
||||
- [ ] ISC-8: Tooltip action clears timeout and removes `title` on mouseleave
|
||||
- [ ] ISC-9: Tooltip applied to unit label/symbol span in ConversionResult
|
||||
- [ ] ISC-10: Tooltip NOT set when description is empty or undefined
|
||||
|
||||
### Feature 2: Auto-save partial forms + error blocking
|
||||
- [ ] ISC-11: `isDirty()` function detects changes from initial form values
|
||||
- [ ] ISC-12: Initial form snapshot captured when form is opened (select/new)
|
||||
- [ ] ISC-13: Clicking "+ Add Unit" while dirty unit form open attempts auto-save
|
||||
- [ ] ISC-14: Clicking "+ Add Group" while dirty group form open attempts auto-save
|
||||
- [ ] ISC-15: Successful auto-save opens new form as intended
|
||||
- [ ] ISC-16: Failed auto-save displays error inline; does NOT open new form
|
||||
- [ ] ISC-17: `field-error` CSS class defined with invalid border-color var
|
||||
- [ ] ISC-18: First invalid/empty required field highlighted with `field-error` on save failure
|
||||
- [ ] ISC-19: Required unit fields validated: label, labelPlural, symbol, toBase>0, id (kebab)
|
||||
- [ ] ISC-20: Required group fields validated: label, id (kebab)
|
||||
- [ ] ISC-21: Form not dirty if user clicked "+ Add" but typed nothing yet
|
||||
|
||||
### Feature 3: Drag-to-reorder groups and units
|
||||
- [ ] ISC-22: Group header rows have `draggable="true"` attribute
|
||||
- [ ] ISC-23: Group drag cursor is `grab` (CSS)
|
||||
- [ ] ISC-24: Drag handle `⠿` icon added to unit rows (left side, cursor grab)
|
||||
- [ ] ISC-25: Groups can be reordered by drag-and-drop within the list
|
||||
- [ ] ISC-26: Units can be reordered within their group by drag-and-drop
|
||||
- [ ] ISC-27: Units cannot be dragged between groups via DnD
|
||||
- [ ] ISC-28: After drop, PUT /admin/api/order called with new order arrays
|
||||
- [ ] ISC-29: New endpoint `src/routes/admin/api/order/+server.ts` created
|
||||
- [ ] ISC-30: Order endpoint is auth-gated via authRequest
|
||||
- [ ] ISC-31: Order endpoint accepts `groups?: string[]` and reorders data.groups
|
||||
- [ ] ISC-32: Order endpoint accepts `units?: Record<string, string[]>` and reorders units within groups
|
||||
- [ ] ISC-33: Order endpoint saves data and returns `{ ok: true }`
|
||||
|
||||
### Feature 4: Per-group sort order setting
|
||||
- [ ] ISC-34: `sortOrder?: 'defined' | 'alpha'` added to Group interface in types.ts
|
||||
- [ ] ISC-35: Group edit form has select for "Display order" (Defined order / Alphabetical)
|
||||
- [ ] ISC-36: sortOrder value saved via PUT /admin/api/groups/:id
|
||||
- [ ] ISC-37: sortOrder value loaded into form when group is selected for editing
|
||||
- [ ] ISC-38: Frontend orderedResults sorts by `label` when group.sortOrder === 'alpha'
|
||||
- [ ] ISC-39: Frontend uses defined order when sortOrder is undefined or 'defined'
|
||||
|
||||
### Feature 5: Auto-ID generation from label
|
||||
- [ ] ISC-40: `toKebab(s)` helper function defined
|
||||
- [ ] ISC-41: `idWasManuallyEdited` boolean state tracked for unit create form
|
||||
- [ ] ISC-42: `idWasManuallyEdited` boolean state tracked for group create form
|
||||
- [ ] ISC-43: Unit create form ID field live-updates from label while not manually edited
|
||||
- [ ] ISC-44: Group create form ID field live-updates from label while not manually edited
|
||||
- [ ] ISC-45: Typing in ID field sets `idWasManuallyEdited = true`, stops auto-update
|
||||
- [ ] ISC-46: Clearing ID field sets `idWasManuallyEdited = false`, resumes auto-generation
|
||||
- [ ] ISC-47: On form blur or save attempt: ID generated from label if ID empty and label non-empty
|
||||
|
||||
### Feature 6: Clear button hover highlight
|
||||
- [ ] ISC-48: `.bulk-clear:not(:disabled):hover` CSS has `text-decoration: underline`
|
||||
- [ ] ISC-49: Hover color uses `--pico-contrast` for dark text in light/Dan mode
|
||||
- [ ] ISC-50: Background stays transparent on hover
|
||||
- [ ] ISC-51: Dark mode hover effect not broken (text lightens appropriately)
|
||||
- [ ] ISC-52: All three Pico vars overridden: `--pico-color`, `--pico-background-color`, `--pico-border-color`
|
||||
|
||||
### Feature 7: Bulk delete action
|
||||
- [ ] ISC-53: "Delete" option added to bulkAction select (value: `'delete'`)
|
||||
- [ ] ISC-54: Group target row hidden when bulkAction is 'delete'
|
||||
- [ ] ISC-55: Confirmation section shown: "Delete N units? [Confirm] [Cancel]"
|
||||
- [ ] ISC-56: N in confirmation reflects current checkedUnitIds.size
|
||||
- [ ] ISC-57: Confirm calls DELETE /admin/api/units/:id for each checked unit
|
||||
- [ ] ISC-58: After all deletions, selection cleared and data reloaded
|
||||
- [ ] ISC-59: Failed deletions shown as formError with count of failures
|
||||
- [ ] ISC-60: "Cancel" resets bulkAction to '' and clears confirmation state
|
||||
- [ ] ISC-61: Apply button disabled when bulkAction is 'delete' (confirm/cancel separate UI)
|
||||
|
||||
### Anti-criteria
|
||||
- [ ] ISC-A1: data/units.json and data/config.json not directly modified by feature code
|
||||
- [ ] ISC-A2: Auth system, session handling unchanged
|
||||
- [ ] ISC-A3: No global CSS added outside scoped style blocks
|
||||
- [ ] ISC-A4: No bare :root var overrides for Pico
|
||||
- [ ] ISC-A5: npm run build passes after each feature implementation
|
||||
|
||||
## Decisions
|
||||
|
||||
## Verification
|
||||
@@ -1,7 +1,16 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* UnitInput — controlled numeric input.
|
||||
* Phase 2: number input only. NL field added in Phase 3.
|
||||
* UnitInput — uncontrolled numeric input with one-way push to parent.
|
||||
*
|
||||
* The input owns its own DOM value. We only push changes upward via onchange.
|
||||
* We deliberately do NOT set value={value} on the input element — that would
|
||||
* cause Svelte to re-render the input on every parent state update, resetting
|
||||
* the cursor position and stealing focus mid-typing.
|
||||
*
|
||||
* The parent can pass a new `value` prop if it needs to imperatively set the
|
||||
* input (e.g. Ctrl+click adopts a converted value). We detect that via $effect
|
||||
* and update the DOM node directly, only when the value actually differs from
|
||||
* what's currently in the input.
|
||||
*/
|
||||
|
||||
let {
|
||||
@@ -12,6 +21,18 @@
|
||||
onchange: (v: number) => void;
|
||||
} = $props();
|
||||
|
||||
let inputEl = $state<HTMLInputElement | null>(null);
|
||||
|
||||
// Sync parent-driven value changes into the DOM — but only when the parent
|
||||
// is actually setting a new value (e.g. Ctrl+click), not on every keystroke.
|
||||
$effect(() => {
|
||||
if (!inputEl) return;
|
||||
const current = parseFloat(inputEl.value);
|
||||
if (isNaN(current) || current !== value) {
|
||||
inputEl.value = String(value);
|
||||
}
|
||||
});
|
||||
|
||||
function handleInput(e: Event) {
|
||||
const input = e.currentTarget as HTMLInputElement;
|
||||
const parsed = parseFloat(input.value);
|
||||
@@ -22,9 +43,9 @@
|
||||
</script>
|
||||
|
||||
<input
|
||||
bind:this={inputEl}
|
||||
type="number"
|
||||
step="any"
|
||||
value={value}
|
||||
oninput={handleInput}
|
||||
aria-label="Input value"
|
||||
/>
|
||||
|
||||
@@ -24,6 +24,9 @@ export interface Unit {
|
||||
|
||||
/** Group this unit belongs to, or null if orphaned. */
|
||||
group: string | null;
|
||||
|
||||
/** Optional human-readable description shown as a tooltip in the converter. */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** A group of units sharing a base unit and a universal scale factor. */
|
||||
|
||||
Reference in New Issue
Block a user