5 KiB
Executable file
5 KiB
Executable file
Task 10 — Presets UI (folder/file tree manager + Script tab save targets)
Task ID: 10 Agent: full-stack-developer (Presets UI) Status: COMPLETE — lint clean, tsc clean on touched files, verified end-to-end in a headless browser.
What was built
1. src/components/ava/presets-tab.tsx — complete rewrite
Left column (58% on sm+, full width on mobile): the folder/file tree
- Built from
store.folders+store.presets. Defensive: presets whosefolderPathhas no Folder row synthesize their node + ancestors (buildTree). Folders sorted alphabetically per level (segment-wise compare, parents before children); presets alphabetically inside folders. - Folder row: chevron button (size-9 hit area, rotates ChevronRight) is the
ONLY expand/collapse control; clicking the row selects the folder
(
selectFolder— also updates the randomize scope). FolderOpen/Folder icon swaps with expansion. Subtree preset count badge (font-mono, tooltip "N presets in this folder and below"). - Default expansion:
expandedFolders[path] ?? path.split('/').length <= 2(same rule as the store) — root, root/embedded, root/user created open. - Preset row: FileText icon; single click selects, double-click loads.
Running preset (
activePresetId): emerald Play icon + emerald-300 name +bg-emerald-400/10. Selected row: emerald left border +bg-zinc-800/70. originalFilerows get a .avs download anchor (title "Download original Winamp .avs"), always visible on mobile, hover-visible onsm+.- Empty folders show an italic "(empty)" hint row. Tree body scrolls
(
max-h-[420px] lg:max-h-[480px], thinScrollbar).role="tree"/treeitem/group, aria-expanded/selected/level throughout.
Right column (42%): context-sensitive management panel
- Header: icon + name + font-mono full path (+ "N presets · M subfolders" for folders).
- Preset context: LOAD PRESET (emerald; applyScriptText → toast → setActivePresetId + setEditTarget; compile error → destructive toast + switch to script tab), RENAME, DELETE.
- Folder context: CREATE PRESET (POST STARTER_TEMPLATE → refresh →
editTarget + editor + engine + select +
setTab('script')+ toast "created — now edit and Save"), NEW SUBFOLDER (POST → expand parent + select new), RENAME (PATCH folder / PUT preset; selection, randomScope and editTarget follow the renamed paths), DELETE (AlertDialog with client-side blast radius — "removes N presets in M folders", empty-folder and preset variants), EXPORT .ZIP (asChild anchor to/api/presets/export?path=…with download attr; root exports everything), IMPORT .ZIP (hidden .zip input → FormData POST → summary toast with duplicates-skipped / folders-created). - PROTECTED_FOLDERS (root, root/embedded, root/user created): Rename + Delete disabled with "Protected folder" tooltip. Import/export allowed.
- All 409/400/404/500 responses → destructive toasts with the server message. Empty names → inline dialog validation.
- RANDOMIZE section (border-t): Checkbox + delay input (3–60 s; invalid input shows a red hint and reverts to the committed store value on blur), scope line ("Random from: <folder | all presets>") with amber "no presets in this folder yet" hint, pulsing emerald "ON · every Ns" badge while active. The timer runs in the store; the UI only wires it.
- NameDialog is fully controlled (parent owns value/error) — this was
needed to pass the
react-hooks/set-state-in-effectlint rule.
2. src/components/ava/script-tab.tsx — edits
- "Saving to: root/…/Name" chip (middle-truncated path, emerald, X button
clears the target) shown while
editTargetis set. - Save button (emerald, editTarget only): PUT
/api/presets/[id]{content, author from validateScript meta} → refresh + toastSaved to "Name"; disabled while saving. - Save As… button (editTarget only): Dialog prefilled with current name → POST same folder → setEditTarget(new) + refresh + toast; 409 handled.
- New button also clears the edit target.
Verification performed (headless browser + curl)
- Tree structure/counts/expansion/selection; Load (toast, chip appears,
Save PUT returns 200); Save As (copy created server-side, chip
retargeted); delete preset + folder (blast-radius + empty wording);
create preset (editor + tab switch); new subfolder; rename folder
(paths follow); import .zip end-to-end; randomize persistence
(localStorage
ava.randomize), scope line, invalid-delay revert, ON badge in DOM with pulse animation. Zero page/console errors. - Library restored to pristine state after testing (95 presets / 7 folders).
Notes for future agents
- Do NOT re-add setState-in-effect in NameDialog — the react-hooks v6 rule forbids it; keep it controlled.
setRandomIntervalSecis typed(sec: number); convert withNumber()before calling (the store would tolerate strings at runtime).- agent-browser
find … dblclickdoes not exist — double-click was verified through the identical Load Preset code path.