Skip to content

first-run-ux: canary foundations + empty states#1149

Merged
joelteply merged 1 commit into
canaryfrom
intake/first-run-ux-foundations-canary
May 14, 2026
Merged

first-run-ux: canary foundations + empty states#1149
joelteply merged 1 commit into
canaryfrom
intake/first-run-ux-foundations-canary

Conversation

@joelteply
Copy link
Copy Markdown
Contributor

Refs #1126
Refs #1101
Supersedes the canary-intake portion of draft PR #1107; this branch replays PR-A onto canary without rewriting RebelTechPro fork branch.

Summary

  • replay PR-A first-run UX foundations onto canary
  • add UserEntity.hasOnboarded, reusable ModalWidget, and reusable EmptyStateWidget
  • wire guided empty states into chat, room list, and user list
  • preserve canary a11y typing-indicator attributes while resolving the ChatWidget conflict
  • apply safe review fixes: ModalWidget uses Document-or-ShadowRoot activeElement typing, attaches keydown only while open, checks focus target is still connected, and uses crypto.randomUUID for aria ids
  • exclude generated/build outputs from the ESLint ratchet and lower the baseline from 5529 to 5464

Proof

  • precommit during cherry-pick/amend: TypeScript compilation passed
  • bash scripts/ratchets/check-eslint-baseline.sh -> 5464 held
  • pre-push: TypeScript clean; ESLint ratchet held at 5464; Rust compile clean; Rust tests passed with --features metal,accelerate
  • browser precommit tests skipped because the local stack was not running; ./jtag ping unavailable
  • native Docker image push was attempted by the hook and skipped/fell through because Rust tests regenerated tracked ts-rs whitespace in unrelated cognition binding files; no images claimed for this PR

Notes

Three additive pieces that PR-B (welcome modal) will sit on top of, and
that already improve UX in their own right by replacing blank list
panels with explanatory empty states.

1. UserEntity gains `hasOnboarded?: boolean` (BooleanField, nullable).
   Per-user, cross-device — falsy on existing rows so the welcome
   modal (PR-B) defaults to "show." entity_schemas.json regenerated.

2. New shared widget `widgets/shared/ModalWidget.ts` — generic Lit
   dialog. Reactive `open` / `modalTitle` / `closable` properties;
   focus trap, Escape + backdrop dismiss, focus restoration on close;
   role=dialog + aria-modal=true + aria-labelledby out of the box.
   Slots for default body + footer. Reusable for any future modal
   need beyond onboarding (settings dialogs, confirms, etc.).

3. New shared widget `widgets/shared/EmptyStateWidget.ts` — `<empty-state>`
   custom element with icon / title / subtitle / optional action button.
   Fires `empty-state-action` event when the action is clicked. Drops
   into any list or content area that can be legitimately empty.

Wiring (3 surfaces, all behind a load-completed gate so the empty
state never flashes during initial scroller hydration):

   ReactiveEntityScrollerWidget — new `protected get isEmpty()`
   returns true only after the scroller's first load has completed
   AND the entity count is zero. Subclasses use this to decide
   whether to render an empty UI.

   ReactiveListWidget — new virtual `renderEmptyState()` returning
   `nothing` by default; main render hides the container and shows
   the empty state when `isEmpty` is true.

   RoomListWidget — overrides `renderEmptyState()`. Copy depends on
   the active filter (DM filter → "No direct messages yet" / "Open
   a DM..."; rooms filter → "No rooms yet" / "Rooms are shared
   spaces..."). No "create your first room" CTA wired yet; left for
   a follow-up once room-creation UX lands.

   UserListWidget — overrides `renderEmptyState()`. Copy depends on
   whether any type/status filter is active. The widget overrides
   `render()` directly (bypasses base render) so we mirror the same
   ?hidden + empty-state conditional locally.

   ChatWidget — uses string-based templates (not Lit), so wired
   differently. `<empty-state>` element added to renderTemplate with
   `hidden` set; `updateEntityCount()` is overridden to toggle the
   attribute based on `getEntityCount() === 0` after every CRUD
   event + the initial post-load count update. Initial `hidden`
   prevents flash during room-switch loading.

Out of scope (PR-B): welcome modal, first-run gate in MainWidget,
write-back of `hasOnboarded=true` on modal completion, tutorial-
persona seeding verification.

`npm run build:ts` is green. Not visually validated locally —
deploy + screenshot is the gate before un-drafting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joelteply joelteply force-pushed the intake/first-run-ux-foundations-canary branch from d5d9615 to 853defb Compare May 14, 2026 11:35
@joelteply joelteply merged commit d4ea1fe into canary May 14, 2026
3 checks passed
@joelteply joelteply deleted the intake/first-run-ux-foundations-canary branch May 14, 2026 11:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant