@exxatdesignux/ui 0.0.5 → 0.0.7

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,537 @@
1
+ "use client"
2
+
3
+ /**
4
+ * SettingsClient — System banner + coach mark utilities.
5
+ *
6
+ * Provides:
7
+ * • System banner: edit copy, variant, action link, dismissibility; Apply / Discard / Reset
8
+ * • Coach marks: list flows, reset, preview on target page
9
+ */
10
+
11
+ import * as React from "react"
12
+ import { useRouter } from "next/navigation"
13
+ import { Button } from "@/components/ui/button"
14
+ import { Badge } from "@/components/ui/badge"
15
+ import { Input } from "@/components/ui/input"
16
+ import { Textarea } from "@/components/ui/textarea"
17
+ import { ToggleSwitch } from "@/components/ui/toggle-switch"
18
+ import {
19
+ Select,
20
+ SelectContent,
21
+ SelectItem,
22
+ SelectTrigger,
23
+ SelectValue,
24
+ } from "@/components/ui/select"
25
+ import { SystemBanner } from "@/components/ui/banner"
26
+ import { Tip } from "@/components/ui/tip"
27
+ import { cn } from "@/lib/utils"
28
+ import { COACH_MARK_FLOWS, type CoachMarkFlowDef } from "@/lib/coach-mark-registry"
29
+ import {
30
+ resetCoachMarkFlow,
31
+ resetAllCoachMarks,
32
+ } from "@/hooks/use-coach-mark"
33
+ import {
34
+ useSystemBanner,
35
+ type SystemBannerConfig,
36
+ type SystemBannerVariant,
37
+ } from "@/contexts/system-banner-context"
38
+ import { SettingsAppearanceCard } from "@/components/settings-appearance-card"
39
+ import { SettingsFormRow } from "@/components/settings-form-row"
40
+ import { FieldGroup } from "@/components/ui/field"
41
+ import { FilterTextValueInput } from "@/components/data-table/filter-text-value-input"
42
+
43
+ const SYSTEM_BANNER_VARIANTS: SystemBannerVariant[] = [
44
+ "info",
45
+ "warning",
46
+ "error",
47
+ "success",
48
+ "promo",
49
+ ]
50
+
51
+ /* ── Helpers ─────────────────────────────────────────────────────────────── */
52
+
53
+ const STORAGE_PREFIX = "exxat-coach-mark:"
54
+
55
+ function isFlowDismissed(flowId: string): boolean {
56
+ if (typeof window === "undefined") return false
57
+ try {
58
+ return localStorage.getItem(`${STORAGE_PREFIX}${flowId}`) === "dismissed"
59
+ } catch {
60
+ return false
61
+ }
62
+ }
63
+
64
+ /* ── Flow card ──────────────────────────────────────────────────────────── */
65
+
66
+ function FlowCard({
67
+ flow,
68
+ dismissed,
69
+ onReset,
70
+ onPreview,
71
+ }: {
72
+ flow: CoachMarkFlowDef
73
+ dismissed: boolean
74
+ onReset: () => void
75
+ onPreview: () => void
76
+ }) {
77
+ return (
78
+ <div className="flex items-start gap-4 rounded-lg border border-border bg-card p-4 transition-colors hover:bg-interactive-hover-soft">
79
+ {/* Icon */}
80
+ <span
81
+ className={cn(
82
+ "shrink-0 flex h-10 w-10 items-center justify-center rounded-lg text-sm",
83
+ dismissed
84
+ ? "bg-muted text-muted-foreground"
85
+ : "bg-brand/12 text-brand"
86
+ )}
87
+ aria-hidden="true"
88
+ >
89
+ <i className={cn("fa-light", dismissed ? "fa-circle-check" : "fa-route")} />
90
+ </span>
91
+
92
+ {/* Content */}
93
+ <div className="flex-1 min-w-0">
94
+ <div className="flex items-center gap-2 mb-1">
95
+ <h3 className="text-sm font-semibold text-foreground">{flow.name}</h3>
96
+ <Badge variant={dismissed ? "secondary" : "default"} className="text-xs h-5">
97
+ {dismissed ? "Completed" : "Active"}
98
+ </Badge>
99
+ </div>
100
+ <p className="text-xs text-muted-foreground leading-relaxed mb-2">
101
+ {flow.description}
102
+ </p>
103
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
104
+ <span className="flex items-center gap-1">
105
+ <i className="fa-light fa-file text-xs" aria-hidden="true" />
106
+ {flow.page}
107
+ </span>
108
+ <span className="flex items-center gap-1">
109
+ <i className="fa-light fa-list-ol text-xs" aria-hidden="true" />
110
+ {flow.stepCount} steps
111
+ </span>
112
+ </div>
113
+ </div>
114
+
115
+ {/* Actions */}
116
+ <div className="flex items-center gap-2 shrink-0">
117
+ <Tip side="bottom" label="Reset and replay this tour">
118
+ <Button
119
+ type="button"
120
+ size="sm"
121
+ variant="outline"
122
+ onClick={onReset}
123
+ className="h-8 text-xs gap-1.5"
124
+ >
125
+ <i className="fa-light fa-rotate-left text-xs" aria-hidden="true" />
126
+ Reset
127
+ </Button>
128
+ </Tip>
129
+ <Tip side="bottom" label={`Go to ${flow.page} to preview`}>
130
+ <Button
131
+ type="button"
132
+ size="sm"
133
+ variant="ghost"
134
+ onClick={onPreview}
135
+ className="h-8 text-xs gap-1.5"
136
+ >
137
+ <i className="fa-light fa-arrow-up-right text-xs" aria-hidden="true" />
138
+ Preview
139
+ </Button>
140
+ </Tip>
141
+ </div>
142
+ </div>
143
+ )
144
+ }
145
+
146
+ /* ── System banner editor ───────────────────────────────────────────────── */
147
+
148
+ function SystemBannerSettingsCard() {
149
+ const { config, applyConfig, reset } = useSystemBanner()
150
+ const [draft, setDraft] = React.useState<SystemBannerConfig>(() => ({ ...config }))
151
+ const dirty = React.useMemo(() => JSON.stringify(draft) !== JSON.stringify(config), [draft, config])
152
+
153
+ React.useEffect(() => {
154
+ setDraft({ ...config })
155
+ }, [config])
156
+
157
+ return (
158
+ <section id="banner" className="scroll-mt-20">
159
+ <header className="mb-8 space-y-1">
160
+ <h2 className="text-lg font-semibold text-foreground">System banner &amp; alerts</h2>
161
+ <p className="text-sm text-muted-foreground">
162
+ Top-of-app strip for maintenance, promos, and notices. Stored locally as{" "}
163
+ <span className="font-mono text-xs">exxat:system-banner-config</span>.
164
+ </p>
165
+ </header>
166
+ <div className="flex flex-col gap-8">
167
+ <FieldGroup className="gap-8">
168
+ <SettingsFormRow
169
+ label="Show banner"
170
+ description="When off, the strip stays hidden until you turn it back on."
171
+ htmlFor="banner-enabled"
172
+ >
173
+ <ToggleSwitch
174
+ id="banner-enabled"
175
+ checked={draft.enabled}
176
+ onChange={(enabled) => setDraft((d) => ({ ...d, enabled }))}
177
+ />
178
+ </SettingsFormRow>
179
+
180
+ <SettingsFormRow
181
+ label="Variant"
182
+ description="Info, warning, error, success, or promo styling for the strip."
183
+ htmlFor="banner-variant"
184
+ >
185
+ <Select
186
+ value={draft.variant}
187
+ onValueChange={(v) =>
188
+ setDraft((d) => ({ ...d, variant: v as SystemBannerVariant }))
189
+ }
190
+ >
191
+ <SelectTrigger id="banner-variant" className="w-full">
192
+ <SelectValue placeholder="Variant" />
193
+ </SelectTrigger>
194
+ <SelectContent>
195
+ {SYSTEM_BANNER_VARIANTS.map((v) => (
196
+ <SelectItem key={v} value={v}>
197
+ {v.charAt(0).toUpperCase() + v.slice(1)}
198
+ </SelectItem>
199
+ ))}
200
+ </SelectContent>
201
+ </Select>
202
+ </SettingsFormRow>
203
+
204
+ <SettingsFormRow
205
+ label="Emphasis"
206
+ description="Prominent uses a solid dark fill; subtle uses a soft tinted background."
207
+ htmlFor="banner-emphasis"
208
+ >
209
+ <Select
210
+ value={draft.emphasis}
211
+ onValueChange={(v) =>
212
+ setDraft((d) => ({ ...d, emphasis: v as "prominent" | "subtle" }))
213
+ }
214
+ >
215
+ <SelectTrigger id="banner-emphasis" className="w-full">
216
+ <SelectValue placeholder="Emphasis" />
217
+ </SelectTrigger>
218
+ <SelectContent>
219
+ <SelectItem value="prominent">Prominent</SelectItem>
220
+ <SelectItem value="subtle">Subtle</SelectItem>
221
+ </SelectContent>
222
+ </Select>
223
+ </SettingsFormRow>
224
+
225
+ <SettingsFormRow label="Title" description="Short headline shown in the strip." htmlFor="banner-title">
226
+ <Input
227
+ id="banner-title"
228
+ value={draft.title}
229
+ onChange={(e) => setDraft((d) => ({ ...d, title: e.target.value }))}
230
+ placeholder="Short headline"
231
+ autoComplete="off"
232
+ className="w-full"
233
+ />
234
+ </SettingsFormRow>
235
+
236
+ <SettingsFormRow
237
+ label="Message"
238
+ description="Supporting line under the title (optional if you only want a title)."
239
+ htmlFor="banner-message"
240
+ >
241
+ <Textarea
242
+ id="banner-message"
243
+ value={draft.message}
244
+ onChange={(e) => setDraft((d) => ({ ...d, message: e.target.value }))}
245
+ placeholder="Supporting line shown under the title"
246
+ rows={3}
247
+ className="min-h-[4.5rem] w-full resize-y"
248
+ />
249
+ </SettingsFormRow>
250
+
251
+ <SettingsFormRow
252
+ label="Action label"
253
+ description="Optional button text, e.g. Learn more."
254
+ htmlFor="banner-action-label"
255
+ >
256
+ <Input
257
+ id="banner-action-label"
258
+ value={draft.actionLabel ?? ""}
259
+ onChange={(e) =>
260
+ setDraft((d) => ({
261
+ ...d,
262
+ actionLabel: e.target.value.trim() ? e.target.value : undefined,
263
+ }))
264
+ }
265
+ placeholder="Learn more"
266
+ autoComplete="off"
267
+ className="w-full"
268
+ />
269
+ </SettingsFormRow>
270
+
271
+ <SettingsFormRow
272
+ label="Action URL"
273
+ description="Where the action button navigates (https link or #)."
274
+ htmlFor="banner-action-href"
275
+ >
276
+ <Input
277
+ id="banner-action-href"
278
+ value={draft.actionHref ?? ""}
279
+ onChange={(e) =>
280
+ setDraft((d) => ({
281
+ ...d,
282
+ actionHref: e.target.value.trim() ? e.target.value : undefined,
283
+ }))
284
+ }
285
+ placeholder="https://… or #"
286
+ autoComplete="off"
287
+ className="w-full"
288
+ />
289
+ </SettingsFormRow>
290
+
291
+ <SettingsFormRow
292
+ label="Allow dismiss"
293
+ description="Shows a close control; dismissing turns the banner off."
294
+ htmlFor="banner-dismissible"
295
+ >
296
+ <ToggleSwitch
297
+ id="banner-dismissible"
298
+ checked={draft.dismissible}
299
+ onChange={(dismissible) => setDraft((d) => ({ ...d, dismissible }))}
300
+ />
301
+ </SettingsFormRow>
302
+
303
+ <SettingsFormRow label="Preview" description="Approximates the live strip above your sidebar.">
304
+ <div className="rounded-lg border border-dashed border-border bg-muted/20 p-3 w-full">
305
+ {draft.enabled ? (
306
+ <SystemBanner
307
+ variant={draft.variant}
308
+ emphasis={draft.emphasis}
309
+ title={draft.title || undefined}
310
+ dismissible={false}
311
+ action={
312
+ draft.actionLabel
313
+ ? { label: draft.actionLabel, href: draft.actionHref || "#" }
314
+ : undefined
315
+ }
316
+ >
317
+ {draft.message || (draft.title ? "" : "…")}
318
+ </SystemBanner>
319
+ ) : (
320
+ <p className="text-sm text-muted-foreground py-2 text-center">
321
+ Banner hidden — turn on “Show banner” to preview.
322
+ </p>
323
+ )}
324
+ </div>
325
+ </SettingsFormRow>
326
+ </FieldGroup>
327
+
328
+ <div className="flex flex-wrap items-center gap-2 pt-8">
329
+ <Button
330
+ type="button"
331
+ size="sm"
332
+ disabled={!dirty}
333
+ onClick={() => applyConfig({ ...draft })}
334
+ >
335
+ Apply to system banner
336
+ </Button>
337
+ <Button
338
+ type="button"
339
+ size="sm"
340
+ variant="outline"
341
+ disabled={!dirty}
342
+ onClick={() => setDraft({ ...config })}
343
+ >
344
+ Discard changes
345
+ </Button>
346
+ <Tip side="top" label="Restore the default copy and turn the banner on">
347
+ <Button type="button" size="sm" variant="ghost" onClick={() => reset()}>
348
+ Reset to defaults
349
+ </Button>
350
+ </Tip>
351
+ </div>
352
+ </div>
353
+ </section>
354
+ )
355
+ }
356
+
357
+ /* ── Main component ─────────────────────────────────────────────────────── */
358
+
359
+ function buildFlowStatuses() {
360
+ return COACH_MARK_FLOWS.map((f) => ({
361
+ flow: f,
362
+ dismissed: isFlowDismissed(f.id),
363
+ }))
364
+ }
365
+
366
+ export function SettingsClient() {
367
+ const router = useRouter()
368
+
369
+ const [demoPhone, setDemoPhone] = React.useState("")
370
+ const [demoZip, setDemoZip] = React.useState("")
371
+ const [demoDate, setDemoDate] = React.useState("")
372
+
373
+ /** SSR + first client paint: all undismissed so markup matches; sync from storage after mount. */
374
+ const [flowStatuses, setFlowStatuses] = React.useState(() =>
375
+ COACH_MARK_FLOWS.map((f) => ({ flow: f, dismissed: false })),
376
+ )
377
+
378
+ React.useEffect(() => {
379
+ setFlowStatuses(buildFlowStatuses())
380
+ }, [])
381
+
382
+ const completedCount = flowStatuses.filter((f) => f.dismissed).length
383
+ const totalCount = flowStatuses.length
384
+
385
+ function refreshFlowStatuses() {
386
+ setFlowStatuses(buildFlowStatuses())
387
+ }
388
+
389
+ function handleResetFlow(flowId: string) {
390
+ resetCoachMarkFlow(flowId)
391
+ refreshFlowStatuses()
392
+ }
393
+
394
+ function handleResetAll() {
395
+ resetAllCoachMarks()
396
+ refreshFlowStatuses()
397
+ }
398
+
399
+ function handlePreview(pageUrl: string, flowId: string) {
400
+ resetCoachMarkFlow(flowId)
401
+ refreshFlowStatuses()
402
+ router.push(pageUrl)
403
+ }
404
+
405
+ return (
406
+ <div className="flex w-full min-w-0 flex-col">
407
+ <div>
408
+ <h1
409
+ className="text-2xl font-semibold tracking-tight leading-tight text-foreground"
410
+ style={{ fontFamily: "var(--font-heading)" }}
411
+ >
412
+ Settings
413
+ </h1>
414
+ <p className="mt-1 text-sm leading-relaxed text-muted-foreground">
415
+ Preferences and tools for this workspace. Display options apply on this device and are stored in your browser.
416
+ </p>
417
+ </div>
418
+
419
+ <div className="mt-10 flex flex-col gap-20">
420
+ <section id="account" className="scroll-mt-20">
421
+ <header className="mb-6 space-y-1">
422
+ <h2 className="text-lg font-semibold text-foreground">Account</h2>
423
+ <p className="text-sm text-muted-foreground">
424
+ Profile, billing, and notification shortcuts still live in the sidebar avatar menu.
425
+ </p>
426
+ </header>
427
+ <p className="text-sm leading-relaxed text-muted-foreground">
428
+ Use <span className="font-medium text-foreground">your avatar</span> at the bottom of the left sidebar for
429
+ account details, billing, and alerts. This page focuses on app-wide display and onboarding preferences.
430
+ </p>
431
+ </section>
432
+
433
+ <SettingsAppearanceCard />
434
+
435
+ <section id="input-formats" className="scroll-mt-20">
436
+ <header className="mb-6 space-y-1">
437
+ <h2 className="text-lg font-semibold text-foreground">Input formats</h2>
438
+ <p className="text-sm text-muted-foreground">
439
+ Phone, ZIP, and date masks match table filters and properties drawer fields. Values here are only a local
440
+ preview.
441
+ </p>
442
+ </header>
443
+ <FieldGroup>
444
+ <SettingsFormRow
445
+ label="Phone"
446
+ description="US NANP display; digits-only matching in filters."
447
+ htmlFor="settings-demo-phone"
448
+ >
449
+ <FilterTextValueInput
450
+ id="settings-demo-phone"
451
+ mask="phone"
452
+ aria-label="Phone (masked preview)"
453
+ placeholder="(555) 555-5555"
454
+ value={demoPhone}
455
+ onValueChange={setDemoPhone}
456
+ className="w-full max-w-sm"
457
+ />
458
+ </SettingsFormRow>
459
+ <SettingsFormRow
460
+ label="ZIP"
461
+ description="ZIP or ZIP+4."
462
+ htmlFor="settings-demo-zip"
463
+ >
464
+ <FilterTextValueInput
465
+ id="settings-demo-zip"
466
+ mask="zip"
467
+ aria-label="ZIP (masked preview)"
468
+ placeholder="12345 or 12345-6789"
469
+ value={demoZip}
470
+ onValueChange={setDemoZip}
471
+ className="w-full max-w-xs"
472
+ />
473
+ </SettingsFormRow>
474
+ <SettingsFormRow
475
+ label="Date"
476
+ description="MM/DD/YYYY display; validate separately if you persist it."
477
+ htmlFor="settings-demo-date"
478
+ >
479
+ <FilterTextValueInput
480
+ id="settings-demo-date"
481
+ mask="dateMDY"
482
+ aria-label="Date (masked preview)"
483
+ placeholder="MM/DD/YYYY"
484
+ value={demoDate}
485
+ onValueChange={setDemoDate}
486
+ className="w-full max-w-xs"
487
+ />
488
+ </SettingsFormRow>
489
+ </FieldGroup>
490
+ </section>
491
+
492
+ <SystemBannerSettingsCard />
493
+
494
+ <section id="tours" className="scroll-mt-20">
495
+ <header className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
496
+ <div className="min-w-0 space-y-1">
497
+ <h2 className="text-lg font-semibold text-foreground">Guided tours</h2>
498
+ <p className="text-sm text-muted-foreground">
499
+ Tours start when you first open a page, highlight one control at a time, and stop after you finish or
500
+ skip. They won’t repeat until you reset them here.
501
+ </p>
502
+ </div>
503
+ <div className="flex shrink-0 items-center gap-3">
504
+ <span className="tabular-nums text-xs text-muted-foreground">
505
+ {completedCount}/{totalCount} completed
506
+ </span>
507
+ <Tip side="bottom" label="Reset all tours so they replay on next visit">
508
+ <Button
509
+ type="button"
510
+ size="sm"
511
+ variant="outline"
512
+ onClick={handleResetAll}
513
+ className="h-8 gap-1.5 text-xs"
514
+ >
515
+ <i className="fa-light fa-rotate-left text-xs" aria-hidden="true" />
516
+ Reset all
517
+ </Button>
518
+ </Tip>
519
+ </div>
520
+ </header>
521
+
522
+ <div className="flex flex-col gap-3">
523
+ {flowStatuses.map(({ flow, dismissed }) => (
524
+ <FlowCard
525
+ key={flow.id}
526
+ flow={flow}
527
+ dismissed={dismissed}
528
+ onReset={() => handleResetFlow(flow.id)}
529
+ onPreview={() => handlePreview(flow.pageUrl, flow.id)}
530
+ />
531
+ ))}
532
+ </div>
533
+ </section>
534
+ </div>
535
+ </div>
536
+ )
537
+ }
@@ -0,0 +1,42 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Label } from "@/components/ui/label"
5
+ import { cn } from "@/lib/utils"
6
+
7
+ /**
8
+ * Two-column settings row: label + helper on the left, controls on the right.
9
+ */
10
+ export function SettingsFormRow({
11
+ label,
12
+ description,
13
+ htmlFor,
14
+ children,
15
+ className,
16
+ }: {
17
+ label: string
18
+ description?: string
19
+ htmlFor?: string
20
+ children: React.ReactNode
21
+ className?: string
22
+ }) {
23
+ return (
24
+ <div
25
+ className={cn(
26
+ "grid grid-cols-1 gap-3 sm:gap-4 lg:grid-cols-[minmax(0,220px)_minmax(0,1fr)] lg:gap-10 lg:items-start",
27
+ "border-b border-border/70 pb-8 last:border-0 last:pb-0",
28
+ className,
29
+ )}
30
+ >
31
+ <div className="space-y-1 lg:pt-1 text-start">
32
+ <Label htmlFor={htmlFor} className="text-sm font-medium text-foreground">
33
+ {label}
34
+ </Label>
35
+ {description ? (
36
+ <p className="text-xs text-muted-foreground leading-snug text-start">{description}</p>
37
+ ) : null}
38
+ </div>
39
+ <div className="min-w-0 space-y-2">{children}</div>
40
+ </div>
41
+ )
42
+ }
@@ -0,0 +1,23 @@
1
+ "use client"
2
+
3
+ import { useEffect, useRef } from "react"
4
+ import { useSidebar } from "@/components/ui/sidebar"
5
+
6
+ /**
7
+ * Collapses the sidebar on mount. Restores previous state on unmount.
8
+ * Used on the new-placement page so the sidebar collapses when entering
9
+ * and reverts to whatever it was when leaving.
10
+ */
11
+ export function SidebarAutoCollapse() {
12
+ const { open, setOpen } = useSidebar()
13
+ const prevOpen = useRef(open)
14
+
15
+ useEffect(() => {
16
+ prevOpen.current = open
17
+ setOpen(false)
18
+ return () => { setOpen(prevOpen.current) }
19
+ // eslint-disable-next-line react-hooks/exhaustive-deps
20
+ }, [])
21
+
22
+ return null
23
+ }
@@ -0,0 +1,18 @@
1
+ "use client"
2
+
3
+ import { useEffect, useRef } from "react"
4
+ import { useSidebar } from "@/components/ui/sidebar"
5
+
6
+ /** Reopens the sidebar once when the data-list page mounts (after returning from new-placement). */
7
+ export function SidebarAutoOpen() {
8
+ const { setOpen } = useSidebar()
9
+ const ran = useRef(false)
10
+ useEffect(() => {
11
+ if (!ran.current) {
12
+ ran.current = true
13
+ setOpen(true)
14
+ }
15
+ // eslint-disable-next-line react-hooks/exhaustive-deps
16
+ }, [])
17
+ return null
18
+ }
@@ -0,0 +1,37 @@
1
+ "use client"
2
+
3
+ /**
4
+ * SidebarShell — SidebarProvider with layout-aware widths.
5
+ */
6
+
7
+ import * as React from "react"
8
+ import { SidebarProvider } from "@/components/ui/sidebar"
9
+
10
+ export function SidebarShell({
11
+ children,
12
+ headerHeight = "calc(var(--spacing) * 12)",
13
+ defaultOpen = true,
14
+ wrapperClassName,
15
+ }: {
16
+ children: React.ReactNode
17
+ headerHeight?: string
18
+ defaultOpen?: boolean
19
+ /** Extra classes on the SidebarProvider wrapper div */
20
+ wrapperClassName?: string
21
+ }) {
22
+ return (
23
+ <SidebarProvider
24
+ defaultOpen={defaultOpen}
25
+ className={wrapperClassName}
26
+ style={
27
+ {
28
+ "--sidebar-width": "16rem",
29
+ "--sidebar-width-icon": "3rem",
30
+ "--header-height": headerHeight,
31
+ } as React.CSSProperties
32
+ }
33
+ >
34
+ {children}
35
+ </SidebarProvider>
36
+ )
37
+ }