@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
@@ -198,23 +198,53 @@ export interface KeyMetricsProps {
198
198
  className?: string
199
199
  }
200
200
 
201
- /** Wrap KPI columns when the strip is narrow (high zoom, 5+ tiles) instead of squeezing cells. */
202
- const METRICS_GRID_TEMPLATE =
203
- "repeat(auto-fit, minmax(min(100%, 11.5rem), 1fr))"
204
-
205
- /** Equal columns in one row up to 4 KPIs beside an insight rail without premature wrap. */
206
- function metricsRowColumns(
207
- rowLength: number,
208
- metricsSingleRow: boolean,
209
- metricsHalfWidthLayout: boolean,
210
- ): string {
211
- if (metricsHalfWidthLayout) {
212
- return `repeat(${rowLength}, minmax(0, 1fr))`
213
- }
214
- if (metricsSingleRow) {
215
- return rowLength > 4 ? METRICS_GRID_TEMPLATE : `repeat(${rowLength}, minmax(0, 1fr))`
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
- "grid grid-cols-2 divide-x lg:hidden",
575
- isFlatBand ? "divide-border/40" : "divide-border",
576
- )}
577
- style={
607
+ "@container/metrics-strip grid lg:hidden",
578
608
  metricsSingleRow
579
- ? {
580
- gridTemplateColumns: metricsRowColumns(
581
- metrics.length,
582
- metricsSingleRow,
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
- <MetricCell key={m.id} {...m} dense />
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 gap-px lg:hidden",
597
- "grid-cols-1 md:grid-cols-2",
598
- isFlatBand ? "bg-foreground/[0.04]" : "bg-border",
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
- {/* lg+: row-by-row 3-col with horizontal separator between rows */}
610
- <div className="hidden lg:block">
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 divide-x",
622
- isFlatBand ? "divide-border/40" : "divide-border",
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
- <MetricCell key={m.id} {...m} dense={metricsHalfWidthLayout} />
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-[var(--key-metrics-flat-cell-bg)]" : "bg-card"
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
- const formData = form.watch()
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