@exxatdesignux/ui 0.3.0 → 0.4.1
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 +701 -6
- package/README.md +138 -0
- package/bin/init.mjs +134 -31
- package/consumer-extras/cursor-rules/exxat-board-cards.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-centralized-list-dataset.mdc +2 -2
- package/consumer-extras/cursor-rules/exxat-collaboration-access.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-data-tables.mdc +2 -0
- package/consumer-extras/cursor-rules/exxat-dedicated-search-surfaces.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +3 -3
- package/consumer-extras/cursor-rules/exxat-library-hub-header.mdc +28 -0
- package/consumer-extras/cursor-rules/exxat-mono-ids.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-person-identity-display.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-primary-nav-secondary-panel.mdc +6 -6
- package/consumer-extras/cursor-rules/exxat-reuse-before-custom.mdc +1 -1
- package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +2 -2
- package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -3
- package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +2 -2
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +7 -7
- package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-list-page-view-shells/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +4 -4
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +8 -8
- package/consumer-extras/cursor-skills/exxat-token-economy/SKILL.md +277 -0
- package/consumer-extras/handbook/HANDBOOK.md +2 -0
- package/consumer-extras/handbook/glossary.md +2 -1
- package/consumer-extras/handbook/reference-implementations.md +31 -4
- package/consumer-extras/patterns/collaboration-access-pattern.md +7 -7
- package/consumer-extras/patterns/data-views-pattern.md +18 -16
- package/consumer-extras/patterns/kpi-flat-band-pattern.md +2 -2
- package/dist/components/data-table/index.js +2 -2
- package/dist/components/data-table/index.js.map +1 -1
- package/dist/components/data-table/pagination.js +3 -3
- package/dist/components/data-table/pagination.js.map +1 -1
- package/dist/components/data-table/use-table-state.d.ts +1 -1
- package/dist/components/data-table/use-table-state.js.map +1 -1
- package/dist/components/data-views/data-row-list.js.map +1 -1
- package/dist/components/data-views/finder-panel-view.d.ts +1 -1
- package/dist/components/data-views/finder-panel-view.js.map +1 -1
- package/dist/components/data-views/hub-table.d.ts +9 -3
- package/dist/components/data-views/hub-table.js +262 -40
- package/dist/components/data-views/hub-table.js.map +1 -1
- package/dist/components/data-views/index.js +262 -40
- package/dist/components/data-views/index.js.map +1 -1
- package/dist/components/data-views/list-page-split-hub-tokens.d.ts +2 -2
- package/dist/components/data-views/list-page-split-hub-tokens.js.map +1 -1
- package/dist/components/data-views/list-page-tree-column-header.d.ts +1 -1
- package/dist/components/data-views/list-page-tree-column-header.js.map +1 -1
- package/dist/components/data-views/list-page-tree-panel-shell.js.map +1 -1
- package/dist/components/data-views/os-folder-glyph.d.ts +1 -1
- package/dist/components/data-views/os-folder-glyph.js.map +1 -1
- package/dist/components/ui/avatar.d.ts +1 -1
- package/dist/components/ui/key-metrics.js.map +1 -1
- package/dist/index.js +136 -39
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/components/data-table/index.tsx +2 -2
- package/src/components/data-table/pagination.tsx +5 -1
- package/src/components/data-table/use-table-state.ts +1 -1
- package/src/components/data-views/data-row-list.tsx +1 -1
- package/src/components/data-views/finder-panel-view.tsx +2 -2
- package/src/components/data-views/hub-table.tsx +149 -41
- package/src/components/data-views/list-page-split-hub-tokens.ts +2 -2
- package/src/components/data-views/list-page-tree-column-header.tsx +1 -1
- package/src/components/data-views/os-folder-glyph.tsx +1 -1
- package/src/components/ui/key-metrics.tsx +1 -1
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +8 -7
- package/template/.cursor/rules/exxat-accessibility.mdc +1 -1
- package/template/.cursor/rules/exxat-command-menu.mdc +1 -1
- package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +6 -6
- package/template/.cursor/rules/exxat-data-tables.mdc +3 -3
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +5 -5
- package/template/.cursor/rules/exxat-mono-ids.mdc +1 -1
- package/template/.cursor/rules/exxat-page-vs-drawer.mdc +1 -1
- package/template/.cursor/rules/exxat-table-properties-drawer.mdc +1 -1
- package/template/AGENTS.md +43 -37
- package/template/app/(app)/columns/page.tsx +11 -0
- package/template/app/(app)/library/all/page.tsx +11 -0
- package/template/app/(app)/library/find/page.tsx +12 -0
- package/template/app/(app)/{question-bank → library}/layout.tsx +16 -16
- package/template/app/(app)/library/list/page.tsx +12 -0
- package/template/app/(app)/{question-bank → library}/new/page.tsx +10 -10
- package/template/app/(app)/library/page.tsx +11 -0
- package/template/app/(app)/tokens-themes/page.tsx +11 -0
- package/template/components/ask-leo-composer.tsx +2 -2
- package/template/components/columns-client.tsx +158 -0
- package/template/components/columns-showcase.tsx +541 -0
- package/template/components/data-views/index.ts +32 -6
- package/template/components/data-views/{question-bank-folder-tree-branch.tsx → library-folder-tree-branch.tsx} +19 -19
- package/template/components/data-views/table-cells.tsx +673 -0
- package/template/components/folder-details-shell.tsx +11 -11
- package/template/components/hub-tree-panel-view.tsx +24 -24
- package/template/components/{question-bank-board-view.tsx → library-board-view.tsx} +44 -44
- package/template/components/{question-bank-client.tsx → library-client.tsx} +82 -82
- package/template/components/{question-bank-dashboard-charts.tsx → library-dashboard-charts.tsx} +14 -14
- package/template/components/{question-bank-favorite-button.tsx → library-favorite-button.tsx} +7 -7
- package/template/components/{question-bank-hub-client.tsx → library-hub-client.tsx} +43 -43
- package/template/components/{question-bank-new-folder-sheet.tsx → library-new-folder-sheet.tsx} +14 -14
- package/template/components/{question-bank-os-folder-view.tsx → library-os-folder-view.tsx} +31 -31
- package/template/components/{question-bank-page-header.tsx → library-page-header.tsx} +6 -6
- package/template/components/library-panel-activator.tsx +8 -0
- package/template/components/{question-bank-secondary-nav.tsx → library-secondary-nav.tsx} +60 -60
- package/template/components/{question-bank-table.tsx → library-table.tsx} +97 -97
- package/template/components/list-hub-status-badge.tsx +2 -2
- package/template/components/{new-question-composer.tsx → new-library-item-form.tsx} +37 -37
- package/template/components/sidebar/app-sidebar.tsx +61 -5
- package/template/components/sidebar/secondary-panel.tsx +109 -56
- package/template/components/sidebar/sidebar-auto-collapse.tsx +2 -2
- package/template/components/sidebar/sidebar-auto-open.tsx +2 -1
- package/template/components/table-properties/types.ts +1 -1
- package/template/components/templates/discovery-hub-template.tsx +1 -1
- package/template/components/templates/new-focus-template.tsx +2 -2
- package/template/components/templates/secondary-panel-hub-template.tsx +1 -1
- package/template/components/tokens-secondary-nav.tsx +192 -0
- package/template/components/tokens-themes-client.tsx +476 -0
- package/template/components/tokens-themes-section.tsx +386 -0
- package/template/docs/HANDBOOK.md +187 -0
- package/template/docs/blueprints/README.md +1 -1
- package/template/docs/blueprints/board-card.md +1 -1
- package/template/docs/blueprints/data-table.md +2 -2
- package/template/docs/blueprints/list-page-template.md +3 -3
- package/template/docs/blueprints/page-header.md +4 -4
- package/template/docs/collaboration-access-pattern.md +7 -7
- package/template/docs/component-selection-guide.md +1 -1
- package/template/docs/data-views-pattern.md +18 -16
- package/template/docs/glossary.md +58 -0
- package/template/docs/kpi-flat-band-pattern.md +3 -3
- package/template/docs/kpi-trend-pattern.md +18 -3
- package/template/docs/large-dataset-strategy.md +155 -0
- package/template/docs/library-hub-header-pattern.md +25 -0
- package/template/docs/migrations/_template.md +1 -1
- package/template/docs/reference-implementations.md +151 -0
- package/template/docs/token-taxonomy.md +1 -1
- package/template/docs/voice-and-tone.md +262 -0
- package/template/eslint.config.mjs +9 -39
- package/template/hooks/use-secondary-panel-hub-nav.ts +10 -10
- package/template/lib/ask-leo-route-context.ts +6 -18
- package/template/lib/coach-mark-registry.ts +0 -16
- package/template/lib/command-menu-config.ts +5 -12
- package/template/lib/command-menu-search-data.ts +8 -39
- package/template/lib/{question-bank-authoring.ts → library-authoring.ts} +89 -88
- package/template/lib/library-dedicated-search.ts +19 -0
- package/template/lib/library-hub-search.ts +90 -0
- package/template/lib/library-nav.ts +477 -0
- package/template/lib/library-recent-searches.ts +22 -0
- package/template/lib/{placements-supported-views.ts → library-supported-views.ts} +2 -2
- package/template/lib/list-status-badges.ts +16 -104
- package/template/lib/mock/dashboard.ts +1 -1
- package/template/lib/mock/{question-bank-folders.ts → library-folders.ts} +30 -30
- package/template/lib/mock/library-header-collaborators.ts +54 -0
- package/template/lib/mock/{question-bank-inspector.ts → library-inspector.ts} +29 -29
- package/template/lib/mock/{question-bank-kpi.ts → library-kpi.ts} +20 -20
- package/template/lib/mock/library.ts +249 -0
- package/template/lib/mock/navigation.tsx +32 -26
- package/template/lib/table-state-lifecycle.ts +1 -1
- package/template/next.config.mjs +7 -4
- package/template/package.json +0 -1
- package/tokens/hooks-index.json +2874 -0
- package/consumer-extras/cursor-rules/exxat-question-bank-hub-header.mdc +0 -28
- package/template/app/(app)/examples/page.tsx +0 -41
- package/template/app/(app)/question-bank/find/page.tsx +0 -12
- package/template/app/(app)/question-bank/library/page.tsx +0 -11
- package/template/app/(app)/question-bank/list/page.tsx +0 -12
- package/template/app/(app)/question-bank/page.tsx +0 -11
- package/template/components/compliance-board-view.tsx +0 -142
- package/template/components/compliance-client.tsx +0 -92
- package/template/components/compliance-page-header.tsx +0 -89
- package/template/components/compliance-table.tsx +0 -468
- package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
- package/template/components/data-view-dashboard-charts-team.tsx +0 -971
- package/template/components/data-view-dashboard-charts.tsx +0 -1503
- package/template/components/new-placement-back-btn.tsx +0 -28
- package/template/components/new-placement-form.tsx +0 -942
- package/template/components/placement-board-card.tsx +0 -250
- package/template/components/placement-detail.tsx +0 -438
- package/template/components/placements-board-view.tsx +0 -397
- package/template/components/placements-client.tsx +0 -220
- package/template/components/placements-list-view.tsx +0 -124
- package/template/components/placements-page-header.tsx +0 -166
- package/template/components/placements-table-cells.test.tsx +0 -22
- package/template/components/placements-table-cells.tsx +0 -173
- package/template/components/placements-table-columns.tsx +0 -210
- package/template/components/placements-table.tsx +0 -934
- package/template/components/question-bank-panel-activator.tsx +0 -8
- package/template/components/rotations-empty-state.tsx +0 -50
- package/template/components/rotations-panel-activator.tsx +0 -8
- package/template/components/sites-board-view.tsx +0 -67
- package/template/components/sites-client.tsx +0 -154
- package/template/components/sites-table.tsx +0 -249
- package/template/components/team-board-view.tsx +0 -122
- package/template/components/team-client.tsx +0 -100
- package/template/components/team-page-header.tsx +0 -92
- package/template/components/team-table.tsx +0 -553
- package/template/docs/question-bank-hub-header-pattern.md +0 -25
- package/template/lib/compliance-supported-views.ts +0 -10
- package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
- package/template/lib/mock/compliance-kpi.ts +0 -61
- package/template/lib/mock/compliance.ts +0 -146
- package/template/lib/mock/placements-kpi.ts +0 -134
- package/template/lib/mock/placements.ts +0 -176
- package/template/lib/mock/question-bank-header-collaborators.ts +0 -54
- package/template/lib/mock/question-bank.ts +0 -249
- package/template/lib/mock/sites-directory.ts +0 -16
- package/template/lib/mock/sites-kpi.ts +0 -25
- package/template/lib/mock/team-kpi.ts +0 -60
- package/template/lib/mock/team.ts +0 -118
- package/template/lib/placement-board-card-layout.ts +0 -79
- package/template/lib/question-bank-dedicated-search.ts +0 -19
- package/template/lib/question-bank-hub-search.ts +0 -90
- package/template/lib/question-bank-nav.ts +0 -477
- package/template/lib/question-bank-recent-searches.ts +0 -22
- package/template/lib/question-bank-supported-views.ts +0 -12
- package/template/lib/sites-supported-views.ts +0 -10
- package/template/lib/team-supported-views.ts +0 -10
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tokens & themes — visualizer primitives + token index.
|
|
5
|
+
*
|
|
6
|
+
* This module exports the building blocks consumed by `tokens-themes-client.tsx`
|
|
7
|
+
* (which wires them into `PrimaryPageTemplate` + `ListPageTemplate`). It does
|
|
8
|
+
* NOT render the page shell.
|
|
9
|
+
*
|
|
10
|
+
* Each category gets a representation that matches the token's nature:
|
|
11
|
+
*
|
|
12
|
+
* | Tab | Renders tokens as… |
|
|
13
|
+
* |--------------|---------------------------------------------------------------|
|
|
14
|
+
* | Colors | 56-px swatch (`background: var(--name)`) |
|
|
15
|
+
* | Gradients | 96×40 fill swatch (paint-based, value usually multi-line) |
|
|
16
|
+
* | Radius | 64×64 muted box with `border-radius: var(--name)` |
|
|
17
|
+
* | Size | bar with `height: var(--name)` (scaled visually) |
|
|
18
|
+
* | Shadow | floating mini-card with `box-shadow: var(--name)` |
|
|
19
|
+
* | Typography | "Aa Sample" in `font-family: var(--name)` |
|
|
20
|
+
* | Motion | a dot that translates on hover using `transition: var(--name)`|
|
|
21
|
+
* | Aliases | `name → var(--target)` row (resolves the indirection) |
|
|
22
|
+
* | Other | raw text value |
|
|
23
|
+
*
|
|
24
|
+
* All tiles share click-to-copy on the `var(--name)` reference. The token
|
|
25
|
+
* index is the single source of truth: `packages/ui/tokens/hooks-index.json`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import * as React from "react"
|
|
29
|
+
import { useTheme } from "next-themes"
|
|
30
|
+
import tokensIndex from "@exxatdesignux/ui/tokens/hooks-index.json"
|
|
31
|
+
import { Button } from "@/components/ui/button"
|
|
32
|
+
import { Badge } from "@/components/ui/badge"
|
|
33
|
+
import { Tip } from "@/components/ui/tip"
|
|
34
|
+
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
|
35
|
+
import { cn } from "@/lib/utils"
|
|
36
|
+
|
|
37
|
+
/* ── Token index types ────────────────────────────────────────────────── */
|
|
38
|
+
|
|
39
|
+
export type TokenCategory =
|
|
40
|
+
| "color"
|
|
41
|
+
| "gradient"
|
|
42
|
+
| "radius"
|
|
43
|
+
| "size"
|
|
44
|
+
| "shadow"
|
|
45
|
+
| "typography"
|
|
46
|
+
| "transition"
|
|
47
|
+
| "alias"
|
|
48
|
+
| "other"
|
|
49
|
+
|
|
50
|
+
export type TokenRecord = {
|
|
51
|
+
namespace: string
|
|
52
|
+
category: TokenCategory | string
|
|
53
|
+
values: Record<string, string>
|
|
54
|
+
tailwindUtilities?: string[]
|
|
55
|
+
deprecated?: boolean
|
|
56
|
+
deprecatedMessage?: string | null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type TokensIndex = {
|
|
60
|
+
version: string
|
|
61
|
+
tokenCount: number
|
|
62
|
+
namespaces: string[]
|
|
63
|
+
themeKeys: string[]
|
|
64
|
+
tokens: Record<string, TokenRecord>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const TOKENS_INDEX = tokensIndex as unknown as TokensIndex
|
|
68
|
+
|
|
69
|
+
/** First available theme value — used for the "raw value" text under each tile. */
|
|
70
|
+
export function primaryValueText(t: TokenRecord): string {
|
|
71
|
+
return t.values.light ?? Object.values(t.values)[0] ?? ""
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* ── Category tab catalogue ────────────────────────────────────────────── */
|
|
75
|
+
|
|
76
|
+
export interface CategoryTabDef {
|
|
77
|
+
id: TokenCategory
|
|
78
|
+
label: string
|
|
79
|
+
icon: string
|
|
80
|
+
matches: (cat: string) => boolean
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const CATEGORY_TABS: CategoryTabDef[] = [
|
|
84
|
+
{ id: "color", label: "Colors", icon: "fa-palette", matches: (c) => c === "color" },
|
|
85
|
+
{ id: "gradient", label: "Gradients", icon: "fa-circle-half-stroke", matches: (c) => c === "gradient" },
|
|
86
|
+
{ id: "radius", label: "Radius", icon: "fa-rectangle-vertical", matches: (c) => c === "radius" },
|
|
87
|
+
{ id: "size", label: "Size", icon: "fa-ruler-horizontal", matches: (c) => c === "size" },
|
|
88
|
+
{ id: "shadow", label: "Shadow", icon: "fa-clone", matches: (c) => c === "shadow" },
|
|
89
|
+
{ id: "typography", label: "Typography", icon: "fa-text-size", matches: (c) => c === "typography" },
|
|
90
|
+
{ id: "transition", label: "Motion", icon: "fa-wave-sine", matches: (c) => c === "transition" },
|
|
91
|
+
{ id: "alias", label: "Aliases", icon: "fa-link", matches: (c) => c === "alias" },
|
|
92
|
+
{ id: "other", label: "Other", icon: "fa-hashtag", matches: (c) => c === "other" },
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
/** Pre-compute counts per category — same shape `getTabCount(filterId)` expects. */
|
|
96
|
+
export const CATEGORY_COUNTS: Record<TokenCategory, number> = CATEGORY_TABS.reduce(
|
|
97
|
+
(acc, tab) => { acc[tab.id] = 0; return acc },
|
|
98
|
+
{} as Record<TokenCategory, number>,
|
|
99
|
+
)
|
|
100
|
+
export const DEPRECATED_COUNT = (() => {
|
|
101
|
+
let n = 0
|
|
102
|
+
for (const t of Object.values(TOKENS_INDEX.tokens)) {
|
|
103
|
+
if (t.deprecated) n += 1
|
|
104
|
+
for (const tab of CATEGORY_TABS) {
|
|
105
|
+
if (tab.matches(t.category)) {
|
|
106
|
+
CATEGORY_COUNTS[tab.id] += 1
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return n
|
|
112
|
+
})()
|
|
113
|
+
|
|
114
|
+
/* ── Theme switcher ────────────────────────────────────────────────────── */
|
|
115
|
+
|
|
116
|
+
export function TokensThemeSwitcher({ className }: { className?: string }) {
|
|
117
|
+
const { theme = "system", setTheme } = useTheme()
|
|
118
|
+
const [mounted, setMounted] = React.useState(false)
|
|
119
|
+
React.useEffect(() => setMounted(true), [])
|
|
120
|
+
const value = mounted ? theme : "system"
|
|
121
|
+
return (
|
|
122
|
+
<RadioGroup
|
|
123
|
+
value={value}
|
|
124
|
+
onValueChange={setTheme}
|
|
125
|
+
className={cn("inline-flex items-center gap-1 rounded-md border border-border bg-card p-1", className)}
|
|
126
|
+
aria-label="Theme preview"
|
|
127
|
+
>
|
|
128
|
+
{(["light", "dark", "system"] as const).map((t) => (
|
|
129
|
+
<label
|
|
130
|
+
key={t}
|
|
131
|
+
htmlFor={`theme-${t}`}
|
|
132
|
+
className={cn(
|
|
133
|
+
"flex cursor-pointer items-center gap-1.5 rounded px-2.5 py-1 text-xs transition-colors",
|
|
134
|
+
"hover:bg-foreground/[0.04]",
|
|
135
|
+
value === t && "bg-foreground/[0.06] text-foreground",
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
138
|
+
<RadioGroupItem value={t} id={`theme-${t}`} className="sr-only" />
|
|
139
|
+
<i
|
|
140
|
+
className={cn(
|
|
141
|
+
"fa-light text-xs",
|
|
142
|
+
t === "light" && "fa-sun",
|
|
143
|
+
t === "dark" && "fa-moon",
|
|
144
|
+
t === "system" && "fa-desktop",
|
|
145
|
+
)}
|
|
146
|
+
aria-hidden="true"
|
|
147
|
+
/>
|
|
148
|
+
<span className="capitalize">{t}</span>
|
|
149
|
+
</label>
|
|
150
|
+
))}
|
|
151
|
+
</RadioGroup>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ── Clipboard hook ────────────────────────────────────────────────────── */
|
|
156
|
+
|
|
157
|
+
export function useTokenClipboard() {
|
|
158
|
+
const [copied, setCopied] = React.useState<string | null>(null)
|
|
159
|
+
const copy = React.useCallback((text: string) => {
|
|
160
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) return
|
|
161
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
162
|
+
setCopied(text)
|
|
163
|
+
window.setTimeout(() => setCopied((c) => (c === text ? null : c)), 1200)
|
|
164
|
+
}).catch(() => {})
|
|
165
|
+
}, [])
|
|
166
|
+
return { copied, copy }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* ── Tile shell ────────────────────────────────────────────────────────── */
|
|
170
|
+
|
|
171
|
+
interface TokenTileProps {
|
|
172
|
+
name: string
|
|
173
|
+
record: TokenRecord
|
|
174
|
+
onCopy: (text: string) => void
|
|
175
|
+
preview: React.ReactNode
|
|
176
|
+
valueText?: string
|
|
177
|
+
density?: "tight" | "wide"
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function TokenTile({ name, record, onCopy, preview, valueText, density = "wide" }: TokenTileProps) {
|
|
181
|
+
const cssRef = `var(${name})`
|
|
182
|
+
const raw = valueText ?? primaryValueText(record)
|
|
183
|
+
return (
|
|
184
|
+
<div
|
|
185
|
+
className={cn(
|
|
186
|
+
"group flex items-stretch gap-3 rounded-md border border-border bg-card p-3 transition-colors",
|
|
187
|
+
"hover:border-brand/40 hover:bg-interactive-hover-soft",
|
|
188
|
+
)}
|
|
189
|
+
>
|
|
190
|
+
<div className={cn("shrink-0", density === "tight" ? "w-14" : "w-20")}>{preview}</div>
|
|
191
|
+
<div className="min-w-0 flex-1">
|
|
192
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
193
|
+
<code className="font-mono text-xs text-foreground truncate tabular-nums">{name}</code>
|
|
194
|
+
{record.deprecated && (
|
|
195
|
+
<Badge variant="destructive" className="text-[10px] h-4 px-1.5 shrink-0">
|
|
196
|
+
deprecated
|
|
197
|
+
</Badge>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
<div className="mt-0.5 flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
201
|
+
<span className="rounded-sm bg-muted/60 px-1.5 py-0.5">{record.namespace}</span>
|
|
202
|
+
</div>
|
|
203
|
+
<div className="mt-1 truncate font-mono text-[11px] text-muted-foreground" title={raw}>
|
|
204
|
+
{raw || "—"}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
<Tip side="left" label={`Copy ${cssRef}`}>
|
|
208
|
+
<Button
|
|
209
|
+
type="button"
|
|
210
|
+
size="icon"
|
|
211
|
+
variant="ghost"
|
|
212
|
+
className="size-7 shrink-0 self-center opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
|
|
213
|
+
onClick={() => onCopy(cssRef)}
|
|
214
|
+
aria-label={`Copy ${cssRef}`}
|
|
215
|
+
>
|
|
216
|
+
<i className="fa-light fa-copy text-sm" aria-hidden="true" />
|
|
217
|
+
</Button>
|
|
218
|
+
</Tip>
|
|
219
|
+
</div>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* ── Category-specific previews ───────────────────────────────────────── */
|
|
224
|
+
|
|
225
|
+
function ColorPreview({ name }: { name: string }) {
|
|
226
|
+
return (
|
|
227
|
+
<div
|
|
228
|
+
className="h-14 w-14 rounded-md border border-border"
|
|
229
|
+
style={{ backgroundColor: `var(${name})` }}
|
|
230
|
+
aria-hidden="true"
|
|
231
|
+
/>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
function GradientPreview({ name }: { name: string }) {
|
|
235
|
+
return (
|
|
236
|
+
<div
|
|
237
|
+
className="h-14 w-20 rounded-md border border-border"
|
|
238
|
+
style={{ background: `var(${name})` }}
|
|
239
|
+
aria-hidden="true"
|
|
240
|
+
/>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
function RadiusPreview({ name }: { name: string }) {
|
|
244
|
+
return (
|
|
245
|
+
<div
|
|
246
|
+
className="h-14 w-14 border border-border bg-muted/50"
|
|
247
|
+
style={{ borderRadius: `var(${name})` }}
|
|
248
|
+
aria-hidden="true"
|
|
249
|
+
/>
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
function SizePreview({ name, record }: { name: string; record: TokenRecord }) {
|
|
253
|
+
const raw = primaryValueText(record)
|
|
254
|
+
return (
|
|
255
|
+
<div className="flex h-14 w-20 items-center justify-center" aria-hidden="true">
|
|
256
|
+
<div
|
|
257
|
+
className="w-full bg-brand rounded-sm"
|
|
258
|
+
style={{ height: `min(56px, var(${name}))` }}
|
|
259
|
+
title={raw}
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
function ShadowPreview({ name }: { name: string }) {
|
|
265
|
+
return (
|
|
266
|
+
<div
|
|
267
|
+
className="m-1 h-12 w-12 rounded-md bg-card"
|
|
268
|
+
style={{ boxShadow: `var(${name})` }}
|
|
269
|
+
aria-hidden="true"
|
|
270
|
+
/>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
function TypographyPreview({ name }: { name: string }) {
|
|
274
|
+
return (
|
|
275
|
+
<div
|
|
276
|
+
className="flex h-14 w-20 items-center justify-center rounded-md border border-border bg-muted/30"
|
|
277
|
+
style={{ fontFamily: `var(${name})` }}
|
|
278
|
+
aria-hidden="true"
|
|
279
|
+
>
|
|
280
|
+
<span className="text-2xl text-foreground">Aa</span>
|
|
281
|
+
</div>
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
function MotionPreview({ name }: { name: string }) {
|
|
285
|
+
return (
|
|
286
|
+
<div className="relative h-14 w-20 overflow-hidden rounded-md border border-border bg-muted/30" aria-hidden="true">
|
|
287
|
+
<div
|
|
288
|
+
className="absolute left-2 top-1/2 size-3 -translate-y-1/2 rounded-full bg-brand group-hover:translate-x-12"
|
|
289
|
+
style={{ transition: `var(${name})` }}
|
|
290
|
+
/>
|
|
291
|
+
</div>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
function AliasPreview({ record }: { record: TokenRecord }) {
|
|
295
|
+
const v = primaryValueText(record)
|
|
296
|
+
const targetMatch = v.match(/var\((--[a-z0-9-]+)\)/i)
|
|
297
|
+
const target = targetMatch?.[1]
|
|
298
|
+
return (
|
|
299
|
+
<div className="flex h-14 w-20 flex-col items-center justify-center rounded-md border border-border bg-muted/30 text-center" aria-hidden="true">
|
|
300
|
+
<i className="fa-light fa-link text-base text-muted-foreground" />
|
|
301
|
+
{target && (
|
|
302
|
+
<code className="mt-0.5 truncate max-w-full px-1 font-mono text-[9px] text-muted-foreground">{target}</code>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
function OtherPreview() {
|
|
308
|
+
return (
|
|
309
|
+
<div className="flex h-14 w-20 items-center justify-center rounded-md border border-dashed border-border text-muted-foreground" aria-hidden="true">
|
|
310
|
+
<i className="fa-light fa-hashtag text-base" />
|
|
311
|
+
</div>
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Pick the right visualizer for a token. Used both by the legacy `TokensCategoryGrid`
|
|
317
|
+
* and by the DataTable Preview cell in `tokens-themes-client.tsx`.
|
|
318
|
+
*/
|
|
319
|
+
export function categoryPreview(name: string, record: TokenRecord): React.ReactNode {
|
|
320
|
+
switch (record.category) {
|
|
321
|
+
case "color": return <ColorPreview name={name} />
|
|
322
|
+
case "gradient": return <GradientPreview name={name} />
|
|
323
|
+
case "radius": return <RadiusPreview name={name} />
|
|
324
|
+
case "size": return <SizePreview name={name} record={record} />
|
|
325
|
+
case "shadow": return <ShadowPreview name={name} />
|
|
326
|
+
case "typography": return <TypographyPreview name={name} />
|
|
327
|
+
case "transition": return <MotionPreview name={name} />
|
|
328
|
+
case "alias": return <AliasPreview record={record} />
|
|
329
|
+
default: return <OtherPreview />
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* ── Category grid (the page body for one view tab) ───────────────────── */
|
|
334
|
+
|
|
335
|
+
export interface TokensCategoryGridProps {
|
|
336
|
+
query: string
|
|
337
|
+
showDeprecated: boolean
|
|
338
|
+
category: CategoryTabDef
|
|
339
|
+
onCopy: (text: string) => void
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function TokensCategoryGrid({ query, showDeprecated, category, onCopy }: TokensCategoryGridProps) {
|
|
343
|
+
const filtered = React.useMemo(() => {
|
|
344
|
+
const q = query.trim().toLowerCase()
|
|
345
|
+
const out: Array<[string, TokenRecord]> = []
|
|
346
|
+
for (const [name, record] of Object.entries(TOKENS_INDEX.tokens)) {
|
|
347
|
+
if (!category.matches(record.category)) continue
|
|
348
|
+
if (!showDeprecated && record.deprecated) continue
|
|
349
|
+
if (q && !(name.toLowerCase().includes(q) || record.namespace.toLowerCase().includes(q))) continue
|
|
350
|
+
out.push([name, record])
|
|
351
|
+
}
|
|
352
|
+
return out.sort(([a], [b]) => a.localeCompare(b))
|
|
353
|
+
}, [query, showDeprecated, category])
|
|
354
|
+
|
|
355
|
+
if (filtered.length === 0) {
|
|
356
|
+
return (
|
|
357
|
+
<div className="rounded-md border border-dashed border-border bg-muted/20 p-8 text-center text-sm text-muted-foreground">
|
|
358
|
+
No {category.label.toLowerCase()} tokens match your filter.
|
|
359
|
+
</div>
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Colors are dense (100+ tokens) → 3-column on wide. Other categories 1–2 col. */
|
|
364
|
+
const isDense = category.id === "color"
|
|
365
|
+
return (
|
|
366
|
+
<div
|
|
367
|
+
className={cn(
|
|
368
|
+
"grid gap-2",
|
|
369
|
+
isDense
|
|
370
|
+
? "grid-cols-1 md:grid-cols-2 xl:grid-cols-3"
|
|
371
|
+
: "grid-cols-1 md:grid-cols-2",
|
|
372
|
+
)}
|
|
373
|
+
>
|
|
374
|
+
{filtered.map(([name, record]) => (
|
|
375
|
+
<TokenTile
|
|
376
|
+
key={name}
|
|
377
|
+
name={name}
|
|
378
|
+
record={record}
|
|
379
|
+
onCopy={onCopy}
|
|
380
|
+
density={isDense ? "tight" : "wide"}
|
|
381
|
+
preview={categoryPreview(name, record)}
|
|
382
|
+
/>
|
|
383
|
+
))}
|
|
384
|
+
</div>
|
|
385
|
+
)
|
|
386
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Exxat DS — Handbook
|
|
2
|
+
|
|
3
|
+
> **Start here.** One page. Read in 10 minutes. Links out to everything else.
|
|
4
|
+
>
|
|
5
|
+
> **Audience:** designers, engineers, contributors, AI agents — anyone shipping UI in the Exxat product.
|
|
6
|
+
>
|
|
7
|
+
> **Working with an AI assistant?** Read [`.cursor/skills/exxat-token-economy/SKILL.md`](../../.cursor/skills/exxat-token-economy/SKILL.md) **first** (or `.claude/skills/exxat-token-economy/SKILL.md` for Claude Code). It's a one-page pre-flight that cuts token usage by ~50%: a task → minimum-file-set table, the five-question rule check, and tiny scaffolds that mean the assistant never has to re-read this handbook for the common case.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Five principles
|
|
12
|
+
|
|
13
|
+
Every screen, primitive, and pattern in this design system serves one or more of these. When in doubt, the principle wins over the convenience.
|
|
14
|
+
|
|
15
|
+
1. **Clarity over decoration.** Users see one product, one shell, one rhythm. Surprise (mystery icons, hidden states, novel layouts) is a tax on attention.
|
|
16
|
+
2. **Progressive disclosure.** Beginners see what they need. Power users reach what they want. Default to the simpler surface; expose density on opt-in (Properties drawer, view tabs, secondary panel).
|
|
17
|
+
3. **Same-shaped tools.** A hub looks like a hub. A drawer looks like a drawer. A KPI looks like a KPI. Pick the canonical primitive (§5 reference pages) before composing your own.
|
|
18
|
+
4. **Accessibility is non-negotiable.** WCAG 2.1 AA is the **floor**, not the goal. Keyboard, screen-reader, contrast, touch-target, format hints — all enforced by rules, lints, and a checklist.
|
|
19
|
+
5. **The data drives the chrome.** KPIs, status, trend polarity, descriptions — they come from the dataset (or the product) and are honest. No spin arrows, no decorative placeholders.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2. How to build a hub in 6 steps
|
|
24
|
+
|
|
25
|
+
This is the **happy path** for the most common task: "I have an entity (records, library items, tokens, …); ship a hub for it." Follow these in order and the page lands at "best UX/UI", not "random design".
|
|
26
|
+
|
|
27
|
+
| Step | What to do | Where it lives | Rule |
|
|
28
|
+
|---|---|---|---|
|
|
29
|
+
| 1 | Add typed mock rows in `lib/mock/<entity>.ts`. Aim for ~12 realistic records. | `apps/web/lib/mock/` | `.cursor/rules/exxat-centralized-list-dataset.mdc` |
|
|
30
|
+
| 2 | Write **one** KPI helper `lib/mock/<entity>-kpi.ts` returning `MetricItem[]` (≤ 4 tiles). | same | `exxat-kpi-max-four.mdc`, `exxat-kpi-trends.mdc` |
|
|
31
|
+
| 3 | Build the column defs (`ColumnDef[]`). Set `filter:` per column to get filter chips automatically. | `apps/web/components/<entity>-table.tsx` | `exxat-data-tables.mdc` |
|
|
32
|
+
| 4 | Mount **`HubTable`** (NOT raw `<DataTable>`) inside `ListPageTemplate.renderContent`. `HubTable` wires `useTableState`, toolbar (search + filter chips + sort), and the **Properties drawer** in one place. | `apps/web/components/<entity>-table.tsx` | `exxat-data-tables.mdc` |
|
|
33
|
+
| 5 | Compose the page client with `PrimaryPageTemplate` → `ListPageTemplate` (KPIs in `metrics`, view tabs in `defaultTabs`, the `HubTable` in `renderContent`). | `apps/web/components/<entity>-client.tsx` | `exxat-list-page-connected-views.mdc` |
|
|
34
|
+
| 6 | Add to nav (`lib/mock/navigation.tsx`). If the hub needs scoped sub-navigation (e.g. categories), declare `secondaryPanel: "<id>"` and register the panel. | `apps/web/lib/mock/navigation.tsx`, `apps/web/components/sidebar/secondary-panel.tsx` | `exxat-primary-nav-secondary-panel.mdc` |
|
|
35
|
+
|
|
36
|
+
**Reference pages to copy:** `apps/web/components/columns-showcase.tsx` (single-view catalog hub composing every reusable cell), `apps/web/components/tokens-themes-client.tsx` (hub with a secondary panel + URL-driven scope), or `apps/web/components/library-table.tsx` (full hub: table / board / dashboard + conditional rules).
|
|
37
|
+
|
|
38
|
+
> **Stop signs.** If you find yourself building a parallel table stack, a second metrics strip, a custom filter row, or pasting raw `<DataTable>` into `renderContent` — **stop and re-read** `.cursor/rules/exxat-reuse-before-custom.mdc`.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 3. Where everything lives
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
46
|
+
│ PRINCIPLES + HANDBOOK → docs/HANDBOOK.md (this file) │
|
|
47
|
+
│ │
|
|
48
|
+
│ FOUNDATIONS │
|
|
49
|
+
│ tokens → docs/token-taxonomy.md │
|
|
50
|
+
│ icons (Font Awesome) → .cursor/rules/exxat-fontawesome-icons │
|
|
51
|
+
│ typography → docs/token-taxonomy.md (font-* tokens) │
|
|
52
|
+
│ spacing / radius → docs/token-taxonomy.md (--exxat-*) │
|
|
53
|
+
│ color & themes → apps/web/components/tokens-themes-* │
|
|
54
|
+
│ voice & tone → docs/voice-and-tone.md │
|
|
55
|
+
│ glossary → docs/glossary.md │
|
|
56
|
+
│ reference pages → docs/reference-implementations.md │
|
|
57
|
+
│ │
|
|
58
|
+
│ DECIDING (selection guides) │
|
|
59
|
+
│ which component? → docs/component-selection-guide.md │
|
|
60
|
+
│ page vs drawer vs → docs/drawer-vs-dialog-pattern.md │
|
|
61
|
+
│ dialog vs route + .cursor/rules/exxat-{drawer-vs-dialog, │
|
|
62
|
+
│ page-vs-drawer}.mdc │
|
|
63
|
+
│ card vs row vs list → docs/card-vs-rows-pattern.md │
|
|
64
|
+
│ │
|
|
65
|
+
│ BLUEPRINTS (framework-agnostic specs — one per pattern) │
|
|
66
|
+
│ → docs/blueprints/ │
|
|
67
|
+
│ page-header · data-table · │
|
|
68
|
+
│ list-page-template · board-card · │
|
|
69
|
+
│ key-metrics │
|
|
70
|
+
│ │
|
|
71
|
+
│ PATTERNS (long-form narrative — the "why" + the "how") │
|
|
72
|
+
│ → docs/*.md (data-views-pattern, │
|
|
73
|
+
│ kpi-trend-pattern, drawer-vs-dialog- │
|
|
74
|
+
│ pattern, dedicated-search, │
|
|
75
|
+
│ command-menu, …) │
|
|
76
|
+
│ │
|
|
77
|
+
│ RULES (binding MUST / MUST NOT — for AI agents + reviewers) │
|
|
78
|
+
│ → .cursor/rules/*.mdc │
|
|
79
|
+
│ │
|
|
80
|
+
│ SKILLS (workflows + checklists — for AI agents doing a task) │
|
|
81
|
+
│ → .cursor/skills/ + .claude/skills/ │
|
|
82
|
+
│ │
|
|
83
|
+
│ MIGRATIONS (deprecation history, every breaking change) │
|
|
84
|
+
│ → docs/migrations/ │
|
|
85
|
+
│ │
|
|
86
|
+
│ AGENT HANDBOOK (authoritative §-numbered manual) │
|
|
87
|
+
│ → apps/web/AGENTS.md │
|
|
88
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Quick "which file should I open?" cheat-sheet
|
|
92
|
+
|
|
93
|
+
| You want to… | Open this |
|
|
94
|
+
|---|---|
|
|
95
|
+
| Pick the right component for a job | [`docs/component-selection-guide.md`](./component-selection-guide.md) |
|
|
96
|
+
| Know what "hub" / "view tab" / "KPI band" mean | [`docs/glossary.md`](./glossary.md) |
|
|
97
|
+
| Write empty-state / error / button copy | [`docs/voice-and-tone.md`](./voice-and-tone.md) |
|
|
98
|
+
| Find the canonical reference page to copy | [`docs/reference-implementations.md`](./reference-implementations.md) |
|
|
99
|
+
| Know the spec for a pattern | [`docs/blueprints/`](./blueprints/) |
|
|
100
|
+
| Understand the "why" of a pattern | [`docs/<pattern>-pattern.md`](.) |
|
|
101
|
+
| Know the binding MUST / MUST NOT | [`.cursor/rules/`](../../../.cursor/rules/) |
|
|
102
|
+
| Run a recurring agent workflow | [`.cursor/skills/`](../../../.cursor/skills/) or [`.claude/skills/`](../../../.claude/skills/) |
|
|
103
|
+
| Token name & semantics | [`docs/token-taxonomy.md`](./token-taxonomy.md) |
|
|
104
|
+
| Full authoritative handbook | [`apps/web/AGENTS.md`](../AGENTS.md) |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 4. Rule precedence (when sources conflict)
|
|
109
|
+
|
|
110
|
+
If two docs say different things, **the higher row wins**:
|
|
111
|
+
|
|
112
|
+
1. **`.cursor/rules/*.mdc`** — these are MUST / MUST NOT; they bind the AI agent and the reviewer.
|
|
113
|
+
2. **`apps/web/AGENTS.md`** — authoritative §-numbered handbook. The rules above are summaries of this.
|
|
114
|
+
3. **`docs/blueprints/*.md`** — framework-agnostic specs for a single pattern.
|
|
115
|
+
4. **`docs/*-pattern.md`** — long-form narrative for a pattern.
|
|
116
|
+
5. **Reference page in code** (`apps/web/components/<reference>.tsx`) — the working implementation.
|
|
117
|
+
6. **This handbook** — orientation only. If it conflicts with rules or AGENTS, the rules/AGENTS win.
|
|
118
|
+
|
|
119
|
+
> **Found a conflict?** Open a PR that updates the *binding* layer (rule or AGENTS section) first, then propagate down. Don't fork the truth.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 5. The canonical primitives (memorize these)
|
|
124
|
+
|
|
125
|
+
These are the ones you'll use on >90% of screens. If a screen needs something else, it almost certainly already exists — search `components/` before building.
|
|
126
|
+
|
|
127
|
+
| Need | Primitive | Lives in |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| Page chrome (breadcrumbs, site header, max-width content rail) | `PrimaryPageTemplate` | `apps/web/components/templates/primary-page-template.tsx` |
|
|
130
|
+
| Hub frame (header + metrics + view tabs + content) | `ListPageTemplate` | `packages/ui` |
|
|
131
|
+
| **Hub view body** (table + search + filters + Properties drawer + bulk-actions) | **`HubTable`** | `packages/ui` (re-exported from `@/components/data-views`) |
|
|
132
|
+
| Page header (title + subtitle + actions + collaborators rail) | `PageHeader` | `apps/web/components/page-header.tsx` |
|
|
133
|
+
| KPI strip / band | `KeyMetrics` (`variant="flat"` on hubs) | `packages/ui` |
|
|
134
|
+
| Status chip + icon | `ListHubStatusBadge` + `lib/list-status-badges.ts` | `apps/web/components/` |
|
|
135
|
+
| Board / kanban card | `ListPageBoardCard` + primitives | `packages/ui` |
|
|
136
|
+
| Side overlay | `Sheet` / `Drawer` (NOT toast — `exxat-no-toast.mdc`) | `packages/ui` |
|
|
137
|
+
| Persistent banner | `LocalBanner` / `SystemBanner` | `packages/ui` |
|
|
138
|
+
| Inline status / format hint | `FormDescription`, inline `<small>` | `packages/ui` |
|
|
139
|
+
| Tooltip | `Tip` / `Tooltip` | `packages/ui` |
|
|
140
|
+
| Keyboard shortcut hint | `Kbd` (`variant="bare"` inside buttons) | `packages/ui` |
|
|
141
|
+
| Global search | `CommandMenu` (⌘K) | `apps/web/components/command-menu.tsx` |
|
|
142
|
+
| AI assistant chrome | Ask Leo side panel (⌘⌥K) | `apps/web/components/` |
|
|
143
|
+
|
|
144
|
+
For a fuller decision tree see [`docs/component-selection-guide.md`](./component-selection-guide.md).
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 6. The shortest accessibility checklist
|
|
149
|
+
|
|
150
|
+
Run this on every PR. If you can't tick every box, the change isn't ready. (Full list: `.cursor/skills/exxat-accessibility/SKILL.md` and [`AGENTS.md` §8](../AGENTS.md).)
|
|
151
|
+
|
|
152
|
+
- [ ] **Keyboard.** Every interactive thing reachable via Tab + activatable via Enter / Space. Focus ring visible (≥ 3:1).
|
|
153
|
+
- [ ] **Touch target ≥ 24×24 CSS px** (or 24 px spacing) per WCAG 2.5.8.
|
|
154
|
+
- [ ] **Icons that mean something** have a text alt — either adjacent label (Case A, `aria-hidden`), or `role="img" + aria-label + Tooltip` (Case B), or `aria-label + Tooltip` on the button (Case C). No silent icons.
|
|
155
|
+
- [ ] **Contrast.** Text ≥ 4.5:1; UI components ≥ 3:1. Don't encode state with color alone — pair with icon or label.
|
|
156
|
+
- [ ] **Format hints are persistent**, never placeholder-only. Use `FormDescription`.
|
|
157
|
+
- [ ] **Dialogs / drawers / sheets** have a `Title` (use `sr-only` if visually hidden).
|
|
158
|
+
- [ ] **Tabs** use `role="tablist"` correctly (no mixed children); composite switchers use `role="toolbar"` instead.
|
|
159
|
+
- [ ] **No toast.** Use banners, inline status, or dialogs (`exxat-no-toast.mdc`).
|
|
160
|
+
- [ ] **HC modes.** Forced-colors and `data-contrast="high"` covered for any fill-only state (progress, gauge, pill).
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 7. The "you're done" definition
|
|
165
|
+
|
|
166
|
+
A hub or screen is **done** when:
|
|
167
|
+
|
|
168
|
+
1. It uses `PrimaryPageTemplate` + `ListPageTemplate` (or another canonical template).
|
|
169
|
+
2. The data surface is `HubTable` (or, for non-hubs, the right primitive from §5).
|
|
170
|
+
3. KPIs use `KeyMetrics` with `delta` for counts, `description` for prose, ≤ 4 tiles, polarity set correctly.
|
|
171
|
+
4. The §6 accessibility checklist is green.
|
|
172
|
+
5. Copy passes [`docs/voice-and-tone.md`](./voice-and-tone.md).
|
|
173
|
+
6. No new shared primitives were added without `.cursor/rules/exxat-reuse-before-custom.mdc` approval.
|
|
174
|
+
7. The §13 PR-review checklist in [`AGENTS.md`](../AGENTS.md#section-13) is green.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 8. Where to ask for help
|
|
179
|
+
|
|
180
|
+
- **Code-level questions** — open the file referenced in a rule's "See also" section.
|
|
181
|
+
- **Pattern-level questions** — open the matching `docs/*-pattern.md`.
|
|
182
|
+
- **"Is this the right approach?"** — read [`.cursor/rules/exxat-reuse-before-custom.mdc`](../../../.cursor/rules/exxat-reuse-before-custom.mdc). If still unsure, ask before building.
|
|
183
|
+
- **AGENTS.md is too long** — that's why this handbook exists. Bring the §-number you're stuck on; we'll split it out.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
*This file is intentionally short. If you want to add something, ask whether it belongs in a rule, a pattern, a blueprint, or the glossary instead.*
|
|
@@ -62,7 +62,7 @@ dive.
|
|
|
62
62
|
|
|
63
63
|
| Blueprint | Narrative doc | Cursor rule(s) | React component(s) |
|
|
64
64
|
|---|---|---|---|
|
|
65
|
-
| [page-header](./page-header.md) | `apps/web/docs/data-views-pattern.md` (Page header section) | `exxat-collaboration-access.mdc` (variant), `exxat-mono-ids.mdc` (subtitle IDs) | `PageHeader`, `PlacementsPageHeader`, `TeamPageHeader`, `
|
|
65
|
+
| [page-header](./page-header.md) | `apps/web/docs/data-views-pattern.md` (Page header section) | `exxat-collaboration-access.mdc` (variant), `exxat-mono-ids.mdc` (subtitle IDs) | `PageHeader`, `PlacementsPageHeader`, `TeamPageHeader`, `LibraryPageHeader` |
|
|
66
66
|
| [data-table](./data-table.md) | `apps/web/docs/data-views-pattern.md` | `exxat-data-tables.mdc`, `exxat-list-page-connected-views.mdc`, `exxat-centralized-list-dataset.mdc`, `exxat-table-properties-drawer.mdc` | `DataTable`, `DataTablePaginated`, `useTableState`, `TablePropertiesDrawer` |
|
|
67
67
|
| [list-page-template](./list-page-template.md) | `apps/web/docs/data-views-pattern.md`, `kpi-flat-band-pattern.md` | `exxat-list-page-connected-views.mdc`, `exxat-centralized-list-dataset.mdc`, `exxat-table-properties-drawer.mdc`, `exxat-list-page-view-shells.mdc` | `ListPageTemplate`, `HubTable`, `useTableState`, `TablePropertiesDrawer` |
|
|
68
68
|
| [board-card](./board-card.md) | `apps/web/docs/data-views-pattern.md` (board UI section) | `exxat-board-cards.mdc`, `exxat-centralized-list-dataset.mdc`, `exxat-card-vs-list-rows.mdc` | `ListPageBoardCard`, `ListPageBoardCardTitleRow`, `ListPageBoardCardBadgeRow`, `BoardCardTwoLineBlock`, `BoardCardIconRow`, `ListHubStatusBadge` |
|
|
@@ -85,7 +85,7 @@ a bespoke `<button>` + border-class wrapper for the same pattern.
|
|
|
85
85
|
| Variant | When to use | Differences from default |
|
|
86
86
|
|---|---|---|
|
|
87
87
|
| `base` | Most hubs | Title + status badge + body |
|
|
88
|
-
| `no-status` | Hubs without lifecycle (
|
|
88
|
+
| `no-status` | Hubs without lifecycle (Library items) | Omit `badgeRow`; body carries the differentiator |
|
|
89
89
|
| `compact` | Dense boards (10+ cards visible) | Drop body; show title + status + avatar only |
|
|
90
90
|
| `with-progress` | Workflow boards with completion bar | Add a `<Progress>` row in the body slot |
|
|
91
91
|
|
|
@@ -106,7 +106,7 @@ dashboard, folder, panel — reads from.
|
|
|
106
106
|
|
|
107
107
|
| Framework | Component(s) | File |
|
|
108
108
|
|---|---|---|
|
|
109
|
-
| **React (this app)** | `DataTable`, `DataTablePaginated`, `useTableState`, `DataTableToolbar`, `TablePropertiesDrawer`,
|
|
109
|
+
| **React (this app)** | `DataTable`, `DataTablePaginated`, `useTableState`, `DataTableToolbar`, `TablePropertiesDrawer`, **importable cell renderers** in `components/data-views/table-cells.tsx` (`ProgressCell`, `CurrencyCell`, `RatingCell`, `RowActionsCell<TRow>`, …) | [`packages/ui/src/components/data-table/`](../../../../packages/ui/src/components/data-table/), [`apps/web/components/data-views/table-cells.tsx`](../../components/data-views/table-cells.tsx), [`packages/ui/src/components/table-properties/`](../../../../packages/ui/src/components/table-properties/) |
|
|
110
110
|
| Mobile | — | — |
|
|
111
111
|
| Figma | — | — |
|
|
112
112
|
|
|
@@ -115,7 +115,7 @@ Reference compositions:
|
|
|
115
115
|
- **Placements** — `PlacementsClient` + `PlacementsTable` (most complete reference)
|
|
116
116
|
- **Team** — `TeamClient` + `TeamTable`
|
|
117
117
|
- **Compliance** — `ComplianceClient` + `ComplianceTable`
|
|
118
|
-
- **
|
|
118
|
+
- **Library** — `LibraryClient` + `LibraryTable` (adds folder
|
|
119
119
|
scope from URL)
|
|
120
120
|
|
|
121
121
|
## 8. Do / Don't
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
## 1. Intent
|
|
6
6
|
|
|
7
7
|
A **list page** is the canonical hub surface for browsing a homogeneous
|
|
8
|
-
collection of records (Placements, Team, Compliance,
|
|
8
|
+
collection of records (Placements, Team, Compliance, Library items, …).
|
|
9
9
|
It pairs a single `DataTable` with view-tab variants (table · list · board ·
|
|
10
10
|
dashboard · folder · panel · tree), one shared filter / sort / column model,
|
|
11
11
|
and a metric strip that consumes the same filtered row bag.
|
|
@@ -93,9 +93,9 @@ The view body is rendered by `HubTable` (or a custom non-table view) and
|
|
|
93
93
|
| Variant | When to use | Differences from default |
|
|
94
94
|
|---|---|---|
|
|
95
95
|
| `base` | Most hubs | Header + metrics + tabs + view body |
|
|
96
|
-
| `no-metrics` | Hubs where KPI summary adds noise (e.g.
|
|
96
|
+
| `no-metrics` | Hubs where KPI summary adds noise (e.g. Library: too many heterogeneous folders) | Omit `metrics` prop |
|
|
97
97
|
| `with-banner` | Pages that need a promo or alert above the header | Use `beforeSiteHeader` slot |
|
|
98
|
-
| `secondary-panel-hub` | Hubs scoped by a sidebar panel (
|
|
98
|
+
| `secondary-panel-hub` | Hubs scoped by a sidebar panel (Library) | Wrap in `SecondaryPanelHubTemplate`; same `useTableState` |
|
|
99
99
|
|
|
100
100
|
## 7. Implementation
|
|
101
101
|
|