@exxatdesignux/ui 0.5.5 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +16 -5
  3. package/consumer-extras/cursor-rules/exxat-ux-discovery-protocol.mdc +122 -0
  4. package/consumer-extras/cursor-rules/exxat-ux-principles.mdc +186 -0
  5. package/consumer-extras/cursor-skills/exxat-senior-ux/SKILL.md +145 -0
  6. package/consumer-extras/patterns/jobs/README.md +59 -0
  7. package/consumer-extras/patterns/jobs/record-detail.md +177 -0
  8. package/consumer-extras/patterns/modern-saas-patterns.md +165 -0
  9. package/dist/components/data-table/index.js +28 -22
  10. package/dist/components/data-table/index.js.map +1 -1
  11. package/dist/components/data-table/pagination.js +28 -22
  12. package/dist/components/data-table/pagination.js.map +1 -1
  13. package/dist/components/data-table/use-table-state.js +20 -17
  14. package/dist/components/data-table/use-table-state.js.map +1 -1
  15. package/dist/components/data-views/hub-table.js +28 -22
  16. package/dist/components/data-views/hub-table.js.map +1 -1
  17. package/dist/components/data-views/index.js +28 -22
  18. package/dist/components/data-views/index.js.map +1 -1
  19. package/dist/components/ui/badge.d.ts +1 -1
  20. package/dist/components/ui/banner.d.ts +3 -3
  21. package/dist/components/ui/button.d.ts +2 -2
  22. package/dist/components/ui/tabs.d.ts +1 -1
  23. package/dist/hooks/use-app-theme.d.ts +1 -1
  24. package/dist/index.js +28 -22
  25. package/dist/index.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/components/data-table/index.tsx +10 -6
  28. package/src/components/data-table/use-table-state.ts +33 -26
  29. package/template/docs/jobs/README.md +59 -0
  30. package/template/docs/jobs/record-detail.md +177 -0
  31. package/template/docs/modern-saas-patterns.md +165 -0
  32. package/tokens/hooks-index.json +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exxatdesignux/ui",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Exxat shared design system (components, hooks, tokens). Monorepo setup: clone repo then pnpm bootstrap at workspace root — see github.com/ExxatDesign/Exxat-DS-Workspace README.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Exxat Design",
@@ -797,7 +797,9 @@ function DataTableInner<TData extends Record<string, unknown>>({
797
797
  } = state
798
798
 
799
799
  // Mount overflow check + scrollport width for sticky group headers on horizontal scroll.
800
- React.useEffect(() => {
800
+ // Re-run when column widths / visibility change — scrollWidth alone is unreliable when
801
+ // the table used to stretch with `w-full` (scrollWidth === clientWidth even with many cols).
802
+ React.useLayoutEffect(() => {
801
803
  const syncScrollport = () => {
802
804
  const el = scrollRef.current
803
805
  if (el) {
@@ -810,9 +812,10 @@ function DataTableInner<TData extends Record<string, unknown>>({
810
812
  if (!el) return
811
813
  const ro = new ResizeObserver(syncScrollport)
812
814
  ro.observe(el)
815
+ const table = el.querySelector("table")
816
+ if (table) ro.observe(table)
813
817
  return () => ro.disconnect()
814
- // eslint-disable-next-line react-hooks/exhaustive-deps
815
- }, [])
818
+ }, [totalWidth, displayCols.length, checkOverflow, scrollRef])
816
819
 
817
820
  /** Pending action queued from a column-menu item that should run *after* the menu
818
821
  * has fully closed. The Properties drawer is a non-modal Radix Sheet (`modal=false`)
@@ -1109,11 +1112,12 @@ function DataTableInner<TData extends Record<string, unknown>>({
1109
1112
  )}
1110
1113
  >
1111
1114
  <table
1112
- className="w-full text-sm border-separate border-spacing-0"
1115
+ className="text-sm border-separate border-spacing-0"
1113
1116
  style={{
1114
1117
  tableLayout: "fixed",
1115
- minWidth: totalWidth,
1116
- width: headerIsStuck ? floatingHeaderTableWidth : undefined,
1118
+ // Explicit column-sum width — `w-full` made the grid stretch to the scrollport
1119
+ // so scrollWidth === clientWidth and the overflow-gated pin rule never fired.
1120
+ width: totalWidth,
1117
1121
  }}
1118
1122
  >
1119
1123
  <colgroup>
@@ -536,23 +536,6 @@ export function useTableState<TData extends Record<string, unknown>>(
536
536
  .map(([key, groupRows]) => ({ groupKey: key, groupLabel: key, rows: groupRows }))
537
537
  }, [rows, groupBy])
538
538
 
539
- // ── Effective pins (respect overflow) ─────────────────────────────────────
540
- const LOCKED_KEYS = React.useMemo(() => new Set(Object.keys(lockedPins)), [lockedPins])
541
-
542
- // When the table fits within its container (not overflowing) there is no need
543
- // to sticky-pin any column — even locked ones. Pins only activate once the
544
- // user has to scroll horizontally so the selection / action edges stay visible.
545
- // In reflow viewports (high zoom), disable all column stickies — shadow + sticky
546
- // fight the short viewport and overlap content.
547
- const effectivePins = React.useMemo(() => {
548
- if (isReflowViewport || !isOverflowing) return {}
549
- const result: Record<string, "left" | "right"> = {}
550
- for (const [key, pin] of Object.entries(colPins)) {
551
- result[key] = pin
552
- }
553
- return result
554
- }, [colPins, isOverflowing, isReflowViewport])
555
-
556
539
  // ── Display columns ───────────────────────────────────────────────────────
557
540
  const displayCols = React.useMemo(() => {
558
541
  const leftPinned: string[] = []
@@ -574,6 +557,28 @@ export function useTableState<TData extends Record<string, unknown>>(
574
557
  return out
575
558
  }, [colOrder, colPins, hiddenCols, columnsByKey])
576
559
 
560
+ const totalWidth = React.useMemo(
561
+ () => displayCols.reduce((s, c) => s + (colWidths[c.key] ?? c.width ?? 100), 0),
562
+ [displayCols, colWidths],
563
+ )
564
+
565
+ // ── Effective pins (respect overflow) ─────────────────────────────────────
566
+ const LOCKED_KEYS = React.useMemo(() => new Set(Object.keys(lockedPins)), [lockedPins])
567
+
568
+ // When the table fits within its container (not overflowing) there is no need
569
+ // to sticky-pin any column — even locked ones. Pins only activate once the
570
+ // user has to scroll horizontally so the selection / action edges stay visible.
571
+ // In reflow viewports (high zoom), disable all column stickies — shadow + sticky
572
+ // fight the short viewport and overlap content.
573
+ const effectivePins = React.useMemo(() => {
574
+ if (isReflowViewport || !isOverflowing) return {}
575
+ const result: Record<string, "left" | "right"> = {}
576
+ for (const [key, pin] of Object.entries(colPins)) {
577
+ result[key] = pin
578
+ }
579
+ return result
580
+ }, [colPins, isOverflowing, isReflowViewport])
581
+
577
582
  // ── Column actions ────────────────────────────────────────────────────────
578
583
  function startResize(key: string, e: React.MouseEvent) {
579
584
  e.preventDefault()
@@ -629,19 +634,26 @@ export function useTableState<TData extends Record<string, unknown>>(
629
634
  }
630
635
 
631
636
  // ── Scroll handlers ───────────────────────────────────────────────────────
632
- function checkOverflow() {
637
+ const checkOverflow = React.useCallback(() => {
633
638
  const el = scrollRef.current
634
639
  if (!el) return
635
- setIsOverflowing(el.scrollWidth > el.clientWidth + 1)
636
- }
640
+ // Compare declared column width to scrollport — not scrollWidth, which matched
641
+ // clientWidth when the table stretched with `w-full`.
642
+ setIsOverflowing(totalWidth > el.clientWidth + 1)
643
+ }, [totalWidth])
644
+
637
645
  function handleScroll() {
638
646
  const el = scrollRef.current
639
647
  if (!el) return
640
648
  setScrolled(el.scrollLeft > 1)
641
649
  setScrollEnd(el.scrollLeft >= el.scrollWidth - el.clientWidth - 1)
642
- setIsOverflowing(el.scrollWidth > el.clientWidth + 1)
650
+ setIsOverflowing(totalWidth > el.clientWidth + 1)
643
651
  }
644
652
 
653
+ React.useLayoutEffect(() => {
654
+ checkOverflow()
655
+ }, [checkOverflow])
656
+
645
657
  // ── Selection helpers ─────────────────────────────────────────────────────
646
658
  function getRowId(row: TData, index: number, getIdFn?: (r: TData, i: number) => string | number): string | number {
647
659
  return getIdFn ? getIdFn(row, index) : (row.id as string | number ?? index)
@@ -711,11 +723,6 @@ export function useTableState<TData extends Record<string, unknown>>(
711
723
  [effectivePins, isReflowViewport, stickyOffsets],
712
724
  )
713
725
 
714
- const totalWidth = React.useMemo(
715
- () => displayCols.reduce((s, c) => s + (colWidths[c.key] ?? c.width ?? 100), 0),
716
- [displayCols, colWidths],
717
- )
718
-
719
726
  return {
720
727
  // Sort
721
728
  sortRules, setSortRules,
@@ -0,0 +1,59 @@
1
+ # Exxat DS — Jobs library
2
+
3
+ > **What:** Canonical reference per **job-to-be-done**, not per component.
4
+ > **Why:** Components answer "how to render". Jobs answer "what to build".
5
+ > **Who:** Humans + AI agents writing design briefs
6
+ > ([`exxat-ux-discovery-protocol.mdc`](../../../.cursor/rules/exxat-ux-discovery-protocol.mdc)).
7
+
8
+ A **job** is what the user is trying to accomplish — not the screen they end
9
+ up on. Two products with the same component stack can ship different jobs;
10
+ two products with different stacks can ship the same job. **The job is the
11
+ contract.**
12
+
13
+ ## When to use this library
14
+
15
+ - The user prompt names a screen ("student detail", "settings page", "compose
16
+ form"). Map it to a **job** first, then pick the reference.
17
+ - Before building, the design brief MUST cite the relevant job doc as a
18
+ reference.
19
+ - If no job doc matches, **write one** (even short) as part of the work.
20
+
21
+ ## Job → screen mapping
22
+
23
+ | Job | Doc | Canonical references |
24
+ |-----|-----|----------------------|
25
+ | **Review a record's full state** | [`record-detail.md`](./record-detail.md) | Students / Placements / Library detail |
26
+ | **Triage a list of records** (find at-risk / actionable) | *future* | `PlacementsClient`, `ComplianceClient` |
27
+ | **Compose / create a new record** | *future* | `new-library-item-form.tsx` |
28
+ | **Configure preferences or workspace settings** | *future* | `app/(app)/settings/page.tsx` |
29
+ | **Scan many metrics for anomalies** | *future* | Dashboard route, `DashboardTabs` |
30
+ | **Search / find anything** | *future* | `CommandMenu`, `DedicatedSearch*` |
31
+ | **Onboarding / first-run setup** | *future* | `getting-started.tsx`, `CoachMark` |
32
+ | **Approve / review submitted work** | *future* | (TBD) |
33
+
34
+ ## How a job doc is structured
35
+
36
+ Every job doc must include:
37
+
38
+ 1. **The job** — one paragraph; user pain, decision, action.
39
+ 2. **When this job applies** — triggers, scale, frequency.
40
+ 3. **Pattern: route vs sheet vs inline** — with a decision table.
41
+ 4. **Information architecture** — ASCII diagram of the IA.
42
+ 5. **Layers, in priority order** — identity → status → groups → activity →
43
+ related.
44
+ 6. **When to use tabs / subsections.**
45
+ 7. **Navigation — the way back** (P1 enforcement).
46
+ 8. **Actions** — primary, overflow, inline.
47
+ 9. **States** — loading, empty, error, stale.
48
+ 10. **Accessibility** — A11y concerns specific to the job.
49
+ 11. **Modern SaaS analogues** — cite by product + Mx codes.
50
+ 12. **Anti-patterns** — what NOT to do.
51
+ 13. **Quick checklist** for the post-build audit.
52
+
53
+ ## See also
54
+
55
+ - [`../modern-saas-patterns.md`](../modern-saas-patterns.md) — the canon
56
+ - [`../component-selection-guide.md`](../component-selection-guide.md) —
57
+ decision tree from job → composition
58
+ - [`../blueprints/`](../blueprints/) — framework-agnostic specs
59
+ - [`../../../.cursor/skills/exxat-senior-ux/SKILL.md`](../../../.cursor/skills/exxat-senior-ux/SKILL.md)
@@ -0,0 +1,177 @@
1
+ # Job: Review a record's full state
2
+
3
+ > **Surface type:** Standalone route OR side-panel sheet.
4
+ > **Examples in product:** Student profile, Placement record, Library item,
5
+ > Site profile, Team member profile.
6
+
7
+ ## 1. The job
8
+
9
+ The user lands on a single record and needs to **understand its current
10
+ state** in order to **decide what to do next**. Decisions can include:
11
+ contact someone, edit a field, change status, place / unplace, archive,
12
+ follow up later, or simply confirm correctness before another action.
13
+
14
+ This job is **not**: edit (separate flow), bulk action (lives on the list),
15
+ configure workspace settings.
16
+
17
+ ## 2. When this job applies
18
+
19
+ - The record has > ~5 displayable fields.
20
+ - The user reaches it from a list, search, deep link, or notification.
21
+ - The user's next action depends on the record's state (status, ownership,
22
+ data freshness).
23
+ - The record may have children (placements, attachments, comments) that
24
+ matter to the decision.
25
+
26
+ ## 3. Pattern: route vs sheet vs inline
27
+
28
+ | Pattern | When | DS comp |
29
+ |---------|------|---------|
30
+ | **Standalone route** (`/students/[id]`) | Deep link from email / notifications / reports; user may keep tab open; record has > 10 fields | `PrimaryPageTemplate` + `PageHeader` |
31
+ | **Sheet over hub** | User is triaging from a list and may open many records in sequence | `Sheet` over `ListPageTemplate` |
32
+ | **Inline expansion** (`HoverCard`) | Read-only quick preview; < 5 fields | `HoverCard` on row hover |
33
+
34
+ Pick **route** by default. Pick **sheet** if the user's primary path is
35
+ triaging from a list and they typically open many in a single session.
36
+
37
+ ## 4. Information architecture
38
+
39
+ ```
40
+ ┌─ SiteHeader (breadcrumb only) ──────────────────────────────────┐
41
+ │ Dashboard › Students › Jordan Lee │ ← ancestors + title (P1, P2)
42
+ └──────────────────────────────────────────────────────────────────┘
43
+ ┌─ Identity row ──────────────────────────────────────────────────┐
44
+ │ [Avatar] Jordan Lee [Edit] [⋯] │ ← name, ID, primary action + overflow
45
+ │ STU-2026-1042 · jordan.lee@example.edu │
46
+ └──────────────────────────────────────────────────────────────────┘
47
+ ┌─ Status row (above the fold, P13) ──────────────────────────────┐
48
+ │ ● Active ✓ Compliant ⊕ Placed at Sinai │
49
+ └──────────────────────────────────────────────────────────────────┘
50
+ ┌─ Field groups (2-col card grid) ────────────────────────────────┐
51
+ │ Program │ Academic │
52
+ │ Placement │ Compliance │
53
+ └──────────────────────────────────────────────────────────────────┘
54
+ ┌─ Activity timeline (M7, optional) ──────────────────────────────┐
55
+ │ Today · Compliance status updated by Maria │
56
+ │ Tue · Placement assigned · Sinai Hospital │
57
+ └──────────────────────────────────────────────────────────────────┘
58
+ ```
59
+
60
+ ### Layers, in priority order
61
+
62
+ 1. **Identity** — name, system ID (mono), avatar, primary contact link.
63
+ 2. **Status** — every status badge that drives a decision; visible without
64
+ scrolling (P13).
65
+ 3. **Field groups** — 2-column card grid by default. Group by **conceptual
66
+ coherence** (Program / Academic / Placement / Compliance), not by table
67
+ structure.
68
+ 4. **Activity timeline** — if the record changes over time and the history
69
+ matters (M7).
70
+ 5. **Related lists** — placements, comments, attachments — if they exist for
71
+ this entity.
72
+
73
+ ### When to use section tabs
74
+
75
+ - **Don't tab** if total field count is ≤ ~20 and groups are ≤ 4. Single
76
+ scroll is faster.
77
+ - **Do tab** if the record has rich children (placements history,
78
+ supervisors, contracts, conversation threads) and the user typically
79
+ deep-dives into one section.
80
+ - Tabs use `Tabs` + `TabsList` `w-fit` `variant="line"` — never full-width
81
+ (`exxat-tabs-chrome.mdc`).
82
+
83
+ ## 5. Navigation — the way back
84
+
85
+ **Exactly one path back** (P1).
86
+
87
+ | Reached from | Way back |
88
+ |--------------|----------|
89
+ | List route | `SiteHeader` breadcrumb (`Students`) |
90
+ | Deep link / email | `SiteHeader` breadcrumb to the canonical parent |
91
+ | Sheet from hub | `Sheet` close (X / Esc) |
92
+ | Notification | Breadcrumb to canonical parent (not "Back to notification") |
93
+
94
+ **Never** add a body-level "Back to <parent>" button when the breadcrumb is
95
+ present (`exxat-breadcrumbs-no-back.mdc`).
96
+
97
+ ## 6. Actions
98
+
99
+ | Slot | Component | What goes here |
100
+ |------|-----------|----------------|
101
+ | Primary | `PageHeader.primaryAction` (`Button variant="default" size="lg"`) | The single most common next action — "Edit", "Place", "Approve" |
102
+ | Overflow | `Button variant="outline" size="icon-lg"` → `DropdownMenu` | Export, Archive, Duplicate, Share, dangerous-but-rare actions |
103
+ | Inline | Icon-only buttons with tooltip | Copy email, dial phone, open site |
104
+ | Status flip | `Sheet` or inline `Select` | Status changes that are audited (multi-step) |
105
+
106
+ Exactly **one** filled CTA per surface (P3).
107
+
108
+ ## 7. States
109
+
110
+ | State | What to show |
111
+ |-------|--------------|
112
+ | **Loading** | `Skeleton` matching the IA shape (identity → status → cards) — M9 |
113
+ | **Empty / not found** | `EmptyState` + "Return to <list>" outline button |
114
+ | **Error** | `LocalBanner` inside the body; identity row still renders if available |
115
+ | **Stale data** | Subtle "Updated <relative>" in identity meta |
116
+
117
+ ## 8. Accessibility
118
+
119
+ - One `<h1>` (P2) — typically `PageHeader.title`.
120
+ - Status row is a `role="list"`; each badge is `role="listitem"` (already in
121
+ `ListHubStatusBadge`).
122
+ - Inline mailto / tel links are real `<a>`s, not buttons.
123
+ - Tab order: breadcrumb → identity → primary action → overflow → status →
124
+ fields → activity → related.
125
+ - Icon-only actions carry `aria-label` + a tooltip with the same text.
126
+
127
+ ## 9. Modern SaaS analogues
128
+
129
+ | Product | What to study |
130
+ |---------|---------------|
131
+ | **Linear** issue detail | Identity + status + properties + activity; sheet variant for triage |
132
+ | **Stripe Dashboard** customer / charge | Identity + status badges + grouped data + audit log |
133
+ | **Notion** page-as-database row | Inline editing (M5), property panel |
134
+ | **Plain / Pylon** ticket | Conversation-as-document for support records |
135
+
136
+ Cite these in the design brief by name + Mx codes:
137
+ `Linear issue detail (M1, M4, M7)`.
138
+
139
+ ## 10. Anti-patterns
140
+
141
+ | Anti-pattern | Use instead |
142
+ |--------------|-------------|
143
+ | `<h1>` + `PageHeader title` + breadcrumb leaf all repeating the name | One carrier: `PageHeader title` + ancestors-only breadcrumb (P2) |
144
+ | "Back to <parent>" button alongside the breadcrumb | Breadcrumb only (P1) |
145
+ | Two filled CTAs in the header ("Save" + "Submit") | One filled, others outline (P3) |
146
+ | Status only in detail (hidden on list / breadcrumb count) | Status visible everywhere the record appears (M4) |
147
+ | Status communicated by color only | Color + icon + label (`ListHubStatusBadge`) |
148
+ | Full-width section tab bar | `Tabs` `w-fit` (`exxat-tabs-chrome.mdc`) |
149
+ | Centered modal for "Edit name" | Inline edit (M5) or `Sheet` (M3) |
150
+ | Spinner overlay for initial load | `Skeleton` matched to IA (M9) |
151
+ | Activity timeline buried in a tab nobody opens | Show inline below cards, or remove it |
152
+ | New shared "ProfileHero" component invented per entity | Compose: `PageHeader` + identity-row primitives (P8) |
153
+ | `toast()` on save | Inline button label change + `LocalBanner` if persistent |
154
+
155
+ ## 11. Quick checklist (post-build audit)
156
+
157
+ - [ ] Breadcrumb shows ancestors + title; no duplicate name.
158
+ - [ ] No body "Back to <parent>" button.
159
+ - [ ] One H1.
160
+ - [ ] Status row visible without scrolling.
161
+ - [ ] One filled primary action; overflow has the rest.
162
+ - [ ] 2-col card grid for fields (or tabs if ≥ 4 sections / 20+ fields).
163
+ - [ ] `Skeleton` matches the IA on load; empty state designed for "not found".
164
+ - [ ] Tab order: breadcrumb → identity → primary → overflow → fields.
165
+ - [ ] All status chips use `ListHubStatusBadge` + `lib/list-status-badges.ts`.
166
+ - [ ] No `toast()`, no Vaul, no pixel-copy of legacy.
167
+
168
+ ## 12. Reference
169
+
170
+ - [`../../../.cursor/skills/exxat-senior-ux/SKILL.md`](../../../.cursor/skills/exxat-senior-ux/SKILL.md)
171
+ - [`../../../.cursor/rules/exxat-ux-discovery-protocol.mdc`](../../../.cursor/rules/exxat-ux-discovery-protocol.mdc)
172
+ - [`../../../.cursor/rules/exxat-ux-principles.mdc`](../../../.cursor/rules/exxat-ux-principles.mdc)
173
+ - [`../../../.cursor/rules/exxat-breadcrumbs-no-back.mdc`](../../../.cursor/rules/exxat-breadcrumbs-no-back.mdc)
174
+ - [`../../../.cursor/rules/exxat-tabs-chrome.mdc`](../../../.cursor/rules/exxat-tabs-chrome.mdc)
175
+ - [`../modern-saas-patterns.md`](../modern-saas-patterns.md)
176
+ - [`../blueprints/page-header.md`](../blueprints/page-header.md)
177
+ - [`../component-selection-guide.md`](../component-selection-guide.md) §1
@@ -0,0 +1,165 @@
1
+ # Modern SaaS patterns — the canon Exxat DS works against
2
+
3
+ > **Audience:** humans + AI agents.
4
+ > **Companion to:** [`exxat-senior-ux/SKILL.md`](../../../.cursor/skills/exxat-senior-ux/SKILL.md),
5
+ > [`exxat-ux-principles.mdc`](../../../.cursor/rules/exxat-ux-principles.mdc),
6
+ > [`jobs/`](./jobs/).
7
+
8
+ A senior designer recognizes the work. This doc names the patterns the best
9
+ products converged on in 2024–2026, so the agent can pattern-match instead
10
+ of inventing. **Cite by name + code** when you reference one in a design brief
11
+ (e.g. `Linear issue detail (M1, M4, M7)`).
12
+
13
+ ## Canon products — cite by product, not by URL
14
+
15
+ | Product | What to learn from it |
16
+ |---------|-----------------------|
17
+ | **Linear** | Issue detail, command palette spine, density, keyboard-first, presence, "less chrome" |
18
+ | **Notion** | Inline editing, property panels, page-as-database, blocks, slash-commands |
19
+ | **Stripe Dashboard** | Record home, status chips, activity log, money-grade precision |
20
+ | **Figma** | Multiplayer, presence, side panel inspector, infinite-canvas chrome discipline |
21
+ | **Vercel** | Type-first hierarchy, dark-first, minimal chrome, log streams |
22
+ | **Height** | View tabs (table/board/timeline), custom fields, view parity |
23
+ | **Plain / Pylon** | Support detail = identity + timeline + actions; conversation-as-document |
24
+ | **Cron / Amie** | Keyboard-only flows, dense calendar, command surface |
25
+ | **Raycast** | Command palette as full app, extension surface, deep keyboard |
26
+ | **Arc Browser** | Control-stripped, content-first, sidebar as the only chrome |
27
+
28
+ ## The 12 patterns that make products feel modern
29
+
30
+ ### M1. Content-first chrome
31
+ The data is the star. Sidebars collapse to icons. Headers stay 48–56px. Page
32
+ surfaces are generous. No purple gradient on the top bar.
33
+
34
+ - **In DS:** `SiteHeader` ~h-12; `PrimaryPageTemplate` body has generous
35
+ `max-w-[1440px]`; sidebar collapsible (`SidebarTrigger`).
36
+
37
+ ### M2. Command palette as the spine
38
+ `⌘K` opens search + navigation + AI + recent. It is the primary navigation
39
+ for power users. The sidebar exists for orientation, not speed.
40
+
41
+ - **In DS:** `CommandMenu` (⌘K) + Ask Leo split (⌘⌥K).
42
+ Rule: `exxat-command-menu.mdc`.
43
+
44
+ ### M3. Side-panel detail over modal dialog
45
+ For non-destructive context-keeping actions (properties, export, invite,
46
+ preview, single-step compose), use a slide-in sheet — never a centered modal.
47
+
48
+ - **In DS:** `Sheet` (no Vaul).
49
+ Rule: `exxat-drawer-vs-dialog.mdc`. Pattern:
50
+ `apps/web/docs/drawer-vs-dialog-pattern.md`.
51
+
52
+ ### M4. Status as a first-class citizen
53
+ Colored dots, chips, counts visible everywhere data appears. Status isn't
54
+ hidden in a detail page; it appears on rows, cards, headers, navigation.
55
+
56
+ - **In DS:** `ListHubStatusBadge` + `lib/list-status-badges.ts`. Available per
57
+ row, board card, detail header, breadcrumb count slot.
58
+
59
+ ### M5. Inline editing where data is read
60
+ Click the value, edit it. No bounce to forms for single-field changes.
61
+
62
+ - **In DS:** Inline-edit primitive emerging; until then, use a `Sheet` for
63
+ multi-field, popover or contenteditable for single-field. Principle: P15.
64
+
65
+ ### M6. Optimistic UI + undo
66
+ Low-risk actions (favorite, archive, status flip) feel instant; reconcile on
67
+ error. Undo via banner or in-place affordance — **never** toast
68
+ (`exxat-no-toast.mdc`).
69
+
70
+ - **In DS:** Optimistic state in `useTableState` selection ops; undo via
71
+ `LocalBanner` with action.
72
+
73
+ ### M7. Activity timeline on every record
74
+ Who did what when. The audit log is also a navigation device.
75
+
76
+ - **In DS:** `ActivityTimeline` primitive emerging. Compose from
77
+ `lib/mock/activity` shape until then.
78
+
79
+ ### M8. Empty states with one CTA + one sentence
80
+ Illustration optional. One sentence of context. One primary action. Never a
81
+ wall of help text or a tutorial inline.
82
+
83
+ - **In DS:** `EmptyState` primitive; voice from `docs/voice-and-tone.md`.
84
+
85
+ ### M9. Skeleton, not spinner
86
+ Loading shapes content. Spinners only for indeterminate < 200ms.
87
+
88
+ - **In DS:** `Skeleton` primitive; suspense boundaries shape the body.
89
+
90
+ ### M10. Type-first hierarchy
91
+ Weight, size, color do the heavy lifting before borders, boxes, or tints.
92
+ Ornament is rare and intentional.
93
+
94
+ - **In DS:** Token scale; body in `Inter`; display in `Ivy Presto` (only on
95
+ `PageHeader` H1 by default).
96
+
97
+ ### M11. Real-time presence + collaboration as default
98
+ If the product is shared, show who else is here — face rails, cursors,
99
+ status. Inviting others is one click from the page.
100
+
101
+ - **In DS:** `PageHeader variant="collaboration"` face rail +
102
+ `InviteCollaboratorsDrawer`.
103
+ Rule: `exxat-collaboration-access.mdc`.
104
+
105
+ ### M12. AI as opt-in sidecar
106
+ Ask Leo style — never the primary path, never auto-runs on a record, always
107
+ discoverable, always cancellable. Deterministic path still exists.
108
+
109
+ - **In DS:** `AskLeoSidebar` + `⌘⌥K` toggle; never a "magic" button on a
110
+ destructive surface.
111
+
112
+ ---
113
+
114
+ ## The anti-modern signals (what NOT to do)
115
+
116
+ | Signal | Why it's dated |
117
+ |--------|----------------|
118
+ | Toasts everywhere | Modern products use inline + banner + undo. Toasts get missed. |
119
+ | Full-width tab bars stretched edge-to-edge | 2010s pattern. Modern uses `w-fit` segmented controls. |
120
+ | Centered modals for everything | Sheets and routes carry more without breaking flow. |
121
+ | Color-coded sidebar sections | Modern is monochrome chrome + status color on data. |
122
+ | Modal wizards with progress bar | If it has > 3 steps, give it a route. |
123
+ | Spinners on initial load | Skeleton the content shape instead. |
124
+ | Hover-only affordances | Touch + keyboard need parity. |
125
+ | Icons without text labels in primary nav | Recognition fails for new users. |
126
+ | "Beautiful" gradients on chrome | Data should be beautiful; chrome should disappear. |
127
+ | Aggressive empty-state illustrations | One sentence + one action beats illustration + 5 tips. |
128
+ | Edit-takes-you-to-a-form for single fields | Inline edit (M5). |
129
+ | "Back to <parent>" button alongside breadcrumb | P1 — choose one. |
130
+ | Auto-running AI on record open | M12 — opt-in only. |
131
+
132
+ ---
133
+
134
+ ## Density layers (Linear / Vercel model)
135
+
136
+ | Layer | When |
137
+ |-------|------|
138
+ | **Cozy** (default) | Most surfaces; balances scan + breathing room |
139
+ | **Compact** | Power users on daily workflow; tables, command results |
140
+ | **Comfortable** | Accessibility (low-vision, motor); marketing-adjacent |
141
+
142
+ Provide a user-level setting where audiences differ; default to **cozy**.
143
+ Don't fork stacks per density.
144
+
145
+ ---
146
+
147
+ ## How to use this doc in a design brief
148
+
149
+ ```
150
+ Reference (modern): Linear issue detail (M1, M4, M7),
151
+ Stripe customer record (M4, M11)
152
+ ```
153
+
154
+ Cite the **patterns (Mx)** the agent applied, not just the product. Forces
155
+ clear thinking and lets reviewers verify intent.
156
+
157
+ ## See also
158
+
159
+ - [`../../../.cursor/skills/exxat-senior-ux/SKILL.md`](../../../.cursor/skills/exxat-senior-ux/SKILL.md) — persona
160
+ - [`../../../.cursor/rules/exxat-ux-discovery-protocol.mdc`](../../../.cursor/rules/exxat-ux-discovery-protocol.mdc) — brief gate
161
+ - [`../../../.cursor/rules/exxat-ux-principles.mdc`](../../../.cursor/rules/exxat-ux-principles.mdc) — principles + breaks
162
+ - [`./jobs/`](./jobs/) — canonical reference per job type
163
+ - [`./component-selection-guide.md`](./component-selection-guide.md) — picking the composition
164
+ - [`./blueprints/`](./blueprints/) — framework-agnostic surface specs
165
+ - [`./voice-and-tone.md`](./voice-and-tone.md) — copy rules
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.5.5",
2
+ "version": "0.5.7",
3
3
  "source": "packages/ui/src/globals.css",
4
- "generatedAt": "2026-05-22T12:39:10.883Z",
4
+ "generatedAt": "2026-05-22T16:08:56.863Z",
5
5
  "tokenCount": 197,
6
6
  "themeKeys": [
7
7
  "tailwind-bridge",