Part of #68 (Roadmap to 1.0.0), Phase 3 — Windows. Small, independently-shippable polish items grouped, in the style of #85.
1. Disclosure chevron: rotate vs swap
Problem. The schema tree's expand/collapse chevron swaps between two distinct icons (Icon.chevDown() / Icon.chev(), src/ui/schema.js) while the login screen's "Advanced" disclosure rotates the same chevron via a CSS transform (src/ui/login.js:60-77). Same semantic action (disclosure), two different visual techniques.
Scope. Pick one (rotate is cheaper — one icon, no icon-swap flash) and apply it consistently to both.
Reactivity note (considered, not needed): state.expanded is already a real signal (src/state.js:85, migrated in #91) and the schema tree's treeRow() render function already reacts to it correctly — the icon-swap→rotate change is a pure rendering choice inside an already-reactive function. Login's advOpen is a deliberately plain closure variable (one-shot pre-auth screen, no other consumer) — leave it as-is.
Acceptance.
2. Toast: no manual dismiss
Problem. flashToast (src/ui/toast.js) only auto-dismisses after ~1600ms; there's no way to dismiss early or reread text after it fades. Also: .share-toast currently has pointer-events: none (src/styles.css:575), so a click handler alone won't work — clicks pass straight through the element.
Scope.
- Change
.share-toast (or scope it to .share-toast.show) to allow pointer events while visible.
- Add a click-to-dismiss handler in
flashToast() that clears the pending timer and hides the toast immediately.
- Keep it lightweight — passive notification, not a modal.
Acceptance.
3. Popover autofocus inconsistency (narrowed — see verified surfaces)
Problem, precisely. Of the surfaces that open a floating panel or inline input:
openSavePopover (src/ui/app.js:1061) already autofocuses + selects its input — reference implementation, no change needed.
- The library-title rename input (
renderLibraryTitle, src/ui/file-menu.js:58) already autofocuses + selects — reference implementation, no change needed.
- The saved-query edit form (
src/ui/saved-history.js) already autofocuses — no change needed.
openUserMenu (src/ui/app.js, via anchoredPopover()) does not focus anything on open.
openFileMenu's dropdown list itself (src/ui/file-menu.js — note: this one has its own fixedAnchor()/zoomScale() positioning, it does not use anchoredPopover() the way the save/user popovers do, so the fix touches file-menu.js directly) does not focus anything on open.
- File-menu's confirm dialogs (
openConfirm, src/ui/file-menu.js:225-242 — New Library / replace-library confirmations) are out of scope: they're Cancel/Confirm button dialogs with no text input to focus, not the same kind of surface as the others here.
- The row-limit control (
src/ui/results.js:300-301) is a native <select>, not an anchored popover — opening it is native OS/browser behavior; out of scope, nothing to fix.
Scope. Add sensible autofocus to exactly the two gaps: openUserMenu (first meaningful item, e.g. "Log out") and openFileMenu's dropdown (first menu item).
Acceptance.
4. Open-state tracking: unify on signals
Problem. app.state.shortcutsOpen (src/state.js:114), app.editingSavedId (src/ui/saved-history.js), and app._bannerDismissedFor (src/ui/app.js:362,373) are all plain fields/booleans read-and-written directly — none of the three are actually signals today (verified: src/state.js's real signals — tabs, schema, expanded, resultView, etc. — are all explicitly wrapped in signal(...); shortcutsOpen: false is not).
Scope. Migrate all three to signals, consistent with the ADR-0001 migration (CLAUDE.md rule 5). All are bare value flags with no DOM-lifecycle coupling, so this is mechanical: wrap in signal(), update read sites to .value, update write sites to .value = .
Explicitly out of scope (considered and rejected): app.dom.fileMenu/savePopover/userMenu are not bare state — they're refs to live, positioned DOM nodes already managed by one shared primitive, anchoredPopover() (src/ui/app.js:1004), which handles positioning (getBoundingClientRect at open time), Esc, and click-outside in one place (note: file-menu.js has its own separate, non-anchoredPopover positioning — see item 3). The ref is load-bearing; a signal next to it would be a redundant isOpen flag with no current reader. Revisit only if a real second consumer needs to react to "a popover is open" from outside anchoredPopover() itself — none exists today.
Acceptance.
Part of #68 (Roadmap to 1.0.0), Phase 3 — Windows. Small, independently-shippable polish items grouped, in the style of #85.
1. Disclosure chevron: rotate vs swap
Problem. The schema tree's expand/collapse chevron swaps between two distinct icons (
Icon.chevDown()/Icon.chev(),src/ui/schema.js) while the login screen's "Advanced" disclosure rotates the same chevron via a CSS transform (src/ui/login.js:60-77). Same semantic action (disclosure), two different visual techniques.Scope. Pick one (rotate is cheaper — one icon, no icon-swap flash) and apply it consistently to both.
Reactivity note (considered, not needed):
state.expandedis already a real signal (src/state.js:85, migrated in #91) and the schema tree'streeRow()render function already reacts to it correctly — the icon-swap→rotate change is a pure rendering choice inside an already-reactive function. Login'sadvOpenis a deliberately plain closure variable (one-shot pre-auth screen, no other consumer) — leave it as-is.Acceptance.
npm testgreen at the per-file coverage gate.2. Toast: no manual dismiss
Problem.
flashToast(src/ui/toast.js) only auto-dismisses after ~1600ms; there's no way to dismiss early or reread text after it fades. Also:.share-toastcurrently haspointer-events: none(src/styles.css:575), so a click handler alone won't work — clicks pass straight through the element.Scope.
.share-toast(or scope it to.share-toast.show) to allow pointer events while visible.flashToast()that clears the pending timer and hides the toast immediately.Acceptance.
npm testgreen.3. Popover autofocus inconsistency (narrowed — see verified surfaces)
Problem, precisely. Of the surfaces that open a floating panel or inline input:
openSavePopover(src/ui/app.js:1061) already autofocuses + selects its input — reference implementation, no change needed.renderLibraryTitle,src/ui/file-menu.js:58) already autofocuses + selects — reference implementation, no change needed.src/ui/saved-history.js) already autofocuses — no change needed.openUserMenu(src/ui/app.js, viaanchoredPopover()) does not focus anything on open.openFileMenu's dropdown list itself (src/ui/file-menu.js— note: this one has its ownfixedAnchor()/zoomScale()positioning, it does not useanchoredPopover()the way the save/user popovers do, so the fix touchesfile-menu.jsdirectly) does not focus anything on open.openConfirm,src/ui/file-menu.js:225-242— New Library / replace-library confirmations) are out of scope: they're Cancel/Confirm button dialogs with no text input to focus, not the same kind of surface as the others here.src/ui/results.js:300-301) is a native<select>, not an anchored popover — opening it is native OS/browser behavior; out of scope, nothing to fix.Scope. Add sensible autofocus to exactly the two gaps:
openUserMenu(first meaningful item, e.g. "Log out") andopenFileMenu's dropdown (first menu item).Acceptance.
openUserMenuandopenFileMenufocus a sensible element on open; the already-correct surfaces (save popover, library-title rename, saved-edit form) are untouched.npm testgreen.4. Open-state tracking: unify on signals
Problem.
app.state.shortcutsOpen(src/state.js:114),app.editingSavedId(src/ui/saved-history.js), andapp._bannerDismissedFor(src/ui/app.js:362,373) are all plain fields/booleans read-and-written directly — none of the three are actually signals today (verified:src/state.js's real signals —tabs,schema,expanded,resultView, etc. — are all explicitly wrapped insignal(...);shortcutsOpen: falseis not).Scope. Migrate all three to signals, consistent with the ADR-0001 migration (CLAUDE.md rule 5). All are bare value flags with no DOM-lifecycle coupling, so this is mechanical: wrap in
signal(), update read sites to.value, update write sites to.value =.Explicitly out of scope (considered and rejected):
app.dom.fileMenu/savePopover/userMenuare not bare state — they're refs to live, positioned DOM nodes already managed by one shared primitive,anchoredPopover()(src/ui/app.js:1004), which handles positioning (getBoundingClientRectat open time), Esc, and click-outside in one place (note:file-menu.jshas its own separate, non-anchoredPopoverpositioning — see item 3). The ref is load-bearing; a signal next to it would be a redundantisOpenflag with no current reader. Revisit only if a real second consumer needs to react to "a popover is open" from outsideanchoredPopover()itself — none exists today.Acceptance.
state.shortcutsOpen,state.editingSavedId,state.bannerDismissedForare signals; behavior unchanged. (Shipped asstate.editingSavedId/state.bannerDismissedForrather thanapp.editingSavedId/app._bannerDismissedForas literally written above — consolidating instate.jsmatches every other slice in that file, incl. the already-therelibraryFilter/resultSortsession-only fields; see ADR-0001's UI consistency polish: disclosure chevrons, toast dismiss, popover autofocus, open-state tracking #102 addendum.)npm testgreen at the per-file coverage gate.