Skip to content

hub-client: UI/UX modernization Phase 1 — component consistency - #607

Merged
shikokuchuo merged 17 commits into
hub-client-uiux-phase0from
hub-client-uiux-phase1
Aug 26, 2026
Merged

hub-client: UI/UX modernization Phase 1 — component consistency#607
shikokuchuo merged 17 commits into
hub-client-uiux-phase0from
hub-client-uiux-phase1

Conversation

@shikokuchuo

Copy link
Copy Markdown
Member

Phase 1 of the hub-client UI/UX modernization: one canonical implementation per UI primitive. Stacked on #605 (Phase 0). New visible chrome (the file-row ⋯ button, styled tooltips, notification placement) is token-styled and gets a final review at the Phase 5 design gate.

What changes

  • Icons: all per-component SVG icons move into one shared module, icons.tsx, with a documented contract (decorative only, one stroke style, one size prop).
  • Monospace: the three ad-hoc monospace font stacks across 10 CSS files migrate to the --font-mono token.
  • Buttons: one system with two sizes — .qh-btn (variants primary/outline/danger/ghost-accent, plus .small) and .qh-icon-btn (plus .boxed). Disabled and focus-ring states are defined once on the base classes.
  • Menus: one shared Menu component with full keyboard support (arrow keys, Home/End, type-ahead, submenus, Escape, focus return). Used by the file-sidebar context menu and all four projects-home menus. File rows gain a visible ⋯ button, so menus are no longer right-click-only.
  • Tooltips: a styled Tooltip component replaces every native title attribute in the app chrome. 400ms on hover, immediate on keyboard focus, Escape dismisses.
  • Notifications: Toast, UpdateAvailableToast, and the ephemeral-session banner become one documented system with three tiers: transient, dismissible-persistent, and session banner.
  • Dialogs: all eight hand-rolled projects-home dialogs now use the shared ModalDialog (focus trap, Escape, focus restoration).
  • Forms: .qh-input gains disabled and invalid states; one documented validation pattern; the settings checkboxes get a visible focus ring.
  • Docs: new hub-client/design-system.md describes the token layers, the primitives, and how to add a component.

Bugs found and fixed along the way

  • Bare .qh-btn declared no text color, so default buttons were black-on-dark in the dark theme (1.35:1 contrast).
  • ModalDialog captured its focus-restore target after autoFocus had already run, so closing a dialog lost focus to the page body.
  • A menu item that opens a dialog had the dialog's focus stolen back by the menu.

Verification

  • npm run build:all and npm run test:ci pass (1005 + 112 + 133 tests).
  • Visual suite passes 56/56, including 9 new keyboard specs for the menu and 2 new dialog interaction specs.
  • axe-core baselines unchanged; the gallery scan exposed the dark-theme button bug above, now fixed.
  • lint:css is clean; two more exceptions burned down.

Notes

  • Changelog entries included for the user-visible changes (menus, tooltips, dialogs).
  • The status-indicator work item needed no changes: an audit found every status already pairs color with text or an accessible label.

Tracks bd-2q55e6rc (Phase 1: bd-iguk0hpd, closed). Plan: claude-notes/plans/2026-08-26-hub-client-uiux-modernization.md.

@posit-snyk-bot

posit-snyk-bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

shikokuchuo and others added 17 commits August 26, 2026 18:12
Single source for UI icons with a documented contract: decorative
(aria-hidden), 24x24 stroke style (currentColor, stroke-width 2), size
prop. Consolidates 14 icons from FileSidebar, MinimalHeader,
ViewToggleControl, ProjectsHome, and ReplayDrawer; adds MoreIcon for
the upcoming kebab affordance. Brand logo and the replay waveform
(data viz, not an icon) stay local. Visual baselines diff clean.

Phase 1 of the UI/UX modernization (bd-iguk0hpd).
Migrates the three ad-hoc stacks ('SF Mono'…, 'JetBrains Mono'…,
'Source Code Pro'…) across 10 CSS files to var(--font-mono), adopting
the dominant JetBrains Mono stack per the phase's alignment rule.
src/debug/debug.css keeps its local token (standalone page). Visual
baselines diff clean.

Phase 1 of the UI/UX modernization (bd-iguk0hpd).
One canonical system in ui.css, two sizes: .qh-btn (variants primary/
outline/danger/ghost-accent, .small) and .qh-icon-btn (with .boxed
28x28 fixed target). Disabled and focus-visible states are now defined
once on the base classes (token focus ring) instead of per-variant.
MinimalHeader's ad-hoc .icon-btn becomes .qh-icon-btn.boxed with only
header-context colors overridden locally. Documented boundaries:
view-toggle is a segmented control, .qh-pager a nav strip, and the
header's .preview-btn a filled primary pill on header tokens — none
are restyled. Font sizes/weights migrate to type-scale tokens; radii
and paddings await the Phase 5 grid pass. Visual baselines diff clean.

Phase 1 of the UI/UX modernization (bd-iguk0hpd).
…llery

New components/Menu.tsx implements the APG menu-button pattern:
arrow/Home/End navigation (wrapping, skipping disabled), type-ahead,
submenus (ArrowRight/ArrowLeft, hover parity), Escape with focus
return, pointer-outside close, scroll-into-view, and viewport-edge
flip for fixed placement. Destructive items use danger styling and
the confirm-guard rule is documented in the module header.

Adopted by: FileSidebar context menu (was right-click-only divs; gains
a visible hover/focus kebab button per file row — new chrome, token-
styled, to be ratified at the Phase 5 gate) and all four ProjectsHome
action menus (project card/row, collection, both sort menus, New).
The avatar popover stays on .qh-menu styling: it contains a form and
is a popover, not an action menu.

Copy-feedback items (Share link, Copy project ID) keep the menu open
via a keepOpen option, preserving the in-place 'copied!' feedback.

Also: DevHarness grows a #/dev/gallery component gallery (buttons,
icon buttons, menu, form controls, icons) covered by new visual + axe
baselines, and e2e/menu-keyboard.visual.spec.ts covers the keyboard
contract (9 specs). Two lint:css exceptions burned down with the
deleted .context-menu block.

Phase 1 of the UI/UX modernization (bd-iguk0hpd).
New components/Tooltip.tsx implements the APG tooltip pattern: 400ms
hover delay, immediate on focus, aria-describedby wiring, Escape
dismiss, viewport-edge flip with horizontal clamp. Token-styled;
appearance to be ratified at the Phase 5 gate. Hover handlers live on
the wrapper span so disabled controls keep their tooltips.

Converts every title= in the app chrome (MinimalHeader, FileSidebar,
ViewToggleControl, ReplayDrawer, ProjectsHome, OutlinePanel,
EphemeralSessionBanner, AboutTab, ProjectTab, JoinCollectionLanding).
Not converted, deliberately: iframe title= (accessible name, not a
tooltip), AST-rendered content links (document content), the classic
ProjectSelector (out of scope), and src/debug tooling. Redundant
titles duplicating visible text (project card/row names) were dropped;
icon-only buttons gained aria-labels where the title was the only
name (peek, fork, menu, pager, swatches).

Also fixes a real dark-theme bug the gallery axe scan exposed: bare
.qh-btn declared no color, so default buttons rendered black-on-dark
(1.35:1). Remaining gallery contrast entries are token-level brand
fills/accents, deferred to the Phase 5 visual gate per the plan.

Phase 1 of the UI/UX modernization (bd-iguk0hpd).
The eight ad-hoc ProjectsHome dialogs (duplicate, new/rename collection,
confirm, move warning, rename project, new project, connect/import)
hand-rolled role=dialog with no focus trap and no focus restoration.
They now use the shared ModalDialog: header/content/actions via the
shared classes (.qh-form-dialog zeroes the padding so the hairlines
span), Escape closes, Tab cycles, focus restores on close.

Two focus bugs fixed along the way:
- ModalDialog captured its focus-restore target in a mount effect —
  after autoFocus children had already taken focus — so restoration
  targeted a detached input and focus was lost to <body>. Capture now
  happens during render.
- Menu returned focus to its trigger synchronously on activation,
  stealing focus back from a dialog the item had just opened. The
  return is now deferred past the commit and skipped when focus is
  inside a dialog.

Coverage: e2e/projects-home-dialogs.visual.spec.ts drives the New and
Connect/Import dialogs through the real UI (menu keyboard activation,
focus trap wrap, Escape/close-button with focus return).

Phase 1 of the UI/UX modernization (bd-iguk0hpd).
One system, three documented tiers in components/notifications.css:
transient status (.toast, auto-dismiss, bottom-center), dismissible
persistent (.update-available-toast, bottom-right — persistent wins
the corner so transient never covers its actions), and session banner
(.ephemeral-session-banner, in-flow mode indicator, never dismissible).
Toast.css and UpdateAvailableToast.css are gone; the banner rule moves
out of Editor.css. Class names are unchanged (baselines/tests reference
them); values are preserved, with font sizes/radii migrated to scale
tokens and the redundant font-family overrides dropped (identical to
the root stack). The persistent toast's dismiss button gains the token
focus ring, and the fixed placements are now logical-property based
(two lint:css exceptions burned down properly rather than carried).

Phase 1 of the UI/UX modernization (bd-iguk0hpd).
Form controls: .qh-input gains disabled and aria-invalid states; the
validation pattern is documented (field-level aria-invalid + error text
via aria-describedby; form-level .qh-error.inline). The SettingsTab
custom checkbox was invisible to keyboard users — the token focus ring
now lands on the custom box (WCAG 2.4.7). Gallery gains disabled-input
and tooltip sections.

Status-indicator audit (plan item): every status now pairs color with
text or label — connection (dot + Online/Offline text), renderer status
(dot + label), collaborators (color dot + name), diagnostics (Monaco
markers, not color-only). No changes needed beyond the Phase 1 header
work; recorded here for the plan checkbox.

Adds hub-client/design-system.md: the token-layer contract, the
primitives table, and the how-to-add-a-component rules — the
drift-prevention counterpart to lint:css.

Phase 1 of the UI/UX modernization (bd-iguk0hpd).
…clip them

The sidebar is overflow-hidden, so the in-place absolute-positioned
bubble was cut off at the sidebar edge — native title tooltips never
clipped because the OS drew them. The bubble now renders on
document.body with fixed positioning measured from the anchor (centered
above, flip below, horizontal clamp), tracks the anchor on scroll and
resize, and stays hidden until measured so it never flashes
unpositioned.

Phase 1 of the UI/UX modernization (bd-iguk0hpd).
The tooltip wrapper spans became the header's flex items, so the
buttons' flex:1 growth went inert and they clustered right (40px each
starting at x=76), leaving a gap that read as a missing button. The
header is now justify-content: flex-start with compact natural-width
buttons. The 1% baseline tolerance masked the shift; the sidebar
baseline is regenerated for the intended layout.

Phase 1 of the UI/UX modernization (bd-iguk0hpd).
Above-the-anchor placement read as awkward bars floating over the
chrome; below is where native title tooltips always appeared. The
bubble now centers below the anchor and flips above only at the
viewport bottom; horizontal clamping is unchanged.

Phase 1 of the UI/UX modernization (bd-iguk0hpd).
Review follow-ups on the new primitives:
- Clicking a disabled menu item no longer closes the menu (the click
  bubbled to the root closer even though the item didn't activate).
  Regression spec added.
- The type-ahead timer is cleared on unmount, and Space no longer
  pollutes the type-ahead buffer (it activates the focused item).
- Submenus get an accessible name via aria-labelledby on the parent item.
- Elements made focusable for their tooltips (connection indicator,
  ephemeral banner, footer and About commit hashes) gain the token
  focus ring — focusable without a visible indicator fails WCAG 2.4.7.
  This cleared the last outline:none exception in ProjectsHome.css,
  burning down that lint exception.
- The file context menu is keyed by file path, so right-clicking a
  different row remounts it — first-item focus and the viewport flip
  are mount-time effects and must re-run.

Phase 1 of the UI/UX modernization (bd-iguk0hpd).
The New-project menu was its only user; MenuItem's strong + subtext
props cover it.

Phase 1 of the UI/UX modernization (bd-iguk0hpd).
CI's E2E — interactive leg (the only place this suite runs) failed on
four tests after Phase 1; all were stale expectations, plus one real
small bug the staleness hid:

- files-header.spec.ts asserted the sidebar header buttons fill the row
  at equal width — the layout the Phase 1 review fixes replaced with
  compact left-aligned buttons. The spec now asserts the current intent:
  one compact row, no wrapping, equal sizes, left-aligned at the padding
  edge (the original guard was against buttons wrapping to a second row).
- projects-home.spec.ts located the project row's menu button by its
  old accessible name '⋯'; Phase 1 gave it aria-label='Actions for …'.
  Menu items were queried by role 'button'; the Phase 1 Menu uses the
  APG role 'menuitem'.
- MenuSubmenu click was a toggle, but hover opens the submenu first —
  so a pointer click (which hovers before it clicks) closed the submenu
  it meant to open. Click/Enter is now open-only and focuses the first
  submenu item, matching the ArrowRight path and the APG pattern.

Verified locally against the interactive config (VITE_E2E=1 build):
files-header 2/2, projects-home 2/2, menu-keyboard + projects-home
-dialogs visual specs green, component vitests green.
Two CI failures on this branch, both in the visual e2e suite:

- projects-home-dialogs 'New project dialog … focus return' failed
  deterministically in CI (reproduced locally with the WASM build
  present): the menu's deferred (microtask) focus return raced the
  opening dialog's autoFocus and stole focus back to the trigger.
  Backports the Phase 2 focus-chain fix (9b91c96): Menu returns focus
  synchronously before the commit and activates Enter/Space explicitly
  (the browser's default activation click otherwise lands on the
  re-focused trigger); ModalDialog defers its focus restore past the
  commit and skips while the dialog is still connected (StrictMode's
  mount-time cleanup was stealing the autoFocus).

- baseline-screens 'dialog-share — dark' flaked with 'Execution
  context was destroyed' at bootHarness's identity pin: on a cold
  cache the vite dev server re-optimizes dependencies and reloads the
  page mid-probe. The waitForFunction + evaluate pair is idempotent
  and now retries past the reload.

No changelog entry: the focus bug is in the unreleased Phase 1
menu/dialog work this branch introduces, and the bootHarness change is
test-only.
@shikokuchuo
shikokuchuo force-pushed the hub-client-uiux-phase1 branch from 427eb35 to f310c87 Compare August 26, 2026 17:13
@shikokuchuo
shikokuchuo merged commit 1ccb9cc into main Aug 26, 2026
14 of 18 checks passed
@shikokuchuo
shikokuchuo deleted the hub-client-uiux-phase1 branch August 26, 2026 19:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants