openvisengine/agent-ctx/10-presets-ui.md
2026-10-27 03:42:05 +00:00

86 lines
5 KiB
Markdown
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.