@exxatdesignux/ui 0.2.16 → 0.2.17
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 +11 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +148 -3
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/accessibility.md +142 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/coach-marks.md +169 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/data-table-pattern.md +382 -0
- package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +56 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +14 -0
- package/package.json +3 -3
- package/src/components/ui/banner.tsx +2 -0
- package/src/components/ui/chart.tsx +57 -2
- package/src/components/ui/sidebar.tsx +1 -0
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +1 -1
- package/template/.cursor/rules/exxat-mono-ids.mdc +30 -0
- package/template/AGENTS.md +18 -15
- package/template/app/(app)/data-list/page.tsx +2 -2
- package/template/app/(app)/question-bank/layout.tsx +18 -5
- package/template/app/(app)/question-bank/new/page.tsx +58 -0
- package/template/app/globals.css +108 -1
- package/template/app/layout.tsx +41 -5
- package/template/components/app-sidebar.tsx +68 -34
- package/template/components/ask-leo-sidebar.tsx +0 -2
- package/template/components/brand-color-picker.tsx +344 -0
- package/template/components/compliance-list-view.tsx +33 -51
- package/template/components/compliance-table.tsx +24 -0
- package/template/components/data-table/index.tsx +68 -24
- package/template/components/data-table/pagination.tsx +0 -1
- package/template/components/data-table/types.ts +4 -1
- package/template/components/data-table/use-table-state.ts +243 -94
- package/template/components/data-views/data-row-list.tsx +183 -0
- package/template/components/data-views/index.ts +7 -3
- package/template/components/data-views/os-folder-glyph.tsx +8 -0
- package/template/components/export-drawer.tsx +1 -1
- package/template/components/exxat-product-logo.tsx +172 -317
- package/template/components/invite-collaborators-drawer.tsx +5 -3
- package/template/components/key-metrics.tsx +74 -46
- package/template/components/new-placement-form.tsx +4 -2
- package/template/components/new-question-composer.tsx +2208 -0
- package/template/components/page-breadcrumb-trail.tsx +131 -0
- package/template/components/page-header.tsx +2 -1
- package/template/components/{data-views/placement-board-card.tsx → placement-board-card.tsx} +1 -1
- package/template/components/placement-detail.tsx +1 -1
- package/template/components/placements-board-view.tsx +1 -1
- package/template/components/{data-list-client.tsx → placements-client.tsx} +9 -7
- package/template/components/placements-list-view.tsx +18 -132
- package/template/components/{data-list-table-cells.test.tsx → placements-table-cells.test.tsx} +2 -2
- package/template/components/{data-list-table-cells.tsx → placements-table-cells.tsx} +1 -1
- package/template/components/placements-table-columns.tsx +2 -2
- package/template/components/{data-list-table.tsx → placements-table.tsx} +67 -58
- package/template/components/product-switcher.tsx +26 -8
- package/template/components/product-wordmark.tsx +285 -0
- package/template/components/question-bank-client.tsx +20 -2
- package/template/components/question-bank-hub-client.tsx +108 -115
- package/template/components/question-bank-list-view.tsx +30 -54
- package/template/components/question-bank-new-folder-sheet.tsx +1 -1
- package/template/components/question-bank-secondary-nav.tsx +0 -3
- package/template/components/question-bank-table.tsx +30 -5
- package/template/components/rotations-empty-state.tsx +3 -0
- package/template/components/secondary-panel.tsx +23 -3
- package/template/components/settings-appearance-card.tsx +584 -141
- package/template/components/site-header.tsx +36 -31
- package/template/components/sites-list-view.tsx +31 -36
- package/template/components/sites-table.tsx +24 -0
- package/template/components/table-properties/drawer.tsx +1 -1
- package/template/components/team-client.tsx +1 -1
- package/template/components/team-list-view.tsx +34 -50
- package/template/components/team-table.tsx +29 -3
- package/template/components/templates/nested-secondary-panel-shell.tsx +8 -2
- package/template/components/ui/dot-pattern.tsx +50 -26
- package/template/components/ui/leo-icon.tsx +23 -3
- package/template/contexts/product-context.tsx +51 -7
- package/template/contexts/system-banner-context.tsx +112 -4
- package/template/eslint.config.mjs +18 -0
- package/template/hooks/use-sidebar-reflow-zoom.ts +21 -11
- package/template/lib/data-list-persistence.ts +57 -257
- package/template/lib/dev-log.test.ts +6 -5
- package/template/lib/exxat-palette.json +1462 -0
- package/template/lib/exxat-palette.ts +136 -0
- package/template/lib/list-page-table-properties.ts +1 -1
- package/template/lib/list-status-badges.ts +1 -1
- package/template/lib/mailto.ts +29 -0
- package/template/lib/placement-board-card-layout.ts +1 -1
- package/template/lib/product-brand.ts +268 -0
- package/template/lib/question-bank-authoring.ts +308 -0
- package/template/lib/question-bank-nav.ts +44 -0
- package/template/lib/raf-throttle.ts +45 -0
- package/template/lib/table-state-lifecycle.ts +474 -0
- package/template/next.config.mjs +156 -0
- package/template/package.json +3 -3
- package/template/stores/app-store.ts +46 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DataRowList — generic vertical-stack list view used by every hub's "list"
|
|
5
|
+
* tab (placements, team, compliance, sites, question-bank, …). Replaces the
|
|
6
|
+
* hand-rolled `<ul …flex-col gap-2 px-4 pb-8 pt-2 lg:px-6> {rows.map(<li>…)}`
|
|
7
|
+
* shell that was duplicated across `*-list-view.tsx` files.
|
|
8
|
+
*
|
|
9
|
+
* Composition over inheritance: callers provide a `renderRow(row)` that
|
|
10
|
+
* returns whatever ListPageBoardCard / link / chip-stack they need — this
|
|
11
|
+
* component owns the chrome (spacing, empty state, virtualization), not the
|
|
12
|
+
* row body.
|
|
13
|
+
*
|
|
14
|
+
* Auto-virtualises with `@tanstack/react-virtual` when the row count meets
|
|
15
|
+
* `virtualizeThreshold` (default 100). Disable by passing `0`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as React from "react"
|
|
19
|
+
import { useWindowVirtualizer } from "@tanstack/react-virtual"
|
|
20
|
+
import { cn } from "@/lib/utils"
|
|
21
|
+
|
|
22
|
+
const DEFAULT_VIRTUALIZE_THRESHOLD = 100
|
|
23
|
+
const DEFAULT_ESTIMATED_ROW_HEIGHT = 96
|
|
24
|
+
const DEFAULT_OVERSCAN = 8
|
|
25
|
+
|
|
26
|
+
export interface DataRowListProps<TRow> {
|
|
27
|
+
/** The filtered/sorted rows from `tableState.rows` (or wherever). */
|
|
28
|
+
rows: readonly TRow[]
|
|
29
|
+
/** Stable id used as the React `key` and (for virtualizer) the v-key. */
|
|
30
|
+
getRowId: (row: TRow, index: number) => string | number
|
|
31
|
+
/** Render the body of one row. Wrap with `<ListPageBoardCard layout="row">` etc. */
|
|
32
|
+
renderRow: (row: TRow, index: number) => React.ReactNode
|
|
33
|
+
/**
|
|
34
|
+
* Shown when `rows.length === 0`. Strings render as muted body copy; pass
|
|
35
|
+
* a `ReactNode` for richer empty states (illustration, CTA, etc.).
|
|
36
|
+
*/
|
|
37
|
+
emptyState?: React.ReactNode
|
|
38
|
+
/**
|
|
39
|
+
* Auto-virtualise when `rows.length >= virtualizeThreshold`. Default 100.
|
|
40
|
+
* Pass `0` to never virtualise (preserves predictable layout for short
|
|
41
|
+
* lists like dashboards / pinned tabs).
|
|
42
|
+
*/
|
|
43
|
+
virtualizeThreshold?: number
|
|
44
|
+
/** Hint for the virtualizer; clamps to measured size after first paint. */
|
|
45
|
+
estimatedRowHeight?: number
|
|
46
|
+
/** Override the default container padding / gap if needed. */
|
|
47
|
+
className?: string
|
|
48
|
+
/** Override the per-row `<li>` className (e.g. tighter spacing). */
|
|
49
|
+
rowClassName?: string
|
|
50
|
+
/** `aria-label` for the `<ul>` (screen-reader name for the list). */
|
|
51
|
+
ariaLabel?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const DEFAULT_OUTER_CLASS = "flex list-none flex-col gap-2 px-4 pb-8 pt-2 lg:px-6"
|
|
55
|
+
|
|
56
|
+
export function DataRowList<TRow>(props: DataRowListProps<TRow>) {
|
|
57
|
+
const {
|
|
58
|
+
rows,
|
|
59
|
+
getRowId,
|
|
60
|
+
renderRow,
|
|
61
|
+
emptyState,
|
|
62
|
+
virtualizeThreshold = DEFAULT_VIRTUALIZE_THRESHOLD,
|
|
63
|
+
estimatedRowHeight = DEFAULT_ESTIMATED_ROW_HEIGHT,
|
|
64
|
+
className,
|
|
65
|
+
rowClassName,
|
|
66
|
+
ariaLabel,
|
|
67
|
+
} = props
|
|
68
|
+
|
|
69
|
+
if (rows.length === 0) {
|
|
70
|
+
if (emptyState == null) return null
|
|
71
|
+
if (typeof emptyState === "string") {
|
|
72
|
+
return (
|
|
73
|
+
<div className="px-4 py-16 text-center lg:px-6">
|
|
74
|
+
<p className="text-sm text-muted-foreground">{emptyState}</p>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
return <div className="px-4 py-16 text-center lg:px-6">{emptyState}</div>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (virtualizeThreshold > 0 && rows.length >= virtualizeThreshold) {
|
|
82
|
+
return (
|
|
83
|
+
<DataRowListVirtualized
|
|
84
|
+
rows={rows}
|
|
85
|
+
getRowId={getRowId}
|
|
86
|
+
renderRow={renderRow}
|
|
87
|
+
estimatedRowHeight={estimatedRowHeight}
|
|
88
|
+
className={className}
|
|
89
|
+
rowClassName={rowClassName}
|
|
90
|
+
ariaLabel={ariaLabel}
|
|
91
|
+
/>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<ul aria-label={ariaLabel} className={cn(DEFAULT_OUTER_CLASS, className)}>
|
|
97
|
+
{rows.map((row, i) => (
|
|
98
|
+
<li key={getRowId(row, i)} className={rowClassName}>
|
|
99
|
+
{renderRow(row, i)}
|
|
100
|
+
</li>
|
|
101
|
+
))}
|
|
102
|
+
</ul>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
107
|
+
// Virtualised variant — keeps the DOM short on long lists (e.g. 1000+ rows).
|
|
108
|
+
// Uses `useWindowVirtualizer` so the page scroll drives row recycling; this
|
|
109
|
+
// is the right tool for hub-level lists (not nested-scroll containers).
|
|
110
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
function DataRowListVirtualized<TRow>({
|
|
113
|
+
rows,
|
|
114
|
+
getRowId,
|
|
115
|
+
renderRow,
|
|
116
|
+
estimatedRowHeight,
|
|
117
|
+
className,
|
|
118
|
+
rowClassName,
|
|
119
|
+
ariaLabel,
|
|
120
|
+
}: {
|
|
121
|
+
rows: readonly TRow[]
|
|
122
|
+
getRowId: (row: TRow, index: number) => string | number
|
|
123
|
+
renderRow: (row: TRow, index: number) => React.ReactNode
|
|
124
|
+
estimatedRowHeight: number
|
|
125
|
+
className?: string
|
|
126
|
+
rowClassName?: string
|
|
127
|
+
ariaLabel?: string
|
|
128
|
+
}) {
|
|
129
|
+
const anchorRef = React.useRef<HTMLDivElement | null>(null)
|
|
130
|
+
// `scrollMargin` is read by the virtualizer during render, so it has to
|
|
131
|
+
// be state (not a ref). We measure with `useLayoutEffect` after the first
|
|
132
|
+
// paint and on resize so window-scroll math stays accurate when the page
|
|
133
|
+
// layout shifts (sidebar collapse, banner, etc.).
|
|
134
|
+
const [scrollMargin, setScrollMargin] = React.useState(0)
|
|
135
|
+
|
|
136
|
+
const updateScrollMargin = React.useCallback(() => {
|
|
137
|
+
const el = anchorRef.current
|
|
138
|
+
if (!el) return
|
|
139
|
+
setScrollMargin(el.getBoundingClientRect().top + window.scrollY)
|
|
140
|
+
}, [])
|
|
141
|
+
|
|
142
|
+
React.useLayoutEffect(() => {
|
|
143
|
+
updateScrollMargin()
|
|
144
|
+
window.addEventListener("resize", updateScrollMargin)
|
|
145
|
+
return () => window.removeEventListener("resize", updateScrollMargin)
|
|
146
|
+
}, [updateScrollMargin, rows.length])
|
|
147
|
+
|
|
148
|
+
const virtualizer = useWindowVirtualizer({
|
|
149
|
+
count: rows.length,
|
|
150
|
+
estimateSize: () => estimatedRowHeight,
|
|
151
|
+
overscan: DEFAULT_OVERSCAN,
|
|
152
|
+
scrollMargin,
|
|
153
|
+
getItemKey: i => String(getRowId(rows[i], i)),
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const totalSize = virtualizer.getTotalSize()
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div ref={anchorRef} className={cn("px-4 pb-8 pt-2 lg:px-6", className)}>
|
|
160
|
+
<ul
|
|
161
|
+
aria-label={ariaLabel}
|
|
162
|
+
className="relative m-0 w-full list-none p-0"
|
|
163
|
+
style={{ height: `${totalSize}px` }}
|
|
164
|
+
>
|
|
165
|
+
{virtualizer.getVirtualItems().map(vr => {
|
|
166
|
+
const row = rows[vr.index]
|
|
167
|
+
if (!row) return null
|
|
168
|
+
return (
|
|
169
|
+
<li
|
|
170
|
+
key={vr.key}
|
|
171
|
+
data-index={vr.index}
|
|
172
|
+
ref={virtualizer.measureElement}
|
|
173
|
+
className={cn("absolute left-0 top-0 w-full pb-2", rowClassName)}
|
|
174
|
+
style={{ transform: `translateY(${vr.start}px)` }}
|
|
175
|
+
>
|
|
176
|
+
{renderRow(row, vr.index)}
|
|
177
|
+
</li>
|
|
178
|
+
)
|
|
179
|
+
})}
|
|
180
|
+
</ul>
|
|
181
|
+
</div>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Central exports for list-page data surfaces and shared view chrome.
|
|
3
3
|
*
|
|
4
|
-
* **Pattern:** `ListPageTemplate` + `
|
|
4
|
+
* **Pattern:** `ListPageTemplate` + `PlacementsTable` (or any hub-specific `*-table.tsx`) — one `useTableState`, one toolbar,
|
|
5
5
|
* table | list | board | dashboard from the same component (`AGENTS.md` §4, `docs/data-views-pattern.md`).
|
|
6
6
|
*
|
|
7
7
|
* **View UI:** `ViewSegmentedControl` matches the template’s views toolbar (`bg-muted/60` pills).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
export {
|
|
11
|
-
export type {
|
|
10
|
+
export { PlacementsTable } from "@/components/placements-table"
|
|
11
|
+
export type { PlacementsTableProps, PlacementsTableHandle } from "@/components/placements-table"
|
|
12
12
|
export type { PlacementLifecycleTabId } from "@/lib/placement-lifecycle"
|
|
13
13
|
export type { DataListViewType } from "@/lib/data-list-view"
|
|
14
14
|
export { DATA_LIST_VIEW_TILES, dataListViewIcon, dataListViewLabel } from "@/lib/data-list-view"
|
|
@@ -103,6 +103,10 @@ export {
|
|
|
103
103
|
/** Generic folder icon-grid — reusable across all list hubs. */
|
|
104
104
|
export { FolderGridView, type FolderGridViewProps } from "@/components/data-views/folder-grid-view"
|
|
105
105
|
|
|
106
|
+
/** Generic vertical row list — used by every hub's "list" tab. Composes
|
|
107
|
+
* `ListPageBoardCard layout="row"` via a `renderRow` prop. */
|
|
108
|
+
export { DataRowList, type DataRowListProps } from "@/components/data-views/data-row-list"
|
|
109
|
+
|
|
106
110
|
|
|
107
111
|
/** Unified hub tile + list row surface — see `list-page-board-card.tsx`. */
|
|
108
112
|
export {
|
|
@@ -91,12 +91,20 @@ export function OsFolderGlyph({
|
|
|
91
91
|
aria-label={!decorative ? label : undefined}
|
|
92
92
|
aria-hidden={decorative ? true : undefined}
|
|
93
93
|
>
|
|
94
|
+
{/* Static SVG — `next/image` can't optimize SVGs without
|
|
95
|
+
`dangerouslyAllowSVG`, so we stay on plain <img> but add the same
|
|
96
|
+
loading/decoding hints `next/image` would. Folder grids render many
|
|
97
|
+
of these at once; lazy-loading lets the browser skip off-screen
|
|
98
|
+
glyphs until they scroll near the viewport. */}
|
|
99
|
+
{/* eslint-disable-next-line @next/next/no-img-element -- SVG; next/image can't optimize without dangerouslyAllowSVG */}
|
|
94
100
|
<img
|
|
95
101
|
src={OS_FOLDER_GLYPH_SRC}
|
|
96
102
|
alt=""
|
|
97
103
|
width={240}
|
|
98
104
|
height={240}
|
|
99
105
|
draggable={false}
|
|
106
|
+
loading="lazy"
|
|
107
|
+
decoding="async"
|
|
100
108
|
className={cn(
|
|
101
109
|
"h-full w-full object-contain",
|
|
102
110
|
"transition-[filter] duration-200",
|
|
@@ -145,7 +145,7 @@ export function ExportDrawer({
|
|
|
145
145
|
side="right"
|
|
146
146
|
showCloseButton={false}
|
|
147
147
|
showOverlay={false}
|
|
148
|
-
className="z-[
|
|
148
|
+
className="z-[80] w-80 sm:max-w-80 p-0 gap-0 flex flex-col border border-border shadow-xl rounded-xl overflow-hidden"
|
|
149
149
|
style={{ top: "0.5rem", bottom: "0.5rem", right: "0.5rem", height: "calc(100vh - 1rem)" }}
|
|
150
150
|
>
|
|
151
151
|
{/* Header */}
|