@exxatdesignux/ui 0.5.4 → 0.5.6
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.
- package/CHANGELOG.md +18 -0
- package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +16 -3
- package/consumer-extras/cursor-rules/exxat-no-image-pixel-copy.mdc +35 -0
- package/consumer-extras/cursor-rules/exxat-reuse-before-custom.mdc +2 -0
- package/consumer-extras/cursor-rules/exxat-sidebar-shell.mdc +35 -0
- package/consumer-extras/cursor-rules/exxat-ux-discovery-protocol.mdc +122 -0
- package/consumer-extras/cursor-rules/exxat-ux-principles.mdc +186 -0
- package/consumer-extras/cursor-skills/exxat-senior-ux/SKILL.md +145 -0
- package/consumer-extras/cursor-skills/exxat-token-economy/SKILL.md +2 -1
- package/consumer-extras/handbook/reference-implementations.md +2 -0
- package/consumer-extras/patterns/consumer-upgrade-checklist.md +1 -0
- package/consumer-extras/patterns/jobs/README.md +59 -0
- package/consumer-extras/patterns/jobs/record-detail.md +177 -0
- package/consumer-extras/patterns/modern-saas-patterns.md +165 -0
- package/dist/components/data-table/index.js +28 -22
- package/dist/components/data-table/index.js.map +1 -1
- package/dist/components/data-table/pagination.js +28 -22
- package/dist/components/data-table/pagination.js.map +1 -1
- package/dist/components/data-table/use-table-state.js +20 -17
- package/dist/components/data-table/use-table-state.js.map +1 -1
- package/dist/components/data-views/hub-table.js +28 -22
- package/dist/components/data-views/hub-table.js.map +1 -1
- package/dist/components/data-views/index.js +28 -22
- package/dist/components/data-views/index.js.map +1 -1
- package/dist/index.js +28 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/data-table/index.tsx +10 -6
- package/src/components/data-table/use-table-state.ts +33 -26
- package/template/docs/consumer-upgrade-checklist.md +1 -0
- package/template/docs/jobs/README.md +59 -0
- package/template/docs/jobs/record-detail.md +177 -0
- package/template/docs/modern-saas-patterns.md +165 -0
- package/template/docs/reference-implementations.md +2 -0
- package/template/lib/mock/navigation.tsx +1 -1
- package/tokens/hooks-index.json +2 -2
|
@@ -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
|
|
@@ -1443,15 +1443,6 @@ function useTableState(data, columns, defaultSort, paginationOverride, syncedSea
|
|
|
1443
1443
|
});
|
|
1444
1444
|
return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, groupRows]) => ({ groupKey: key, groupLabel: key, rows: groupRows }));
|
|
1445
1445
|
}, [rows, groupBy]);
|
|
1446
|
-
const LOCKED_KEYS = React9.useMemo(() => new Set(Object.keys(lockedPins)), [lockedPins]);
|
|
1447
|
-
const effectivePins = React9.useMemo(() => {
|
|
1448
|
-
if (isReflowViewport || !isOverflowing) return {};
|
|
1449
|
-
const result = {};
|
|
1450
|
-
for (const [key, pin] of Object.entries(colPins)) {
|
|
1451
|
-
result[key] = pin;
|
|
1452
|
-
}
|
|
1453
|
-
return result;
|
|
1454
|
-
}, [colPins, isOverflowing, isReflowViewport]);
|
|
1455
1446
|
const displayCols = React9.useMemo(() => {
|
|
1456
1447
|
const leftPinned = [];
|
|
1457
1448
|
const free = [];
|
|
@@ -1471,6 +1462,19 @@ function useTableState(data, columns, defaultSort, paginationOverride, syncedSea
|
|
|
1471
1462
|
}
|
|
1472
1463
|
return out;
|
|
1473
1464
|
}, [colOrder, colPins, hiddenCols, columnsByKey]);
|
|
1465
|
+
const totalWidth = React9.useMemo(
|
|
1466
|
+
() => displayCols.reduce((s, c) => s + (colWidths[c.key] ?? c.width ?? 100), 0),
|
|
1467
|
+
[displayCols, colWidths]
|
|
1468
|
+
);
|
|
1469
|
+
const LOCKED_KEYS = React9.useMemo(() => new Set(Object.keys(lockedPins)), [lockedPins]);
|
|
1470
|
+
const effectivePins = React9.useMemo(() => {
|
|
1471
|
+
if (isReflowViewport || !isOverflowing) return {};
|
|
1472
|
+
const result = {};
|
|
1473
|
+
for (const [key, pin] of Object.entries(colPins)) {
|
|
1474
|
+
result[key] = pin;
|
|
1475
|
+
}
|
|
1476
|
+
return result;
|
|
1477
|
+
}, [colPins, isOverflowing, isReflowViewport]);
|
|
1474
1478
|
function startResize(key, e) {
|
|
1475
1479
|
e.preventDefault();
|
|
1476
1480
|
e.stopPropagation();
|
|
@@ -1531,18 +1535,21 @@ function useTableState(data, columns, defaultSort, paginationOverride, syncedSea
|
|
|
1531
1535
|
function toggleWrap(key) {
|
|
1532
1536
|
setColWrap((p) => ({ ...p, [key]: !p[key] }));
|
|
1533
1537
|
}
|
|
1534
|
-
|
|
1538
|
+
const checkOverflow = React9.useCallback(() => {
|
|
1535
1539
|
const el = scrollRef.current;
|
|
1536
1540
|
if (!el) return;
|
|
1537
|
-
setIsOverflowing(
|
|
1538
|
-
}
|
|
1541
|
+
setIsOverflowing(totalWidth > el.clientWidth + 1);
|
|
1542
|
+
}, [totalWidth]);
|
|
1539
1543
|
function handleScroll() {
|
|
1540
1544
|
const el = scrollRef.current;
|
|
1541
1545
|
if (!el) return;
|
|
1542
1546
|
setScrolled(el.scrollLeft > 1);
|
|
1543
1547
|
setScrollEnd(el.scrollLeft >= el.scrollWidth - el.clientWidth - 1);
|
|
1544
|
-
setIsOverflowing(
|
|
1548
|
+
setIsOverflowing(totalWidth > el.clientWidth + 1);
|
|
1545
1549
|
}
|
|
1550
|
+
React9.useLayoutEffect(() => {
|
|
1551
|
+
checkOverflow();
|
|
1552
|
+
}, [checkOverflow]);
|
|
1546
1553
|
function getRowId(row, index, getIdFn) {
|
|
1547
1554
|
return getIdFn ? getIdFn(row, index) : row.id ?? index;
|
|
1548
1555
|
}
|
|
@@ -1595,10 +1602,6 @@ function useTableState(data, columns, defaultSort, paginationOverride, syncedSea
|
|
|
1595
1602
|
},
|
|
1596
1603
|
[effectivePins, isReflowViewport, stickyOffsets]
|
|
1597
1604
|
);
|
|
1598
|
-
const totalWidth = React9.useMemo(
|
|
1599
|
-
() => displayCols.reduce((s, c) => s + (colWidths[c.key] ?? c.width ?? 100), 0),
|
|
1600
|
-
[displayCols, colWidths]
|
|
1601
|
-
);
|
|
1602
1605
|
return {
|
|
1603
1606
|
// Sort
|
|
1604
1607
|
sortRules,
|
|
@@ -2338,7 +2341,7 @@ function DataTableInner({
|
|
|
2338
2341
|
setSheetOpen,
|
|
2339
2342
|
setSheetInitialPanel
|
|
2340
2343
|
} = state;
|
|
2341
|
-
React9.
|
|
2344
|
+
React9.useLayoutEffect(() => {
|
|
2342
2345
|
const syncScrollport = () => {
|
|
2343
2346
|
const el2 = scrollRef.current;
|
|
2344
2347
|
if (el2) {
|
|
@@ -2351,8 +2354,10 @@ function DataTableInner({
|
|
|
2351
2354
|
if (!el) return;
|
|
2352
2355
|
const ro = new ResizeObserver(syncScrollport);
|
|
2353
2356
|
ro.observe(el);
|
|
2357
|
+
const table = el.querySelector("table");
|
|
2358
|
+
if (table) ro.observe(table);
|
|
2354
2359
|
return () => ro.disconnect();
|
|
2355
|
-
}, []);
|
|
2360
|
+
}, [totalWidth, displayCols.length, checkOverflow]);
|
|
2356
2361
|
const columnMenuPendingActionRef = React9.useRef(null);
|
|
2357
2362
|
const pinnedScrollHintDoneRef = React9.useRef(false);
|
|
2358
2363
|
React9.useEffect(() => {
|
|
@@ -2589,11 +2594,12 @@ function DataTableInner({
|
|
|
2589
2594
|
children: /* @__PURE__ */ jsxs(
|
|
2590
2595
|
"table",
|
|
2591
2596
|
{
|
|
2592
|
-
className: "
|
|
2597
|
+
className: "text-sm border-separate border-spacing-0",
|
|
2593
2598
|
style: {
|
|
2594
2599
|
tableLayout: "fixed",
|
|
2595
|
-
|
|
2596
|
-
|
|
2600
|
+
// Explicit column-sum width — `w-full` made the grid stretch to the scrollport
|
|
2601
|
+
// so scrollWidth === clientWidth and the overflow-gated pin rule never fired.
|
|
2602
|
+
width: totalWidth
|
|
2597
2603
|
},
|
|
2598
2604
|
children: [
|
|
2599
2605
|
/* @__PURE__ */ jsx("colgroup", { children: displayCols.map((col) => /* @__PURE__ */ jsx("col", { style: { width: colWidths[col.key] ?? col.width ?? 100 } }, col.key)) }),
|