@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,625 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { DashboardSectionIntro } from "@/components/dashboard-section-heading"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Checkbox } from "@/components/ui/checkbox"
7
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuTrigger,
13
+ } from "@/components/ui/dropdown-menu"
14
+ import {
15
+ Select,
16
+ SelectContent,
17
+ SelectItem,
18
+ SelectTrigger,
19
+ SelectValue,
20
+ } from "@/components/ui/select"
21
+ import { Card, CardContent, CardHeader } from "@/components/ui/card"
22
+ import { cn } from "@/lib/utils"
23
+
24
+ export const GETTING_STARTED_STORAGE_KEY = "exxat:getting-started-variant"
25
+
26
+ export type GettingStartedVariant = "checklist" | "workspace" | "numbered" | "quickstart"
27
+
28
+ const GETTING_STARTED_VARIANTS: { value: GettingStartedVariant; label: string }[] = [
29
+ { value: "checklist", label: "Checklist" },
30
+ { value: "workspace", label: "Workspace setup" },
31
+ { value: "numbered", label: "Stepped flow" },
32
+ { value: "quickstart", label: "Quick start" },
33
+ ]
34
+
35
+ function isGettingStartedVariant(value: string): value is GettingStartedVariant {
36
+ return GETTING_STARTED_VARIANTS.some((variant) => variant.value === value)
37
+ }
38
+
39
+ interface ChecklistStep {
40
+ id: string
41
+ title: string
42
+ description: string
43
+ completed: boolean
44
+ actionLabel: string
45
+ }
46
+
47
+ const CHECKLIST_STEPS: ChecklistStep[] = [
48
+ {
49
+ id: "profile",
50
+ title: "Complete your profile",
51
+ description: "Add your name, photo, and role so your team knows who you are.",
52
+ completed: true,
53
+ actionLabel: "Edit profile",
54
+ },
55
+ {
56
+ id: "workspace",
57
+ title: "Set up your workspace",
58
+ description: "Customize your workspace with a name, icon, and default settings.",
59
+ completed: false,
60
+ actionLabel: "Configure workspace",
61
+ },
62
+ {
63
+ id: "invite",
64
+ title: "Invite your team",
65
+ description: "Bring your teammates on board so you can collaborate in real time.",
66
+ completed: false,
67
+ actionLabel: "Send invites",
68
+ },
69
+ {
70
+ id: "integrations",
71
+ title: "Connect integrations",
72
+ description: "Link tools like Slack or GitHub to streamline clinical education workflows.",
73
+ completed: false,
74
+ actionLabel: "Browse integrations",
75
+ },
76
+ {
77
+ id: "workflow",
78
+ title: "Create your first workflow",
79
+ description: "Automate a repetitive task with a simple drag-and-drop workflow builder.",
80
+ completed: false,
81
+ actionLabel: "Build workflow",
82
+ },
83
+ {
84
+ id: "notifications",
85
+ title: "Set up notifications",
86
+ description: "Choose how and when you want to be notified about updates and mentions.",
87
+ completed: false,
88
+ actionLabel: "Manage notifications",
89
+ },
90
+ ]
91
+
92
+ interface SetupStep {
93
+ title: string
94
+ description: string
95
+ iconClass: string
96
+ actionLabel: string
97
+ }
98
+
99
+ const WORKSPACE_STEPS: SetupStep[] = [
100
+ {
101
+ title: "Create your workspace",
102
+ description:
103
+ "Name your workspace and pick a URL. This is where your team will collaborate on placements and compliance.",
104
+ iconClass: "fa-rocket-launch",
105
+ actionLabel: "Get started",
106
+ },
107
+ {
108
+ title: "Invite your team",
109
+ description: "Add teammates by email so everyone can collaborate in real time across programs.",
110
+ iconClass: "fa-users",
111
+ actionLabel: "Send invites",
112
+ },
113
+ {
114
+ title: "Set up your first program",
115
+ description: "Create a program to organize sites, rotations, and milestones in one place.",
116
+ iconClass: "fa-folder-tree",
117
+ actionLabel: "Create program",
118
+ },
119
+ {
120
+ title: "Connect your tools",
121
+ description: "Link email, calendar, or LMS integrations to keep your workflow in sync.",
122
+ iconClass: "fa-plug",
123
+ actionLabel: "Browse integrations",
124
+ },
125
+ ]
126
+
127
+ const NUMBERED_STEPS = [
128
+ {
129
+ id: "1.",
130
+ title: "Confirm your program profile",
131
+ description: "Set the program name, term dates, and default placement policies for your cohort.",
132
+ },
133
+ {
134
+ id: "2.",
135
+ title: "Add sites and rotations",
136
+ description: "Register clinical sites, capacity, and rotation templates so students can be matched.",
137
+ },
138
+ {
139
+ id: "3.",
140
+ title: "Connect compliance rules",
141
+ description: "Link requirements and document types so clearance status stays accurate.",
142
+ },
143
+ {
144
+ id: "4.",
145
+ title: "Open the placement cycle",
146
+ description: "Publish the cycle, invite supervisors, and start assigning students to sites.",
147
+ },
148
+ ] as const
149
+
150
+ const QUICK_START_STEPS = [
151
+ {
152
+ title: "Program defaults",
153
+ subtitle: "Policies & term",
154
+ iconClass: "fa-sliders",
155
+ description: "Set placement policies, term dates, and notification preferences for your program.",
156
+ buttonText: "Edit settings",
157
+ },
158
+ {
159
+ title: "Sites & capacity",
160
+ subtitle: "Clinical partners",
161
+ iconClass: "fa-hospital",
162
+ description: "Add sites, supervisors, and rotation capacity so students can be placed accurately.",
163
+ buttonText: "Manage sites",
164
+ },
165
+ {
166
+ title: "Compliance templates",
167
+ subtitle: "Requirements",
168
+ iconClass: "fa-table",
169
+ description: "Map document types and clearance rules so student status stays audit-ready.",
170
+ buttonText: "Open templates",
171
+ },
172
+ {
173
+ title: "Go live",
174
+ subtitle: "Launch cycle",
175
+ iconClass: "fa-chart-line",
176
+ description: "Open the cycle, monitor fill rate, and export placement reports for leadership.",
177
+ buttonText: "View dashboard",
178
+ },
179
+ ] as const
180
+
181
+ function CircularProgress({ completedCount, total }: { completedCount: number; total: number }) {
182
+ const progress = total > 0 ? (completedCount / total) * 100 : 0
183
+ const strokeDashoffset = 100 - progress
184
+ return (
185
+ <svg className="-rotate-90" width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
186
+ <circle className="stroke-muted" cx="7" cy="7" r="6" fill="none" pathLength="100" strokeWidth="2" />
187
+ <circle
188
+ className="stroke-[var(--brand-color)]"
189
+ cx="7"
190
+ cy="7"
191
+ r="6"
192
+ fill="none"
193
+ pathLength="100"
194
+ strokeWidth="2"
195
+ strokeDasharray="100"
196
+ strokeLinecap="round"
197
+ style={{ strokeDashoffset }}
198
+ />
199
+ </svg>
200
+ )
201
+ }
202
+
203
+ function ChecklistVariant() {
204
+ const [steps, setSteps] = React.useState<ChecklistStep[]>(() => [...CHECKLIST_STEPS])
205
+ const [openStepId, setOpenStepId] = React.useState<string | null>(() => {
206
+ const firstIncomplete = CHECKLIST_STEPS.find((step) => !step.completed)
207
+ return firstIncomplete?.id ?? CHECKLIST_STEPS[0]?.id ?? null
208
+ })
209
+ const [dismissed, setDismissed] = React.useState(false)
210
+ const completedCount = steps.filter((step) => step.completed).length
211
+
212
+ if (dismissed) {
213
+ return (
214
+ <div className="flex flex-col items-center gap-2 py-6 text-center">
215
+ <p className="text-sm text-muted-foreground">Checklist dismissed</p>
216
+ <Button type="button" variant="link" className="h-auto text-xs" onClick={() => setDismissed(false)}>
217
+ Show again
218
+ </Button>
219
+ </div>
220
+ )
221
+ }
222
+
223
+ return (
224
+ <div className="flex flex-col gap-3 overflow-hidden">
225
+ <div className="flex flex-col justify-between gap-3 sm:flex-row sm:items-center">
226
+ <h3 className="text-balance text-sm font-medium leading-snug text-foreground">Get started with Exxat</h3>
227
+ <div className="flex flex-wrap items-center justify-end gap-2">
228
+ <CircularProgress completedCount={completedCount} total={steps.length} />
229
+ <div className="text-xs text-muted-foreground">
230
+ <span className="font-medium text-foreground">{completedCount}</span>
231
+ {" / "}
232
+ <span className="font-medium text-foreground">{steps.length}</span> completed
233
+ </div>
234
+ <DropdownMenu>
235
+ <DropdownMenuTrigger asChild>
236
+ <Button type="button" size="icon-sm" variant="ghost" className="size-8 shrink-0" aria-label="Checklist options">
237
+ <i className="fa-light fa-ellipsis-vertical" aria-hidden="true" />
238
+ </Button>
239
+ </DropdownMenuTrigger>
240
+ <DropdownMenuContent align="end" className="w-44">
241
+ <DropdownMenuItem onSelect={() => setDismissed(true)}>
242
+ <i className="fa-light fa-box-archive me-2 text-xs" aria-hidden="true" />
243
+ Dismiss
244
+ </DropdownMenuItem>
245
+ </DropdownMenuContent>
246
+ </DropdownMenu>
247
+ </div>
248
+ </div>
249
+ <div className="space-y-0">
250
+ {steps.map((step, index) => {
251
+ const isOpen = openStepId === step.id
252
+ const prev = steps[index - 1]
253
+ const prevOpen = prev != null && openStepId === prev.id
254
+ const showBorderTop = !(index === 0 || isOpen || prevOpen)
255
+
256
+ return (
257
+ <div key={step.id} className={cn("group", isOpen && "rounded-lg", showBorderTop && "border-t border-border")}>
258
+ <div className={cn("overflow-hidden rounded-lg transition-colors", isOpen && "border border-border bg-muted/60")}>
259
+ <button
260
+ type="button"
261
+ className="flex w-full cursor-pointer text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
262
+ onClick={() => setOpenStepId((prevId) => (prevId === step.id ? null : step.id))}
263
+ >
264
+ <div className="relative flex w-full items-center justify-between gap-3 py-3 pe-2 ps-4">
265
+ <div className="flex min-w-0 flex-1 gap-3">
266
+ {step.completed ? (
267
+ <span
268
+ className="mt-1 inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-emerald-300/70 bg-emerald-50 text-emerald-700 dark:border-emerald-500/40 dark:bg-emerald-500/15 dark:text-emerald-300"
269
+ aria-label={`${step.title} — completed`}
270
+ >
271
+ <i className="fa-light fa-check text-xs" aria-hidden="true" />
272
+ </span>
273
+ ) : (
274
+ <span
275
+ className="mt-1 inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-input bg-background text-muted-foreground"
276
+ aria-label={`${step.title} — not completed`}
277
+ >
278
+ <i className="fa-light fa-minus text-xs" aria-hidden="true" />
279
+ </span>
280
+ )}
281
+ <h4 className={cn("pt-0.5 text-sm font-semibold", step.completed ? "text-[var(--brand-color)]" : "text-foreground")}>
282
+ {step.title}
283
+ </h4>
284
+ </div>
285
+ {!isOpen ? <i className="fa-light fa-chevron-right shrink-0 text-xs text-muted-foreground" aria-hidden="true" /> : null}
286
+ </div>
287
+ </button>
288
+ {isOpen ? (
289
+ <div className="border-t border-border/60 px-4 pb-4 pt-0 ps-[3.25rem]">
290
+ <p className="max-w-md text-pretty text-xs leading-snug text-muted-foreground sm:text-sm">{step.description}</p>
291
+ <Button
292
+ type="button"
293
+ className="mt-3"
294
+ size="sm"
295
+ onClick={() => {
296
+ setSteps((prev) => prev.map((current) => (current.id === step.id ? { ...current, completed: true } : current)))
297
+ const nextIncomplete = steps.find((current) => !current.completed && current.id !== step.id)
298
+ setOpenStepId(nextIncomplete?.id ?? null)
299
+ }}
300
+ >
301
+ {step.actionLabel}
302
+ </Button>
303
+ </div>
304
+ ) : null}
305
+ </div>
306
+ </div>
307
+ )
308
+ })}
309
+ </div>
310
+ </div>
311
+ )
312
+ }
313
+
314
+ function WorkspaceVariant() {
315
+ const [completed, setCompleted] = React.useState<Set<number>>(() => new Set([0]))
316
+ const currentStep = WORKSPACE_STEPS.findIndex((_, index) => !completed.has(index))
317
+ const completedCount = completed.size
318
+
319
+ return (
320
+ <div className="flex flex-col gap-3 overflow-hidden">
321
+ <div className="flex flex-col gap-1">
322
+ <h3 className="text-sm font-medium leading-snug text-foreground">Set up your workspace</h3>
323
+ <p className="text-sm text-muted-foreground">Complete these steps to get your team up and running.</p>
324
+ <div className="pt-2">
325
+ <p className="text-xs text-muted-foreground">
326
+ <span className="font-medium text-foreground">{completedCount}</span> of {WORKSPACE_STEPS.length} completed
327
+ </p>
328
+ <div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-muted" aria-hidden="true">
329
+ <div
330
+ className="h-full rounded-full transition-all duration-500"
331
+ style={{ width: `${(completedCount / WORKSPACE_STEPS.length) * 100}%`, background: "var(--brand-color)" }}
332
+ />
333
+ </div>
334
+ </div>
335
+ </div>
336
+ <div className="flex flex-col gap-3">
337
+ {WORKSPACE_STEPS.map((step, index) => {
338
+ const isCompleted = completed.has(index)
339
+ const isActive = index === currentStep
340
+ return (
341
+ <div key={step.title} className={cn("rounded-lg border p-4 transition-colors", isActive ? "border-[var(--brand-color)]/35 bg-muted/40" : "border-border bg-card")}>
342
+ <div className="flex gap-3">
343
+ {isCompleted ? (
344
+ <Checkbox checked disabled size="lg" motion="none" variant="success" className="mt-0.5 shrink-0" aria-label={`Step ${index + 1} completed`} />
345
+ ) : (
346
+ <span className={cn("mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-semibold", isActive ? "bg-[var(--brand-color)] text-primary-foreground" : "bg-muted text-muted-foreground")}>
347
+ {index + 1}
348
+ </span>
349
+ )}
350
+ <div className="min-w-0 flex-1">
351
+ <p className={cn("text-sm font-medium leading-snug", isCompleted ? "text-muted-foreground line-through" : "text-foreground")}>{step.title}</p>
352
+ <p className="mt-1 text-xs leading-snug text-muted-foreground sm:text-sm">{step.description}</p>
353
+ {isActive ? (
354
+ <Button
355
+ type="button"
356
+ className="mt-3 gap-2"
357
+ size="sm"
358
+ onClick={() => setCompleted((prev) => new Set(prev).add(index))}
359
+ >
360
+ <i className={cn("fa-light shrink-0 text-xs", step.iconClass)} aria-hidden="true" />
361
+ {step.actionLabel}
362
+ </Button>
363
+ ) : null}
364
+ </div>
365
+ </div>
366
+ </div>
367
+ )
368
+ })}
369
+ </div>
370
+ </div>
371
+ )
372
+ }
373
+
374
+ function NumberedVariant() {
375
+ const [activeStep, setActiveStep] = React.useState(0)
376
+ const progress = ((activeStep + 1) / NUMBERED_STEPS.length) * 100
377
+ return (
378
+ <div className="flex flex-col gap-3 overflow-hidden">
379
+ <div className="flex flex-col gap-1">
380
+ <h3 className="text-sm font-medium leading-snug text-foreground">Placement cycle setup</h3>
381
+ <p className="text-sm text-muted-foreground">Complete each step to run placements with your team.</p>
382
+ <div className="pt-2">
383
+ <p className="text-xs font-medium text-muted-foreground">
384
+ Step <span className="tabular-nums text-foreground">{activeStep + 1}</span> / {NUMBERED_STEPS.length}
385
+ </p>
386
+ <div className="mt-2 h-2 w-full overflow-hidden rounded-full bg-muted" aria-hidden="true">
387
+ <div className="h-full rounded-full transition-all duration-300" style={{ width: `${progress}%`, background: "var(--brand-color)" }} />
388
+ </div>
389
+ </div>
390
+ </div>
391
+ <div className="space-y-2">
392
+ {NUMBERED_STEPS.map((step, index) => (
393
+ <button
394
+ key={step.title}
395
+ type="button"
396
+ onClick={() => setActiveStep(index)}
397
+ className={cn(
398
+ "w-full rounded-lg border px-3 py-3 text-left outline-none transition-colors",
399
+ "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
400
+ index === activeStep ? "border-[var(--brand-color)]/40 bg-muted/50" : "border-border bg-card hover:bg-muted/30",
401
+ )}
402
+ >
403
+ <div className="flex items-start gap-3">
404
+ {index < activeStep ? (
405
+ <span
406
+ className="mt-0.5 inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-emerald-300/70 bg-emerald-50 text-emerald-700 dark:border-emerald-500/40 dark:bg-emerald-500/15 dark:text-emerald-300"
407
+ aria-label={`${step.title} — completed`}
408
+ >
409
+ <i className="fa-light fa-check text-xs" aria-hidden="true" />
410
+ </span>
411
+ ) : (
412
+ <span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full border border-border bg-background text-xs font-semibold text-muted-foreground">
413
+ <span aria-hidden="true">{step.id}</span>
414
+ </span>
415
+ )}
416
+ <div className="min-w-0">
417
+ <p className="text-sm font-semibold text-foreground">{step.title}</p>
418
+ {index === activeStep ? <p className="mt-1 text-xs leading-snug text-muted-foreground sm:text-sm">{step.description}</p> : null}
419
+ </div>
420
+ </div>
421
+ </button>
422
+ ))}
423
+ </div>
424
+ </div>
425
+ )
426
+ }
427
+
428
+ function QuickStartVariant() {
429
+ const [activeStep, setActiveStep] = React.useState(1)
430
+ const [openTitle, setOpenTitle] = React.useState<string>(() => QUICK_START_STEPS[1]!.title)
431
+
432
+ return (
433
+ <div className="flex flex-col gap-3 overflow-hidden">
434
+ <div className="flex flex-col gap-1">
435
+ <h3 className="text-sm font-medium leading-snug text-foreground">Quick start</h3>
436
+ <p className="text-sm text-muted-foreground">Wire up your workspace for placements and compliance in four steps.</p>
437
+ </div>
438
+ <div className="flex flex-col gap-1">
439
+ {QUICK_START_STEPS.map((step, index) => {
440
+ const status: "complete" | "current" | "upcoming" = index < activeStep ? "complete" : index === activeStep ? "current" : "upcoming"
441
+ const isOpen = openTitle === step.title
442
+ return (
443
+ <Collapsible key={step.title} open={isOpen} onOpenChange={(open) => setOpenTitle(open ? step.title : "")}>
444
+ <div className={cn("rounded-lg border transition-colors", status === "current" ? "border-[var(--brand-color)]/35 bg-muted/40" : "border-border bg-card")}>
445
+ <CollapsibleTrigger
446
+ type="button"
447
+ className="flex w-full items-start gap-3 px-3 py-3 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
448
+ >
449
+ <span className="mt-0.5 shrink-0 text-muted-foreground">
450
+ {status === "complete" ? (
451
+ <span
452
+ className="inline-flex size-5 items-center justify-center rounded border border-emerald-300/70 bg-emerald-50 text-emerald-700 dark:border-emerald-500/40 dark:bg-emerald-500/15 dark:text-emerald-300"
453
+ aria-label={`${step.title} — completed`}
454
+ >
455
+ <i className="fa-light fa-check text-[11px]" aria-hidden="true" />
456
+ </span>
457
+ ) : (
458
+ <i className={cn("fa-light text-sm", step.iconClass)} aria-hidden="true" />
459
+ )}
460
+ </span>
461
+ <span className="min-w-0 flex-1">
462
+ <span className="block text-sm font-semibold text-foreground">{step.title}</span>
463
+ <span className="mt-0.5 block text-xs text-muted-foreground">{step.subtitle}</span>
464
+ </span>
465
+ <i className={cn("fa-light fa-chevron-down mt-1 shrink-0 text-xs transition-transform", isOpen && "rotate-180")} aria-hidden="true" />
466
+ </CollapsibleTrigger>
467
+ <CollapsibleContent>
468
+ <div className="border-t border-border/60 px-3 pb-3 pt-0 ps-11">
469
+ <p className="text-xs leading-snug text-muted-foreground sm:text-sm">{step.description}</p>
470
+ <Button
471
+ type="button"
472
+ size="sm"
473
+ className="mt-3"
474
+ variant={status === "complete" ? "outline" : "default"}
475
+ onClick={() => {
476
+ if (status !== "current") return
477
+ const next = activeStep + 1
478
+ setActiveStep(next)
479
+ if (next < QUICK_START_STEPS.length) setOpenTitle(QUICK_START_STEPS[next]!.title)
480
+ }}
481
+ >
482
+ {step.buttonText}
483
+ </Button>
484
+ </div>
485
+ </CollapsibleContent>
486
+ </div>
487
+ </Collapsible>
488
+ )
489
+ })}
490
+ </div>
491
+ </div>
492
+ )
493
+ }
494
+
495
+ export function renderGettingStartedVariant(variant: GettingStartedVariant) {
496
+ switch (variant) {
497
+ case "checklist":
498
+ return <ChecklistVariant />
499
+ case "workspace":
500
+ return <WorkspaceVariant />
501
+ case "numbered":
502
+ return <NumberedVariant />
503
+ case "quickstart":
504
+ return <QuickStartVariant />
505
+ default:
506
+ return <ChecklistVariant />
507
+ }
508
+ }
509
+
510
+ export function GettingStartedVariantView({ variant }: { variant: GettingStartedVariant }) {
511
+ return renderGettingStartedVariant(variant)
512
+ }
513
+
514
+ export function GettingStarted({
515
+ inset = true,
516
+ titleAs = "h2",
517
+ title = "Getting started",
518
+ description = "Pick a layout. Your choice is saved on this device.",
519
+ }: {
520
+ inset?: boolean
521
+ titleAs?: "h1" | "h2"
522
+ title?: string
523
+ description?: string
524
+ }) {
525
+ const pad = inset ? "px-4 lg:px-6" : ""
526
+ const [variant, setVariant] = React.useState<GettingStartedVariant>("checklist")
527
+ const [ready, setReady] = React.useState(false)
528
+
529
+ React.useEffect(() => {
530
+ try {
531
+ const raw = window.localStorage.getItem(GETTING_STARTED_STORAGE_KEY)
532
+ if (raw && isGettingStartedVariant(raw)) setVariant(raw)
533
+ } catch {
534
+ /* ignore storage errors */
535
+ }
536
+ setReady(true)
537
+ }, [])
538
+
539
+ return (
540
+ <section aria-labelledby="getting-started-heading" className="flex flex-col gap-4">
541
+ <DashboardSectionIntro
542
+ className={pad}
543
+ title={title}
544
+ titleId="getting-started-heading"
545
+ titleAs={titleAs}
546
+ description={description}
547
+ actions={(
548
+ <div className="w-full sm:w-auto sm:min-w-[13rem]">
549
+ <Select
550
+ value={ready ? variant : "checklist"}
551
+ onValueChange={(value) => {
552
+ if (!isGettingStartedVariant(value)) return
553
+ setVariant(value)
554
+ try {
555
+ window.localStorage.setItem(GETTING_STARTED_STORAGE_KEY, value)
556
+ } catch {
557
+ /* ignore storage errors */
558
+ }
559
+ }}
560
+ >
561
+ <SelectTrigger className="w-full" aria-label="Getting started layout">
562
+ <SelectValue placeholder="Choose layout" />
563
+ </SelectTrigger>
564
+ <SelectContent align="end" sideOffset={4}>
565
+ {GETTING_STARTED_VARIANTS.map((option) => (
566
+ <SelectItem key={option.value} value={option.value}>
567
+ {option.label}
568
+ </SelectItem>
569
+ ))}
570
+ </SelectContent>
571
+ </Select>
572
+ </div>
573
+ )}
574
+ />
575
+ <div className={cn(pad)}>{renderGettingStartedVariant(variant)}</div>
576
+ </section>
577
+ )
578
+ }
579
+
580
+ export function GettingStartedProgressCard({
581
+ steps,
582
+ title = "Getting Started",
583
+ }: {
584
+ steps: { id: number; label: string; done: boolean }[]
585
+ title?: string
586
+ }) {
587
+ const done = steps.filter((step) => step.done).length
588
+ const total = steps.length
589
+ const pct = total > 0 ? Math.round((done / total) * 100) : 0
590
+
591
+ return (
592
+ <Card size="sm">
593
+ <CardHeader>
594
+ <div className="flex items-center justify-between gap-2">
595
+ <h2 className="text-sm font-semibold text-foreground">{title}</h2>
596
+ <span className="text-xs font-semibold tabular-nums" style={{ color: "var(--brand-color)" }}>
597
+ {done}/{total}
598
+ </span>
599
+ </div>
600
+ <div className="mt-1 h-1.5 overflow-hidden rounded-full bg-muted" aria-hidden="true">
601
+ <div className="h-full rounded-full transition-all duration-500" style={{ width: `${pct}%`, background: "var(--brand-color)" }} />
602
+ </div>
603
+ <p className="mt-0.5 text-xs text-muted-foreground">{pct}% complete</p>
604
+ </CardHeader>
605
+ <CardContent className="flex flex-col gap-0.5">
606
+ {steps.map((step) => (
607
+ <div key={step.id} className="flex items-center gap-2.5 rounded-lg px-1 py-1.5">
608
+ <Checkbox
609
+ checked={step.done}
610
+ disabled
611
+ size="sm"
612
+ motion="none"
613
+ variant={step.done ? "success" : "outline"}
614
+ className="shrink-0"
615
+ aria-label={step.done ? `Completed: ${step.label}` : `Not completed — step ${step.id}: ${step.label}`}
616
+ />
617
+ <p className={cn("flex-1 text-xs leading-snug", step.done ? "text-muted-foreground line-through" : "font-medium text-foreground")}>
618
+ {step.label}
619
+ </p>
620
+ </div>
621
+ ))}
622
+ </CardContent>
623
+ </Card>
624
+ )
625
+ }