@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,364 @@
1
+ "use client"
2
+
3
+ /**
4
+ * LeoInsightIndicator — reusable AI insight chip + popover.
5
+ *
6
+ * Usage:
7
+ * <LeoInsightIndicator insight={leoInsight} chartTitle="Placement Trends" />
8
+ * <LeoInsightIndicator insight={leoInsight} chartTitle="..." triggerLayout="plot-marker" />
9
+ *
10
+ * Two trigger layouts:
11
+ * "toolbar" — compact "Insight" pill in a card header corner (default)
12
+ * "plot-marker" — sits above an anchored data point on the chart canvas
13
+ *
14
+ * Palette is brand-only. Direction is communicated via icon SHAPE + kind LABEL
15
+ * + signed delta value (never colour alone — WCAG 1.4.1).
16
+ */
17
+
18
+ import * as React from "react"
19
+ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
20
+ import { Button } from "@/components/ui/button"
21
+ import { Kbd } from "@/components/ui/kbd"
22
+ import {
23
+ Tooltip,
24
+ TooltipContent,
25
+ TooltipTrigger,
26
+ } from "@/components/ui/tooltip"
27
+ import { AskLeoShortcutKbds, useAskLeo } from "@/components/ask-leo-sidebar"
28
+ import { cn } from "@/lib/utils"
29
+
30
+ // ─────────────────────────────────────────────────────────────────────────────
31
+ // Types
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+
34
+ /** Optional anchor for drawing Leo on the plot (reference line + marker). */
35
+ export type ChartLeoInsightAnchor = {
36
+ /** Categorical value on the chart's X axis (e.g. month label). */
37
+ xValue: string
38
+ /** Fixed Y in data space; overrides yDataKeys / yCombine when set. */
39
+ yValue?: number
40
+ /** Row keys to combine; required when yValue is omitted. */
41
+ yDataKeys?: string[]
42
+ /** How to derive Y from yDataKeys: top of stacked bars = "sum", overlaid lines = "max". */
43
+ yCombine?: "max" | "sum"
44
+ }
45
+
46
+ /**
47
+ * Semantic kind of the insight — drives color, icon, and chip label.
48
+ * Defaults to "anomaly" when unset.
49
+ */
50
+ export type ChartLeoInsightKind = "spike" | "dip" | "anomaly" | "trend"
51
+
52
+ /** Smart scan copy for a chart — opens in a popover; CTA can prefill Ask Leo. */
53
+ export type ChartLeoInsight = {
54
+ /** Short attention line (what stands out). */
55
+ headline: string
56
+ /** Plain-language explanation. */
57
+ explanation: string
58
+ /** Overrides the default prompt sent to Ask Leo. */
59
+ askLeoPrompt?: string
60
+ /**
61
+ * When set, pair with `ChartLeoPlotInsightOverlay` for an on-point pulse
62
+ * + guide line + chip positioned directly on the chart.
63
+ */
64
+ anchor?: ChartLeoInsightAnchor
65
+ /** Semantic shape of the insight. Defaults to "anomaly". */
66
+ kind?: ChartLeoInsightKind
67
+ /** Magnitude chip, e.g. `{ value: "-24%", label: "vs last Dec" }`. */
68
+ delta?: { value: string; label?: string }
69
+ /** 2–4 supporting facts shown as bullets in the popover. */
70
+ bullets?: string[]
71
+ /** Optional secondary quick-actions alongside the Ask Leo CTA. */
72
+ actions?: Array<{
73
+ label: string
74
+ icon?: string
75
+ onSelect?: () => void
76
+ href?: string
77
+ }>
78
+ }
79
+
80
+ // ─────────────────────────────────────────────────────────────────────────────
81
+ // Internal constants
82
+ // ─────────────────────────────────────────────────────────────────────────────
83
+
84
+ const LEO_KIND_META: Record<ChartLeoInsightKind, { icon: string; label: string }> = {
85
+ spike: { icon: "fa-arrow-trend-up", label: "Spike" },
86
+ dip: { icon: "fa-arrow-trend-down", label: "Dip" },
87
+ anomaly: { icon: "fa-wave-pulse", label: "Anomaly" },
88
+ trend: { icon: "fa-sparkles", label: "Insight" },
89
+ }
90
+
91
+ export const LEO_TOKENS = {
92
+ dotClass: "bg-brand",
93
+ iconClass: "text-brand",
94
+ softBgClass: "bg-brand/10",
95
+ borderClass: "border-brand/50",
96
+ cssVar: "var(--brand-color)",
97
+ } as const
98
+
99
+ function resolveLeoMeta(insight: ChartLeoInsight) {
100
+ return LEO_KIND_META[insight.kind ?? "anomaly"]
101
+ }
102
+
103
+ // ─────────────────────────────────────────────────────────────────────────────
104
+ // Component
105
+ // ─────────────────────────────────────────────────────────────────────────────
106
+
107
+ /**
108
+ * Reusable Leo insight chip + popover.
109
+ *
110
+ * Renders a pill trigger button that opens a Radix Popover containing:
111
+ * - Header: "Leo spotted" serif label + kind chip + close button
112
+ * - Body: headline, delta label, explanation, optional bullets
113
+ * - Footer: optional secondary actions + full-width "Ask Leo" CTA
114
+ */
115
+ export function LeoInsightIndicator({
116
+ insight,
117
+ chartTitle,
118
+ triggerLayout = "toolbar",
119
+ }: {
120
+ insight: ChartLeoInsight
121
+ chartTitle: string
122
+ triggerLayout?: "toolbar" | "plot-marker"
123
+ }) {
124
+ const { openWithPrompt } = useAskLeo()
125
+ const [open, setOpen] = React.useState(false)
126
+ const titleId = React.useId()
127
+ const descriptionId = React.useId()
128
+
129
+ const defaultPrompt =
130
+ insight.askLeoPrompt ??
131
+ `For the chart "${chartTitle}": ${insight.headline} — ${insight.explanation} What should we do next?`
132
+
133
+ const meta = resolveLeoMeta(insight)
134
+ const isPlot = triggerLayout === "plot-marker"
135
+ const deltaValue = insight.delta?.value
136
+
137
+ const directionLabel =
138
+ insight.kind === "dip" ? "decreased" :
139
+ insight.kind === "spike" ? "increased" :
140
+ insight.kind === "anomaly" ? "anomaly detected" : "insight"
141
+
142
+ const ariaFull = deltaValue
143
+ ? `Leo insight: ${directionLabel} ${deltaValue}. ${insight.headline}.`
144
+ : `Leo insight: ${insight.headline}.`
145
+
146
+ return (
147
+ <Popover open={open} onOpenChange={setOpen}>
148
+ <PopoverTrigger asChild>
149
+ <button
150
+ type="button"
151
+ aria-label={ariaFull}
152
+ aria-expanded={open}
153
+ aria-haspopup="dialog"
154
+ className={cn(
155
+ // WCAG 2.5.5 Target Size (AA): 28×28 min.
156
+ "relative inline-flex h-7 min-h-7 shrink-0 items-center gap-1.5 rounded-full border bg-card px-2.5 text-xs font-semibold shadow-sm",
157
+ "text-foreground",
158
+ LEO_TOKENS.borderClass,
159
+ "transition-[transform,background-color] duration-150",
160
+ "hover:-translate-y-[0.5px] hover:bg-card",
161
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
162
+ )}
163
+ >
164
+ {/* Soft brand fill */}
165
+ <span
166
+ aria-hidden
167
+ className={cn("pointer-events-none absolute inset-0 rounded-full", LEO_TOKENS.softBgClass)}
168
+ />
169
+ <i
170
+ className={cn("fa-solid", meta.icon, "relative text-[12px]", LEO_TOKENS.iconClass)}
171
+ aria-hidden="true"
172
+ />
173
+ {deltaValue ? (
174
+ <span className="relative tabular-nums">{deltaValue}</span>
175
+ ) : !isPlot ? (
176
+ <span className="relative hidden sm:inline">Insight</span>
177
+ ) : null}
178
+ </button>
179
+ </PopoverTrigger>
180
+
181
+ <PopoverContent
182
+ className={cn(
183
+ "relative w-[min(20rem,calc(100vw-1.5rem))] overflow-hidden p-0",
184
+ "border border-border bg-background shadow-xl",
185
+ "hc:border-border hc:bg-card hc:shadow-none",
186
+ "forced-colors:border forced-colors:border-[CanvasText] forced-colors:bg-[Canvas] forced-colors:shadow-none",
187
+ )}
188
+ align={isPlot ? "center" : "end"}
189
+ side={isPlot ? "top" : "bottom"}
190
+ sideOffset={isPlot ? 12 : 6}
191
+ aria-labelledby={titleId}
192
+ aria-describedby={descriptionId}
193
+ >
194
+ {/* Ambient brand glow */}
195
+ <div
196
+ aria-hidden
197
+ className="pointer-events-none absolute inset-0 forced-colors:hidden"
198
+ style={{
199
+ background:
200
+ "radial-gradient(ellipse 120% 80% at 50% 100%, oklch(from var(--brand-color) l c h / 0.08) 0%, transparent 68%)",
201
+ }}
202
+ />
203
+
204
+ {/* Screen-reader announcement */}
205
+ <span className="sr-only">
206
+ {`Leo spotted a ${meta.label.toLowerCase()}${deltaValue ? ` of ${deltaValue}` : ""}: ${insight.headline}`}
207
+ </span>
208
+
209
+ <div className="relative">
210
+ {/* ── Header ─────────────────────────────────────────────────── */}
211
+ <div className="flex items-center gap-2.5 border-b border-border px-3.5 pb-3 pt-3">
212
+ <span
213
+ aria-hidden
214
+ className="inline-flex size-5 shrink-0 items-center justify-center rounded-full bg-brand/10"
215
+ >
216
+ <i className="fa-duotone fa-solid fa-star-christmas text-[11px] text-brand" aria-hidden="true" />
217
+ </span>
218
+
219
+ {/* Serif "Leo spotted" heading */}
220
+ <h1
221
+ className="text-base font-bold leading-tight tracking-tight text-foreground"
222
+ style={{ fontFamily: "var(--font-heading)" }}
223
+ >
224
+ Leo spotted
225
+ </h1>
226
+
227
+ {/* Kind + delta chip */}
228
+ <span
229
+ className={cn(
230
+ "ml-auto inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-semibold text-foreground",
231
+ LEO_TOKENS.softBgClass,
232
+ LEO_TOKENS.borderClass,
233
+ )}
234
+ >
235
+ <i
236
+ className={cn("fa-solid", meta.icon, "text-[10px]", LEO_TOKENS.iconClass)}
237
+ aria-hidden="true"
238
+ />
239
+ <span>{meta.label}</span>
240
+ {deltaValue ? (
241
+ <>
242
+ <span aria-hidden className="text-muted-foreground">·</span>
243
+ <span className="tabular-nums">{deltaValue}</span>
244
+ </>
245
+ ) : null}
246
+ </span>
247
+
248
+ {/* Close — WCAG 2.5.5: 28×28 target + Tooltip */}
249
+ <Tooltip>
250
+ <TooltipTrigger asChild>
251
+ <button
252
+ type="button"
253
+ aria-label="Close insight"
254
+ onClick={() => setOpen(false)}
255
+ className={cn(
256
+ "inline-flex size-7 min-h-7 min-w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground",
257
+ "transition-colors hover:bg-muted hover:text-foreground",
258
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
259
+ )}
260
+ >
261
+ <i className="fa-solid fa-xmark text-[12px]" aria-hidden="true" />
262
+ </button>
263
+ </TooltipTrigger>
264
+ <TooltipContent side="top" className="flex items-center gap-1.5">
265
+ <span>Close</span>
266
+ <Kbd>Esc</Kbd>
267
+ </TooltipContent>
268
+ </Tooltip>
269
+ </div>
270
+
271
+ {/* ── Body ───────────────────────────────────────────────────── */}
272
+ <div className="px-3.5 pb-3 pt-3">
273
+ <h3
274
+ id={titleId}
275
+ className="text-[13px] font-semibold leading-snug text-foreground"
276
+ >
277
+ {insight.headline}
278
+ </h3>
279
+ {insight.delta?.label ? (
280
+ <p className="mt-0.5 text-[11px] text-muted-foreground">
281
+ {insight.delta.label}
282
+ </p>
283
+ ) : null}
284
+ <p
285
+ id={descriptionId}
286
+ className="mt-2 text-[12.5px] leading-relaxed text-foreground"
287
+ >
288
+ {insight.explanation}
289
+ </p>
290
+
291
+ {insight.bullets && insight.bullets.length > 0 ? (
292
+ <ul className="mt-3 space-y-1.5">
293
+ {insight.bullets.map((b, i) => (
294
+ <li
295
+ key={i}
296
+ className="flex items-start gap-2 text-[12px] leading-snug text-foreground"
297
+ >
298
+ <span
299
+ aria-hidden
300
+ className={cn(
301
+ "mt-1.5 inline-block size-1.5 shrink-0 rounded-full",
302
+ LEO_TOKENS.dotClass,
303
+ )}
304
+ />
305
+ <span className="min-w-0 flex-1">{b}</span>
306
+ </li>
307
+ ))}
308
+ </ul>
309
+ ) : null}
310
+ </div>
311
+
312
+ {/* ── Footer ─────────────────────────────────────────────────── */}
313
+ <div className="flex flex-col gap-1.5 border-t border-border px-2.5 py-2">
314
+ {insight.actions?.map((a) => {
315
+ const content = (
316
+ <>
317
+ {a.icon ? (
318
+ <i className={cn("fa-light", a.icon, "text-[11px]")} aria-hidden="true" />
319
+ ) : null}
320
+ <span>{a.label}</span>
321
+ </>
322
+ )
323
+ return (
324
+ <Button
325
+ key={a.label}
326
+ type="button"
327
+ variant="ghost"
328
+ size="sm"
329
+ className="h-8 min-h-8 w-full justify-center gap-1.5 px-2 text-[11.5px] text-foreground hover:text-foreground"
330
+ onClick={() => {
331
+ setOpen(false)
332
+ a.onSelect?.()
333
+ }}
334
+ asChild={!!a.href}
335
+ >
336
+ {a.href ? <a href={a.href}>{content}</a> : content}
337
+ </Button>
338
+ )
339
+ })}
340
+ <Button
341
+ type="button"
342
+ size="sm"
343
+ className="h-8 min-h-8 w-full justify-center gap-1.5 px-3 text-[11.5px]"
344
+ onClick={() => {
345
+ setOpen(false)
346
+ openWithPrompt(defaultPrompt)
347
+ }}
348
+ >
349
+ <i
350
+ className="fa-duotone fa-solid fa-star-christmas text-[11px] text-primary-foreground"
351
+ aria-hidden="true"
352
+ />
353
+ <span>Ask Leo</span>
354
+ <AskLeoShortcutKbds
355
+ variant="bare"
356
+ className="ml-0.5 hidden sm:inline-flex"
357
+ />
358
+ </Button>
359
+ </div>
360
+ </div>
361
+ </PopoverContent>
362
+ </Popover>
363
+ )
364
+ }
@@ -0,0 +1,121 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Leo "thinking" dots.
5
+ *
6
+ * Character arc embedded in motion:
7
+ * 1. Enter — dots fade in from below, staggered.
8
+ * 2. Slow phase (~900 ms) — very gentle, meditative pulse. Almost still.
9
+ * This is the "hmm, let me think…" moment.
10
+ * 3. Fast phase — suddenly switches to an energetic, high-amplitude pulse.
11
+ * The "oh! I'm working on it" moment.
12
+ * 4. Exit — instead of vanishing, each dot continues its motion while
13
+ * floating upward and fading. Feels like thoughts dispersing into an
14
+ * answer. Runs inside an `<AnimatePresence>` at the call site.
15
+ *
16
+ * Used in the Ask Leo sidebar while a reply is pending; can be reused for
17
+ * any "thinking" affordance.
18
+ */
19
+
20
+ import * as React from "react"
21
+ import { motion, useReducedMotion, type Variants } from "motion/react"
22
+ import { cn } from "@/lib/utils"
23
+
24
+ type Phase = "slow" | "fast"
25
+
26
+ // Each dot reads its index from `custom` so its delay is a clean function of
27
+ // position, not hard-coded — keeps the stagger readable.
28
+ const dotVariants: Variants = {
29
+ // Slow: barely moving, meditative. Duration long, amplitudes tiny.
30
+ slow: (i: number) => ({
31
+ opacity: [0.55, 0.8, 0.55],
32
+ scale: [0.9, 1.0, 0.9],
33
+ y: 0,
34
+ transition: {
35
+ duration: 2.8,
36
+ repeat: Infinity,
37
+ ease: [0.45, 0.05, 0.5, 1],
38
+ delay: i * 0.32,
39
+ },
40
+ }),
41
+ // Fast: energetic, high-amplitude, "processing at full speed".
42
+ fast: (i: number) => ({
43
+ opacity: [0.6, 1, 0.6],
44
+ scale: [0.72, 1.28, 0.72],
45
+ y: 0,
46
+ transition: {
47
+ duration: 0.9,
48
+ repeat: Infinity,
49
+ ease: "easeInOut",
50
+ delay: i * 0.14,
51
+ },
52
+ }),
53
+ }
54
+
55
+ export type LeoTypingDotsProps = {
56
+ className?: string
57
+ /** `status` = polite live region; `decorative` = aria-hidden only. */
58
+ variant?: "status" | "decorative"
59
+ /** Announced when variant is `status`. */
60
+ statusLabel?: string
61
+ }
62
+
63
+ export function LeoTypingDots({
64
+ className,
65
+ variant = "status",
66
+ statusLabel = "Leo is thinking",
67
+ }: LeoTypingDotsProps) {
68
+ const reduced = useReducedMotion() ?? false
69
+ const [phase, setPhase] = React.useState<Phase>("slow")
70
+
71
+ React.useEffect(() => {
72
+ if (reduced) return
73
+ // After the slow "settling" period, snap to the fast tempo.
74
+ const t = setTimeout(() => setPhase("fast"), 900)
75
+ return () => clearTimeout(t)
76
+ }, [reduced])
77
+
78
+ const ariaProps = variant === "decorative"
79
+ ? ({ "aria-hidden": true } as const)
80
+ : ({ role: "status", "aria-live": "polite" } as const)
81
+
82
+ return (
83
+ <motion.span
84
+ {...ariaProps}
85
+ className={cn("inline-flex items-center gap-1", className)}
86
+ // Container fades together on the way out. Each dot's exit adds the motion.
87
+ initial={reduced ? false : { opacity: 0 }}
88
+ animate={{ opacity: 1 }}
89
+ exit={reduced ? { opacity: 0 } : {
90
+ opacity: 0,
91
+ transition: { duration: 0.6, ease: [0.4, 0, 0.2, 1] },
92
+ }}
93
+ >
94
+ {variant === "status" && <span className="sr-only">{statusLabel}</span>}
95
+ {[0, 1, 2].map(i => (
96
+ <motion.span
97
+ key={i}
98
+ aria-hidden
99
+ className="inline-block size-1.5 rounded-full bg-brand"
100
+ custom={i}
101
+ variants={dotVariants}
102
+ initial={reduced ? false : { opacity: 0, scale: 0.5, y: 4 }}
103
+ animate={reduced ? { opacity: 0.9, scale: 1 } : phase}
104
+ // Exit: keep pulsing once more, float up, fade. Staggered so they
105
+ // leave in a little wave rather than simultaneously.
106
+ exit={reduced ? { opacity: 0 } : {
107
+ opacity: [0.9, 1, 0],
108
+ scale: [1, 1.35, 0.3],
109
+ y: [0, -4, -14],
110
+ transition: {
111
+ duration: 0.55,
112
+ delay: i * 0.06,
113
+ times: [0, 0.35, 1],
114
+ ease: [0.3, 0, 0.2, 1],
115
+ },
116
+ }}
117
+ />
118
+ ))}
119
+ </motion.span>
120
+ )
121
+ }
@@ -0,0 +1,51 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Consistent status chip for list hubs (Team, Compliance, Question bank, future entities).
5
+ * Pair label + tint + icon from `lib/list-status-badges.ts`; do not hand-roll Badge markup per page.
6
+ */
7
+
8
+ import * as React from "react"
9
+ import { Badge } from "@/components/ui/badge"
10
+ import { cn } from "@/lib/utils"
11
+
12
+ /** Table column + list view rows — same shell as Team / Compliance / Question bank grids. */
13
+ export const LIST_HUB_STATUS_BADGE_TABLE_SHELL =
14
+ "inline-flex items-center gap-1 text-xs font-medium"
15
+
16
+ /** Kanban card badge row — fixed height, no default outline border clash. */
17
+ export const LIST_HUB_STATUS_BADGE_BOARD_SHELL =
18
+ "inline-flex h-6 items-center gap-1 border-0 px-2 py-1 text-xs font-medium leading-none shadow-none"
19
+
20
+ export interface ListHubStatusBadgeProps {
21
+ label: string
22
+ /** Tails from `*_STATUS_BADGE_CLASS` in `@/lib/list-status-badges` */
23
+ tintClassName: string
24
+ /** Font Awesome icon class suffix, e.g. `fa-circle-check` (paired with `fa-light` here). */
25
+ icon: string
26
+ /** `table` — DataTable cells and list rows; `board` — `ListPageBoardCardBadgeRow`. */
27
+ surface?: "table" | "board"
28
+ className?: string
29
+ }
30
+
31
+ export function ListHubStatusBadge({
32
+ label,
33
+ tintClassName,
34
+ icon,
35
+ surface = "table",
36
+ className,
37
+ }: ListHubStatusBadgeProps) {
38
+ return (
39
+ <Badge
40
+ variant="outline"
41
+ className={cn(
42
+ surface === "board" ? LIST_HUB_STATUS_BADGE_BOARD_SHELL : LIST_HUB_STATUS_BADGE_TABLE_SHELL,
43
+ tintClassName,
44
+ className,
45
+ )}
46
+ >
47
+ <i className={`fa-light ${icon} text-[11px]`} aria-hidden="true" />
48
+ {label}
49
+ </Badge>
50
+ )
51
+ }
@@ -0,0 +1,18 @@
1
+ "use client"
2
+
3
+ /**
4
+ * List-page **dashboard** view — thin alias over {@link DashboardReportCharts}.
5
+ * Uses a single-row KPI strip (`metricsSingleRow`) for dense hubs.
6
+ */
7
+
8
+ import * as React from "react"
9
+ import {
10
+ DashboardReportCharts,
11
+ type DashboardReportChartsProps,
12
+ } from "@/components/dashboard-report-charts"
13
+
14
+ export type ListPageDashboardChartsProps = Omit<DashboardReportChartsProps, "metricsSingleRow">
15
+
16
+ export function ListPageDashboardCharts(props: ListPageDashboardChartsProps) {
17
+ return <DashboardReportCharts {...props} metricsSingleRow />
18
+ }
@@ -0,0 +1,89 @@
1
+ "use client"
2
+
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuSeparator,
8
+ DropdownMenuTrigger,
9
+ } from "@/components/ui/dropdown-menu"
10
+ import {
11
+ SidebarGroup,
12
+ SidebarGroupLabel,
13
+ SidebarMenu,
14
+ SidebarMenuAction,
15
+ SidebarMenuButton,
16
+ SidebarMenuItem,
17
+ useSidebar,
18
+ } from "@/components/ui/sidebar"
19
+
20
+ export function NavDocuments({
21
+ items,
22
+ }: {
23
+ items: {
24
+ name: string
25
+ url: string
26
+ icon: React.ReactNode
27
+ }[]
28
+ }) {
29
+ const { isMobile } = useSidebar()
30
+
31
+ return (
32
+ <SidebarGroup
33
+ className="group-data-[collapsible=icon]:hidden"
34
+ role="group"
35
+ aria-labelledby="nav-documents-section-label"
36
+ >
37
+ <SidebarGroupLabel id="nav-documents-section-label">Documents</SidebarGroupLabel>
38
+ <SidebarMenu>
39
+ {items.map((item) => (
40
+ <SidebarMenuItem key={item.name}>
41
+ <SidebarMenuButton asChild>
42
+ <a href={item.url}>
43
+ {item.icon}
44
+ <span>{item.name}</span>
45
+ </a>
46
+ </SidebarMenuButton>
47
+ <DropdownMenu>
48
+ <DropdownMenuTrigger asChild>
49
+ <SidebarMenuAction
50
+ showOnHover
51
+ suppressHydrationWarning
52
+ className="rounded-sm data-[state=open]:bg-accent"
53
+ >
54
+ <i className="fa-light fa-ellipsis" aria-hidden="true" />
55
+ <span className="sr-only">More</span>
56
+ </SidebarMenuAction>
57
+ </DropdownMenuTrigger>
58
+ <DropdownMenuContent
59
+ className="w-24 rounded-lg"
60
+ side={isMobile ? "bottom" : "right"}
61
+ align={isMobile ? "end" : "start"}
62
+ >
63
+ <DropdownMenuItem>
64
+ <i className="fa-light fa-folder" aria-hidden="true" />
65
+ <span>Open</span>
66
+ </DropdownMenuItem>
67
+ <DropdownMenuItem>
68
+ <i className="fa-light fa-share-nodes" aria-hidden="true" />
69
+ <span>Share</span>
70
+ </DropdownMenuItem>
71
+ <DropdownMenuSeparator />
72
+ <DropdownMenuItem variant="destructive">
73
+ <i className="fa-light fa-trash" aria-hidden="true" />
74
+ <span>Delete</span>
75
+ </DropdownMenuItem>
76
+ </DropdownMenuContent>
77
+ </DropdownMenu>
78
+ </SidebarMenuItem>
79
+ ))}
80
+ <SidebarMenuItem>
81
+ <SidebarMenuButton className="text-sidebar-foreground/70">
82
+ <i className="fa-light fa-ellipsis text-sidebar-foreground/70" aria-hidden="true" />
83
+ <span>More</span>
84
+ </SidebarMenuButton>
85
+ </SidebarMenuItem>
86
+ </SidebarMenu>
87
+ </SidebarGroup>
88
+ )
89
+ }