@exxatdesignux/ui 0.0.6 → 0.0.8

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 (264) hide show
  1. package/bin/init.mjs +29 -0
  2. package/package.json +7 -2
  3. package/template/.nvmrc +1 -0
  4. package/template/.prettierignore +7 -0
  5. package/template/.prettierrc +11 -0
  6. package/template/AGENTS.md +485 -0
  7. package/template/Logo/Exxat_Prism.svg +39 -0
  8. package/template/Logo/Exxat_one.svg +36 -0
  9. package/template/README.md +58 -0
  10. package/template/app/(app)/compliance/page.tsx +10 -0
  11. package/template/app/(app)/dashboard/loading.tsx +18 -0
  12. package/template/app/(app)/dashboard/page.tsx +36 -0
  13. package/template/app/(app)/data-list/[id]/page.tsx +28 -0
  14. package/template/app/(app)/data-list/new/page.tsx +31 -0
  15. package/template/app/(app)/data-list/page.tsx +10 -0
  16. package/template/app/(app)/error.tsx +43 -0
  17. package/template/app/(app)/help/page.tsx +34 -0
  18. package/template/app/(app)/layout.tsx +54 -0
  19. package/template/app/(app)/loading.tsx +18 -0
  20. package/template/app/(app)/question-bank/page.tsx +10 -0
  21. package/template/app/(app)/rotations/page.tsx +15 -0
  22. package/template/app/(app)/settings/page.tsx +17 -0
  23. package/template/app/(app)/sites/all/page.tsx +13 -0
  24. package/template/app/(app)/team/page.tsx +10 -0
  25. package/template/app/favicon.ico +0 -0
  26. package/template/app/globals.css +1811 -0
  27. package/template/app/layout.tsx +95 -0
  28. package/template/app/page.tsx +9 -0
  29. package/template/components/.gitkeep +0 -0
  30. package/template/components/app-sidebar-dynamic.tsx +15 -0
  31. package/template/components/app-sidebar.tsx +901 -0
  32. package/template/components/ask-leo-composer.tsx +216 -0
  33. package/template/components/ask-leo-sidebar.tsx +509 -0
  34. package/template/components/chart-area-interactive.tsx +293 -0
  35. package/template/components/charts-overview.tsx +2321 -0
  36. package/template/components/command-menu-01.tsx +133 -0
  37. package/template/components/command-menu-02.tsx +386 -0
  38. package/template/components/command-menu.tsx +182 -0
  39. package/template/components/compliance-board-view.tsx +134 -0
  40. package/template/components/compliance-client.tsx +92 -0
  41. package/template/components/compliance-list-view.tsx +59 -0
  42. package/template/components/compliance-page-header.tsx +89 -0
  43. package/template/components/compliance-table.tsx +525 -0
  44. package/template/components/dashboard-onboarding-gallery.tsx +13 -0
  45. package/template/components/dashboard-onboarding.tsx +21 -0
  46. package/template/components/dashboard-promo-banner.tsx +67 -0
  47. package/template/components/dashboard-quota-progress-card.tsx +369 -0
  48. package/template/components/dashboard-report-charts.tsx +69 -0
  49. package/template/components/dashboard-section-heading.tsx +68 -0
  50. package/template/components/dashboard-tabs.tsx +598 -0
  51. package/template/components/data-list-client.tsx +239 -0
  52. package/template/components/data-list-table-cells.test.tsx +22 -0
  53. package/template/components/data-list-table-cells.tsx +173 -0
  54. package/template/components/data-list-table.tsx +879 -0
  55. package/template/components/data-table/filter-date-calendar.tsx +38 -0
  56. package/template/components/data-table/filter-text-value-input.tsx +77 -0
  57. package/template/components/data-table/index.tsx +1612 -0
  58. package/template/components/data-table/pagination.tsx +256 -0
  59. package/template/components/data-table/types.ts +91 -0
  60. package/template/components/data-table/use-table-state.ts +566 -0
  61. package/template/components/data-view-dashboard-charts-compliance.tsx +960 -0
  62. package/template/components/data-view-dashboard-charts-team.tsx +968 -0
  63. package/template/components/data-view-dashboard-charts.tsx +1668 -0
  64. package/template/components/data-views/board-card-primitives.tsx +93 -0
  65. package/template/components/data-views/index.ts +41 -0
  66. package/template/components/data-views/list-page-board-card.tsx +192 -0
  67. package/template/components/data-views/list-page-board-template.tsx +122 -0
  68. package/template/components/data-views/placement-board-card.tsx +262 -0
  69. package/template/components/export-drawer.tsx +375 -0
  70. package/template/components/exxat-product-logo.tsx +453 -0
  71. package/template/components/form-layout-01.tsx +131 -0
  72. package/template/components/getting-started.tsx +625 -0
  73. package/template/components/key-metrics.tsx +920 -0
  74. package/template/components/leo-insight-indicator.tsx +364 -0
  75. package/template/components/leo-typing-dots.tsx +121 -0
  76. package/template/components/list-hub-status-badge.tsx +51 -0
  77. package/template/components/list-page-dashboard-charts.tsx +18 -0
  78. package/template/components/nav-documents.tsx +89 -0
  79. package/template/components/nav-main.tsx +58 -0
  80. package/template/components/nav-secondary.tsx +64 -0
  81. package/template/components/nav-user.tsx +190 -0
  82. package/template/components/new-placement-back-btn.tsx +28 -0
  83. package/template/components/new-placement-form.tsx +1066 -0
  84. package/template/components/onboarding/index.ts +4 -0
  85. package/template/components/onboarding/onboarding-01.tsx +7 -0
  86. package/template/components/onboarding/onboarding-02.tsx +7 -0
  87. package/template/components/onboarding/onboarding-03.tsx +7 -0
  88. package/template/components/onboarding/onboarding-04.tsx +7 -0
  89. package/template/components/page-header.tsx +57 -0
  90. package/template/components/placement-detail.tsx +438 -0
  91. package/template/components/placements-board-view.tsx +404 -0
  92. package/template/components/placements-list-view.tsx +285 -0
  93. package/template/components/placements-page-header.tsx +160 -0
  94. package/template/components/placements-table-columns.tsx +639 -0
  95. package/template/components/product-switcher.tsx +116 -0
  96. package/template/components/question-bank-board-view.tsx +205 -0
  97. package/template/components/question-bank-client.tsx +77 -0
  98. package/template/components/question-bank-list-view.tsx +59 -0
  99. package/template/components/question-bank-page-header.tsx +89 -0
  100. package/template/components/question-bank-table.tsx +586 -0
  101. package/template/components/rotations-empty-state.tsx +47 -0
  102. package/template/components/rotations-panel-activator.tsx +8 -0
  103. package/template/components/secondary-nav.tsx +394 -0
  104. package/template/components/secondary-panel.tsx +239 -0
  105. package/template/components/section-cards.tsx +106 -0
  106. package/template/components/settings-appearance-card.tsx +424 -0
  107. package/template/components/settings-client.tsx +537 -0
  108. package/template/components/settings-form-row.tsx +42 -0
  109. package/template/components/sidebar-auto-collapse.tsx +23 -0
  110. package/template/components/sidebar-auto-open.tsx +18 -0
  111. package/template/components/sidebar-shell.tsx +37 -0
  112. package/template/components/site-header.tsx +93 -0
  113. package/template/components/sites-all-client.tsx +154 -0
  114. package/template/components/sites-board-view.tsx +67 -0
  115. package/template/components/sites-list-view.tsx +47 -0
  116. package/template/components/sites-table.tsx +312 -0
  117. package/template/components/system-banner-slot.tsx +66 -0
  118. package/template/components/table-properties/column-row.tsx +90 -0
  119. package/template/components/table-properties/draggable-list.ts +49 -0
  120. package/template/components/table-properties/drawer-button.tsx +231 -0
  121. package/template/components/table-properties/drawer.tsx +1102 -0
  122. package/template/components/table-properties/filter-card.tsx +251 -0
  123. package/template/components/table-properties/index.ts +22 -0
  124. package/template/components/table-properties/sort-card.tsx +59 -0
  125. package/template/components/table-properties/types.ts +124 -0
  126. package/template/components/task-list-panel.tsx +98 -0
  127. package/template/components/task-priority-badge.tsx +28 -0
  128. package/template/components/team-board-view.tsx +114 -0
  129. package/template/components/team-client.tsx +93 -0
  130. package/template/components/team-list-view.tsx +62 -0
  131. package/template/components/team-page-header.tsx +92 -0
  132. package/template/components/team-table.tsx +525 -0
  133. package/template/components/templates/list-page.tsx +576 -0
  134. package/template/components/templates/primary-page-template.tsx +56 -0
  135. package/template/components/theme-color-sync.tsx +32 -0
  136. package/template/components/theme-provider.tsx +71 -0
  137. package/template/components/tinted-icon-disc.tsx +53 -0
  138. package/template/components/ui/ai-thinking-surface.tsx +121 -0
  139. package/template/components/ui/avatar.tsx +1 -0
  140. package/template/components/ui/badge.tsx +1 -0
  141. package/template/components/ui/banner.tsx +1 -0
  142. package/template/components/ui/breadcrumb.tsx +1 -0
  143. package/template/components/ui/button.tsx +1 -0
  144. package/template/components/ui/calendar.tsx +1 -0
  145. package/template/components/ui/card.tsx +1 -0
  146. package/template/components/ui/chart.tsx +1 -0
  147. package/template/components/ui/checkbox.tsx +1 -0
  148. package/template/components/ui/coach-mark.tsx +1 -0
  149. package/template/components/ui/collapsible.tsx +1 -0
  150. package/template/components/ui/command.tsx +1 -0
  151. package/template/components/ui/date-picker-field.tsx +1 -0
  152. package/template/components/ui/dialog.tsx +1 -0
  153. package/template/components/ui/dot-pattern.tsx +159 -0
  154. package/template/components/ui/drag-handle-grip.tsx +1 -0
  155. package/template/components/ui/drawer.tsx +1 -0
  156. package/template/components/ui/dropdown-menu.tsx +1 -0
  157. package/template/components/ui/field.tsx +1 -0
  158. package/template/components/ui/form.tsx +1 -0
  159. package/template/components/ui/input-group.tsx +1 -0
  160. package/template/components/ui/input-mask.tsx +1 -0
  161. package/template/components/ui/input.tsx +1 -0
  162. package/template/components/ui/kbd.tsx +1 -0
  163. package/template/components/ui/label.tsx +1 -0
  164. package/template/components/ui/leo-icon.tsx +726 -0
  165. package/template/components/ui/payment-card-fields.tsx +1 -0
  166. package/template/components/ui/popover.tsx +1 -0
  167. package/template/components/ui/radio-group.tsx +1 -0
  168. package/template/components/ui/select.tsx +1 -0
  169. package/template/components/ui/selection-tile-grid.tsx +1 -0
  170. package/template/components/ui/separator.tsx +1 -0
  171. package/template/components/ui/sheet.tsx +1 -0
  172. package/template/components/ui/sidebar.tsx +1 -0
  173. package/template/components/ui/skeleton.tsx +1 -0
  174. package/template/components/ui/sonner.tsx +1 -0
  175. package/template/components/ui/status-badge.tsx +1 -0
  176. package/template/components/ui/table.tsx +1 -0
  177. package/template/components/ui/tabs.tsx +1 -0
  178. package/template/components/ui/textarea.tsx +1 -0
  179. package/template/components/ui/tip.tsx +1 -0
  180. package/template/components/ui/toggle-group.tsx +1 -0
  181. package/template/components/ui/toggle-switch.tsx +1 -0
  182. package/template/components/ui/toggle.tsx +1 -0
  183. package/template/components/ui/tooltip.tsx +1 -0
  184. package/template/components/ui/view-segmented-control.tsx +1 -0
  185. package/template/components.json +27 -0
  186. package/template/contexts/chart-variant-context.tsx +35 -0
  187. package/template/contexts/command-menu-context.tsx +28 -0
  188. package/template/contexts/dashboard-view-context.tsx +35 -0
  189. package/template/contexts/product-context.tsx +38 -0
  190. package/template/contexts/system-banner-context.tsx +127 -0
  191. package/template/docs/command-menu-pattern.md +45 -0
  192. package/template/docs/data-views-pattern.md +160 -0
  193. package/template/ecosystem.config.cjs +20 -0
  194. package/template/eslint.config.mjs +18 -0
  195. package/template/fontawesome-subset.manifest.json +190 -0
  196. package/template/hooks/.gitkeep +0 -0
  197. package/template/hooks/use-app-theme.ts +1 -0
  198. package/template/hooks/use-coach-mark.ts +1 -0
  199. package/template/hooks/use-mobile.ts +1 -0
  200. package/template/hooks/use-mod-key-label.ts +1 -0
  201. package/template/lib/.gitkeep +0 -0
  202. package/template/lib/ask-leo-route-context.ts +133 -0
  203. package/template/lib/chart-keyboard-selection.test.ts +20 -0
  204. package/template/lib/chart-keyboard-selection.ts +17 -0
  205. package/template/lib/chart-line-dash.ts +16 -0
  206. package/template/lib/coach-mark-registry.ts +68 -0
  207. package/template/lib/command-menu-config.ts +127 -0
  208. package/template/lib/command-menu-search-data.ts +44 -0
  209. package/template/lib/conditional-rule-match.ts +32 -0
  210. package/template/lib/dashboard-customize-coach-mark.ts +18 -0
  211. package/template/lib/dashboard-layout-merge.ts +63 -0
  212. package/template/lib/data-list-display-options.ts +35 -0
  213. package/template/lib/data-list-persistence.ts +280 -0
  214. package/template/lib/data-list-view-surface.ts +58 -0
  215. package/template/lib/data-list-view.ts +29 -0
  216. package/template/lib/data-view-dashboard-storage.ts +101 -0
  217. package/template/lib/date-filter.ts +8 -0
  218. package/template/lib/dev-log.test.ts +28 -0
  219. package/template/lib/dev-log.ts +8 -0
  220. package/template/lib/editable-target.ts +10 -0
  221. package/template/lib/floating-sheet-panel.ts +72 -0
  222. package/template/lib/initials-from-name.ts +7 -0
  223. package/template/lib/list-page-table-properties.ts +52 -0
  224. package/template/lib/list-status-badges.ts +168 -0
  225. package/template/lib/logo-dev.ts +12 -0
  226. package/template/lib/mock/compliance-kpi.ts +61 -0
  227. package/template/lib/mock/compliance.ts +146 -0
  228. package/template/lib/mock/dashboard.ts +105 -0
  229. package/template/lib/mock/navigation.tsx +231 -0
  230. package/template/lib/mock/placements-kpi.ts +134 -0
  231. package/template/lib/mock/placements.ts +183 -0
  232. package/template/lib/mock/question-bank-kpi.ts +61 -0
  233. package/template/lib/mock/question-bank.ts +142 -0
  234. package/template/lib/mock/sites-directory.ts +16 -0
  235. package/template/lib/mock/sites-kpi.ts +25 -0
  236. package/template/lib/mock/team-kpi.ts +60 -0
  237. package/template/lib/mock/team.ts +118 -0
  238. package/template/lib/motion-ui.ts +17 -0
  239. package/template/lib/placement-board-card-layout.ts +79 -0
  240. package/template/lib/placement-lifecycle.ts +5 -0
  241. package/template/lib/row-height.ts +10 -0
  242. package/template/lib/stock-portrait.ts +11 -0
  243. package/template/lib/utils.test.ts +13 -0
  244. package/template/lib/utils.ts +1 -0
  245. package/template/next.config.mjs +15 -0
  246. package/template/package.json +83 -0
  247. package/template/postcss.config.mjs +8 -0
  248. package/template/public/.gitkeep +0 -0
  249. package/template/public/Illustration/Rotation.svg +74 -0
  250. package/template/public/avatars/user.svg +11 -0
  251. package/template/public/favicon/favicon.ico +0 -0
  252. package/template/public/favicon.ico +0 -0
  253. package/template/public/logos/exxat-one.svg +36 -0
  254. package/template/public/logos/exxat-prism.svg +39 -0
  255. package/template/public/mock-schools/emory.svg +4 -0
  256. package/template/public/mock-schools/rush.svg +4 -0
  257. package/template/scripts/fontawesome-subset-audit.mjs +190 -0
  258. package/template/scripts/pm2-startup-macos.sh +13 -0
  259. package/template/skills-lock.json +10 -0
  260. package/template/stores/app-store.ts +33 -0
  261. package/template/tests/setup.ts +1 -0
  262. package/template/tsconfig.json +35 -0
  263. package/template/types/react-payment-inputs.d.ts +19 -0
  264. package/template/vitest.config.ts +18 -0
@@ -0,0 +1,106 @@
1
+ "use client"
2
+
3
+ import { Badge } from "@/components/ui/badge"
4
+ import {
5
+ Card,
6
+ CardAction,
7
+ CardDescription,
8
+ CardFooter,
9
+ CardHeader,
10
+ CardTitle,
11
+ } from "@/components/ui/card"
12
+
13
+ export function SectionCards() {
14
+ return (
15
+ <div className="grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4 dark:*:data-[slot=card]:bg-card">
16
+ <Card className="@container/card">
17
+ <CardHeader>
18
+ <CardDescription>Total Revenue</CardDescription>
19
+ <CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
20
+ $1,250.00
21
+ </CardTitle>
22
+ <CardAction>
23
+ <Badge variant="outline">
24
+ <i className="fa-light fa-arrow-trend-up" aria-hidden="true" />
25
+ +12.5%
26
+ </Badge>
27
+ </CardAction>
28
+ </CardHeader>
29
+ <CardFooter className="flex-col items-start gap-1.5 text-sm">
30
+ <div className="line-clamp-1 flex gap-2 font-medium">
31
+ Trending up this month{" "}
32
+ <i className="fa-light fa-arrow-trend-up size-4" aria-hidden="true" />
33
+ </div>
34
+ <div className="text-muted-foreground">
35
+ Visitors for the last 6 months
36
+ </div>
37
+ </CardFooter>
38
+ </Card>
39
+ <Card className="@container/card">
40
+ <CardHeader>
41
+ <CardDescription>New Customers</CardDescription>
42
+ <CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
43
+ 1,234
44
+ </CardTitle>
45
+ <CardAction>
46
+ <Badge variant="outline">
47
+ <i className="fa-light fa-arrow-trend-down" aria-hidden="true" />
48
+ -20%
49
+ </Badge>
50
+ </CardAction>
51
+ </CardHeader>
52
+ <CardFooter className="flex-col items-start gap-1.5 text-sm">
53
+ <div className="line-clamp-1 flex gap-2 font-medium">
54
+ Down 20% this period{" "}
55
+ <i className="fa-light fa-arrow-trend-down size-4" aria-hidden="true" />
56
+ </div>
57
+ <div className="text-muted-foreground">
58
+ Acquisition needs attention
59
+ </div>
60
+ </CardFooter>
61
+ </Card>
62
+ <Card className="@container/card">
63
+ <CardHeader>
64
+ <CardDescription>Active Accounts</CardDescription>
65
+ <CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
66
+ 45,678
67
+ </CardTitle>
68
+ <CardAction>
69
+ <Badge variant="outline">
70
+ <i className="fa-light fa-arrow-trend-up" aria-hidden="true" />
71
+ +12.5%
72
+ </Badge>
73
+ </CardAction>
74
+ </CardHeader>
75
+ <CardFooter className="flex-col items-start gap-1.5 text-sm">
76
+ <div className="line-clamp-1 flex gap-2 font-medium">
77
+ Strong user retention{" "}
78
+ <i className="fa-light fa-arrow-trend-up size-4" aria-hidden="true" />
79
+ </div>
80
+ <div className="text-muted-foreground">Engagement exceed targets</div>
81
+ </CardFooter>
82
+ </Card>
83
+ <Card className="@container/card">
84
+ <CardHeader>
85
+ <CardDescription>Growth Rate</CardDescription>
86
+ <CardTitle className="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
87
+ 4.5%
88
+ </CardTitle>
89
+ <CardAction>
90
+ <Badge variant="outline">
91
+ <i className="fa-light fa-arrow-trend-up" aria-hidden="true" />
92
+ +4.5%
93
+ </Badge>
94
+ </CardAction>
95
+ </CardHeader>
96
+ <CardFooter className="flex-col items-start gap-1.5 text-sm">
97
+ <div className="line-clamp-1 flex gap-2 font-medium">
98
+ Steady performance increase{" "}
99
+ <i className="fa-light fa-arrow-trend-up size-4" aria-hidden="true" />
100
+ </div>
101
+ <div className="text-muted-foreground">Meets growth projections</div>
102
+ </CardFooter>
103
+ </Card>
104
+ </div>
105
+ )
106
+ }
@@ -0,0 +1,424 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { useTheme } from "next-themes"
5
+ import { FieldGroup } from "@/components/ui/field"
6
+ import { RadioGroup, RadioGroupItem, RadioGroupLabel } from "@/components/ui/radio-group"
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from "@/components/ui/select"
14
+ import { SelectionTileGrid } from "@/components/ui/selection-tile-grid"
15
+ import { useAppTheme, type Brand, type TextSizePreference } from "@/hooks/use-app-theme"
16
+ import { useDashboardView, type DashboardView } from "@/contexts/dashboard-view-context"
17
+ import { useChartVariant, type ChartVariant } from "@/contexts/chart-variant-context"
18
+ import { SettingsFormRow } from "@/components/settings-form-row"
19
+ import { cn } from "@/lib/utils"
20
+
21
+ function RadioRow({
22
+ value,
23
+ id,
24
+ label,
25
+ iconClass,
26
+ }: {
27
+ value: string
28
+ id: string
29
+ label: string
30
+ iconClass?: string
31
+ }) {
32
+ return (
33
+ <div className="flex items-center gap-3">
34
+ <RadioGroupItem value={value} id={id} className="shrink-0" />
35
+ <RadioGroupLabel htmlFor={id} className="flex min-h-0 flex-1 items-center gap-2 py-0 text-sm font-normal">
36
+ {iconClass ? (
37
+ <i className={cn("fa-light w-4 shrink-0 text-center text-muted-foreground", iconClass)} aria-hidden="true" />
38
+ ) : null}
39
+ {label}
40
+ </RadioGroupLabel>
41
+ </div>
42
+ )
43
+ }
44
+
45
+ /** Illustrative split sidebars when “system” shows light+dark (tokens follow active brand hue). */
46
+ const SPLIT_SIDEBAR: Record<Brand, { light: string; dark: string }> = {
47
+ one: { light: "oklch(0.935 0.024 286.1)", dark: "oklch(0.38 0.09 286.1)" },
48
+ prism: { light: "oklch(0.96 0.04 342)", dark: "oklch(0.4 0.14 342)" },
49
+ }
50
+
51
+ const THEME_SVG_CLASS = "h-12 w-auto max-w-[9rem] shrink-0"
52
+
53
+ /** Fills the square preview in Settings appearance tiles (see SelectionTileGraphic below-mode sizing). */
54
+ const APPEARANCE_TILE_SVG = "block h-full w-auto max-h-full max-w-full shrink-0 object-contain"
55
+
56
+ /** Fixed palettes per labeled mode — do not use `var(--background)` etc. (those follow the *current* theme). */
57
+ const CHROME_LIGHT = {
58
+ shell: "oklch(1 0 0)",
59
+ shellStroke: "oklch(0.90 0.003 270)",
60
+ headerBar: "oklch(0.93 0.004 270)",
61
+ content: "oklch(0.96 0.004 270)",
62
+ } as const
63
+
64
+ const CHROME_DARK = {
65
+ shell: "oklch(0.12 0.01 270)",
66
+ shellStroke: "oklch(0.38 0.02 270)",
67
+ headerBar: "oklch(0.22 0.02 270)",
68
+ content: "oklch(0.17 0.015 270)",
69
+ } as const
70
+
71
+ /** Mini browser chrome: illustrative light / dark / split (brand sidebars from SPLIT_SIDEBAR). */
72
+ function ThemeModeSvg({ mode, brand }: { mode: "system" | "light" | "dark"; brand: Brand }) {
73
+ const split = SPLIT_SIDEBAR[brand]
74
+ if (mode === "light") {
75
+ return (
76
+ <svg className={APPEARANCE_TILE_SVG} viewBox="0 0 56 32" fill="none" aria-hidden="true">
77
+ <rect x="0.5" y="0.5" width="55" height="31" rx="4" fill={CHROME_LIGHT.shell} stroke={CHROME_LIGHT.shellStroke} />
78
+ <rect x="3" y="3" width="13" height="26" rx="2" fill={split.light} />
79
+ <rect x="19" y="6" width="34" height="4.5" rx="1" fill={CHROME_LIGHT.headerBar} />
80
+ <rect x="19" y="14" width="34" height="14" rx="2" fill={CHROME_LIGHT.content} />
81
+ </svg>
82
+ )
83
+ }
84
+ if (mode === "dark") {
85
+ return (
86
+ <svg className={APPEARANCE_TILE_SVG} viewBox="0 0 56 32" fill="none" aria-hidden="true">
87
+ <rect x="0.5" y="0.5" width="55" height="31" rx="4" fill={CHROME_DARK.shell} stroke={CHROME_DARK.shellStroke} />
88
+ <rect x="3" y="3" width="13" height="26" rx="2" fill={split.dark} />
89
+ <rect x="19" y="6" width="34" height="4.5" rx="1" fill={CHROME_DARK.headerBar} />
90
+ <rect x="19" y="14" width="34" height="14" rx="2" fill={CHROME_DARK.content} />
91
+ </svg>
92
+ )
93
+ }
94
+ return (
95
+ <svg className={APPEARANCE_TILE_SVG} viewBox="0 0 56 32" fill="none" aria-hidden="true">
96
+ <rect x="0.5" y="0.5" width="26" height="31" rx="4" fill={CHROME_LIGHT.shell} stroke={CHROME_LIGHT.shellStroke} />
97
+ <rect x="3" y="3" width="10" height="26" rx="1.5" fill={split.light} />
98
+ <rect x="15" y="6" width="10" height="4" rx="0.75" fill={CHROME_LIGHT.headerBar} />
99
+ <rect x="15" y="13" width="10" height="14" rx="1.5" fill={CHROME_LIGHT.content} />
100
+ <rect x="28.5" y="0.5" width="27" height="31" rx="4" fill={CHROME_DARK.shell} stroke={CHROME_DARK.shellStroke} />
101
+ <rect x="31" y="3" width="10" height="26" rx="1.5" fill={split.dark} />
102
+ <rect x="43" y="6" width="10" height="4" rx="0.75" fill={CHROME_DARK.headerBar} />
103
+ <rect x="43" y="13" width="10" height="14" rx="1.5" fill={CHROME_DARK.content} />
104
+ </svg>
105
+ )
106
+ }
107
+
108
+ const HC_STROKE = "oklch(0.22 0.02 270)"
109
+
110
+ /** Illustrative light chrome; stroke weight shows contrast (not tied to active color theme). */
111
+ function ContrastPrefSvg({
112
+ pref,
113
+ brand,
114
+ }: {
115
+ pref: "system" | "normal" | "high" | "windows"
116
+ brand: Brand
117
+ }) {
118
+ const split = SPLIT_SIDEBAR[brand]
119
+ if (pref === "normal") {
120
+ return (
121
+ <svg className={APPEARANCE_TILE_SVG} viewBox="0 0 56 32" fill="none" aria-hidden="true">
122
+ <rect
123
+ x="0.5"
124
+ y="0.5"
125
+ width="55"
126
+ height="31"
127
+ rx="4"
128
+ fill={CHROME_LIGHT.shell}
129
+ stroke={CHROME_LIGHT.shellStroke}
130
+ strokeWidth="1"
131
+ />
132
+ <rect x="3" y="3" width="13" height="26" rx="2" fill={split.light} />
133
+ <rect x="19" y="6" width="34" height="4.5" rx="1" fill={CHROME_LIGHT.headerBar} />
134
+ <rect x="19" y="14" width="34" height="14" rx="2" fill={CHROME_LIGHT.content} />
135
+ </svg>
136
+ )
137
+ }
138
+ if (pref === "high") {
139
+ return (
140
+ <svg className={APPEARANCE_TILE_SVG} viewBox="0 0 56 32" fill="none" aria-hidden="true">
141
+ <rect
142
+ x="0.5"
143
+ y="0.5"
144
+ width="55"
145
+ height="31"
146
+ rx="4"
147
+ fill={CHROME_LIGHT.shell}
148
+ stroke={HC_STROKE}
149
+ strokeWidth="2"
150
+ />
151
+ <rect
152
+ x="3"
153
+ y="3"
154
+ width="13"
155
+ height="26"
156
+ rx="2"
157
+ fill={split.light}
158
+ stroke={HC_STROKE}
159
+ strokeWidth="1.5"
160
+ />
161
+ <rect x="19" y="6" width="34" height="4.5" rx="1" fill="oklch(0.35 0.02 270)" opacity="0.35" />
162
+ <rect x="19" y="14" width="34" height="14" rx="2" fill="oklch(0.35 0.02 270)" opacity="0.22" />
163
+ </svg>
164
+ )
165
+ }
166
+ if (pref === "windows") {
167
+ /* Classic Windows HC cue: black canvas, white border, yellow + cyan accents */
168
+ return (
169
+ <svg className={APPEARANCE_TILE_SVG} viewBox="0 0 56 32" fill="none" aria-hidden="true">
170
+ <rect x="0.5" y="0.5" width="55" height="31" rx="4" fill="#000000" stroke="#ffffff" strokeWidth="2" />
171
+ <rect x="4" y="5" width="14" height="22" rx="2" fill="#000000" stroke="#ffffff" strokeWidth="1.5" />
172
+ <rect x="22" y="7" width="30" height="5" rx="1" fill="#ffff00" />
173
+ <rect x="22" y="15" width="30" height="10" rx="1.5" fill="#000000" stroke="#00ffff" strokeWidth="1.25" />
174
+ </svg>
175
+ )
176
+ }
177
+ return (
178
+ <svg className={APPEARANCE_TILE_SVG} viewBox="0 0 56 32" fill="none" aria-hidden="true">
179
+ <rect x="0.5" y="0.5" width="26" height="31" rx="4" fill={CHROME_LIGHT.shell} stroke={CHROME_LIGHT.shellStroke} strokeWidth="1" />
180
+ <rect x="3" y="3" width="10" height="26" rx="1.5" fill={split.light} />
181
+ <rect x="15" y="6" width="10" height="4" rx="0.75" fill={CHROME_LIGHT.headerBar} />
182
+ <rect x="15" y="13" width="10" height="14" rx="1.5" fill={CHROME_LIGHT.content} />
183
+ <rect x="28.5" y="0.5" width="27" height="31" rx="4" fill={CHROME_LIGHT.shell} stroke={HC_STROKE} strokeWidth="2" />
184
+ <rect x="31" y="3" width="10" height="26" rx="1.5" fill={split.light} stroke={HC_STROKE} strokeWidth="1.5" />
185
+ <rect x="43" y="6" width="10" height="4" rx="0.75" fill="oklch(0.35 0.02 270)" opacity="0.35" />
186
+ <rect x="43" y="13" width="10" height="14" rx="1.5" fill="oklch(0.35 0.02 270)" opacity="0.22" />
187
+ </svg>
188
+ )
189
+ }
190
+
191
+ const CHART_LABELS: Record<ChartVariant, string> = {
192
+ normal: "Normal",
193
+ tabs: "With tabs",
194
+ selector: "With filters",
195
+ "metrics-tabs": "Tabs + metrics",
196
+ "kpi-chart": "KPI + chart",
197
+ }
198
+
199
+ const VIEW_LABELS: Record<DashboardView, string> = {
200
+ report: "Report",
201
+ simple: "Simple",
202
+ mix: "Mix",
203
+ }
204
+
205
+ const VIEW_STROKE = "oklch(0.78 0.01 270)"
206
+ const VIEW_FILL_STRONG = "oklch(0.82 0.02 270)"
207
+ const VIEW_FILL_SOFT = "oklch(0.90 0.008 270)"
208
+
209
+ /** Illustrative dashboard layout previews — same chrome, different content grid. */
210
+ function DashboardViewSvg({ view }: { view: DashboardView }) {
211
+ if (view === "report") {
212
+ // Chart card + two-column data rows below.
213
+ return (
214
+ <svg className={THEME_SVG_CLASS} viewBox="0 0 56 32" fill="none" aria-hidden="true">
215
+ <rect x="0.5" y="0.5" width="55" height="31" rx="4" fill={CHROME_LIGHT.shell} stroke={CHROME_LIGHT.shellStroke} />
216
+ <rect x="3" y="3" width="50" height="12" rx="1.5" fill={VIEW_FILL_SOFT} stroke={VIEW_STROKE} strokeWidth="0.5" />
217
+ {/* bars inside chart */}
218
+ <rect x="6" y="9" width="2" height="5" fill={VIEW_FILL_STRONG} />
219
+ <rect x="10" y="7" width="2" height="7" fill={VIEW_FILL_STRONG} />
220
+ <rect x="14" y="10" width="2" height="4" fill={VIEW_FILL_STRONG} />
221
+ <rect x="18" y="6" width="2" height="8" fill={VIEW_FILL_STRONG} />
222
+ <rect x="22" y="8" width="2" height="6" fill={VIEW_FILL_STRONG} />
223
+ <rect x="26" y="5" width="2" height="9" fill={VIEW_FILL_STRONG} />
224
+ <rect x="30" y="9" width="2" height="5" fill={VIEW_FILL_STRONG} />
225
+ {/* rows */}
226
+ <rect x="3" y="18" width="24" height="4" rx="1" fill={VIEW_FILL_SOFT} />
227
+ <rect x="29" y="18" width="24" height="4" rx="1" fill={VIEW_FILL_SOFT} />
228
+ <rect x="3" y="24" width="24" height="4" rx="1" fill={VIEW_FILL_SOFT} />
229
+ <rect x="29" y="24" width="24" height="4" rx="1" fill={VIEW_FILL_SOFT} />
230
+ </svg>
231
+ )
232
+ }
233
+ if (view === "simple") {
234
+ // 2×3 KPI grid — clean tiles.
235
+ return (
236
+ <svg className={THEME_SVG_CLASS} viewBox="0 0 56 32" fill="none" aria-hidden="true">
237
+ <rect x="0.5" y="0.5" width="55" height="31" rx="4" fill={CHROME_LIGHT.shell} stroke={CHROME_LIGHT.shellStroke} />
238
+ {[0, 1, 2].map((col) => (
239
+ <React.Fragment key={col}>
240
+ <rect x={3 + col * 17} y="3" width="16" height="12" rx="1.5" fill={VIEW_FILL_SOFT} stroke={VIEW_STROKE} strokeWidth="0.5" />
241
+ <rect x={3 + col * 17} y="17" width="16" height="12" rx="1.5" fill={VIEW_FILL_SOFT} stroke={VIEW_STROKE} strokeWidth="0.5" />
242
+ </React.Fragment>
243
+ ))}
244
+ </svg>
245
+ )
246
+ }
247
+ // mix — one big chart + 3 KPI tiles
248
+ return (
249
+ <svg className={THEME_SVG_CLASS} viewBox="0 0 56 32" fill="none" aria-hidden="true">
250
+ <rect x="0.5" y="0.5" width="55" height="31" rx="4" fill={CHROME_LIGHT.shell} stroke={CHROME_LIGHT.shellStroke} />
251
+ <rect x="3" y="3" width="32" height="26" rx="1.5" fill={VIEW_FILL_SOFT} stroke={VIEW_STROKE} strokeWidth="0.5" />
252
+ {/* sparkline */}
253
+ <path d="M5 22 L10 16 L15 19 L20 12 L25 15 L30 9 L33 13" stroke={VIEW_FILL_STRONG} strokeWidth="1" fill="none" />
254
+ {/* side KPIs */}
255
+ <rect x="37" y="3" width="16" height="8" rx="1.5" fill={VIEW_FILL_SOFT} stroke={VIEW_STROKE} strokeWidth="0.5" />
256
+ <rect x="37" y="12" width="16" height="8" rx="1.5" fill={VIEW_FILL_SOFT} stroke={VIEW_STROKE} strokeWidth="0.5" />
257
+ <rect x="37" y="21" width="16" height="8" rx="1.5" fill={VIEW_FILL_SOFT} stroke={VIEW_STROKE} strokeWidth="0.5" />
258
+ </svg>
259
+ )
260
+ }
261
+
262
+ const THEME_CHOICE_LABEL: Record<"system" | "light" | "dark", string> = {
263
+ system: "System default",
264
+ light: "Light",
265
+ dark: "Dark",
266
+ }
267
+
268
+ const TEXT_SIZE_LABEL: Record<TextSizePreference, string> = {
269
+ compact: "Tiny",
270
+ default: "Default",
271
+ large: "Large",
272
+ }
273
+
274
+ export function SettingsAppearanceCard() {
275
+ const { theme, setTheme } = useTheme()
276
+ const { brand, contrastPref, setContrast, textSizePref, setTextSize, mounted } = useAppTheme()
277
+ const { activeView, setActiveView } = useDashboardView()
278
+ const { chartVariant, setChartVariant } = useChartVariant()
279
+
280
+ const safeTheme = mounted ? ((theme ?? "system") as "system" | "light" | "dark") : "system"
281
+ const safeBrand = mounted ? brand : "one"
282
+ const safeContrast = mounted ? contrastPref : "system"
283
+ const safeTextSize = mounted ? textSizePref : "default"
284
+
285
+ const themeTiles = React.useMemo(
286
+ () =>
287
+ (["system", "light", "dark"] as const).map((m) => ({
288
+ value: m,
289
+ label: THEME_CHOICE_LABEL[m],
290
+ leading: <ThemeModeSvg mode={m} brand={safeBrand} />,
291
+ })),
292
+ [safeBrand],
293
+ )
294
+
295
+ const contrastTiles = React.useMemo(
296
+ () =>
297
+ (["system", "normal", "high", "windows"] as const).map((p) => ({
298
+ value: p,
299
+ label:
300
+ p === "system"
301
+ ? "System"
302
+ : p === "normal"
303
+ ? "Normal"
304
+ : p === "high"
305
+ ? "High"
306
+ : "Windows",
307
+ leading: <ContrastPrefSvg pref={p} brand={safeBrand} />,
308
+ })),
309
+ [safeBrand],
310
+ )
311
+
312
+ const textSizeTiles = React.useMemo(
313
+ () =>
314
+ (["compact", "default", "large"] as const).map((v) => ({
315
+ value: v,
316
+ label: TEXT_SIZE_LABEL[v],
317
+ leading: (
318
+ <span
319
+ className={cn(
320
+ "font-semibold leading-none tracking-tight text-foreground",
321
+ v === "compact" && "text-sm",
322
+ v === "default" && "text-xl",
323
+ v === "large" && "text-3xl",
324
+ )}
325
+ aria-hidden
326
+ >
327
+ Aa
328
+ </span>
329
+ ),
330
+ })),
331
+ [],
332
+ )
333
+
334
+ return (
335
+ <section id="appearance" className="scroll-mt-20">
336
+ <header className="mb-8 space-y-1">
337
+ <h2 className="text-lg font-semibold text-foreground">Appearance &amp; display</h2>
338
+ <p className="text-sm text-muted-foreground">Saved in this browser.</p>
339
+ </header>
340
+ <div>
341
+ {!mounted ? (
342
+ <p className="text-sm text-muted-foreground">Loading theme…</p>
343
+ ) : (
344
+ <FieldGroup className="gap-8">
345
+ <SettingsFormRow label="Theme" description="Light, dark, or match your OS.">
346
+ <SelectionTileGrid<"system" | "light" | "dark">
347
+ className="w-full"
348
+ labelPlacement="below"
349
+ options={themeTiles}
350
+ columns={3}
351
+ value={safeTheme}
352
+ onValueChange={(v) => setTheme(v)}
353
+ interaction="button"
354
+ />
355
+ </SettingsFormRow>
356
+
357
+ <SettingsFormRow
358
+ label="Contrast"
359
+ description="High uses the built-in Fluent-style palette. Windows loads colors from the JSON file (edit and refresh)."
360
+ >
361
+ <SelectionTileGrid<"system" | "normal" | "high" | "windows">
362
+ className="w-full"
363
+ labelPlacement="below"
364
+ options={contrastTiles}
365
+ columns={4}
366
+ value={safeContrast}
367
+ onValueChange={(v) => setContrast(v)}
368
+ interaction="button"
369
+ />
370
+ </SettingsFormRow>
371
+
372
+ <SettingsFormRow
373
+ label="Text size"
374
+ description="Scales UI text from the root (like iOS/Android accessibility size). Tiny still enforces an 11px floor for labels."
375
+ >
376
+ <SelectionTileGrid<TextSizePreference>
377
+ className="w-full"
378
+ labelPlacement="below"
379
+ options={textSizeTiles}
380
+ columns={3}
381
+ value={safeTextSize}
382
+ onValueChange={(v) => setTextSize(v)}
383
+ interaction="button"
384
+ />
385
+ </SettingsFormRow>
386
+
387
+ <SettingsFormRow label="Dashboard layout" description="Default dashboard tab.">
388
+ <RadioGroup
389
+ value={activeView}
390
+ onValueChange={(v) => setActiveView(v as DashboardView)}
391
+ className="flex flex-col gap-3"
392
+ aria-label="Dashboard view"
393
+ itemVariant="outline"
394
+ itemMotion="glow"
395
+ >
396
+ <RadioRow value="report" id="dash-report" label={VIEW_LABELS.report} iconClass="fa-chart-mixed" />
397
+ <RadioRow value="simple" id="dash-simple" label={VIEW_LABELS.simple} iconClass="fa-grid-2" />
398
+ <RadioRow value="mix" id="dash-mix" label={VIEW_LABELS.mix} iconClass="fa-layer-group" />
399
+ </RadioGroup>
400
+ </SettingsFormRow>
401
+
402
+ <SettingsFormRow label="Chart style" htmlFor="settings-chart-style">
403
+ <Select
404
+ value={chartVariant}
405
+ onValueChange={(v) => setChartVariant(v as ChartVariant)}
406
+ >
407
+ <SelectTrigger id="settings-chart-style" className="w-full">
408
+ <SelectValue placeholder="Choose style" />
409
+ </SelectTrigger>
410
+ <SelectContent>
411
+ {(Object.keys(CHART_LABELS) as ChartVariant[]).map((key) => (
412
+ <SelectItem key={key} value={key}>
413
+ {CHART_LABELS[key]}
414
+ </SelectItem>
415
+ ))}
416
+ </SelectContent>
417
+ </Select>
418
+ </SettingsFormRow>
419
+ </FieldGroup>
420
+ )}
421
+ </div>
422
+ </section>
423
+ )
424
+ }