@exxatdesignux/ui 0.2.16 → 0.2.18
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 +26 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +149 -4
- 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-kpi-flat-band/SKILL.md +38 -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 +19 -0
- package/consumer-extras/patterns/data-views-pattern.md +2 -0
- package/consumer-extras/patterns/kpi-flat-band-pattern.md +57 -0
- package/consumer-extras/patterns/shell-surface-elevation-pattern.md +52 -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 +3 -2
- package/src/globals.css +65 -14
- package/src/theme.css +3 -3
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +1 -1
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -1
- package/template/.cursor/rules/exxat-mono-ids.mdc +30 -0
- package/template/AGENTS.md +27 -17
- package/template/app/(app)/data-list/page.tsx +2 -2
- package/template/app/(app)/error.tsx +22 -6
- package/template/app/(app)/layout.tsx +13 -6
- package/template/app/(app)/question-bank/layout.tsx +18 -5
- package/template/app/(app)/question-bank/new/page.tsx +58 -0
- package/template/app/global-error.tsx +63 -0
- package/template/app/globals.css +151 -14
- package/template/app/layout.tsx +43 -5
- package/template/components/app-sidebar.tsx +68 -33
- 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 +4 -0
- package/template/components/data-table/index.tsx +99 -91
- 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 +276 -100
- 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/dev-chunk-load-recovery.tsx +41 -0
- package/template/components/export-drawer.tsx +1 -1
- package/template/components/exxat-product-logo.tsx +168 -317
- package/template/components/invite-collaborators-drawer.tsx +5 -3
- package/template/components/key-metrics.tsx +122 -62
- 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} +2 -2
- 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 +19 -133
- 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} +42 -66
- package/template/components/product-switcher.tsx +24 -7
- package/template/components/product-wordmark.tsx +282 -0
- package/template/components/question-bank-client.tsx +20 -2
- package/template/components/question-bank-hub-client.tsx +105 -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 +19 -6
- 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/sidebar-shell.tsx +2 -1
- package/template/components/site-header.tsx +36 -31
- package/template/components/sites-list-view.tsx +31 -36
- package/template/components/sites-table.tsx +4 -0
- package/template/components/table-properties/drawer-button.tsx +38 -20
- package/template/components/table-properties/drawer.tsx +17 -14
- package/template/components/team-client.tsx +1 -1
- package/template/components/team-list-view.tsx +34 -50
- package/template/components/team-table.tsx +8 -3
- package/template/components/templates/list-page.tsx +12 -9
- package/template/components/templates/nested-secondary-panel-shell.tsx +10 -4
- package/template/components/ui/dot-pattern.tsx +50 -26
- package/template/components/ui/leo-icon.tsx +23 -3
- package/template/contexts/product-context.tsx +70 -7
- package/template/contexts/system-banner-context.tsx +112 -4
- package/template/docs/data-views-pattern.md +2 -0
- package/template/docs/kpi-flat-band-pattern.md +57 -0
- package/template/docs/kpi-strip-max-four-pattern.md +1 -0
- package/template/docs/shell-surface-elevation-pattern.md +52 -0
- package/template/eslint.config.mjs +18 -0
- package/template/hooks/use-sidebar-reflow-zoom.ts +21 -11
- package/template/lib/chunk-load-error.ts +13 -0
- package/template/lib/conditional-rule-match.ts +87 -22
- package/template/lib/data-list-persistence.ts +57 -257
- package/template/lib/data-list-view.ts +6 -0
- 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/sidebar-state-cookie.ts +9 -0
- package/template/lib/table-state-lifecycle.ts +521 -0
- package/template/next.config.mjs +156 -0
- package/template/package.json +3 -3
- package/template/stores/app-store.ts +46 -1
|
@@ -198,23 +198,88 @@ export interface KeyMetricsProps {
|
|
|
198
198
|
className?: string
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
/**
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
201
|
+
/**
|
|
202
|
+
* KPI grid column step patterns — Tailwind v4 container-query classes.
|
|
203
|
+
*
|
|
204
|
+
* We deliberately AVOID `repeat(auto-fit, minmax(...))` here because it
|
|
205
|
+
* produces awkward "N + leftover" layouts at intermediate widths (e.g. 3
|
|
206
|
+
* tiles in row 1 + 1 lonely tile in row 2 for a 4-KPI strip). Instead we
|
|
207
|
+
* step the column count through values that evenly divide the row size:
|
|
208
|
+
* 1 → 2 → 4 for a 4-KPI strip (3 is skipped on purpose).
|
|
209
|
+
*
|
|
210
|
+
* The breakpoints are container-query based (`@[Xrem]:…`) so they react to
|
|
211
|
+
* the metrics strip's OWN width, not the viewport — that's what makes the
|
|
212
|
+
* 2×2 fallback kick in when the primary sidebar + secondary panel are
|
|
213
|
+
* both open and the strip column is ~360 px wide, even on a 1280 px display.
|
|
214
|
+
*
|
|
215
|
+
* `metricsHalfWidthLayout` = strip shares its row with the insight rail
|
|
216
|
+
* (3fr / 2fr split). Tighter breakpoints because available width is ~60%
|
|
217
|
+
* of the section.
|
|
218
|
+
*/
|
|
219
|
+
/**
|
|
220
|
+
* Flat KPI hairlines — cell borders only (no grid gap fill / no surface).
|
|
221
|
+
* Four tiles: default 4-across verticals; 2×2 hairlines only when @container is narrow.
|
|
222
|
+
*/
|
|
223
|
+
function flatMetricsHairlineClass(
|
|
224
|
+
itemCount: number,
|
|
209
225
|
metricsHalfWidthLayout: boolean,
|
|
210
226
|
): string {
|
|
211
|
-
if (
|
|
212
|
-
|
|
227
|
+
if (itemCount <= 1) return "gap-0"
|
|
228
|
+
|
|
229
|
+
const childBorder = "[&>*]:border-[color:var(--key-metrics-flat-divider)]"
|
|
230
|
+
|
|
231
|
+
if (itemCount === 2) {
|
|
232
|
+
return cn("gap-0", childBorder, "[&>*:first-child]:border-r")
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (itemCount === 4) {
|
|
236
|
+
const narrow2x2 = metricsHalfWidthLayout
|
|
237
|
+
? "@[max-width:25.99rem]"
|
|
238
|
+
: "@[max-width:29.99rem]"
|
|
239
|
+
return cn(
|
|
240
|
+
"gap-0",
|
|
241
|
+
childBorder,
|
|
242
|
+
/* Wide strip (matches `@[30rem]:grid-cols-4`) — verticals between all tiles, no horizontal */
|
|
243
|
+
"[&>*:not(:last-child)]:border-r",
|
|
244
|
+
/* Narrow strip (`@[18rem]`–`@[30rem]` 2×2) */
|
|
245
|
+
`${narrow2x2}:[&>*:not(:last-child)]:border-r-0`,
|
|
246
|
+
`${narrow2x2}:[&>*:nth-child(odd)]:border-r`,
|
|
247
|
+
`${narrow2x2}:[&>*:not(:nth-last-child(-n+2))]:border-b`,
|
|
248
|
+
)
|
|
213
249
|
}
|
|
214
|
-
|
|
215
|
-
|
|
250
|
+
|
|
251
|
+
return cn("gap-0", childBorder, "[&>*:not(:last-child)]:border-r")
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function metricsRowColumnsClass(rowLength: number, metricsHalfWidthLayout: boolean): string {
|
|
255
|
+
const half = metricsHalfWidthLayout
|
|
256
|
+
switch (rowLength) {
|
|
257
|
+
case 1:
|
|
258
|
+
return "grid-cols-1"
|
|
259
|
+
case 2:
|
|
260
|
+
return half
|
|
261
|
+
? "grid-cols-1 @[14rem]:grid-cols-2"
|
|
262
|
+
: "grid-cols-1 @[18rem]:grid-cols-2"
|
|
263
|
+
case 3:
|
|
264
|
+
// 3 tiles divide evenly already — step 1 → 3.
|
|
265
|
+
return half
|
|
266
|
+
? "grid-cols-1 @[18rem]:grid-cols-3"
|
|
267
|
+
: "grid-cols-1 @[24rem]:grid-cols-3"
|
|
268
|
+
case 4:
|
|
269
|
+
// Step 1 → 2 (2×2 grid) → 4. Skip 3 — that's the awkward 3+1 layout.
|
|
270
|
+
// Aggressive 4-col thresholds so the strip fits all four tiles even
|
|
271
|
+
// when the primary sidebar + secondary panel + insight rail are all
|
|
272
|
+
// expanded (typical question-bank layout puts the KPI grid at ~27rem).
|
|
273
|
+
return half
|
|
274
|
+
? "grid-cols-1 @[14rem]:grid-cols-2 @[26rem]:grid-cols-4"
|
|
275
|
+
: "grid-cols-1 @[18rem]:grid-cols-2 @[30rem]:grid-cols-4"
|
|
276
|
+
default:
|
|
277
|
+
// 5+ KPIs (`exxat-kpi-max-four` caps the strip at 4, but key-metrics
|
|
278
|
+
// is a generic primitive — fall back to a sensible step). 1 → 2 → 3 → 6.
|
|
279
|
+
return half
|
|
280
|
+
? "grid-cols-1 @[14rem]:grid-cols-2 @[26rem]:grid-cols-3 @[40rem]:grid-cols-6"
|
|
281
|
+
: "grid-cols-1 @[18rem]:grid-cols-2 @[30rem]:grid-cols-3 @[56rem]:grid-cols-6"
|
|
216
282
|
}
|
|
217
|
-
return `repeat(${rowLength}, minmax(0, 1fr))`
|
|
218
283
|
}
|
|
219
284
|
|
|
220
285
|
/* ── Default data ─────────────────────────────────────────────────────────── */
|
|
@@ -507,6 +572,9 @@ function KeyMetricsInner({
|
|
|
507
572
|
surfaceVariant = "default",
|
|
508
573
|
}: InnerProps) {
|
|
509
574
|
const isFlatBand = surfaceVariant === "flat"
|
|
575
|
+
const metricsGridClassName = isFlatBand
|
|
576
|
+
? flatMetricsHairlineClass(metrics.length, metricsHalfWidthLayout)
|
|
577
|
+
: "gap-px bg-border"
|
|
510
578
|
/** Side-by-side KPI + insight rail (md+). Disabled for half-width dashboard cards — insight stacks below. */
|
|
511
579
|
const insightSideBySide = insight && !insightFullWidth && !metricsHalfWidthLayout
|
|
512
580
|
const stackedRailInsight = insight && !insightFullWidth && metricsHalfWidthLayout
|
|
@@ -571,31 +639,25 @@ function KeyMetricsInner({
|
|
|
571
639
|
{metricsHalfWidthLayout ? (
|
|
572
640
|
<div
|
|
573
641
|
className={cn(
|
|
574
|
-
"
|
|
575
|
-
isFlatBand ? "divide-border/40" : "divide-border",
|
|
576
|
-
)}
|
|
577
|
-
style={
|
|
642
|
+
"@container/metrics-strip grid lg:hidden",
|
|
578
643
|
metricsSingleRow
|
|
579
|
-
?
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
metricsHalfWidthLayout,
|
|
584
|
-
),
|
|
585
|
-
}
|
|
586
|
-
: undefined
|
|
587
|
-
}
|
|
644
|
+
? metricsRowColumnsClass(metrics.length, /* half */ true)
|
|
645
|
+
: "grid-cols-2",
|
|
646
|
+
metricsGridClassName,
|
|
647
|
+
)}
|
|
588
648
|
>
|
|
589
649
|
{metrics.map((m) => (
|
|
590
|
-
<
|
|
650
|
+
<div key={m.id} className={cn("min-w-0", metricsCellSurfaceClassName)}>
|
|
651
|
+
<MetricCell {...m} dense edgeGutter={false} />
|
|
652
|
+
</div>
|
|
591
653
|
))}
|
|
592
654
|
</div>
|
|
593
655
|
) : (
|
|
594
656
|
<div
|
|
595
657
|
className={cn(
|
|
596
|
-
"grid
|
|
597
|
-
|
|
598
|
-
|
|
658
|
+
"@container/metrics-strip grid lg:hidden",
|
|
659
|
+
metricsRowColumnsClass(metrics.length, /* half */ false),
|
|
660
|
+
metricsGridClassName,
|
|
599
661
|
)}
|
|
600
662
|
>
|
|
601
663
|
{metrics.map((m) => (
|
|
@@ -606,31 +668,31 @@ function KeyMetricsInner({
|
|
|
606
668
|
</div>
|
|
607
669
|
)}
|
|
608
670
|
|
|
609
|
-
{/*
|
|
610
|
-
|
|
671
|
+
{/*
|
|
672
|
+
lg+: row-by-row container-queried grid. Uses a `gap-px + bg` hairline
|
|
673
|
+
instead of `divide-x` so dividers render correctly when the row wraps
|
|
674
|
+
from 4-across to a 2×2 grid (the awkward 3+1 layout is skipped — see
|
|
675
|
+
`metricsRowColumnsClass`).
|
|
676
|
+
*/}
|
|
677
|
+
<div className="@container/metrics-strip hidden lg:block">
|
|
611
678
|
{rows.map((row, rowIdx) => (
|
|
612
679
|
<React.Fragment key={rowIdx}>
|
|
613
|
-
{rowIdx > 0 && (
|
|
614
|
-
<Separator
|
|
615
|
-
aria-hidden="true"
|
|
616
|
-
className={cn("my-1", isFlatBand && "bg-foreground/[0.055]")}
|
|
617
|
-
/>
|
|
680
|
+
{rowIdx > 0 && !isFlatBand && (
|
|
681
|
+
<Separator aria-hidden="true" className="my-1" />
|
|
618
682
|
)}
|
|
619
683
|
<div
|
|
620
684
|
className={cn(
|
|
621
|
-
"grid
|
|
622
|
-
|
|
685
|
+
"grid",
|
|
686
|
+
metricsRowColumnsClass(row.length, metricsHalfWidthLayout),
|
|
687
|
+
isFlatBand
|
|
688
|
+
? flatMetricsHairlineClass(row.length, metricsHalfWidthLayout)
|
|
689
|
+
: metricsGridClassName,
|
|
623
690
|
)}
|
|
624
|
-
style={{
|
|
625
|
-
gridTemplateColumns: metricsRowColumns(
|
|
626
|
-
row.length,
|
|
627
|
-
metricsSingleRow,
|
|
628
|
-
metricsHalfWidthLayout,
|
|
629
|
-
),
|
|
630
|
-
}}
|
|
631
691
|
>
|
|
632
692
|
{row.map((m) => (
|
|
633
|
-
<
|
|
693
|
+
<div key={m.id} className={cn("min-w-0", metricsCellSurfaceClassName)}>
|
|
694
|
+
<MetricCell {...m} dense={metricsHalfWidthLayout} edgeGutter={false} />
|
|
695
|
+
</div>
|
|
634
696
|
))}
|
|
635
697
|
</div>
|
|
636
698
|
</React.Fragment>
|
|
@@ -665,8 +727,9 @@ function KeyMetricsInner({
|
|
|
665
727
|
insightSideBySide &&
|
|
666
728
|
!insightFullWidth &&
|
|
667
729
|
cn(
|
|
668
|
-
"lg:h-full lg:
|
|
669
|
-
|
|
730
|
+
"lg:h-full lg:pl-6",
|
|
731
|
+
/* Flat band: insight card ring is the divider — skip `border-l` (double line). */
|
|
732
|
+
!isFlatBand && "lg:border-l lg:border-border",
|
|
670
733
|
)
|
|
671
734
|
)}
|
|
672
735
|
>
|
|
@@ -828,7 +891,9 @@ export function KeyMetrics({
|
|
|
828
891
|
})()
|
|
829
892
|
|
|
830
893
|
const metricsCellSurfaceClassName =
|
|
831
|
-
variant === "flat"
|
|
894
|
+
variant === "flat"
|
|
895
|
+
? "bg-transparent"
|
|
896
|
+
: "bg-card dark:bg-transparent"
|
|
832
897
|
|
|
833
898
|
const innerProps: InnerProps = {
|
|
834
899
|
title,
|
|
@@ -872,18 +937,13 @@ export function KeyMetrics({
|
|
|
872
937
|
* ─────────────────────────────────────────────────────────────────────────
|
|
873
938
|
*/
|
|
874
939
|
const glowStyle: React.CSSProperties = {
|
|
875
|
-
|
|
876
|
-
background:
|
|
877
|
-
"radial-gradient(ellipse 110% 90% at 50% 100%, oklch(from var(--brand-color) l c h / 0.13) 0%, transparent 65%)",
|
|
940
|
+
background: "var(--key-metrics-card-glow-radial)",
|
|
878
941
|
}
|
|
879
942
|
|
|
880
|
-
/** List-page KPI band
|
|
943
|
+
/** List-page KPI band — transparent; only `--key-metrics-flat-band-radial` glow. */
|
|
881
944
|
const flatBandStyle: React.CSSProperties = {
|
|
882
|
-
background:
|
|
883
|
-
|
|
884
|
-
"linear-gradient(180deg, var(--key-metrics-flat-grad-top) 0%, var(--key-metrics-flat-grad-mid) 48%, var(--background) 100%)",
|
|
885
|
-
].join(", "),
|
|
886
|
-
boxShadow: "0 18px 42px -26px color-mix(in oklch, var(--brand-color) 10%, transparent)",
|
|
945
|
+
background: "var(--key-metrics-flat-band-radial)",
|
|
946
|
+
boxShadow: "var(--key-metrics-flat-band-shadow)",
|
|
887
947
|
}
|
|
888
948
|
|
|
889
949
|
/* ── Card variant — ChartCard-style chrome ───────────────────────────── */
|
|
@@ -954,11 +1014,11 @@ export function KeyMetrics({
|
|
|
954
1014
|
)
|
|
955
1015
|
}
|
|
956
1016
|
|
|
957
|
-
/* ── Flat variant —
|
|
1017
|
+
/* ── Flat variant — no surface; bottom brand glow only ── */
|
|
958
1018
|
return (
|
|
959
1019
|
<section
|
|
960
1020
|
aria-label={title}
|
|
961
|
-
className={cn("relative w-full overflow-hidden pt-5 pb-
|
|
1021
|
+
className={cn("relative w-full overflow-hidden pt-5 pb-8", className)}
|
|
962
1022
|
style={flatBandStyle}
|
|
963
1023
|
>
|
|
964
1024
|
<KeyMetricsInner
|
|
@@ -997,7 +1057,7 @@ export function KeyMetricsContent({
|
|
|
997
1057
|
showHeader={false}
|
|
998
1058
|
insightCompact={insightCompact}
|
|
999
1059
|
insightFullWidth={insightFullWidth}
|
|
1000
|
-
metricsCellSurfaceClassName="bg-card"
|
|
1060
|
+
metricsCellSurfaceClassName="bg-card dark:bg-transparent"
|
|
1001
1061
|
/>
|
|
1002
1062
|
)
|
|
1003
1063
|
}
|
|
@@ -25,6 +25,7 @@ import { useRouter } from "next/navigation"
|
|
|
25
25
|
import {
|
|
26
26
|
useForm,
|
|
27
27
|
useFormContext,
|
|
28
|
+
useWatch,
|
|
28
29
|
type ControllerRenderProps,
|
|
29
30
|
type Resolver,
|
|
30
31
|
} from "react-hook-form"
|
|
@@ -64,7 +65,6 @@ import {
|
|
|
64
65
|
import { RadioGroup, RadioGroupItem, RadioGroupLabel } from "@/components/ui/radio-group"
|
|
65
66
|
import { Card, CardHeader, CardTitle, CardAction, CardContent } from "@/components/ui/card"
|
|
66
67
|
import { Kbd, KbdGroup } from "@/components/ui/kbd"
|
|
67
|
-
import { Tip } from "@/components/ui/tip"
|
|
68
68
|
import { Shortcut } from "@/components/ui/dropdown-menu"
|
|
69
69
|
import { useModKeyLabel, useAltKeyLabel } from "@/hooks/use-mod-key-label"
|
|
70
70
|
|
|
@@ -967,7 +967,9 @@ export function NewPlacementForm() {
|
|
|
967
967
|
router.push("/data-list")
|
|
968
968
|
}
|
|
969
969
|
|
|
970
|
-
|
|
970
|
+
// `useWatch` is memoization-friendly (returns a stable reactive value)
|
|
971
|
+
// unlike `form.watch()`, which the React Compiler can't memoize safely.
|
|
972
|
+
const formData = useWatch({ control: form.control })
|
|
971
973
|
const mod = useModKeyLabel()
|
|
972
974
|
const alt = useAltKeyLabel()
|
|
973
975
|
|