Make navigation dialogs compact and palette-style #286
- MA
Navigation confirmations now use compact headings; Add project and Search documents use palettes with one shared keyboard/pointer selection. The palette field keeps its caret and the selected row marks keyboard position, with the original scoped focus-ring exception retained.
Integration preserves current sidebar behavior and compact rows, with sentence-case Add project labels. Reviewed navigation motion and sidebar class boundaries before renewing only those exact pins. Checks/types and nine unit tests pass; all 45 hosted/creation/shell browser cases pass (42 in the combined run, plus the three corrected cases in a targeted rerun). Two selectors now scope to Projects because the empty page has a second Add project button; the tooltip assertion follows current sentence-case copy. Final-main rebase and required CI follow #285.
<details><summary>Original proposal and screenshots</summary>
## Why
Every navigation dialog used a ~24px bold title, so a simple delete confirmation looked like a page heading. Search documents showed its name twice (heading and placeholder) and gave each result three lines, repeating the repository path even when every document lives in one project. Add project gave each repository a two-line, 52px row with a grey "Add" on every row, plain-text loading, and a loud underlined access link. "Add Project" was the only Title Case label.
## What changed
- **Shared shell** (`navigation-dialog.tsx`): the title is now `text-base` at weight 600 with tighter body spacing. This applies to rename, delete, and new document. Radius, shadow, and button sizes are unchanged. - **Palette mode** (`palette` prop): the search field is the visible header. The dialog keeps its accessible name through a visually hidden title. The field is borderless, has a search icon, and sits above a hairline divider. Palettes are anchored 15vh from the top, so the field doesn't move while the results grow and shrink. - **One active row** (`navigation-palette.tsx`, a `listbox` with roving focus over `option` rows). The keyboard, the pointer, and a typed query all move the same `--color-selected` highlight: - ArrowDown and ArrowUp move between the field and the enabled rows. Home and End jump to the first and last row when focus is on a row. Returning to the field scrolls the list back to the top. - Moving the mouse over a row makes it active. If focus is already on a row, focus moves too. - While the query is non-empty, the first enabled row is pre-highlighted, so Enter in the field acts on it straight away. - The loading and empty text uses `role="status"`. - **Search documents**: each row is one line, about 34px: the title, then the description in tertiary text, truncated. The `owner/repo` suffix shows whenever you have more than one project. Empty state: a centred "No matching documents". Loading: a spinner and "Searching documents". - **Add project**: rows read `octo-org/score`, with the owner in tertiary. The whole row is the option. A small "Add" hint shows only on the active row. Repositories already added show a check and "Added", are `aria-disabled`, and are skipped by the keyboard. "Manage repository access" is now a quiet tertiary footer link above a hairline, underlined only on hover. - **Touch** (`pointer: coarse`): rows are 44px and every addable row shows "Add", because there is no hover. - **Spacing**: the palette rules in `navigation.css` use `--spacing` multiples instead of raw px. - **Label**: "Add Project" is now "Add project" in the dialog title, the sidebar aria-label, and the new document button. The sidebar icon tooltip still reads "Add Project", because `tooltipText` title-cases every icon label ("New Document", "Close Sidebar"). The `shell.e2e.ts` expectation for it is therefore correct, not stale. - **Focus**: the palette field and rows don't draw the focus ring. The caret marks the field and the selected background marks the active row. This is a scoped exception in `theme.css`.
## Screenshots
**After the review follow-ups**: a typed query with the first match pre-highlighted, one shared highlight after a keyboard move and a hover, the empty state, and touch on a 390px phone (44px rows, "Add" on every row):
   
The screenshots below are from the first revision. They are still accurate except for the centred position (palettes are now anchored near the top) and the shared highlight.
Search documents, before / after (results span two projects, so the path shows; the second row is focused from the keyboard):
 
Single-project search, where the path is hidden, and the empty state:
  
Add project, before / after (loading, then the list with a hovered row):
   
Already added:

Delete and rename, before / after:
   
## Needs a design-contract review
`bun run ci` fails only on two reviewed dynamic-owner hashes in `scripts/design-contract/exceptions/dynamic-web.json`. I deliberately left both unedited:
| File | Exception file | New SHA-256 | Why it is safe | | --- | --- | --- | --- | | `apps/web/src/navigation-dialog.tsx` | `dynamic-web.json` | `43604e407a85bfda98028185d27d2e6be744f490402c7b275b6dfa806a385612` | The dynamic `motion.className` boundary is unchanged. The only new class expression picks between two static class names (`navigation-modal-content` and `navigation-palette`). The title switches between `sr-only` and the static `text-base font-semibold text-text-primary`. Palette keyboard handling moved out to `navigation-palette.tsx`. | | `apps/web/src/project-sidebar.tsx` | `dynamic-web.json` | `f65d02665c66cd299acd406120ab66ad2dfc121ebbbea12578a0b0d9533839f9` | The only change is the `aria-label` text, from "Add Project" to "Add project". `NavigationIcon`'s class pass-through is untouched. |
## For Maggie's sign-off
The palette search field and its rows draw no focus ring. The field relies on its caret, and the active row on the selected background, which the keyboard and pointer now share, so only one row is ever highlighted. The exception is scoped in `apps/web/src/theme.css`.
## Testing
- `bun run types`: pass - `bun test apps/web`: 470 pass, 0 fail - `bun scripts/check-design-record.ts`: pass. `node scripts/check-design.mjs` (Impeccable): no new findings - `bun run ci`: dprint, oxlint, tokens and record pass. `check-design-contract` fails only on the two review hashes above. - CI e2e: pass. I updated `hosted`, `document-creation` and `document-navigation` to use `option` rows. A new hosted test covers ArrowDown, End and Home, then a hover that leaves exactly one `aria-selected` row and moves focus to it. The filter test asserts that the first match is pre-highlighted. - Checked by hand in Chromium against a fake-GitHub server: - The first match is pre-highlighted and Enter opens it. - Hover after arrowing leaves one highlight. - Home and End land on `archive-1` and `archive-12`; the added `score` row is skipped. - The dialog's top edge stays at 151px while typing. - Rows are 44px on a touch phone.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
</details>
- MA
**Review:** Looks good
Checked in a real browser (desktop 1440x900, phone 390x844) against the PR branch. CI: only `bun run ci` fails, on the two reviewed-dynamic-owner hashes (`project-sidebar.tsx:24`, `navigation-dialog.tsx:62`); container and e2e pass. The hash renewals are safe: the only logic added is the `palette` class switch and the key handler.
**Works well** - Both palettes read cleanly: search header, single-line `owner/name` rows (owner muted), hover/focus-only "Add", check + "Added" state (disabled, skipped by arrows), quiet loading/empty/footer. Long titles truncate with an ellipsis; 14 repositories scroll inside the list with `scroll-padding`. - Keyboard: input -> ArrowDown/Up moves real focus row to row, stops at both ends, skips disabled rows, scrolls to top on returning to the field, Enter adds/opens, Escape closes and returns focus to the trigger. Roving focus is used consistently (no `aria-activedescendant`), so it is coherent. - Phone: dialogs are 358px wide, centered, and rows stay single-line. Rename, delete and new-document spacing changes are small and look right (Rename verified; delete is only reachable from archived views and `/design-audit`, I only read the diff).
**Findings (non-blocking, in priority order)** 1. Focus/selection contrast (`theme.css` `:where(.navigation-palette-input, .navigation-palette-option):focus-visible { outline: none }`). The active row is `--color-selected` (oklch L 0.942) on white, about 1.15:1. That is the same value as hover, so with the pointer resting on one row and the keyboard on another, two rows look identical-selected (reproduced: focus on archive-2, hover on archive-4). The field with no ring is acceptable (caret plus the header position is the cmdk/Linear convention), but the row is the only keyboard marker. Suggest a stronger `:focus-visible` row treatment (for example a 2px inset ring or a slightly darker `--color-selected-strong`), or only paint hover when the pointer last moved. 2. Home/End do nothing when focus is on a row (only Up/Down handled in `movePaletteFocus`, `navigation-dialog.tsx`). Add them for rows (leave them alone in the input so the caret still works), or drop them from expectations. 3. Typing then Enter does nothing until ArrowDown is pressed. A palette user expects the first match to be actionable; consider ArrowDown-free Enter on the first enabled row when the query is non-empty. 4. Phone touch targets: rows are 34px tall (`.navigation-palette-option` `min-height: 34px`). Below the 44px guideline on touch; use a larger min-height under `(pointer: coarse)`. Also the "Add" hint never shows on touch, so the affordance is just the row tap. 5. A11y semantics: lists are plain buttons with no `listbox`/`option`, and loading/empty states have no `role="status"`, so "No matching repositories" is not announced. Cheap fix: `role="status"` on `.navigation-palette-status` (the search "failed projects" note already has it). 6. Dialog vertical jump: palettes stay centered, so the top edge moves (about 246px -> 384px) as results shrink while typing. Anchoring the dialog near the top (as Linear does) or giving the list a stable min-height would avoid that. 7. Search dialog: the `owner/repo` suffix is conditional on `spansRepositories` computed from the filtered results, so narrowing a query to one repo hides the suffix and two same-titled documents lose their disambiguation. Consider deriving it from `projects.length > 1` instead. 8. Raw px values in `navigation.css` (48px, 34px, 72px, 6px, 16px, 10px) rather than tokens; the token check passes, but worth a look against the design contract.
No functional blockers.
MaggieAppletonreferenced this in Compact the Projects sidebar into single-line rows #284last month on GitHub
- MA
Thanks for the review. All findings are addressed in 3519c3f3, following the orchestrator's design decisions:
1. **One active row**: the keyboard and the pointer now share a single `aria-selected` highlight in `--color-selected`. Moving the mouse over a row makes it active, and moves focus to it when focus is already on a row. 2. **Home/End** jump to the first and last enabled row when focus is on a row. In the field they still move the caret. 3. **Non-empty query**: the first enabled row is pre-highlighted, so Enter in the field acts on it. The arrows still move the highlight. 4. **Touch** (`pointer: coarse`): rows are 44px, and every addable row shows "Add". 5. **Semantics**: the rows are now a `listbox` of `option`s with roving focus, and the loading and empty text uses `role="status"`. 6. **Position**: palettes are anchored 15vh from the top. The dialog's top edge stays at 151px while typing. 7. **Repo suffix** in search now depends on `projects.length > 1`. 8. **Spacing**: the palette rules use `--spacing` multiples. 9. The `shell.e2e.ts` "Add Project" tooltip expectation is not stale. `tooltipText` title-cases icon labels ("New Document", "Close Sidebar"), so it stays.
CI: e2e and container pass. Validation fails only on the two review hashes; the new `navigation-dialog.tsx` hash is in the PR body. `bun test apps/web` passes locally (470/0). The updated screenshots are at the top of the PR's Screenshots section.
lavaman131referenced ef82b81100d9last month on GitHub
lavaman131referenced 7f6cfcdc3abelast month on GitHub
MaggieAppletonhead-ref-force-pushed da54c4c8e259last month on GitHub
MaggieAppletonhead-ref-force-pushed c26e49e41f1blast month on GitHub
- MA
I rechecked the latest failed `ci` run on this head. The only failing job is `format, lint, types, tests`, and it stops in `Format, lint, and enforce design tokens`; `container` and `e2e` both pass. The failure is the reviewed design-contract exception renewal for `apps/web/src/navigation-dialog.tsx` and `apps/web/src/project-sidebar.tsx`, which would require updating `scripts/design-contract/exceptions/dynamic-web.json`. I did not push a `[ci-fix]` commit because this workflow is not allowed to change those protected exception manifests. Next human action: land the design-contract review/update for those hashes, or push a new head that avoids needing the exception change.
> Generated by [PR CI fixer](https://github.com/githubnext/chopin/actions/runs/37618971100) · gpt54 · 81 AIC · ⌖ 4.53 AIC · ⊞ 15.1K · [◷](https://github.com/search?q=repo%3Agithubnext%2Fchopin+%22gh-aw-workflow-call-id%3A+githubnext%2Fchopin%2Fpr-ci-fix%22&type=issues)
<!-- gh-aw-agentic-workflow: PR CI fixer, engine: codex, model: gpt-5.4, id: 37618971100, workflow_id: pr-ci-fix, run: https://github.com/githubnext/chopin/actions/runs/37618971100 --> <!-- gh-aw-workflow-call-id: githubnext/chopin/pr-ci-fix -->
MaggieAppletonhead-ref-force-pushed 66e01c6cf212last month on GitHub
MaggieAppletonhead-ref-force-pushed 891ebd7fc725last month on GitHub
MaggieAppletonhead-ref-force-pushed 4ed63e1d608alast month on GitHub
- MA
I rechecked the latest failed `ci` run for `4ed63e1d608a89f2770d4dd48b800cff5f069633`. The head still matches the recorded candidate, `container` and `e2e` are green, and the only failing job is `format, lint, types, tests`, stopping in `Format, lint, and enforce design tokens`. The blocker is still the reviewed design-contract exception renewal for `apps/web/src/navigation-dialog.tsx` and `apps/web/src/project-sidebar.tsx`, which would require changing `scripts/design-contract/exceptions/dynamic-web.json` that this workflow is not allowed to edit. I did not queue a `[ci-fix]` commit. Local reproduction and verification are also blocked in this runner because `bun` and `mise` are not installed, so I could not run `bun install --frozen-lockfile`, `bun run fix`, or `bun run ci` here. Next human action: review and renew the exact design-contract exception if the ownership change is approved, or push a new head that avoids needing the exception update.
`babysit-head:4ed63e1d608a89f2770d4dd48b800cff5f069633`
> Generated by [PR CI fixer](https://github.com/githubnext/chopin/actions/runs/37627207812) · gpt54 · 43.1 AIC · ⌖ 3.21 AIC · ⊞ 15.1K · [◷](https://github.com/search?q=repo%3Agithubnext%2Fchopin+%22gh-aw-workflow-call-id%3A+githubnext%2Fchopin%2Fpr-ci-fix%22&type=issues)
<!-- gh-aw-agentic-workflow: PR CI fixer, engine: codex, model: gpt-5.4, id: 37627207812, workflow_id: pr-ci-fix, run: https://github.com/githubnext/chopin/actions/runs/37627207812 --> <!-- gh-aw-workflow-call-id: githubnext/chopin/pr-ci-fix -->
MaggieAppletonhead-ref-force-pushed 0eaf0887fc75last month on GitHub
MaggieAppletonhead-ref-force-pushed 250f4f195253last month on GitHub
MaggieAppletonmerged 7991f151f961last month on GitHub
MaggieAppletonhead-ref-deletedlast month on GitHub
MaggieAppletonreferenced this in Align tables to prose and polish unavailable images, formula toggles, and icon titles #287last month on GitHub