86 lines
5 KiB
Markdown
86 lines
5 KiB
Markdown
# 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 whose
|
||
`folderPath` has 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`.
|
||
- `originalFile` rows get a .avs download anchor (title "Download original
|
||
Winamp .avs"), always visible on mobile, hover-visible on `sm+`.
|
||
- 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-effect` lint 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 `editTarget` is set.
|
||
- Save button (emerald, editTarget only): PUT `/api/presets/[id]`
|
||
{content, author from validateScript meta} → refresh + toast
|
||
`Saved 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.
|
||
- `setRandomIntervalSec` is typed `(sec: number)`; convert with `Number()`
|
||
before calling (the store would tolerate strings at runtime).
|
||
- agent-browser `find … dblclick` does not exist — double-click was
|
||
verified through the identical Load Preset code path.
|