Skip to content

UI consistency polish: disclosure chevrons, toast dismiss, popover autofocus, open-state tracking #102

Description

@BorisTyshkevich

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.

  • Both disclosures animate the same way.
  • Existing tests updated for the new markup/class; npm test green 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-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.

  • Clicking a visible toast dismisses it before the timer.
  • Test coverage for the click-dismiss path (including that the pending auto-dismiss timer doesn't fire again/double-hide after a manual dismiss); npm test green.

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.

  • openUserMenu and openFileMenu focus a sensible element on open; the already-correct surfaces (save popover, library-title rename, saved-edit form) are untouched.
  • Test coverage for the two new focus calls; npm test green.

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.

  • state.shortcutsOpen, state.editingSavedId, state.bannerDismissedFor are signals; behavior unchanged. (Shipped as state.editingSavedId/state.bannerDismissedFor rather than app.editingSavedId/app._bannerDismissedFor as literally written above — consolidating in state.js matches every other slice in that file, incl. the already-there libraryFilter/resultSort session-only fields; see ADR-0001's UI consistency polish: disclosure chevrons, toast dismiss, popover autofocus, open-state tracking #102 addendum.)
  • Existing tests for shortcuts modal / saved-edit / banner updated for signal access; npm test green at the per-file coverage gate.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions