@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
|
@@ -198,23 +198,53 @@ export interface KeyMetricsProps {
|
|
|
198
198
|
className?: string
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
/**
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
function metricsRowColumnsClass(rowLength: number, metricsHalfWidthLayout: boolean): string {
|
|
220
|
+
const half = metricsHalfWidthLayout
|
|
221
|
+
switch (rowLength) {
|
|
222
|
+
case 1:
|
|
223
|
+
return "grid-cols-1"
|
|
224
|
+
case 2:
|
|
225
|
+
return half
|
|
226
|
+
? "grid-cols-1 @[14rem]:grid-cols-2"
|
|
227
|
+
: "grid-cols-1 @[18rem]:grid-cols-2"
|
|
228
|
+
case 3:
|
|
229
|
+
// 3 tiles divide evenly already — step 1 → 3.
|
|
230
|
+
return half
|
|
231
|
+
? "grid-cols-1 @[18rem]:grid-cols-3"
|
|
232
|
+
: "grid-cols-1 @[24rem]:grid-cols-3"
|
|
233
|
+
case 4:
|
|
234
|
+
// Step 1 → 2 (2×2 grid) → 4. Skip 3 — that's the awkward 3+1 layout.
|
|
235
|
+
// Aggressive 4-col thresholds so the strip fits all four tiles even
|
|
236
|
+
// when the primary sidebar + secondary panel + insight rail are all
|
|
237
|
+
// expanded (typical question-bank layout puts the KPI grid at ~27rem).
|
|
238
|
+
return half
|
|
239
|
+
? "grid-cols-1 @[14rem]:grid-cols-2 @[26rem]:grid-cols-4"
|
|
240
|
+
: "grid-cols-1 @[18rem]:grid-cols-2 @[30rem]:grid-cols-4"
|
|
241
|
+
default:
|
|
242
|
+
// 5+ KPIs (`exxat-kpi-max-four` caps the strip at 4, but key-metrics
|
|
243
|
+
// is a generic primitive — fall back to a sensible step). 1 → 2 → 3 → 6.
|
|
244
|
+
return half
|
|
245
|
+
? "grid-cols-1 @[14rem]:grid-cols-2 @[26rem]:grid-cols-3 @[40rem]:grid-cols-6"
|
|
246
|
+
: "grid-cols-1 @[18rem]:grid-cols-2 @[30rem]:grid-cols-3 @[56rem]:grid-cols-6"
|
|
216
247
|
}
|
|
217
|
-
return `repeat(${rowLength}, minmax(0, 1fr))`
|
|
218
248
|
}
|
|
219
249
|
|
|
220
250
|
/* ── Default data ─────────────────────────────────────────────────────────── */
|
|
@@ -507,6 +537,9 @@ function KeyMetricsInner({
|
|
|
507
537
|
surfaceVariant = "default",
|
|
508
538
|
}: InnerProps) {
|
|
509
539
|
const isFlatBand = surfaceVariant === "flat"
|
|
540
|
+
const metricsGridClassName = isFlatBand
|
|
541
|
+
? "gap-0 bg-transparent [&>*:not(:last-child)]:border-r [&>*:not(:last-child)]:border-foreground/[0.055]"
|
|
542
|
+
: "gap-px bg-border"
|
|
510
543
|
/** Side-by-side KPI + insight rail (md+). Disabled for half-width dashboard cards — insight stacks below. */
|
|
511
544
|
const insightSideBySide = insight && !insightFullWidth && !metricsHalfWidthLayout
|
|
512
545
|
const stackedRailInsight = insight && !insightFullWidth && metricsHalfWidthLayout
|
|
@@ -571,31 +604,25 @@ function KeyMetricsInner({
|
|
|
571
604
|
{metricsHalfWidthLayout ? (
|
|
572
605
|
<div
|
|
573
606
|
className={cn(
|
|
574
|
-
"
|
|
575
|
-
isFlatBand ? "divide-border/40" : "divide-border",
|
|
576
|
-
)}
|
|
577
|
-
style={
|
|
607
|
+
"@container/metrics-strip grid lg:hidden",
|
|
578
608
|
metricsSingleRow
|
|
579
|
-
?
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
metricsHalfWidthLayout,
|
|
584
|
-
),
|
|
585
|
-
}
|
|
586
|
-
: undefined
|
|
587
|
-
}
|
|
609
|
+
? metricsRowColumnsClass(metrics.length, /* half */ true)
|
|
610
|
+
: "grid-cols-2",
|
|
611
|
+
metricsGridClassName,
|
|
612
|
+
)}
|
|
588
613
|
>
|
|
589
614
|
{metrics.map((m) => (
|
|
590
|
-
<
|
|
615
|
+
<div key={m.id} className={cn("min-w-0", metricsCellSurfaceClassName)}>
|
|
616
|
+
<MetricCell {...m} dense edgeGutter={false} />
|
|
617
|
+
</div>
|
|
591
618
|
))}
|
|
592
619
|
</div>
|
|
593
620
|
) : (
|
|
594
621
|
<div
|
|
595
622
|
className={cn(
|
|
596
|
-
"grid
|
|
597
|
-
|
|
598
|
-
|
|
623
|
+
"@container/metrics-strip grid lg:hidden",
|
|
624
|
+
metricsRowColumnsClass(metrics.length, /* half */ false),
|
|
625
|
+
metricsGridClassName,
|
|
599
626
|
)}
|
|
600
627
|
>
|
|
601
628
|
{metrics.map((m) => (
|
|
@@ -606,8 +633,13 @@ function KeyMetricsInner({
|
|
|
606
633
|
</div>
|
|
607
634
|
)}
|
|
608
635
|
|
|
609
|
-
{/*
|
|
610
|
-
|
|
636
|
+
{/*
|
|
637
|
+
lg+: row-by-row container-queried grid. Uses a `gap-px + bg` hairline
|
|
638
|
+
instead of `divide-x` so dividers render correctly when the row wraps
|
|
639
|
+
from 4-across to a 2×2 grid (the awkward 3+1 layout is skipped — see
|
|
640
|
+
`metricsRowColumnsClass`).
|
|
641
|
+
*/}
|
|
642
|
+
<div className="@container/metrics-strip hidden lg:block">
|
|
611
643
|
{rows.map((row, rowIdx) => (
|
|
612
644
|
<React.Fragment key={rowIdx}>
|
|
613
645
|
{rowIdx > 0 && (
|
|
@@ -618,19 +650,15 @@ function KeyMetricsInner({
|
|
|
618
650
|
)}
|
|
619
651
|
<div
|
|
620
652
|
className={cn(
|
|
621
|
-
"grid
|
|
622
|
-
|
|
653
|
+
"grid",
|
|
654
|
+
metricsRowColumnsClass(row.length, metricsHalfWidthLayout),
|
|
655
|
+
metricsGridClassName,
|
|
623
656
|
)}
|
|
624
|
-
style={{
|
|
625
|
-
gridTemplateColumns: metricsRowColumns(
|
|
626
|
-
row.length,
|
|
627
|
-
metricsSingleRow,
|
|
628
|
-
metricsHalfWidthLayout,
|
|
629
|
-
),
|
|
630
|
-
}}
|
|
631
657
|
>
|
|
632
658
|
{row.map((m) => (
|
|
633
|
-
<
|
|
659
|
+
<div key={m.id} className={cn("min-w-0", metricsCellSurfaceClassName)}>
|
|
660
|
+
<MetricCell {...m} dense={metricsHalfWidthLayout} edgeGutter={false} />
|
|
661
|
+
</div>
|
|
634
662
|
))}
|
|
635
663
|
</div>
|
|
636
664
|
</React.Fragment>
|
|
@@ -828,7 +856,7 @@ export function KeyMetrics({
|
|
|
828
856
|
})()
|
|
829
857
|
|
|
830
858
|
const metricsCellSurfaceClassName =
|
|
831
|
-
variant === "flat" ? "bg-
|
|
859
|
+
variant === "flat" ? "bg-transparent" : "bg-card"
|
|
832
860
|
|
|
833
861
|
const innerProps: InnerProps = {
|
|
834
862
|
title,
|
|
@@ -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
|
|