@exxatdesignux/ui 0.5.5 → 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 +10 -0
- package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +16 -5
- 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/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/hooks/use-app-theme.d.ts +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/jobs/README.md +59 -0
- package/template/docs/jobs/record-detail.md +177 -0
- package/template/docs/modern-saas-patterns.md +165 -0
- 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.
|
|
3
|
+
"version": "0.5.6",
|
|
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
|
-
|
|
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
|
-
|
|
815
|
-
}, [])
|
|
818
|
+
}, [totalWidth, displayCols.length, checkOverflow])
|
|
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="
|
|
1115
|
+
className="text-sm border-separate border-spacing-0"
|
|
1113
1116
|
style={{
|
|
1114
1117
|
tableLayout: "fixed",
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
-
|
|
637
|
+
const checkOverflow = React.useCallback(() => {
|
|
633
638
|
const el = scrollRef.current
|
|
634
639
|
if (!el) return
|
|
635
|
-
|
|
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(
|
|
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
|
package/tokens/hooks-index.json
CHANGED