@exxatdesignux/ui 0.5.2 → 0.5.4
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/README.md +1 -1
- package/consumer-extras/cursor-rules/exxat-accessibility.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-data-tables.mdc +8 -6
- package/consumer-extras/cursor-rules/exxat-drawer-vs-dialog.mdc +4 -4
- package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +6 -1
- package/consumer-extras/cursor-rules/exxat-hub-supported-views.mdc +54 -0
- package/consumer-extras/cursor-rules/exxat-nav-single-active.mdc +31 -0
- package/consumer-extras/cursor-rules/exxat-no-vaul.mdc +25 -0
- package/consumer-extras/cursor-rules/exxat-page-header-actions.mdc +31 -0
- package/consumer-extras/cursor-rules/exxat-table-row-preview.mdc +24 -0
- package/consumer-extras/cursor-rules/exxat-tabs-chrome.mdc +31 -0
- package/consumer-extras/cursor-skills/exxat-drawer-vs-dialog/SKILL.md +5 -5
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +10 -5
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/accessibility.md +1 -1
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/data-table-pattern.md +15 -5
- package/consumer-extras/cursor-skills/exxat-token-economy/SKILL.md +14 -5
- package/consumer-extras/handbook/HANDBOOK.md +1 -1
- package/consumer-extras/handbook/reference-implementations.md +2 -2
- package/consumer-extras/patterns/consumer-upgrade-checklist.md +14 -1
- package/consumer-extras/patterns/data-views-pattern.md +6 -0
- package/consumer-extras/patterns/drawer-vs-dialog-pattern.md +50 -0
- package/consumer-extras/patterns/hub-supported-views-pattern.md +53 -0
- package/dist/components/data-table/index.js +13 -9
- package/dist/components/data-table/index.js.map +1 -1
- package/dist/components/data-table/pagination.js +13 -9
- package/dist/components/data-table/pagination.js.map +1 -1
- package/dist/components/data-views/hub-table.d.ts +8 -4
- package/dist/components/data-views/hub-table.js +25 -10
- package/dist/components/data-views/hub-table.js.map +1 -1
- package/dist/components/data-views/index.d.ts +1 -1
- package/dist/components/data-views/index.js +25 -10
- package/dist/components/data-views/index.js.map +1 -1
- package/dist/components/data-views/list-page-connected-view-body.d.ts +1 -1
- package/dist/components/data-views/list-page-connected-view-body.js +1 -0
- package/dist/components/data-views/list-page-connected-view-body.js.map +1 -1
- package/dist/components/table-properties/drawer-button.js +1 -0
- package/dist/components/table-properties/drawer-button.js.map +1 -1
- package/dist/components/table-properties/drawer.js +1 -0
- package/dist/components/table-properties/drawer.js.map +1 -1
- package/dist/components/table-properties/index.d.ts +1 -1
- package/dist/components/table-properties/index.js +1 -0
- package/dist/components/table-properties/index.js.map +1 -1
- package/dist/components/templates/index.d.ts +1 -1
- package/dist/components/templates/index.js +12 -2
- package/dist/components/templates/index.js.map +1 -1
- package/dist/components/templates/list-page.d.ts +4 -2
- package/dist/components/templates/list-page.js +12 -2
- package/dist/components/templates/list-page.js.map +1 -1
- package/dist/{data-list-view-registry-CyBoBML4.d.ts → data-list-view-registry-BstmlfQ3.d.ts} +16 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +135 -126
- package/dist/index.js.map +1 -1
- package/dist/lib/data-list-view-registry.d.ts +1 -1
- package/dist/lib/data-list-view-registry.js +17 -1
- package/dist/lib/data-list-view-registry.js.map +1 -1
- package/dist/lib/data-list-view-surface.d.ts +1 -1
- package/dist/lib/data-list-view-surface.js +1 -0
- package/dist/lib/data-list-view-surface.js.map +1 -1
- package/dist/lib/list-page-table-properties.d.ts +1 -1
- package/dist/lib/list-page-table-properties.js +1 -0
- package/dist/lib/list-page-table-properties.js.map +1 -1
- package/dist/lib/nav-active.d.ts +38 -0
- package/dist/lib/nav-active.js +104 -0
- package/dist/lib/nav-active.js.map +1 -0
- package/package.json +1 -2
- package/src/components/data-table/index.tsx +25 -17
- package/src/components/data-views/hub-table.tsx +9 -3
- package/src/components/templates/list-page.tsx +9 -3
- package/src/index.ts +1 -1
- package/src/lib/data-list-view-registry.ts +31 -0
- package/src/lib/nav-active.ts +162 -0
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +2 -1
- package/template/.cursor/rules/exxat-accessibility.mdc +1 -1
- package/template/AGENTS.md +18 -3
- package/template/components/columns-client.tsx +3 -2
- package/template/components/columns-showcase.tsx +22 -18
- package/template/components/exxat-product-logo.tsx +1 -1
- package/template/components/library-table.tsx +62 -23
- package/template/components/new-library-item-form.tsx +0 -7
- package/template/components/product-wordmark.tsx +1 -1
- package/template/components/sidebar/app-sidebar.tsx +14 -106
- package/template/components/sidebar/secondary-nav.tsx +22 -4
- package/template/components/site-header.tsx +1 -1
- package/template/components/tokens-hub-auxiliary-views.tsx +301 -0
- package/template/components/tokens-themes-client.tsx +44 -16
- package/template/docs/HANDBOOK.md +2 -2
- package/template/docs/component-selection-guide.md +1 -1
- package/template/docs/consumer-upgrade-checklist.md +51 -0
- package/template/docs/data-views-pattern.md +6 -0
- package/template/docs/drawer-vs-dialog-pattern.md +8 -8
- package/template/docs/glossary.md +2 -1
- package/template/docs/hub-supported-views-pattern.md +53 -0
- package/template/docs/reference-implementations.md +2 -2
- package/template/lib/full-hub-supported-views.ts +8 -0
- package/template/lib/library-supported-views.ts +5 -12
- package/template/lib/motion-ui.ts +2 -2
- package/template/package.json +1 -1
- package/tokens/hooks-index.json +2 -2
- package/dist/components/ui/drawer.d.ts +0 -16
- package/dist/components/ui/drawer.js +0 -125
- package/dist/components/ui/drawer.js.map +0 -1
- package/src/components/ui/drawer.tsx +0 -134
- package/template/components/ui/drawer.tsx +0 -1
package/src/index.ts
CHANGED
|
@@ -46,6 +46,7 @@ export {
|
|
|
46
46
|
export * from "./lib/list-page-table-properties"
|
|
47
47
|
export * from "./lib/data-list-view"
|
|
48
48
|
export * from "./lib/data-list-view-registry"
|
|
49
|
+
export * from "./lib/nav-active"
|
|
49
50
|
export * from "./lib/data-list-view-surface"
|
|
50
51
|
export * from "./lib/data-list-display-options"
|
|
51
52
|
|
|
@@ -68,7 +69,6 @@ export * from "./components/ui/context-menu"
|
|
|
68
69
|
export * from "./components/ui/date-picker-field"
|
|
69
70
|
export * from "./components/ui/dialog"
|
|
70
71
|
export * from "./components/ui/drag-handle-grip"
|
|
71
|
-
export * from "./components/ui/drawer"
|
|
72
72
|
export * from "./components/ui/dropdown-menu"
|
|
73
73
|
export * from "./components/ui/export-drawer"
|
|
74
74
|
export * from "./components/ui/hover-card"
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* `getDataListViewRenderKind` + `ListPageConnectedViewBody` (never a dashboard fallback).
|
|
7
7
|
*
|
|
8
8
|
* @see `docs/data-views-pattern.md` — "View registry and connected bodies"
|
|
9
|
+
* @see `.cursor/rules/exxat-hub-supported-views.mdc` — default hubs use **`FULL_HUB_SUPPORTED_VIEWS`** (seven views)
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import {
|
|
@@ -47,6 +48,36 @@ const BY_VALUE = new Map<DataListViewType, DataListViewDefinition>(
|
|
|
47
48
|
|
|
48
49
|
export const DATA_LIST_VIEW_REGISTRY: readonly DataListViewDefinition[] = DEFINITIONS
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Default view allowlist for **primary list hubs** (Placements / Team / Students-style pages).
|
|
53
|
+
* Pair with `ListPageTemplate` + `HubTable` when the hub implements table, list, board,
|
|
54
|
+
* and dashboard renderers. Omit `supportedViewTypes` on both components to use this default.
|
|
55
|
+
*/
|
|
56
|
+
export const PRIMARY_HUB_SUPPORTED_VIEWS = [
|
|
57
|
+
"table",
|
|
58
|
+
"list",
|
|
59
|
+
"board",
|
|
60
|
+
"dashboard",
|
|
61
|
+
] as const satisfies readonly DataListViewType[]
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Default allowlist for **list-page hubs** in this design system (Library / Column types /
|
|
65
|
+
* Tokens, etc.). Matches the All questions hub: table, list, board, dashboard, folder,
|
|
66
|
+
* panel, and tree-panel. Pair with renderers for each kind on `HubTable` + `ListPageTemplate`.
|
|
67
|
+
*/
|
|
68
|
+
export const FULL_HUB_SUPPORTED_VIEWS = [
|
|
69
|
+
"table",
|
|
70
|
+
"list",
|
|
71
|
+
"board",
|
|
72
|
+
"dashboard",
|
|
73
|
+
"folder",
|
|
74
|
+
"panel",
|
|
75
|
+
"tree-panel",
|
|
76
|
+
] as const satisfies readonly DataListViewType[]
|
|
77
|
+
|
|
78
|
+
/** Every registered view type (includes folder, panel, calendar, tree-panel). */
|
|
79
|
+
export const ALL_DATA_LIST_VIEW_TYPES = DATA_LIST_VIEW_REGISTRY.map(d => d.value)
|
|
80
|
+
|
|
50
81
|
export function dataListViewDefinition(view: DataListViewType): DataListViewDefinition {
|
|
51
82
|
const def = BY_VALUE.get(view)
|
|
52
83
|
if (!def) {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar / secondary-nav active-state helpers.
|
|
3
|
+
*
|
|
4
|
+
* Only one nav target should be "selected" at a time. Prefix matching
|
|
5
|
+
* (`/dashboard` active on `/dashboard/students`) is correct only when no
|
|
6
|
+
* more-specific nav href also matches the current pathname.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function normalizePathname(pathname: string): string {
|
|
10
|
+
if (pathname.length > 1 && pathname.endsWith("/")) return pathname.slice(0, -1)
|
|
11
|
+
return pathname
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Path segment before `#` (if any). */
|
|
15
|
+
export function navUrlPath(url: string): string {
|
|
16
|
+
const hash = url.indexOf("#")
|
|
17
|
+
return hash >= 0 ? url.slice(0, hash) : url
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Hash fragment after `#`, or `null` when the href has no fragment. */
|
|
21
|
+
export function navUrlFragment(url: string): string | null {
|
|
22
|
+
const i = url.indexOf("#")
|
|
23
|
+
return i >= 0 ? url.slice(i + 1) : null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function normalizedLocationHash(locationHash: string): string {
|
|
27
|
+
if (!locationHash) return ""
|
|
28
|
+
return locationHash.startsWith("#") ? locationHash.slice(1) : locationHash
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Path → hash fragments claimed by *another* nav item at the same path. */
|
|
32
|
+
export function buildNavHashClaims(
|
|
33
|
+
urls: readonly string[],
|
|
34
|
+
): ReadonlyMap<string, ReadonlySet<string>> {
|
|
35
|
+
const map = new Map<string, Set<string>>()
|
|
36
|
+
for (const url of urls) {
|
|
37
|
+
const p = navUrlPath(url)
|
|
38
|
+
const f = navUrlFragment(url)
|
|
39
|
+
if (!p || f === null) continue
|
|
40
|
+
let set = map.get(p)
|
|
41
|
+
if (!set) {
|
|
42
|
+
set = new Set<string>()
|
|
43
|
+
map.set(p, set)
|
|
44
|
+
}
|
|
45
|
+
set.add(f)
|
|
46
|
+
}
|
|
47
|
+
return map
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function navHasMoreSpecificHashMatch(
|
|
51
|
+
pathname: string,
|
|
52
|
+
locationHash: string,
|
|
53
|
+
hashClaimsByPath: ReadonlyMap<string, ReadonlySet<string>>,
|
|
54
|
+
): boolean {
|
|
55
|
+
const claims = hashClaimsByPath.get(pathname)
|
|
56
|
+
if (!claims) return false
|
|
57
|
+
const h = normalizedLocationHash(locationHash)
|
|
58
|
+
if (h === "") return false
|
|
59
|
+
return claims.has(h)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function pathnameMatchesNavPath(
|
|
63
|
+
pathname: string,
|
|
64
|
+
pathOnly: string,
|
|
65
|
+
locationHash: string,
|
|
66
|
+
hashClaimsByPath: ReadonlyMap<string, ReadonlySet<string>>,
|
|
67
|
+
): boolean {
|
|
68
|
+
const norm = normalizePathname(pathname)
|
|
69
|
+
const h = normalizedLocationHash(locationHash)
|
|
70
|
+
|
|
71
|
+
if (pathOnly === "/") {
|
|
72
|
+
if (norm !== "/" || h !== "") return false
|
|
73
|
+
return !navHasMoreSpecificHashMatch("/", locationHash, hashClaimsByPath)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (norm === pathOnly) {
|
|
77
|
+
return !navHasMoreSpecificHashMatch(pathOnly, locationHash, hashClaimsByPath)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (pathOnly === "/library") {
|
|
81
|
+
return norm.startsWith("/library/")
|
|
82
|
+
}
|
|
83
|
+
if (pathOnly.startsWith("/library/")) {
|
|
84
|
+
return norm === pathOnly
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return norm.startsWith(`${pathOnly}/`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Longest matching nav path for `pathname` among `candidateUrls` (path + prefix rules).
|
|
92
|
+
* Returns the winning href string, or `null`.
|
|
93
|
+
*/
|
|
94
|
+
export function resolveActiveNavHref(
|
|
95
|
+
pathname: string,
|
|
96
|
+
candidateUrls: readonly string[],
|
|
97
|
+
options?: {
|
|
98
|
+
locationHash?: string
|
|
99
|
+
hashClaimsByPath?: ReadonlyMap<string, ReadonlySet<string>>
|
|
100
|
+
},
|
|
101
|
+
): string | null {
|
|
102
|
+
const norm = normalizePathname(pathname)
|
|
103
|
+
const hashClaims = options?.hashClaimsByPath ?? buildNavHashClaims(candidateUrls)
|
|
104
|
+
const locationHash = options?.locationHash ?? ""
|
|
105
|
+
|
|
106
|
+
let bestHref: string | null = null
|
|
107
|
+
let bestLen = -1
|
|
108
|
+
|
|
109
|
+
for (const url of candidateUrls) {
|
|
110
|
+
const pathOnly = navUrlPath(url)
|
|
111
|
+
const frag = navUrlFragment(url)
|
|
112
|
+
if (!pathOnly || pathOnly === "#") continue
|
|
113
|
+
|
|
114
|
+
let matches = false
|
|
115
|
+
if (frag !== null) {
|
|
116
|
+
const h = normalizedLocationHash(locationHash)
|
|
117
|
+
if (pathOnly === "/") matches = norm === "/" && h === frag
|
|
118
|
+
else if (pathOnly === "/library") matches = norm.startsWith("/library/") && h === frag
|
|
119
|
+
else if (pathOnly.startsWith("/library/")) matches = norm === pathOnly && h === frag
|
|
120
|
+
else matches = norm === pathOnly && h === frag
|
|
121
|
+
} else {
|
|
122
|
+
matches = pathnameMatchesNavPath(norm, pathOnly, locationHash, hashClaims)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (matches && pathOnly.length > bestLen) {
|
|
126
|
+
bestHref = url
|
|
127
|
+
bestLen = pathOnly.length
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return bestHref
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** True when `url` is the single best match for `pathname` among all nav hrefs. */
|
|
135
|
+
export function isNavHrefActive(
|
|
136
|
+
pathname: string,
|
|
137
|
+
url: string,
|
|
138
|
+
allNavUrls: readonly string[],
|
|
139
|
+
options?: {
|
|
140
|
+
locationHash?: string
|
|
141
|
+
hashClaimsByPath?: ReadonlyMap<string, ReadonlySet<string>>
|
|
142
|
+
},
|
|
143
|
+
): boolean {
|
|
144
|
+
const active = resolveActiveNavHref(pathname, allNavUrls, options)
|
|
145
|
+
if (!active) return false
|
|
146
|
+
return navUrlPath(active) === navUrlPath(url) && navUrlFragment(active) === navUrlFragment(url)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Collect every `url` from a nav tree (primary rows + children). */
|
|
150
|
+
export function collectNavUrls(
|
|
151
|
+
items: ReadonlyArray<{ url?: string; children?: ReadonlyArray<{ url?: string; children?: unknown }> }>,
|
|
152
|
+
): string[] {
|
|
153
|
+
const out: string[] = []
|
|
154
|
+
const walk = (list: ReadonlyArray<{ url?: string; children?: ReadonlyArray<{ url?: string; children?: unknown }> }>) => {
|
|
155
|
+
for (const it of list) {
|
|
156
|
+
if (typeof it.url === "string" && it.url.length > 0 && it.url !== "#") out.push(it.url)
|
|
157
|
+
if (Array.isArray(it.children)) walk(it.children)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
walk(items)
|
|
161
|
+
return out
|
|
162
|
+
}
|
|
@@ -112,7 +112,8 @@ ListPageTemplate
|
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
**Reference implementations:**
|
|
115
|
-
- `components/
|
|
115
|
+
- `components/library-table.tsx` + `library-client.tsx` — canonical seven-view hub (start here)
|
|
116
|
+
- `components/columns-showcase.tsx` — cell catalog via `LibraryTable` (custom `columnDefs`, same Add view as Library)
|
|
116
117
|
- `components/tokens-themes-client.tsx` + `components/tokens-secondary-nav.tsx` — hub with secondary panel + URL-driven scope + built-in pagination chrome
|
|
117
118
|
- `components/library-table.tsx` + `components/library-hub-client.tsx` — full multi-view hub (table, board, dashboard)
|
|
118
119
|
|
|
@@ -17,7 +17,7 @@ alwaysApply: true
|
|
|
17
17
|
4. **Touch targets (2.5.8):** **≥ 24×24 CSS px** or **24px** spacing — **`min-h-6 min-w-6` / `size-6`** for icon-only; avoid **`size-4`** as sole target.
|
|
18
18
|
5. **Contrast:** normal text **≥ 4.5:1**; UI / focus **≥ 3:1** where required; muted on tinted surfaces use correct surface tokens.
|
|
19
19
|
6. **Minimum text size:** visible product copy **≥ 11px** — **`text-xs`** or larger (**`AGENTS.md` §8.3**, **`app/globals.css`** `--text-xs`).
|
|
20
|
-
7. **Dialogs / sheets
|
|
20
|
+
7. **Dialogs / sheets:** must have a **Title** (`DialogTitle` / `SheetTitle`); **`sr-only`** if hidden.
|
|
21
21
|
8. **Format hints persistent, not placeholders (SC 3.3.2, 1.3.1).** Fields with required formats — **date, time, phone, currency, GPA, IDs, URLs, unit-bearing numbers** — MUST render the format via **`FormDescription`** (or equivalent `aria-describedby` helper text). Placeholders disappear on focus and **MUST NOT** be the sole carrier. Prefer picker primitives (e.g. `DatePickerField`) over free-text where available.
|
|
22
22
|
9. **Every icon that communicates information MUST have a text alternative** — not just icon-only buttons. Three cases (SC 1.1.1, 3.3.2, 2.4.6):
|
|
23
23
|
|
package/template/AGENTS.md
CHANGED
|
@@ -81,7 +81,7 @@ If two documents conflict, prefer the **more specific** rule for the file type,
|
|
|
81
81
|
|
|
82
82
|
**MUST:** If the main surface is a **`DataTable`** (or equivalent data grid), wrap it in **`ListPageTemplate`** so the **views toolbar** exists (tabs, add view, per-tab settings). Do **not** place `DataTable` only under `PageHeader` without the tab shell.
|
|
83
83
|
|
|
84
|
-
**Reference implementations:** `components/library-
|
|
84
|
+
**Reference implementations:** `components/library-client.tsx` + `components/library-table.tsx` (canonical seven-view hub), `components/columns-showcase.tsx` (cell catalog via `LibraryTable`), `components/tokens-themes-client.tsx` + `components/tokens-hub-auxiliary-views.tsx`.
|
|
85
85
|
|
|
86
86
|
**Rationale:** Consistent navigation, saved views, per-tab view type (table / list / board / dashboard), export at template level.
|
|
87
87
|
|
|
@@ -99,6 +99,20 @@ If two documents conflict, prefer the **more specific** rule for the file type,
|
|
|
99
99
|
|
|
100
100
|
**Details:** `docs/data-views-pattern.md` (mock data, connected views, dashboard view).
|
|
101
101
|
|
|
102
|
+
### 4.1.1 Add view parity (`supportedViewTypes`)
|
|
103
|
+
|
|
104
|
+
**MUST:** Every **`ListPageTemplate`** hub that mounts **`HubTable`** uses the same **seven** Add view options as Library (All questions) unless a narrower list is **documented** in `lib/<entity>-supported-views.ts`:
|
|
105
|
+
|
|
106
|
+
- Registry constant: **`FULL_HUB_SUPPORTED_VIEWS`** (`@/lib/data-list-view-registry` or `@/lib/full-hub-supported-views.ts`).
|
|
107
|
+
- Pass the **same** allowlist to **`ListPageTemplate`** and **`HubTable`** (or omit on both for the default).
|
|
108
|
+
- Implement a **real renderer** for each allowed view (list = **`ListPageBoardCard`** via `renderListRow` — copy **`library-table.tsx`**).
|
|
109
|
+
- **`LibraryItem`** catalogs (Column types): use **`LibraryTable`** with `columnDefs` + `folders` — do not trim to four views or placeholder list rows.
|
|
110
|
+
- **Tokens:** **`tokens-hub-auxiliary-views.tsx`** + **`FULL_HUB_SUPPORTED_VIEWS`**.
|
|
111
|
+
|
|
112
|
+
**MUST NOT:** `supportedViewTypes={["table"]}`, bare two-line `renderListRow`, or `PRIMARY_HUB_SUPPORTED_VIEWS` without documented product exception.
|
|
113
|
+
|
|
114
|
+
**Binding rule:** `.cursor/rules/exxat-hub-supported-views.mdc`. **Pattern doc:** `docs/hub-supported-views-pattern.md`.
|
|
115
|
+
|
|
102
116
|
### 4.2 `TablePropertiesDrawer` and the active view
|
|
103
117
|
|
|
104
118
|
**MUST:** Any page that uses **`ListPageTemplate`** with **`tab.viewType`** (table / list / board / dashboard) and renders **`TablePropertiesDrawer`** **MUST** pass:
|
|
@@ -352,9 +366,9 @@ Follow root **`.cursor/rules/exxat-kbd-shortcuts.mdc`**. Summary:
|
|
|
352
366
|
- **UI components** (borders, focus rings where required): **≥ 3:1**.
|
|
353
367
|
- **Muted text on tinted surfaces** (e.g. sidebar): use tokens mixed against the **correct surface** (e.g. **`--sidebar`** / `--sidebar-section-label-foreground`), not only `--background`.
|
|
354
368
|
|
|
355
|
-
### 8.4 Overlays (Dialog / Sheet
|
|
369
|
+
### 8.4 Overlays (Dialog / Sheet)
|
|
356
370
|
|
|
357
|
-
**MUST:** Provide an accessible **title** — `DialogTitle` / `SheetTitle
|
|
371
|
+
**MUST:** Provide an accessible **title** — `DialogTitle` / `SheetTitle`; use **`className="sr-only"`** when the title is visually hidden (align with shadcn patterns in this repo). Product side panels use **`Sheet`** only (Export, Properties, invite — not a separate Vaul drawer primitive).
|
|
358
372
|
|
|
359
373
|
### 8.5 Verification
|
|
360
374
|
|
|
@@ -634,6 +648,7 @@ Copy and complete when implementing or reviewing:
|
|
|
634
648
|
- [ ] **Primary hub + large data:** Same composition as `PlacementsClient` / `TeamClient` (template + metrics when applicable).
|
|
635
649
|
- [ ] **All view tabs:** List/board/dashboard use **`tableState.rows`**; dashboard view uses **`KeyMetrics`** + shared KPI helpers — no “not wired” placeholders or duplicate metric cards.
|
|
636
650
|
- [ ] **Properties drawer:** **`TablePropertiesDrawer`** receives **`currentView`** and **`onViewChange`** from **`renderContent`** / **`updateTab`** + **`dataListViewIcon`** (§4.2) — not table-only copy on Board/List/Dashboard.
|
|
651
|
+
- [ ] **Add view parity:** **`FULL_HUB_SUPPORTED_VIEWS`** on **`ListPageTemplate`** + **`HubTable`** (in sync); every allowed view has a renderer; list uses **`ListPageBoardCard`** — **`.cursor/rules/exxat-hub-supported-views.mdc`**, **`docs/hub-supported-views-pattern.md`**.
|
|
637
652
|
- [ ] **Data view dashboard (Placements / Team / Compliance):** Charts use **`ChartFigure`** + **`ChartDataTable`**; **Edit layout** on toolbar; **`activeBar` / `activeShape`** keyboard styling from **`lib/chart-keyboard-selection`** — not opacity-only **`Cell`** hacks (§4.3).
|
|
638
653
|
- [ ] **Dashboard layout persistence:** **`lib/data-view-dashboard-storage`** (or **`saveDashboardLayout`** / **`loadDashboardLayout`** on Placements); **`mergeDashboardLayout`** on load — no new ad-hoc storage keys for the same layout (§4.3).
|
|
639
654
|
- [ ] **⌘K palette (§7.1):** If adding or changing **`dataGroups`**, map rows in **`lib/command-menu-search-data.ts`** (not `command-menu.tsx`); use **`searchOnly`** on bulky groups; keep **`docs/command-menu-pattern.md`** aligned.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* (`PrimaryPageTemplate` + `ListPageTemplate`):
|
|
8
8
|
* - `header` : `PageHeader` with title + one-line subtitle describing the demo.
|
|
9
9
|
* - `metrics` : `KeyMetrics` `variant="flat"` — patterns, pinned, sortable, demo rows.
|
|
10
|
-
* - tabs :
|
|
10
|
+
* - tabs : default `table` tab; Add view offers list / board / dashboard (same as Library).
|
|
11
11
|
* - `renderContent` : the `<ColumnsShowcase />` DataTable surface.
|
|
12
12
|
*
|
|
13
13
|
* Cell patterns are exercised inside `columns-showcase.tsx` so the rendered
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
COLUMNS_SHOWCASE_PATTERN_COUNT,
|
|
34
34
|
COLUMNS_SHOWCASE_PINNED_COUNT,
|
|
35
35
|
COLUMNS_SHOWCASE_SORTABLE_COUNT,
|
|
36
|
+
COLUMNS_SUPPORTED_VIEWS,
|
|
36
37
|
} from "@/components/columns-showcase"
|
|
37
38
|
|
|
38
39
|
const COLUMNS_DEFAULT_TABS: ViewTab[] = [
|
|
@@ -127,8 +128,8 @@ export function ColumnsClient() {
|
|
|
127
128
|
onTabsChange={setTabs}
|
|
128
129
|
activeTabId={activeTabId}
|
|
129
130
|
onActiveTabChange={setActiveTabId}
|
|
130
|
-
supportedViewTypes={["table"]}
|
|
131
131
|
getTabCount={getTabCount}
|
|
132
|
+
supportedViewTypes={COLUMNS_SUPPORTED_VIEWS}
|
|
132
133
|
header={
|
|
133
134
|
<PageHeader
|
|
134
135
|
title="Column types"
|
|
@@ -48,7 +48,6 @@
|
|
|
48
48
|
|
|
49
49
|
import * as React from "react"
|
|
50
50
|
import {
|
|
51
|
-
HubTable,
|
|
52
51
|
AttachmentCountCell,
|
|
53
52
|
BooleanToggleCell,
|
|
54
53
|
CurrencyCell,
|
|
@@ -66,6 +65,9 @@ import {
|
|
|
66
65
|
type RowActionDef,
|
|
67
66
|
} from "@/components/data-views"
|
|
68
67
|
import type { DataListViewType } from "@/lib/data-list-view"
|
|
68
|
+
import { FULL_HUB_SUPPORTED_VIEWS } from "@/lib/data-list-view-registry"
|
|
69
|
+
import { LibraryTable } from "@/components/library-table"
|
|
70
|
+
import { DEFAULT_LIBRARY_FOLDERS, type LibraryFolder } from "@/lib/mock/library-folders"
|
|
69
71
|
import { AvatarInitials } from "@/components/ui/avatar"
|
|
70
72
|
import { cn } from "@/lib/utils"
|
|
71
73
|
import {
|
|
@@ -477,7 +479,8 @@ export const COLUMNS_SHOWCASE_PATTERN_COUNT = 18
|
|
|
477
479
|
export const COLUMNS_SHOWCASE_PINNED_COUNT = 3 // select + name + actions
|
|
478
480
|
export const COLUMNS_SHOWCASE_SORTABLE_COUNT = 11 // name, owner, type, level, rating, progress, cost, count, files, lastActivityAt, updatedAt
|
|
479
481
|
|
|
480
|
-
|
|
482
|
+
/** Same seven views as Library / All questions (Add view + Properties). */
|
|
483
|
+
export const COLUMNS_SUPPORTED_VIEWS = FULL_HUB_SUPPORTED_VIEWS
|
|
481
484
|
|
|
482
485
|
export interface ColumnsShowcaseProps {
|
|
483
486
|
/** Active view from `ListPageTemplate.renderContent`. */
|
|
@@ -493,6 +496,9 @@ export interface ColumnsShowcaseProps {
|
|
|
493
496
|
*/
|
|
494
497
|
export function ColumnsShowcase({ view, onViewChange }: ColumnsShowcaseProps) {
|
|
495
498
|
const [rows, setRows] = React.useState<LibraryItem[]>(() => buildRows())
|
|
499
|
+
const [folders, setFolders] = React.useState<LibraryFolder[]>(() =>
|
|
500
|
+
DEFAULT_LIBRARY_FOLDERS.map(f => ({ ...f })),
|
|
501
|
+
)
|
|
496
502
|
const [pagination, setPagination] = React.useState(false)
|
|
497
503
|
|
|
498
504
|
const toggleFavorite = React.useCallback((row: LibraryItem) => {
|
|
@@ -514,28 +520,26 @@ export function ColumnsShowcase({ view, onViewChange }: ColumnsShowcaseProps) {
|
|
|
514
520
|
const columns = useColumns(toggleFavorite, togglePublished)
|
|
515
521
|
|
|
516
522
|
return (
|
|
517
|
-
<
|
|
518
|
-
|
|
519
|
-
|
|
523
|
+
<LibraryTable
|
|
524
|
+
items={rows}
|
|
525
|
+
onItemsChange={setRows}
|
|
526
|
+
folders={folders}
|
|
527
|
+
onFoldersChange={setFolders}
|
|
520
528
|
view={view}
|
|
521
529
|
onViewChange={onViewChange}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
530
|
+
columnDefs={columns}
|
|
531
|
+
hubLabels={{
|
|
532
|
+
hubLabel: "Column types",
|
|
533
|
+
lifecycleTabLabel: "Column types",
|
|
534
|
+
searchAriaLabel: "Search columns showcase",
|
|
535
|
+
listAriaLabel: "Column types",
|
|
536
|
+
defaultSort: { key: "stem", dir: "asc" },
|
|
537
|
+
}}
|
|
529
538
|
pagination={pagination}
|
|
530
539
|
onPaginationChange={setPagination}
|
|
531
540
|
paginationInitialPageSize={5}
|
|
532
541
|
paginationPageSizeOptions={[5, 10, 25]}
|
|
533
|
-
|
|
534
|
-
<p className="text-sm text-muted-foreground">
|
|
535
|
-
No rows match your filters.
|
|
536
|
-
</p>
|
|
537
|
-
}
|
|
538
|
-
renderers={{}}
|
|
542
|
+
showBulkActions={false}
|
|
539
543
|
/>
|
|
540
544
|
)
|
|
541
545
|
}
|
|
@@ -186,7 +186,7 @@ function ExxatLogoBase({
|
|
|
186
186
|
export function ExxatProductLogo({
|
|
187
187
|
product,
|
|
188
188
|
className,
|
|
189
|
-
variant = "default",
|
|
189
|
+
variant: _variant = "default",
|
|
190
190
|
}: ExxatProductLogoProps) {
|
|
191
191
|
const customProductBrand = useAppStore(s => s.customProductBrand)
|
|
192
192
|
const productBrandColors = useAppStore(s => s.productBrandColors)
|
|
@@ -19,8 +19,8 @@ import {
|
|
|
19
19
|
HubTable,
|
|
20
20
|
type HubTableHandle,
|
|
21
21
|
type HubTableRenderers,
|
|
22
|
-
type HubTableRendererArgs,
|
|
23
22
|
} from "@/components/data-views"
|
|
23
|
+
import { Skeleton } from "@/components/ui/skeleton"
|
|
24
24
|
import { LIBRARY_SUPPORTED_VIEWS } from "@/lib/library-supported-views"
|
|
25
25
|
import { Button } from "@/components/ui/button"
|
|
26
26
|
import {
|
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
DropdownMenuTrigger,
|
|
31
31
|
} from "@/components/ui/dropdown-menu"
|
|
32
32
|
import { Tip } from "@/components/ui/tip"
|
|
33
|
-
import { Skeleton } from "@/components/ui/skeleton"
|
|
34
33
|
import {
|
|
35
34
|
ResizableHandle,
|
|
36
35
|
ResizablePanel,
|
|
@@ -581,6 +580,14 @@ function libraryPanelDetail(row: LibraryItem) {
|
|
|
581
580
|
|
|
582
581
|
export type LibraryTableHandle = HubTableHandle
|
|
583
582
|
|
|
583
|
+
export interface LibraryTableHubLabels {
|
|
584
|
+
hubLabel: string
|
|
585
|
+
lifecycleTabLabel: string
|
|
586
|
+
searchAriaLabel: string
|
|
587
|
+
listAriaLabel?: string
|
|
588
|
+
defaultSort?: { key: string; dir: "asc" | "desc" }
|
|
589
|
+
}
|
|
590
|
+
|
|
584
591
|
export interface LibraryTableProps {
|
|
585
592
|
items: LibraryItem[]
|
|
586
593
|
/** When set, table / board / tree rows are limited to this nav scope (secondary sidebar). */
|
|
@@ -596,6 +603,15 @@ export interface LibraryTableProps {
|
|
|
596
603
|
folders: LibraryFolder[]
|
|
597
604
|
onFoldersChange: React.Dispatch<React.SetStateAction<LibraryFolder[]>>
|
|
598
605
|
onItemsChange: React.Dispatch<React.SetStateAction<LibraryItem[]>>
|
|
606
|
+
/** e.g. Column types showcase — custom `ColumnDef`s while reusing list/board/folder renderers. */
|
|
607
|
+
columnDefs?: ColumnDef<LibraryItem>[]
|
|
608
|
+
/** Override default Library copy when {@link columnDefs} is set. */
|
|
609
|
+
hubLabels?: LibraryTableHubLabels
|
|
610
|
+
pagination?: boolean
|
|
611
|
+
onPaginationChange?: (v: boolean) => void
|
|
612
|
+
paginationInitialPageSize?: number
|
|
613
|
+
paginationPageSizeOptions?: number[]
|
|
614
|
+
showBulkActions?: boolean
|
|
599
615
|
}
|
|
600
616
|
|
|
601
617
|
export const LibraryTable = React.forwardRef<LibraryTableHandle, LibraryTableProps>(
|
|
@@ -611,6 +627,13 @@ export const LibraryTable = React.forwardRef<LibraryTableHandle, LibraryTablePro
|
|
|
611
627
|
folders,
|
|
612
628
|
onFoldersChange,
|
|
613
629
|
onItemsChange,
|
|
630
|
+
columnDefs,
|
|
631
|
+
hubLabels,
|
|
632
|
+
pagination,
|
|
633
|
+
onPaginationChange,
|
|
634
|
+
paginationInitialPageSize,
|
|
635
|
+
paginationPageSizeOptions,
|
|
636
|
+
showBulkActions = true,
|
|
614
637
|
},
|
|
615
638
|
ref,
|
|
616
639
|
) {
|
|
@@ -628,10 +651,18 @@ export const LibraryTable = React.forwardRef<LibraryTableHandle, LibraryTablePro
|
|
|
628
651
|
)
|
|
629
652
|
|
|
630
653
|
const columns = React.useMemo(
|
|
631
|
-
() =>
|
|
632
|
-
|
|
654
|
+
() =>
|
|
655
|
+
columnDefs ??
|
|
656
|
+
buildLibraryColumns(tableSourceItems, { onToggleFavorite: toggleFavorite }),
|
|
657
|
+
[columnDefs, tableSourceItems, toggleFavorite],
|
|
633
658
|
)
|
|
634
659
|
|
|
660
|
+
const hubLabel = hubLabels?.hubLabel ?? "Library"
|
|
661
|
+
const lifecycleTabLabel = hubLabels?.lifecycleTabLabel ?? "Library"
|
|
662
|
+
const searchAriaLabel = hubLabels?.searchAriaLabel ?? "Search questions"
|
|
663
|
+
const listAriaLabel = hubLabels?.listAriaLabel ?? "Questions"
|
|
664
|
+
const defaultSort = hubLabels?.defaultSort ?? { key: "updatedAt", dir: "desc" as const }
|
|
665
|
+
|
|
635
666
|
// ─ New-folder / customize-folder modal state (shared by panel + tree-panel) ────
|
|
636
667
|
const [newFolderOpen, setNewFolderOpen] = React.useState(false)
|
|
637
668
|
const [newFolderParentId, setNewFolderParentId] = React.useState<string | null>(null)
|
|
@@ -750,18 +781,22 @@ export const LibraryTable = React.forwardRef<LibraryTableHandle, LibraryTablePro
|
|
|
750
781
|
view={view}
|
|
751
782
|
onViewChange={onViewChange}
|
|
752
783
|
supportedViewTypes={LIBRARY_SUPPORTED_VIEWS}
|
|
753
|
-
hubLabel=
|
|
754
|
-
lifecycleTabLabel=
|
|
755
|
-
searchAriaLabel=
|
|
784
|
+
hubLabel={hubLabel}
|
|
785
|
+
lifecycleTabLabel={lifecycleTabLabel}
|
|
786
|
+
searchAriaLabel={searchAriaLabel}
|
|
756
787
|
getRowId={row => row.id}
|
|
757
788
|
getRowSelectionLabel={row => row.stem}
|
|
758
|
-
defaultSort={
|
|
789
|
+
defaultSort={defaultSort}
|
|
759
790
|
emptyState={<p className="text-sm text-muted-foreground">No questions in the bank.</p>}
|
|
760
791
|
boardGroupByColumnOptions={[...LIBRARY_BOARD_GROUP_OPTIONS]}
|
|
761
792
|
renderFilterOptionValue={renderFilterOptionValue}
|
|
762
793
|
syncedSearchFromUrl={searchLanding ? undefined : urlListSearch}
|
|
763
|
-
listAriaLabel=
|
|
794
|
+
listAriaLabel={listAriaLabel}
|
|
764
795
|
listEmptyState="No questions match your filters."
|
|
796
|
+
pagination={pagination}
|
|
797
|
+
onPaginationChange={onPaginationChange}
|
|
798
|
+
paginationInitialPageSize={paginationInitialPageSize}
|
|
799
|
+
paginationPageSizeOptions={paginationPageSizeOptions}
|
|
765
800
|
renderListRow={row => (
|
|
766
801
|
<ListPageBoardCard
|
|
767
802
|
className={LIBRARY_FAVORITE_HOVER_GROUP}
|
|
@@ -784,20 +819,24 @@ export const LibraryTable = React.forwardRef<LibraryTableHandle, LibraryTablePro
|
|
|
784
819
|
</div>
|
|
785
820
|
</ListPageBoardCard>
|
|
786
821
|
)}
|
|
787
|
-
bulkActionsSlot={
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
822
|
+
bulkActionsSlot={
|
|
823
|
+
showBulkActions
|
|
824
|
+
? selected => {
|
|
825
|
+
if (selected.size === 0) return null
|
|
826
|
+
return (
|
|
827
|
+
<>
|
|
828
|
+
<span className="sr-only">{selected.size} selected</span>
|
|
829
|
+
<Tip label="Export selection (demo)">
|
|
830
|
+
<Button size="sm" variant="outline" type="button">
|
|
831
|
+
<i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
|
|
832
|
+
Export
|
|
833
|
+
</Button>
|
|
834
|
+
</Tip>
|
|
835
|
+
</>
|
|
836
|
+
)
|
|
837
|
+
}
|
|
838
|
+
: undefined
|
|
839
|
+
}
|
|
801
840
|
renderers={renderers}
|
|
802
841
|
handleRef={ref}
|
|
803
842
|
/>
|
|
@@ -338,13 +338,6 @@ function folderBreadcrumb(folderId: string, folders: LibraryFolder[]): string {
|
|
|
338
338
|
return parent ? `${parent.name} / ${f.name}` : f.name
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
-
/** 0–100 percentage of the difficulty meter for the given level. */
|
|
342
|
-
function difficultyToPercent(value: "easy" | "medium" | "hard"): number {
|
|
343
|
-
if (value === "easy") return 18
|
|
344
|
-
if (value === "hard") return 82
|
|
345
|
-
return 50
|
|
346
|
-
}
|
|
347
|
-
|
|
348
341
|
/**
|
|
349
342
|
* Folder-aware difficulty insight — derived deterministically from the
|
|
350
343
|
* folder id so the same folder always returns the same numbers. In a real
|