Skip to content

feat: multichat grid view polish (a11y, unread, tab UX, mobile)#15

Merged
finedesignz merged 8 commits into
mainfrom
feat/grid-view-ship
May 25, 2026
Merged

feat: multichat grid view polish (a11y, unread, tab UX, mobile)#15
finedesignz merged 8 commits into
mainfrom
feat/grid-view-ship

Conversation

@finedesignz
Copy link
Copy Markdown
Owner

@

Summary

Polish pass on top of the grid-view foundation (PR #2) — accessibility, keyboard nav, unread tracking, tab delete UX, and mobile accordion improvements. Cherry-picked clean from feat/multichat-grid-view to exclude unrelated Phase 04/05 work stacked on that branch.

  • Keyboard nav + ARIA roles (tablist/tab/grid/gridcell, arrow keys, F2 rename, Escape, roving tabindex)
  • Per-session unread badge + polite aria-live announcer for inactive cells
  • Tab delete confirm with session-count detail + last-tab protection
  • Mobile accordion: dvh upper bound, design-token status dot, scrolls expanded row into view
  • MAX_CELLS_PER_TAB single constant, SessionPicker a11y
  • Docs updated (docs/grid-view.md)

Ship inventory: .planning/phases/03-multichat-grid-view/SHIP-COMMITS.md

Test plan

  • /grid tab bar keyboard nav (←/→/Home/End/F2/Enter)
  • Add sessions up to cap → role=status announcement
  • Delete last tab → confirm dialog with session-count → last-tab protection holds
  • Inactive cell receives message → unread badge + polite aria-live (no spam)
  • Mobile (< 768px): expand row scrolls into view, no 100vh keyboard collapse
  • tsc clean
  • hub tests green (57 pass / 12 skip / 0 fail)

Generated with Claude Code
@

finedesignz and others added 8 commits May 25, 2026 10:57
…s dot

- expanded row maxHeight: min(100dvh - 16px, 100vw + 88px) so landscape
  / wide-mobile no longer overflows the viewport (was unconditional
  100vw + 88px).
- offline status dot uses --text-muted token instead of bg-gray-500
  to respect light/dark theme.
- Export MAX_CELLS_PER_TAB from chat-tabs-api so GridPage and SessionPicker
  share one source of truth (was duplicated literal 12 across files).
- SessionPicker modal gets role=dialog, aria-modal, aria-labelledby on title.
- Modal max-height switches to 80dvh (was 80vh) so iOS soft-keyboard does
  not crush the picker.
- Overflow banner gets role=status for AT announcement.
Tab bar:
- role=tablist on container, role=tab + aria-selected on each chip.
- Arrow Left/Right cycle focus, Home/End jump, F2 starts rename,
  Enter/Space activate. Roving tabindex (active chip is 0, others -1).
- focus-visible ring for keyboard users.
- Softened border-b to /60 per design rules (was solid).

Grid body:
- role=tabpanel on desktop grid container, aria-labelledby active tab.
- role=grid + aria-label on cells container with visible/total count.
- role=gridcell + aria-selected on each cell wrapper.

Layout picker:
- aria-haspopup=listbox, aria-expanded, aria-label.
- Escape closes and returns focus to trigger.
- Listbox role on menu, option role + aria-selected on items.

Cell header:
- Drop misleading 'open in single-chat' button (was navigating to # /
  and dropping session context). Leave a TODO for when single-chat
  route accepts a sessionId param.
- Remove button gets aria-label with session name.
…n, scroll-snap

- Tab delete confirmation now lists the number of session bindings being
  removed and warns when deleting the only remaining tab.
- Last-tab guard: when the final tab is deleted, immediately create a
  fresh empty tab and route to it instead of leaving the user in an
  empty-state route with no active tab.
- Tab bar: scroll-snap proximity + smooth scroll for overflow rows so the
  bar scrolls cleanly when there are many tabs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a row expands, scroll its header to the top of the scroll container
so the input field below stays visible above the iOS soft keyboard. Uses
scroll-mt-2 so the header isn't flush against the container edge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Per-session unread counter. Increments only on server-emitted assistant
  'message' events (not text_delta) for visible cells that are not the
  currently-active cell. Cleared when the cell is activated.
- Soft indigo badge on the cell header (sr-only label included). No badge
  on the active cell — that's where the user already is.
- Page-level role="status" aria-live="polite" announcer reports the TOTAL
  unread count across cells, never the message content. Quiet by design:
  one summary per change, no per-cell announcement.

Resolves the P2 a11y gap: incoming messages on background cells were
silent for screen-reader users.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the round-2 polish work:
- ARIA tablist/grid/listbox + roving tabindex + keyboard map
- Tab bar scroll-snap behavior
- Tab delete confirm text + last-tab protection
- Per-cell unread counter + polite aria-live announcer policy

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@finedesignz finedesignz merged commit b409cae into main May 25, 2026
@finedesignz finedesignz deleted the feat/grid-view-ship branch May 25, 2026 17:59
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.

1 participant