@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.
Files changed (89) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +148 -3
  3. package/consumer-extras/cursor-skills/exxat-ds-skill/references/accessibility.md +142 -0
  4. package/consumer-extras/cursor-skills/exxat-ds-skill/references/coach-marks.md +169 -0
  5. package/consumer-extras/cursor-skills/exxat-ds-skill/references/data-table-pattern.md +382 -0
  6. package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +56 -0
  7. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +14 -0
  8. package/package.json +3 -3
  9. package/src/components/ui/banner.tsx +2 -0
  10. package/src/components/ui/chart.tsx +57 -2
  11. package/src/components/ui/sidebar.tsx +1 -0
  12. package/template/.claude/skills/exxat-ds-skill/SKILL.md +1 -1
  13. package/template/.cursor/rules/exxat-mono-ids.mdc +30 -0
  14. package/template/AGENTS.md +18 -15
  15. package/template/app/(app)/data-list/page.tsx +2 -2
  16. package/template/app/(app)/question-bank/layout.tsx +18 -5
  17. package/template/app/(app)/question-bank/new/page.tsx +58 -0
  18. package/template/app/globals.css +108 -1
  19. package/template/app/layout.tsx +41 -5
  20. package/template/components/app-sidebar.tsx +68 -34
  21. package/template/components/ask-leo-sidebar.tsx +0 -2
  22. package/template/components/brand-color-picker.tsx +344 -0
  23. package/template/components/compliance-list-view.tsx +33 -51
  24. package/template/components/compliance-table.tsx +24 -0
  25. package/template/components/data-table/index.tsx +68 -24
  26. package/template/components/data-table/pagination.tsx +0 -1
  27. package/template/components/data-table/types.ts +4 -1
  28. package/template/components/data-table/use-table-state.ts +243 -94
  29. package/template/components/data-views/data-row-list.tsx +183 -0
  30. package/template/components/data-views/index.ts +7 -3
  31. package/template/components/data-views/os-folder-glyph.tsx +8 -0
  32. package/template/components/export-drawer.tsx +1 -1
  33. package/template/components/exxat-product-logo.tsx +172 -317
  34. package/template/components/invite-collaborators-drawer.tsx +5 -3
  35. package/template/components/key-metrics.tsx +74 -46
  36. package/template/components/new-placement-form.tsx +4 -2
  37. package/template/components/new-question-composer.tsx +2208 -0
  38. package/template/components/page-breadcrumb-trail.tsx +131 -0
  39. package/template/components/page-header.tsx +2 -1
  40. package/template/components/{data-views/placement-board-card.tsx → placement-board-card.tsx} +1 -1
  41. package/template/components/placement-detail.tsx +1 -1
  42. package/template/components/placements-board-view.tsx +1 -1
  43. package/template/components/{data-list-client.tsx → placements-client.tsx} +9 -7
  44. package/template/components/placements-list-view.tsx +18 -132
  45. package/template/components/{data-list-table-cells.test.tsx → placements-table-cells.test.tsx} +2 -2
  46. package/template/components/{data-list-table-cells.tsx → placements-table-cells.tsx} +1 -1
  47. package/template/components/placements-table-columns.tsx +2 -2
  48. package/template/components/{data-list-table.tsx → placements-table.tsx} +67 -58
  49. package/template/components/product-switcher.tsx +26 -8
  50. package/template/components/product-wordmark.tsx +285 -0
  51. package/template/components/question-bank-client.tsx +20 -2
  52. package/template/components/question-bank-hub-client.tsx +108 -115
  53. package/template/components/question-bank-list-view.tsx +30 -54
  54. package/template/components/question-bank-new-folder-sheet.tsx +1 -1
  55. package/template/components/question-bank-secondary-nav.tsx +0 -3
  56. package/template/components/question-bank-table.tsx +30 -5
  57. package/template/components/rotations-empty-state.tsx +3 -0
  58. package/template/components/secondary-panel.tsx +23 -3
  59. package/template/components/settings-appearance-card.tsx +584 -141
  60. package/template/components/site-header.tsx +36 -31
  61. package/template/components/sites-list-view.tsx +31 -36
  62. package/template/components/sites-table.tsx +24 -0
  63. package/template/components/table-properties/drawer.tsx +1 -1
  64. package/template/components/team-client.tsx +1 -1
  65. package/template/components/team-list-view.tsx +34 -50
  66. package/template/components/team-table.tsx +29 -3
  67. package/template/components/templates/nested-secondary-panel-shell.tsx +8 -2
  68. package/template/components/ui/dot-pattern.tsx +50 -26
  69. package/template/components/ui/leo-icon.tsx +23 -3
  70. package/template/contexts/product-context.tsx +51 -7
  71. package/template/contexts/system-banner-context.tsx +112 -4
  72. package/template/eslint.config.mjs +18 -0
  73. package/template/hooks/use-sidebar-reflow-zoom.ts +21 -11
  74. package/template/lib/data-list-persistence.ts +57 -257
  75. package/template/lib/dev-log.test.ts +6 -5
  76. package/template/lib/exxat-palette.json +1462 -0
  77. package/template/lib/exxat-palette.ts +136 -0
  78. package/template/lib/list-page-table-properties.ts +1 -1
  79. package/template/lib/list-status-badges.ts +1 -1
  80. package/template/lib/mailto.ts +29 -0
  81. package/template/lib/placement-board-card-layout.ts +1 -1
  82. package/template/lib/product-brand.ts +268 -0
  83. package/template/lib/question-bank-authoring.ts +308 -0
  84. package/template/lib/question-bank-nav.ts +44 -0
  85. package/template/lib/raf-throttle.ts +45 -0
  86. package/template/lib/table-state-lifecycle.ts +474 -0
  87. package/template/next.config.mjs +156 -0
  88. package/template/package.json +3 -3
  89. 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` + `DataListTable` — one `useTableState`, one toolbar,
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 { DataListTable } from "@/components/data-list-table"
11
- export type { DataListTableProps, DataListTableHandle } from "@/components/data-list-table"
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-[60] w-80 sm:max-w-80 p-0 gap-0 flex flex-col border border-border shadow-xl rounded-xl overflow-hidden"
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 */}