@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,93 @@
1
+ "use client"
2
+
3
+ /**
4
+ * SiteHeader — breadcrumb / top bar — WCAG 2.1 AA
5
+ *
6
+ * ✓ SidebarTrigger wrapped in Tooltip — icon-only button (WCAG 4.1.2, 1.1.1)
7
+ * ✓ <header role="banner"> landmark for AT navigation (WCAG 1.3.6)
8
+ * ✓ No bottom border (per design spec)
9
+ * ✓ Uses Inter (font-sans) — Ivy Presto is reserved for PageHeader <h1> only
10
+ */
11
+
12
+ import Link from "next/link"
13
+ import { Separator } from "@/components/ui/separator"
14
+ import { SidebarTrigger } from "@/components/ui/sidebar"
15
+ import { Kbd, KbdGroup } from "@/components/ui/kbd"
16
+ import {
17
+ Tooltip,
18
+ TooltipContent,
19
+ TooltipTrigger,
20
+ } from "@/components/ui/tooltip"
21
+ import { AskLeoToggle } from "@/components/ask-leo-sidebar"
22
+ import { useModKeyLabel } from "@/hooks/use-mod-key-label"
23
+
24
+ export interface BreadcrumbItem {
25
+ label: string
26
+ href?: string
27
+ }
28
+
29
+ export interface SiteHeaderProps {
30
+ /** Current page title (last breadcrumb segment) */
31
+ title?: string
32
+ /** Full breadcrumb trail — each item can be a link or plain text. Title is appended automatically as the last segment. */
33
+ breadcrumbs?: BreadcrumbItem[]
34
+ }
35
+
36
+ export function SiteHeader({ title = "Dashboard", breadcrumbs }: SiteHeaderProps) {
37
+ const mod = useModKeyLabel()
38
+
39
+ return (
40
+ <header
41
+ role="banner"
42
+ className="flex h-(--header-height) shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)"
43
+ >
44
+ <div className="flex w-full items-center gap-1 ps-4 pe-2 lg:gap-2 lg:ps-6 lg:pe-2">
45
+ <Tooltip>
46
+ <TooltipTrigger asChild>
47
+ <SidebarTrigger className="-ms-1" />
48
+ </TooltipTrigger>
49
+ <TooltipContent side="bottom" className="flex flex-wrap items-center gap-1.5">
50
+ <span>Toggle sidebar</span>
51
+ <KbdGroup>
52
+ <Kbd>{mod}</Kbd>
53
+ <Kbd>B</Kbd>
54
+ </KbdGroup>
55
+ </TooltipContent>
56
+ </Tooltip>
57
+
58
+ <Separator
59
+ orientation="vertical"
60
+ className="mx-2 data-[orientation=vertical]:h-4 data-[orientation=vertical]:self-auto"
61
+ />
62
+
63
+ {/* Breadcrumb trail */}
64
+ <nav aria-label="Breadcrumb" className="flex items-center gap-1.5 min-w-0 overflow-hidden">
65
+ {breadcrumbs?.map((crumb, i) => (
66
+ <span key={i} className="flex items-center gap-1.5 shrink-0">
67
+ {crumb.href ? (
68
+ <Link
69
+ href={crumb.href}
70
+ className="font-sans text-sm text-muted-foreground hover:text-interactive-hover-foreground transition-colors tracking-normal"
71
+ >
72
+ {crumb.label}
73
+ </Link>
74
+ ) : (
75
+ <span className="font-sans text-sm text-muted-foreground tracking-normal">
76
+ {crumb.label}
77
+ </span>
78
+ )}
79
+ <i className="fa-light fa-chevron-right text-xs text-muted-foreground/50" aria-hidden="true" />
80
+ </span>
81
+ ))}
82
+ <span className="font-sans text-sm font-medium text-foreground tracking-normal truncate">
83
+ {title}
84
+ </span>
85
+ </nav>
86
+
87
+ <div className="ml-auto shrink-0">
88
+ <AskLeoToggle />
89
+ </div>
90
+ </div>
91
+ </header>
92
+ )
93
+ }
@@ -0,0 +1,154 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { ListPageTemplate, type ViewTab, dataListViewIcon, type DataListViewType } from "@/components/data-views"
6
+ import { PageHeader } from "@/components/page-header"
7
+ import { Button } from "@/components/ui/button"
8
+ import { Tip } from "@/components/ui/tip"
9
+ import {
10
+ DropdownMenu,
11
+ DropdownMenuContent,
12
+ DropdownMenuItem,
13
+ DropdownMenuSeparator,
14
+ DropdownMenuTrigger,
15
+ } from "@/components/ui/dropdown-menu"
16
+ import { KeyMetrics } from "@/components/key-metrics"
17
+ import { SitesTable, type SitesTableHandle } from "@/components/sites-table"
18
+ import { useAskLeoPageContext } from "@/components/ask-leo-sidebar"
19
+ import { SITES_DIRECTORY } from "@/lib/mock/sites-directory"
20
+ import { SITES_KPI_INSIGHT, sitesKpiMetrics } from "@/lib/mock/sites-kpi"
21
+
22
+ const DEFAULT_TABS: ViewTab[] = [
23
+ { id: "sites", label: "Sites", viewType: "board", icon: "fa-grid-2", filterId: "all" },
24
+ ]
25
+
26
+ function SitesPageHeader({
27
+ count,
28
+ onAdd,
29
+ onExport,
30
+ showMetrics,
31
+ onToggleMetrics,
32
+ }: {
33
+ count: number
34
+ onAdd: () => void
35
+ onExport: () => void
36
+ showMetrics: boolean
37
+ onToggleMetrics: () => void
38
+ }) {
39
+ const [moreOpen, setMoreOpen] = React.useState(false)
40
+ return (
41
+ <PageHeader
42
+ title="Sites"
43
+ subtitle={`${count} ${count === 1 ? "site" : "sites"} · Last updated now`}
44
+ actions={
45
+ <div className="flex items-center gap-2" role="group" aria-label="Sites actions">
46
+ <Tip side="bottom" label="Add a new site">
47
+ <Button type="button" size="lg" onClick={onAdd}>
48
+ <i className="fa-light fa-plus" aria-hidden="true" />
49
+ Add site
50
+ </Button>
51
+ </Tip>
52
+ <DropdownMenu open={moreOpen} onOpenChange={setMoreOpen}>
53
+ <Tip side="bottom" label="More actions">
54
+ <DropdownMenuTrigger asChild>
55
+ <Button
56
+ type="button"
57
+ size="lg"
58
+ variant="outline"
59
+ className="aspect-square px-0"
60
+ aria-label="More actions"
61
+ >
62
+ <i className="fa-light fa-ellipsis text-base" aria-hidden="true" />
63
+ </Button>
64
+ </DropdownMenuTrigger>
65
+ </Tip>
66
+ <DropdownMenuContent align="end" className="w-52">
67
+ <DropdownMenuItem
68
+ onSelect={() => {
69
+ window.setTimeout(() => onExport(), 0)
70
+ }}
71
+ >
72
+ <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
73
+ Export
74
+ </DropdownMenuItem>
75
+ <DropdownMenuSeparator />
76
+ <DropdownMenuItem
77
+ onSelect={() => {
78
+ window.setTimeout(() => onToggleMetrics(), 0)
79
+ }}
80
+ >
81
+ <i
82
+ className={`fa-light ${showMetrics ? "fa-eye-slash" : "fa-eye"}`}
83
+ aria-hidden="true"
84
+ />
85
+ {showMetrics ? "Hide metric section" : "Show metric section"}
86
+ </DropdownMenuItem>
87
+ </DropdownMenuContent>
88
+ </DropdownMenu>
89
+ </div>
90
+ }
91
+ />
92
+ )
93
+ }
94
+
95
+ export function SitesAllClient() {
96
+ const [exportOpen, setExportOpen] = React.useState(false)
97
+ const [showMetrics, setShowMetrics] = React.useState(true)
98
+ const tableRef = React.useRef<SitesTableHandle>(null)
99
+ const count = SITES_DIRECTORY.length
100
+ const metrics = React.useMemo(() => sitesKpiMetrics(count), [count])
101
+
102
+ useAskLeoPageContext(
103
+ React.useMemo(
104
+ () => ({
105
+ title: "Sites",
106
+ description: `${count} sites in this directory.`,
107
+ suggestions: [
108
+ "Which sites should we onboard next?",
109
+ "Summarize capacity across affiliated sites",
110
+ ],
111
+ }),
112
+ [count],
113
+ ),
114
+ )
115
+
116
+ return (
117
+ <ListPageTemplate
118
+ defaultTabs={DEFAULT_TABS}
119
+ getTabCount={() => count}
120
+ showMetrics={showMetrics}
121
+ tablePropertiesRef={tableRef}
122
+ metrics={
123
+ <KeyMetrics
124
+ variant="flat"
125
+ metrics={metrics}
126
+ insight={SITES_KPI_INSIGHT}
127
+ showHeader={false}
128
+ metricsSingleRow
129
+ />
130
+ }
131
+ exportOpen={exportOpen}
132
+ onExportOpenChange={setExportOpen}
133
+ exportTotalRows={count}
134
+ header={
135
+ <SitesPageHeader
136
+ count={count}
137
+ onAdd={() => {}}
138
+ onExport={() => setExportOpen(true)}
139
+ showMetrics={showMetrics}
140
+ onToggleMetrics={() => setShowMetrics(v => !v)}
141
+ />
142
+ }
143
+ renderContent={(tab, updateTab) => (
144
+ <SitesTable
145
+ key={tab.id}
146
+ ref={tableRef}
147
+ sites={SITES_DIRECTORY}
148
+ view={tab.viewType}
149
+ onViewChange={(v: DataListViewType) => updateTab({ viewType: v, icon: dataListViewIcon(v) })}
150
+ />
151
+ )}
152
+ />
153
+ )
154
+ }
@@ -0,0 +1,67 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Sites hub — **grid of `ListPageBoardCard` tiles** (same card composition as Team board cards),
5
+ * not the kanban `ListPageBoardTemplate` column shell. Insets match other list hubs.
6
+ */
7
+
8
+ import Link from "next/link"
9
+ import type { SiteDirectoryRow } from "@/lib/mock/sites-directory"
10
+ import { initialsFromDisplayName } from "@/lib/initials-from-name"
11
+ import { BoardCardIconRow } from "@/components/data-views/board-card-primitives"
12
+ import {
13
+ HubRecordCard,
14
+ ListPageBoardCardAvatar,
15
+ ListPageBoardCardBody,
16
+ ListPageBoardCardHeader,
17
+ ListPageBoardCardTitleRow,
18
+ } from "@/components/data-views/list-page-board-card"
19
+
20
+ /** Same card building blocks as `TeamBoardView` / board tabs — without the column template. */
21
+ export function SiteBoardCard({ site }: { site: SiteDirectoryRow }) {
22
+ return (
23
+ <Link
24
+ href={site.url}
25
+ className="block h-full rounded-xl text-inherit no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
26
+ >
27
+ <HubRecordCard interactive className="h-full w-full">
28
+ <ListPageBoardCardHeader>
29
+ <ListPageBoardCardTitleRow
30
+ title={site.name}
31
+ titleClassName="truncate"
32
+ trailing={<ListPageBoardCardAvatar initials={initialsFromDisplayName(site.name)} />}
33
+ />
34
+ <ListPageBoardCardBody>
35
+ <BoardCardIconRow iconClass="fa-hashtag">
36
+ <span className="truncate">{site.id}</span>
37
+ </BoardCardIconRow>
38
+ <BoardCardIconRow iconClass="fa-link">
39
+ <span className="truncate" title={site.url}>
40
+ {site.url}
41
+ </span>
42
+ </BoardCardIconRow>
43
+ </ListPageBoardCardBody>
44
+ </ListPageBoardCardHeader>
45
+ </HubRecordCard>
46
+ </Link>
47
+ )
48
+ }
49
+
50
+ /** Responsive card grid + page insets (`px-4` / `lg:px-6`) aligned with `ListPageBoardTemplate` / Team toolbar. */
51
+ export function SitesCardGrid({ rows }: { rows: SiteDirectoryRow[] }) {
52
+ if (rows.length === 0) {
53
+ return (
54
+ <div className="px-4 pb-6 pt-2 lg:px-6">
55
+ <p className="py-8 text-center text-sm text-muted-foreground">No sites match your search.</p>
56
+ </div>
57
+ )
58
+ }
59
+
60
+ return (
61
+ <div className="grid grid-cols-1 gap-3 px-4 pb-6 pt-2 sm:grid-cols-2 lg:grid-cols-3 lg:px-6 xl:grid-cols-4">
62
+ {rows.map(site => (
63
+ <SiteBoardCard key={site.id} site={site} />
64
+ ))}
65
+ </div>
66
+ )
67
+ }
@@ -0,0 +1,47 @@
1
+ "use client"
2
+
3
+ import Link from "next/link"
4
+ import type { SiteDirectoryRow } from "@/lib/mock/sites-directory"
5
+ import { ListPageBoardCard } from "@/components/data-views/list-page-board-card"
6
+
7
+ export function SitesListView({ rows }: { rows: SiteDirectoryRow[] }) {
8
+ if (rows.length === 0) {
9
+ return (
10
+ <div className="px-4 py-16 text-center lg:px-6">
11
+ <p className="text-sm text-muted-foreground">No sites match your search.</p>
12
+ </div>
13
+ )
14
+ }
15
+
16
+ return (
17
+ <ul className="flex list-none flex-col gap-2 px-4 pb-8 pt-2 lg:px-6">
18
+ {rows.map(site => (
19
+ <li key={site.id}>
20
+ <Link
21
+ href={site.url}
22
+ className="block rounded-xl text-inherit no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
23
+ >
24
+ <ListPageBoardCard
25
+ layout="row"
26
+ interactive
27
+ rowContainerClassName="flex flex-row items-center gap-3"
28
+ leading={
29
+ <span className="inline-flex size-9 shrink-0 items-center justify-center rounded-md bg-brand/10 text-brand">
30
+ <i className="fa-light fa-hospital text-sm" aria-hidden="true" />
31
+ </span>
32
+ }
33
+ rowEnd={
34
+ <i className="fa-light fa-chevron-right text-xs text-muted-foreground" aria-hidden="true" />
35
+ }
36
+ >
37
+ <div className="space-y-0.5">
38
+ <p className="truncate text-sm font-semibold text-foreground">{site.name}</p>
39
+ <p className="truncate text-xs text-muted-foreground">{site.id}</p>
40
+ </div>
41
+ </ListPageBoardCard>
42
+ </Link>
43
+ </li>
44
+ ))}
45
+ </ul>
46
+ )
47
+ }
@@ -0,0 +1,312 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Sites hub — same stack as Team / Compliance: DataTableToolbar (search, filters)
5
+ * + TablePropertiesDrawer, shared `useTableState` across table | list | board | dashboard.
6
+ */
7
+
8
+ import * as React from "react"
9
+ import Link from "next/link"
10
+ import type { SiteDirectoryRow } from "@/lib/mock/sites-directory"
11
+ import { DataTable, DataTableToolbar } from "@/components/data-table"
12
+ import { useTableState } from "@/components/data-table/use-table-state"
13
+ import type { ColumnDef } from "@/components/data-table/types"
14
+ import type { DataListViewType } from "@/lib/data-list-view"
15
+ import type { OpenTablePropertiesHandle } from "@/lib/list-page-table-properties"
16
+ import { TablePropertiesDrawerButton } from "@/components/table-properties"
17
+ import type { ConditionalRule, FilterFieldDef, FilterOperator } from "@/components/table-properties/types"
18
+ import { Button } from "@/components/ui/button"
19
+ import {
20
+ DropdownMenu,
21
+ DropdownMenuContent,
22
+ DropdownMenuItem,
23
+ DropdownMenuTrigger,
24
+ } from "@/components/ui/dropdown-menu"
25
+ import { Tip } from "@/components/ui/tip"
26
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar"
27
+ import { SitesCardGrid } from "@/components/sites-board-view"
28
+ import { SitesListView } from "@/components/sites-list-view"
29
+ import { KeyMetrics } from "@/components/key-metrics"
30
+ import { SITES_KPI_INSIGHT, sitesKpiMetrics } from "@/lib/mock/sites-kpi"
31
+ import {
32
+ DEFAULT_DATA_LIST_DISPLAY_OPTIONS,
33
+ type DataListDisplayOptions,
34
+ } from "@/lib/data-list-display-options"
35
+
36
+ function columnToFilterFieldDef(c: ColumnDef<SiteDirectoryRow>): FilterFieldDef | null {
37
+ if (!c.filter) return null
38
+ const f = c.filter
39
+ const defaultOps: FilterOperator[] =
40
+ f.type === "select" || f.type === "date" ? ["is", "is_not"] : ["contains", "not_contains"]
41
+ return {
42
+ key: c.key,
43
+ label: c.label,
44
+ icon: f.icon ?? "fa-filter",
45
+ type: f.type,
46
+ operators: (f.operators ?? defaultOps) as FilterOperator[],
47
+ options: f.options,
48
+ ...(f.textMask ? { textMask: f.textMask } : {}),
49
+ }
50
+ }
51
+
52
+ function columnsToFilterFields(cols: ColumnDef<SiteDirectoryRow>[]) {
53
+ return cols.map(columnToFilterFieldDef).filter((x): x is FilterFieldDef => x !== null)
54
+ }
55
+
56
+ function buildSitesColumns(): ColumnDef<SiteDirectoryRow>[] {
57
+ const COLUMN_SELECT: ColumnDef<SiteDirectoryRow> = {
58
+ key: "select",
59
+ label: "",
60
+ width: 40,
61
+ minWidth: 40,
62
+ defaultPin: "left",
63
+ lockPin: true,
64
+ }
65
+
66
+ return [
67
+ COLUMN_SELECT,
68
+ {
69
+ key: "name",
70
+ label: "Site",
71
+ width: 260,
72
+ minWidth: 160,
73
+ sortable: true,
74
+ sortKey: "name",
75
+ filter: {
76
+ type: "text",
77
+ icon: "fa-hospital",
78
+ operators: ["contains", "not_contains"],
79
+ },
80
+ cell: row => (
81
+ <div className="flex min-w-0 items-center gap-2">
82
+ <Avatar size="sm" className="size-8 shrink-0">
83
+ <AvatarFallback className="bg-brand/10 p-0 text-brand">
84
+ <i className="fa-light fa-hospital text-sm" aria-hidden="true" />
85
+ </AvatarFallback>
86
+ </Avatar>
87
+ <span className="truncate text-sm font-medium text-foreground">{row.name}</span>
88
+ </div>
89
+ ),
90
+ },
91
+ {
92
+ key: "id",
93
+ label: "Key",
94
+ width: 160,
95
+ minWidth: 120,
96
+ sortable: true,
97
+ sortKey: "id",
98
+ filter: {
99
+ type: "text",
100
+ icon: "fa-hashtag",
101
+ operators: ["contains", "not_contains"],
102
+ },
103
+ cell: row => <span className="text-sm text-foreground/90">{row.id}</span>,
104
+ },
105
+ {
106
+ key: "url",
107
+ label: "Path",
108
+ width: 220,
109
+ minWidth: 140,
110
+ sortable: true,
111
+ sortKey: "url",
112
+ filter: {
113
+ type: "text",
114
+ icon: "fa-link",
115
+ operators: ["contains", "not_contains"],
116
+ },
117
+ cell: row => (
118
+ <span className="truncate text-sm text-muted-foreground" title={row.url}>
119
+ {row.url}
120
+ </span>
121
+ ),
122
+ },
123
+ {
124
+ key: "actions",
125
+ label: "",
126
+ width: 48,
127
+ minWidth: 48,
128
+ defaultPin: "right",
129
+ lockPin: true,
130
+ cell: row => (
131
+ <div className="flex items-center justify-center">
132
+ <DropdownMenu>
133
+ <DropdownMenuTrigger asChild>
134
+ <Button size="icon-sm" variant="ghost" aria-label={`Actions for ${row.name}`}>
135
+ <i className="fa-light fa-ellipsis text-sm" aria-hidden="true" />
136
+ </Button>
137
+ </DropdownMenuTrigger>
138
+ <DropdownMenuContent align="end" className="w-40">
139
+ <DropdownMenuItem asChild>
140
+ <Link href={row.url} className="flex cursor-pointer items-center gap-2">
141
+ <i className="fa-light fa-arrow-up-right-from-square" aria-hidden="true" />
142
+ Open site
143
+ </Link>
144
+ </DropdownMenuItem>
145
+ </DropdownMenuContent>
146
+ </DropdownMenu>
147
+ </div>
148
+ ),
149
+ },
150
+ ]
151
+ }
152
+
153
+
154
+ export type SitesTableHandle = OpenTablePropertiesHandle
155
+
156
+ export const SitesTable = React.forwardRef<
157
+ SitesTableHandle,
158
+ { sites: SiteDirectoryRow[]; view?: DataListViewType; onViewChange?: (v: DataListViewType) => void }
159
+ >(function SitesTable({ sites, view = "board", onViewChange }, ref) {
160
+ const columns = React.useMemo(() => buildSitesColumns(), [])
161
+ const filterFields = React.useMemo(() => columnsToFilterFields(columns), [columns])
162
+ const fieldDefinitionsForDrawer = React.useMemo(
163
+ () =>
164
+ columns
165
+ .filter(c => c.key !== "select" && c.key !== "actions")
166
+ .map(c => ({ key: c.key, label: c.label, sortable: !!(c.sortable && (c.sortKey ?? c.key)) })),
167
+ [columns],
168
+ )
169
+
170
+ const resolveColumnLabel = React.useCallback(
171
+ (key: string) => columns.find(c => c.key === key)?.label ?? key,
172
+ [columns],
173
+ )
174
+
175
+ const [displayOptions, setDisplayOptions] = React.useState<DataListDisplayOptions>(DEFAULT_DATA_LIST_DISPLAY_OPTIONS)
176
+ const patchDisplay = React.useCallback((patch: Partial<DataListDisplayOptions>) => {
177
+ setDisplayOptions(prev => ({ ...prev, ...patch }))
178
+ }, [])
179
+
180
+ const [conditionalRules, setConditionalRules] = React.useState<ConditionalRule[]>([])
181
+
182
+ const addConditionalRule = React.useCallback((rule: Omit<ConditionalRule, "id">) => {
183
+ setConditionalRules(prev => [...prev, { ...rule, id: `cr-${Date.now()}` }])
184
+ }, [])
185
+ const removeConditionalRule = React.useCallback((id: string) => {
186
+ setConditionalRules(prev => prev.filter(r => r.id !== id))
187
+ }, [])
188
+ const updateConditionalRule = React.useCallback((id: string, patch: Partial<ConditionalRule>) => {
189
+ setConditionalRules(prev => prev.map(r => (r.id === id ? { ...r, ...patch } : r)))
190
+ }, [])
191
+
192
+ const tableState = useTableState<SiteDirectoryRow>(sites, columns, { key: "name", dir: "asc" })
193
+
194
+ React.useImperativeHandle(
195
+ ref,
196
+ () => ({
197
+ openPropertiesDrawer: () => {
198
+ tableState.setSheetOpen(true)
199
+ },
200
+ }),
201
+ [tableState.setSheetOpen],
202
+ )
203
+
204
+ const dashMetrics = React.useMemo(
205
+ () => sitesKpiMetrics(tableState.rows.length),
206
+ [tableState.rows.length],
207
+ )
208
+
209
+ const drawerToolbarProps = {
210
+ state: tableState,
211
+ totalRows: sites.length,
212
+ filterFields,
213
+ fieldDefinitions: fieldDefinitionsForDrawer,
214
+ resolveColumnLabel,
215
+ displayOptions,
216
+ onDisplayOptionsChange: patchDisplay,
217
+ conditionalRules,
218
+ onAddConditionalRule: addConditionalRule,
219
+ onRemoveConditionalRule: removeConditionalRule,
220
+ onUpdateConditionalRule: updateConditionalRule,
221
+ currentView: view,
222
+ onViewChange,
223
+ lifecycleTabLabel: "Sites",
224
+ }
225
+
226
+ const tableProps = {
227
+ data: sites,
228
+ columns,
229
+ getRowId: (row: SiteDirectoryRow) => row.id,
230
+ getRowSelectionLabel: (row: SiteDirectoryRow) => row.name,
231
+ selectable: true,
232
+ searchable: displayOptions.showToolbarSearch,
233
+ showColumnHeaders: displayOptions.showColumnLabels,
234
+ groupable: true,
235
+ defaultSort: { key: "name", dir: "asc" as const },
236
+ emptyState: <p className="text-sm text-muted-foreground">No sites match your filters.</p>,
237
+ conditionalRules,
238
+ state: tableState,
239
+ toolbarSlot: (s: ReturnType<typeof useTableState<SiteDirectoryRow>>) => (
240
+ <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />
241
+ ),
242
+ bulkActionsSlot: (selected: Set<string | number>) => {
243
+ const n = selected.size
244
+ if (n === 0) return null
245
+ return (
246
+ <>
247
+ <span className="sr-only">{n} selected</span>
248
+ <Tip label="Export selection (demo)">
249
+ <Button size="sm" variant="outline" type="button">
250
+ <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
251
+ Export
252
+ </Button>
253
+ </Tip>
254
+ </>
255
+ )
256
+ },
257
+ }
258
+
259
+ if (view === "table") {
260
+ return (
261
+ <div className="pb-6">
262
+ <DataTable<SiteDirectoryRow> {...tableProps} />
263
+ </div>
264
+ )
265
+ }
266
+
267
+ const sharedToolbar = (
268
+ <DataTableToolbar
269
+ state={tableState}
270
+ columns={columns}
271
+ searchable={displayOptions.showToolbarSearch}
272
+ searchAriaLabel="Search sites"
273
+ toolbarSlot={s => <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />}
274
+ />
275
+ )
276
+
277
+ if (view === "list") {
278
+ return (
279
+ <div className="flex min-h-0 flex-1 flex-col">
280
+ {sharedToolbar}
281
+ <SitesListView rows={tableState.rows} />
282
+ </div>
283
+ )
284
+ }
285
+
286
+ if (view === "board") {
287
+ return (
288
+ <div className="flex min-h-0 flex-1 flex-col">
289
+ {sharedToolbar}
290
+ <SitesCardGrid rows={tableState.rows} />
291
+ </div>
292
+ )
293
+ }
294
+
295
+ return (
296
+ <div className="flex min-h-0 flex-1 flex-col gap-4">
297
+ {sharedToolbar}
298
+ <div className="px-4 pb-2 lg:px-6">
299
+ <KeyMetrics
300
+ variant="flat"
301
+ metrics={dashMetrics}
302
+ insight={SITES_KPI_INSIGHT}
303
+ showHeader={false}
304
+ metricsSingleRow
305
+ />
306
+ </div>
307
+ <SitesCardGrid rows={tableState.rows} />
308
+ </div>
309
+ )
310
+ })
311
+
312
+ SitesTable.displayName = "SitesTable"