@exxatdesignux/ui 0.3.0 → 0.4.1

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 (214) hide show
  1. package/CHANGELOG.md +701 -6
  2. package/README.md +138 -0
  3. package/bin/init.mjs +134 -31
  4. package/consumer-extras/cursor-rules/exxat-board-cards.mdc +1 -1
  5. package/consumer-extras/cursor-rules/exxat-centralized-list-dataset.mdc +2 -2
  6. package/consumer-extras/cursor-rules/exxat-collaboration-access.mdc +1 -1
  7. package/consumer-extras/cursor-rules/exxat-data-tables.mdc +2 -0
  8. package/consumer-extras/cursor-rules/exxat-dedicated-search-surfaces.mdc +1 -1
  9. package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +3 -3
  10. package/consumer-extras/cursor-rules/exxat-library-hub-header.mdc +28 -0
  11. package/consumer-extras/cursor-rules/exxat-mono-ids.mdc +1 -1
  12. package/consumer-extras/cursor-rules/exxat-person-identity-display.mdc +1 -1
  13. package/consumer-extras/cursor-rules/exxat-primary-nav-secondary-panel.mdc +6 -6
  14. package/consumer-extras/cursor-rules/exxat-reuse-before-custom.mdc +1 -1
  15. package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +2 -2
  16. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +1 -1
  17. package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -3
  18. package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +2 -2
  19. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +7 -7
  20. package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +1 -1
  21. package/consumer-extras/cursor-skills/exxat-list-page-view-shells/SKILL.md +1 -1
  22. package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +4 -4
  23. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +8 -8
  24. package/consumer-extras/cursor-skills/exxat-token-economy/SKILL.md +277 -0
  25. package/consumer-extras/handbook/HANDBOOK.md +2 -0
  26. package/consumer-extras/handbook/glossary.md +2 -1
  27. package/consumer-extras/handbook/reference-implementations.md +31 -4
  28. package/consumer-extras/patterns/collaboration-access-pattern.md +7 -7
  29. package/consumer-extras/patterns/data-views-pattern.md +18 -16
  30. package/consumer-extras/patterns/kpi-flat-band-pattern.md +2 -2
  31. package/dist/components/data-table/index.js +2 -2
  32. package/dist/components/data-table/index.js.map +1 -1
  33. package/dist/components/data-table/pagination.js +3 -3
  34. package/dist/components/data-table/pagination.js.map +1 -1
  35. package/dist/components/data-table/use-table-state.d.ts +1 -1
  36. package/dist/components/data-table/use-table-state.js.map +1 -1
  37. package/dist/components/data-views/data-row-list.js.map +1 -1
  38. package/dist/components/data-views/finder-panel-view.d.ts +1 -1
  39. package/dist/components/data-views/finder-panel-view.js.map +1 -1
  40. package/dist/components/data-views/hub-table.d.ts +9 -3
  41. package/dist/components/data-views/hub-table.js +262 -40
  42. package/dist/components/data-views/hub-table.js.map +1 -1
  43. package/dist/components/data-views/index.js +262 -40
  44. package/dist/components/data-views/index.js.map +1 -1
  45. package/dist/components/data-views/list-page-split-hub-tokens.d.ts +2 -2
  46. package/dist/components/data-views/list-page-split-hub-tokens.js.map +1 -1
  47. package/dist/components/data-views/list-page-tree-column-header.d.ts +1 -1
  48. package/dist/components/data-views/list-page-tree-column-header.js.map +1 -1
  49. package/dist/components/data-views/list-page-tree-panel-shell.js.map +1 -1
  50. package/dist/components/data-views/os-folder-glyph.d.ts +1 -1
  51. package/dist/components/data-views/os-folder-glyph.js.map +1 -1
  52. package/dist/components/ui/avatar.d.ts +1 -1
  53. package/dist/components/ui/key-metrics.js.map +1 -1
  54. package/dist/index.js +136 -39
  55. package/dist/index.js.map +1 -1
  56. package/package.json +3 -2
  57. package/src/components/data-table/index.tsx +2 -2
  58. package/src/components/data-table/pagination.tsx +5 -1
  59. package/src/components/data-table/use-table-state.ts +1 -1
  60. package/src/components/data-views/data-row-list.tsx +1 -1
  61. package/src/components/data-views/finder-panel-view.tsx +2 -2
  62. package/src/components/data-views/hub-table.tsx +149 -41
  63. package/src/components/data-views/list-page-split-hub-tokens.ts +2 -2
  64. package/src/components/data-views/list-page-tree-column-header.tsx +1 -1
  65. package/src/components/data-views/os-folder-glyph.tsx +1 -1
  66. package/src/components/ui/key-metrics.tsx +1 -1
  67. package/template/.claude/skills/exxat-ds-skill/SKILL.md +8 -7
  68. package/template/.cursor/rules/exxat-accessibility.mdc +1 -1
  69. package/template/.cursor/rules/exxat-command-menu.mdc +1 -1
  70. package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +6 -6
  71. package/template/.cursor/rules/exxat-data-tables.mdc +3 -3
  72. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +5 -5
  73. package/template/.cursor/rules/exxat-mono-ids.mdc +1 -1
  74. package/template/.cursor/rules/exxat-page-vs-drawer.mdc +1 -1
  75. package/template/.cursor/rules/exxat-table-properties-drawer.mdc +1 -1
  76. package/template/AGENTS.md +43 -37
  77. package/template/app/(app)/columns/page.tsx +11 -0
  78. package/template/app/(app)/library/all/page.tsx +11 -0
  79. package/template/app/(app)/library/find/page.tsx +12 -0
  80. package/template/app/(app)/{question-bank → library}/layout.tsx +16 -16
  81. package/template/app/(app)/library/list/page.tsx +12 -0
  82. package/template/app/(app)/{question-bank → library}/new/page.tsx +10 -10
  83. package/template/app/(app)/library/page.tsx +11 -0
  84. package/template/app/(app)/tokens-themes/page.tsx +11 -0
  85. package/template/components/ask-leo-composer.tsx +2 -2
  86. package/template/components/columns-client.tsx +158 -0
  87. package/template/components/columns-showcase.tsx +541 -0
  88. package/template/components/data-views/index.ts +32 -6
  89. package/template/components/data-views/{question-bank-folder-tree-branch.tsx → library-folder-tree-branch.tsx} +19 -19
  90. package/template/components/data-views/table-cells.tsx +673 -0
  91. package/template/components/folder-details-shell.tsx +11 -11
  92. package/template/components/hub-tree-panel-view.tsx +24 -24
  93. package/template/components/{question-bank-board-view.tsx → library-board-view.tsx} +44 -44
  94. package/template/components/{question-bank-client.tsx → library-client.tsx} +82 -82
  95. package/template/components/{question-bank-dashboard-charts.tsx → library-dashboard-charts.tsx} +14 -14
  96. package/template/components/{question-bank-favorite-button.tsx → library-favorite-button.tsx} +7 -7
  97. package/template/components/{question-bank-hub-client.tsx → library-hub-client.tsx} +43 -43
  98. package/template/components/{question-bank-new-folder-sheet.tsx → library-new-folder-sheet.tsx} +14 -14
  99. package/template/components/{question-bank-os-folder-view.tsx → library-os-folder-view.tsx} +31 -31
  100. package/template/components/{question-bank-page-header.tsx → library-page-header.tsx} +6 -6
  101. package/template/components/library-panel-activator.tsx +8 -0
  102. package/template/components/{question-bank-secondary-nav.tsx → library-secondary-nav.tsx} +60 -60
  103. package/template/components/{question-bank-table.tsx → library-table.tsx} +97 -97
  104. package/template/components/list-hub-status-badge.tsx +2 -2
  105. package/template/components/{new-question-composer.tsx → new-library-item-form.tsx} +37 -37
  106. package/template/components/sidebar/app-sidebar.tsx +61 -5
  107. package/template/components/sidebar/secondary-panel.tsx +109 -56
  108. package/template/components/sidebar/sidebar-auto-collapse.tsx +2 -2
  109. package/template/components/sidebar/sidebar-auto-open.tsx +2 -1
  110. package/template/components/table-properties/types.ts +1 -1
  111. package/template/components/templates/discovery-hub-template.tsx +1 -1
  112. package/template/components/templates/new-focus-template.tsx +2 -2
  113. package/template/components/templates/secondary-panel-hub-template.tsx +1 -1
  114. package/template/components/tokens-secondary-nav.tsx +192 -0
  115. package/template/components/tokens-themes-client.tsx +476 -0
  116. package/template/components/tokens-themes-section.tsx +386 -0
  117. package/template/docs/HANDBOOK.md +187 -0
  118. package/template/docs/blueprints/README.md +1 -1
  119. package/template/docs/blueprints/board-card.md +1 -1
  120. package/template/docs/blueprints/data-table.md +2 -2
  121. package/template/docs/blueprints/list-page-template.md +3 -3
  122. package/template/docs/blueprints/page-header.md +4 -4
  123. package/template/docs/collaboration-access-pattern.md +7 -7
  124. package/template/docs/component-selection-guide.md +1 -1
  125. package/template/docs/data-views-pattern.md +18 -16
  126. package/template/docs/glossary.md +58 -0
  127. package/template/docs/kpi-flat-band-pattern.md +3 -3
  128. package/template/docs/kpi-trend-pattern.md +18 -3
  129. package/template/docs/large-dataset-strategy.md +155 -0
  130. package/template/docs/library-hub-header-pattern.md +25 -0
  131. package/template/docs/migrations/_template.md +1 -1
  132. package/template/docs/reference-implementations.md +151 -0
  133. package/template/docs/token-taxonomy.md +1 -1
  134. package/template/docs/voice-and-tone.md +262 -0
  135. package/template/eslint.config.mjs +9 -39
  136. package/template/hooks/use-secondary-panel-hub-nav.ts +10 -10
  137. package/template/lib/ask-leo-route-context.ts +6 -18
  138. package/template/lib/coach-mark-registry.ts +0 -16
  139. package/template/lib/command-menu-config.ts +5 -12
  140. package/template/lib/command-menu-search-data.ts +8 -39
  141. package/template/lib/{question-bank-authoring.ts → library-authoring.ts} +89 -88
  142. package/template/lib/library-dedicated-search.ts +19 -0
  143. package/template/lib/library-hub-search.ts +90 -0
  144. package/template/lib/library-nav.ts +477 -0
  145. package/template/lib/library-recent-searches.ts +22 -0
  146. package/template/lib/{placements-supported-views.ts → library-supported-views.ts} +2 -2
  147. package/template/lib/list-status-badges.ts +16 -104
  148. package/template/lib/mock/dashboard.ts +1 -1
  149. package/template/lib/mock/{question-bank-folders.ts → library-folders.ts} +30 -30
  150. package/template/lib/mock/library-header-collaborators.ts +54 -0
  151. package/template/lib/mock/{question-bank-inspector.ts → library-inspector.ts} +29 -29
  152. package/template/lib/mock/{question-bank-kpi.ts → library-kpi.ts} +20 -20
  153. package/template/lib/mock/library.ts +249 -0
  154. package/template/lib/mock/navigation.tsx +32 -26
  155. package/template/lib/table-state-lifecycle.ts +1 -1
  156. package/template/next.config.mjs +7 -4
  157. package/template/package.json +0 -1
  158. package/tokens/hooks-index.json +2874 -0
  159. package/consumer-extras/cursor-rules/exxat-question-bank-hub-header.mdc +0 -28
  160. package/template/app/(app)/examples/page.tsx +0 -41
  161. package/template/app/(app)/question-bank/find/page.tsx +0 -12
  162. package/template/app/(app)/question-bank/library/page.tsx +0 -11
  163. package/template/app/(app)/question-bank/list/page.tsx +0 -12
  164. package/template/app/(app)/question-bank/page.tsx +0 -11
  165. package/template/components/compliance-board-view.tsx +0 -142
  166. package/template/components/compliance-client.tsx +0 -92
  167. package/template/components/compliance-page-header.tsx +0 -89
  168. package/template/components/compliance-table.tsx +0 -468
  169. package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
  170. package/template/components/data-view-dashboard-charts-team.tsx +0 -971
  171. package/template/components/data-view-dashboard-charts.tsx +0 -1503
  172. package/template/components/new-placement-back-btn.tsx +0 -28
  173. package/template/components/new-placement-form.tsx +0 -942
  174. package/template/components/placement-board-card.tsx +0 -250
  175. package/template/components/placement-detail.tsx +0 -438
  176. package/template/components/placements-board-view.tsx +0 -397
  177. package/template/components/placements-client.tsx +0 -220
  178. package/template/components/placements-list-view.tsx +0 -124
  179. package/template/components/placements-page-header.tsx +0 -166
  180. package/template/components/placements-table-cells.test.tsx +0 -22
  181. package/template/components/placements-table-cells.tsx +0 -173
  182. package/template/components/placements-table-columns.tsx +0 -210
  183. package/template/components/placements-table.tsx +0 -934
  184. package/template/components/question-bank-panel-activator.tsx +0 -8
  185. package/template/components/rotations-empty-state.tsx +0 -50
  186. package/template/components/rotations-panel-activator.tsx +0 -8
  187. package/template/components/sites-board-view.tsx +0 -67
  188. package/template/components/sites-client.tsx +0 -154
  189. package/template/components/sites-table.tsx +0 -249
  190. package/template/components/team-board-view.tsx +0 -122
  191. package/template/components/team-client.tsx +0 -100
  192. package/template/components/team-page-header.tsx +0 -92
  193. package/template/components/team-table.tsx +0 -553
  194. package/template/docs/question-bank-hub-header-pattern.md +0 -25
  195. package/template/lib/compliance-supported-views.ts +0 -10
  196. package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
  197. package/template/lib/mock/compliance-kpi.ts +0 -61
  198. package/template/lib/mock/compliance.ts +0 -146
  199. package/template/lib/mock/placements-kpi.ts +0 -134
  200. package/template/lib/mock/placements.ts +0 -176
  201. package/template/lib/mock/question-bank-header-collaborators.ts +0 -54
  202. package/template/lib/mock/question-bank.ts +0 -249
  203. package/template/lib/mock/sites-directory.ts +0 -16
  204. package/template/lib/mock/sites-kpi.ts +0 -25
  205. package/template/lib/mock/team-kpi.ts +0 -60
  206. package/template/lib/mock/team.ts +0 -118
  207. package/template/lib/placement-board-card-layout.ts +0 -79
  208. package/template/lib/question-bank-dedicated-search.ts +0 -19
  209. package/template/lib/question-bank-hub-search.ts +0 -90
  210. package/template/lib/question-bank-nav.ts +0 -477
  211. package/template/lib/question-bank-recent-searches.ts +0 -22
  212. package/template/lib/question-bank-supported-views.ts +0 -12
  213. package/template/lib/sites-supported-views.ts +0 -10
  214. package/template/lib/team-supported-views.ts +0 -10
@@ -1,8 +0,0 @@
1
- "use client"
2
-
3
- import { SecondaryPanelHubActivator } from "@/components/templates/secondary-panel-hub-template"
4
-
5
- /** Opens the Question bank secondary panel while this route is mounted. */
6
- export function QuestionBankPanelActivator() {
7
- return <SecondaryPanelHubActivator panelId="question-bank" />
8
- }
@@ -1,50 +0,0 @@
1
- "use client"
2
-
3
- /**
4
- * Rotations hub — main canvas when no rotation detail is selected.
5
- * Pairs with SecondaryPanel (nested sidebar); CTA reopens the panel if closed.
6
- */
7
-
8
- import { Button } from "@/components/ui/button"
9
- import { useSecondaryPanel } from "@/components/sidebar"
10
-
11
- export function RotationsEmptyState() {
12
- const { openPanel } = useSecondaryPanel()
13
-
14
- return (
15
- <section
16
- aria-labelledby="rotations-empty-title"
17
- className="flex flex-1 flex-col items-center justify-center rounded-xl border border-dashed border-border/80 bg-muted/25 px-6 py-12 text-center min-h-[min(420px,calc(100svh-var(--header-height)-6rem))]"
18
- >
19
- <div className="mb-6 w-full max-w-[min(100%,280px)] shrink-0">
20
- {/* Static SVG hero, above the fold — next/image can't optimize SVGs
21
- without `dangerouslyAllowSVG`, and lazy-loading is wrong here. */}
22
- {/* eslint-disable-next-line @next/next/no-img-element -- SVG; next/image can't optimize without dangerouslyAllowSVG */}
23
- <img
24
- src="/Illustration/Rotation.svg"
25
- alt=""
26
- width={622}
27
- height={559}
28
- decoding="async"
29
- className="h-auto w-full select-none"
30
- />
31
- </div>
32
- <h2
33
- id="rotations-empty-title"
34
- className="font-heading text-xl font-semibold tracking-tight text-foreground sm:text-2xl"
35
- >
36
- Select a rotation
37
- </h2>
38
- <p className="mt-2 max-w-md text-sm leading-relaxed text-muted-foreground">
39
- Use the rotations panel next to the sidebar to browse cycles, open a rotation for
40
- details, or review schedules and assigned students.
41
- </p>
42
- <div className="mt-8 flex flex-wrap items-center justify-center gap-3">
43
- <Button type="button" size="lg" onClick={() => openPanel("rotations")}>
44
- <i className="fa-light fa-sidebar text-[15px]" aria-hidden="true" />
45
- Open rotations panel
46
- </Button>
47
- </div>
48
- </section>
49
- )
50
- }
@@ -1,8 +0,0 @@
1
- "use client"
2
-
3
- import { useAutoPanel } from "@/components/sidebar"
4
-
5
- export function RotationsPanelActivator() {
6
- useAutoPanel("rotations")
7
- return null
8
- }
@@ -1,67 +0,0 @@
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
- }
@@ -1,154 +0,0 @@
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">
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 SitesClient() {
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
- }
@@ -1,249 +0,0 @@
1
- "use client"
2
-
3
- /**
4
- * Sites hub — thin wrapper around the centralized `<HubTable>`. Owns only the column defs,
5
- * the renderers for non-table views, and the mock-data wiring. All `useTableState` /
6
- * Properties drawer / `ListPageConnectedViewBody` plumbing lives in `HubTable`.
7
- */
8
-
9
- import * as React from "react"
10
- import Link from "next/link"
11
- import type { SiteDirectoryRow } from "@/lib/mock/sites-directory"
12
- import type { ColumnDef } from "@/components/data-table/types"
13
- import type { DataListViewType } from "@/lib/data-list-view"
14
- import { HubTable, type HubTableHandle, type HubTableRenderers } from "@/components/data-views"
15
- import { Button } from "@/components/ui/button"
16
- import {
17
- DropdownMenu,
18
- DropdownMenuContent,
19
- DropdownMenuItem,
20
- DropdownMenuTrigger,
21
- } from "@/components/ui/dropdown-menu"
22
- import { Avatar, AvatarFallback } from "@/components/ui/avatar"
23
- import { FinderPanelView, type FinderGroup } from "@/components/data-views/finder-panel-view"
24
- import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
25
- import { ListPageBoardCard } from "@/components/data-views/list-page-board-card"
26
- import { SitesCardGrid } from "@/components/sites-board-view"
27
- import { KeyMetrics } from "@/components/key-metrics"
28
- import { SITES_KPI_INSIGHT, sitesKpiMetrics } from "@/lib/mock/sites-kpi"
29
- import { SITES_SUPPORTED_VIEWS } from "@/lib/sites-supported-views"
30
-
31
- function buildSitesColumns(): ColumnDef<SiteDirectoryRow>[] {
32
- return [
33
- {
34
- key: "select",
35
- label: "",
36
- width: 40,
37
- minWidth: 40,
38
- defaultPin: "left",
39
- lockPin: true,
40
- },
41
- {
42
- key: "name",
43
- label: "Site",
44
- width: 260,
45
- minWidth: 160,
46
- sortable: true,
47
- sortKey: "name",
48
- filter: {
49
- type: "text",
50
- icon: "fa-hospital",
51
- operators: ["contains", "not_contains"],
52
- },
53
- cell: row => (
54
- <div className="flex min-w-0 items-center gap-2">
55
- <Avatar size="sm" className="size-8 shrink-0">
56
- <AvatarFallback className="bg-brand/10 p-0 text-brand">
57
- <i className="fa-light fa-hospital text-sm" aria-hidden="true" />
58
- </AvatarFallback>
59
- </Avatar>
60
- <span className="truncate text-sm font-medium text-foreground">{row.name}</span>
61
- </div>
62
- ),
63
- },
64
- {
65
- key: "id",
66
- label: "Key",
67
- width: 160,
68
- minWidth: 120,
69
- sortable: true,
70
- sortKey: "id",
71
- filter: { type: "text", icon: "fa-hashtag", operators: ["contains", "not_contains"] },
72
- cell: row => <span className="text-sm text-foreground/90">{row.id}</span>,
73
- },
74
- {
75
- key: "url",
76
- label: "Path",
77
- width: 220,
78
- minWidth: 140,
79
- sortable: true,
80
- sortKey: "url",
81
- filter: { type: "text", icon: "fa-link", operators: ["contains", "not_contains"] },
82
- cell: row => (
83
- <span className="truncate text-sm text-muted-foreground" title={row.url}>
84
- {row.url}
85
- </span>
86
- ),
87
- },
88
- {
89
- key: "actions",
90
- label: "",
91
- width: 48,
92
- minWidth: 48,
93
- defaultPin: "right",
94
- lockPin: true,
95
- cell: row => (
96
- <div className="flex items-center justify-center">
97
- <DropdownMenu>
98
- <DropdownMenuTrigger asChild>
99
- <Button size="icon-sm" variant="ghost" aria-label={`Actions for ${row.name}`}>
100
- <i className="fa-light fa-ellipsis text-sm" aria-hidden="true" />
101
- </Button>
102
- </DropdownMenuTrigger>
103
- <DropdownMenuContent align="end">
104
- <DropdownMenuItem asChild>
105
- <Link href={row.url} className="flex cursor-pointer items-center gap-2">
106
- <i className="fa-light fa-arrow-up-right-from-square" aria-hidden="true" />
107
- Open site
108
- </Link>
109
- </DropdownMenuItem>
110
- </DropdownMenuContent>
111
- </DropdownMenu>
112
- </div>
113
- ),
114
- },
115
- ]
116
- }
117
-
118
- export type SitesTableHandle = HubTableHandle
119
-
120
- export const SitesTable = React.forwardRef<
121
- SitesTableHandle,
122
- { sites: SiteDirectoryRow[]; view?: DataListViewType; onViewChange?: (v: DataListViewType) => void }
123
- >(function SitesTable({ sites, view = "board", onViewChange }, ref) {
124
- const columns = React.useMemo(() => buildSitesColumns(), [])
125
-
126
- const panelGroups = React.useCallback(
127
- (rows: SiteDirectoryRow[]): FinderGroup[] => [
128
- { id: "all", label: `All sites (${rows.length})`, count: rows.length },
129
- ],
130
- [],
131
- )
132
-
133
- const renderers: HubTableRenderers<SiteDirectoryRow> = {
134
- "board-with-toolbar": ({ state, toolbarShell }) =>
135
- toolbarShell(<SitesCardGrid rows={state.rows} />),
136
- "panel-with-toolbar": ({ state, toolbarShell }) =>
137
- toolbarShell(
138
- <ListPageSplitHubChrome aria-label="Sites directory panel view">
139
- <FinderPanelView<SiteDirectoryRow>
140
- embedded
141
- groupsColumnTitle="Sites"
142
- groups={panelGroups(state.rows)}
143
- rows={state.rows}
144
- getRowId={row => row.id}
145
- getRowGroupId={() => "all"}
146
- autoSaveId="sites-panel-view"
147
- renderListRow={(row) => (
148
- <div className="flex-1 min-w-0 flex items-center gap-2">
149
- <Avatar size="sm" className="size-6 shrink-0">
150
- <AvatarFallback className="bg-brand/10 p-0 text-brand text-xs">
151
- <i className="fa-light fa-hospital text-xs" aria-hidden="true" />
152
- </AvatarFallback>
153
- </Avatar>
154
- <div className="flex-1 min-w-0">
155
- <p className="text-sm font-medium text-foreground truncate">{row.name}</p>
156
- <p className="text-xs text-muted-foreground truncate">{row.url}</p>
157
- </div>
158
- </div>
159
- )}
160
- renderDetail={(row) => (
161
- <div className="flex min-h-0 flex-1 flex-col overflow-y-auto p-4">
162
- <div>
163
- <h3 className="text-sm font-semibold text-foreground mb-2">Site</h3>
164
- <p className="text-sm text-foreground">{row.name}</p>
165
- </div>
166
- <div className="flex flex-col gap-2">
167
- <div>
168
- <span className="text-xs font-medium text-muted-foreground">Key</span>
169
- <p className="text-sm text-foreground font-mono">{row.id}</p>
170
- </div>
171
- <div>
172
- <span className="text-xs font-medium text-muted-foreground">Path</span>
173
- <p className="text-sm text-foreground break-all">{row.url}</p>
174
- </div>
175
- </div>
176
- </div>
177
- )}
178
- emptyList={<p className="text-sm text-muted-foreground">No sites found.</p>}
179
- />
180
- </ListPageSplitHubChrome>,
181
- ),
182
- "dashboard-with-toolbar": ({ state, toolbar }) => {
183
- const metrics = sitesKpiMetrics(state.rows.length)
184
- return (
185
- <div className="flex min-h-0 flex-1 flex-col gap-4">
186
- {toolbar}
187
- <div className="px-4 pb-2 lg:px-6">
188
- <KeyMetrics
189
- variant="flat"
190
- metrics={metrics}
191
- insight={SITES_KPI_INSIGHT}
192
- showHeader={false}
193
- metricsSingleRow
194
- />
195
- </div>
196
- <SitesCardGrid rows={state.rows} />
197
- </div>
198
- )
199
- },
200
- }
201
-
202
- return (
203
- <HubTable<SiteDirectoryRow>
204
- rows={sites}
205
- columns={columns}
206
- view={view}
207
- onViewChange={onViewChange}
208
- supportedViewTypes={SITES_SUPPORTED_VIEWS}
209
- hubLabel="Sites"
210
- lifecycleTabLabel="Sites"
211
- searchAriaLabel="Search sites"
212
- getRowId={row => row.id}
213
- getRowSelectionLabel={row => row.name}
214
- defaultSort={{ key: "name", dir: "asc" }}
215
- emptyState={<p className="text-sm text-muted-foreground">No sites match your filters.</p>}
216
- listAriaLabel="Sites"
217
- listEmptyState="No sites match your search."
218
- renderListRow={site => (
219
- <Link
220
- href={site.url}
221
- className="block rounded-xl text-inherit no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
222
- >
223
- <ListPageBoardCard
224
- layout="row"
225
- interactive
226
- rowContainerClassName="flex flex-row items-center gap-3"
227
- leading={
228
- <span className="inline-flex size-9 shrink-0 items-center justify-center rounded-md bg-brand/10 text-brand">
229
- <i className="fa-light fa-hospital text-sm" aria-hidden="true" />
230
- </span>
231
- }
232
- rowEnd={
233
- <i className="fa-light fa-chevron-right text-xs text-muted-foreground" aria-hidden="true" />
234
- }
235
- >
236
- <div className="space-y-0.5">
237
- <p className="truncate text-sm font-semibold text-foreground">{site.name}</p>
238
- <p className="truncate text-xs text-muted-foreground">{site.id}</p>
239
- </div>
240
- </ListPageBoardCard>
241
- </Link>
242
- )}
243
- renderers={renderers}
244
- handleRef={ref}
245
- />
246
- )
247
- })
248
-
249
- SitesTable.displayName = "SitesTable"
@@ -1,122 +0,0 @@
1
- "use client"
2
-
3
- /**
4
- * Team board — kanban by member status or role. Column layout from `ListPageBoardTemplate`.
5
- */
6
-
7
- import * as React from "react"
8
- import {
9
- TEAM_MEMBER_STATUS_BADGE_CLASS,
10
- TEAM_MEMBER_STATUS_ICON,
11
- TEAM_MEMBER_STATUS_LABEL,
12
- } from "@/lib/list-status-badges"
13
- import type { TeamMember } from "@/lib/mock/team"
14
- import { BoardCardTwoLineBlock } from "@/components/data-views/board-card-primitives"
15
- import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
16
- import {
17
- ListPageBoardCard,
18
- ListPageBoardCardAvatar,
19
- ListPageBoardCardBadgeRow,
20
- ListPageBoardCardBody,
21
- ListPageBoardCardHeader,
22
- ListPageBoardCardTitleRow,
23
- } from "@/components/data-views/list-page-board-card"
24
- import {
25
- ListPageBoardTemplate,
26
- type ListPageBoardColumnDef,
27
- } from "@/components/data-views/list-page-board-template"
28
-
29
- const NEUTRAL_COUNT_BADGE = "bg-muted/90 text-foreground"
30
-
31
- const STATUS_BOARD_COLUMNS: ListPageBoardColumnDef<TeamMember>[] = [
32
- { id: "active", label: "Active", description: "On the team", filter: m => m.status === "active" },
33
- { id: "away", label: "Away", description: "Temporarily away", filter: m => m.status === "away" },
34
- { id: "invited", label: "Invited", description: "Pending acceptance", filter: m => m.status === "invited" },
35
- ]
36
-
37
- function roleBoardColumns(members: TeamMember[]): {
38
- columns: ListPageBoardColumnDef<TeamMember>[]
39
- badgeMap: Record<string, string>
40
- } {
41
- const roles = [...new Set(members.map(m => m.role))].sort((a, b) => a.localeCompare(b))
42
- const columns: ListPageBoardColumnDef<TeamMember>[] = roles.map(role => ({
43
- id: `role:${role}`,
44
- label: role,
45
- filter: (m: TeamMember) => m.role === role,
46
- }))
47
- const badgeMap = Object.fromEntries(roles.map(r => [`role:${r}`, NEUTRAL_COUNT_BADGE]))
48
- return { columns, badgeMap }
49
- }
50
-
51
- function useTeamBoardModel(members: TeamMember[], groupByColumnKey: string) {
52
- return React.useMemo(() => {
53
- if (groupByColumnKey === "role") {
54
- const { columns, badgeMap } = roleBoardColumns(members)
55
- return { columns, badgeMap }
56
- }
57
- return {
58
- columns: STATUS_BOARD_COLUMNS,
59
- badgeMap: TEAM_MEMBER_STATUS_BADGE_CLASS as Record<string, string>,
60
- }
61
- }, [members, groupByColumnKey])
62
- }
63
-
64
- function TeamBoardCard({
65
- member,
66
- onRowActivate,
67
- }: {
68
- member: TeamMember
69
- onRowActivate?: (member: TeamMember) => void
70
- }) {
71
- return (
72
- <ListPageBoardCard className="w-full" onClick={onRowActivate ? () => onRowActivate(member) : undefined}>
73
- <ListPageBoardCardHeader>
74
- <ListPageBoardCardTitleRow
75
- title={member.name}
76
- titleClassName="truncate"
77
- trailing={<ListPageBoardCardAvatar initials={member.initials} />}
78
- />
79
- <ListPageBoardCardBadgeRow>
80
- <ListHubStatusBadge
81
- surface="board"
82
- label={TEAM_MEMBER_STATUS_LABEL[member.status]}
83
- tintClassName={TEAM_MEMBER_STATUS_BADGE_CLASS[member.status]}
84
- icon={TEAM_MEMBER_STATUS_ICON[member.status]}
85
- />
86
- </ListPageBoardCardBadgeRow>
87
- <ListPageBoardCardBody>
88
- <BoardCardTwoLineBlock iconClass="fa-briefcase" line1={member.role} line2={member.email} />
89
- </ListPageBoardCardBody>
90
- </ListPageBoardCardHeader>
91
- </ListPageBoardCard>
92
- )
93
- }
94
-
95
- export const TEAM_BOARD_GROUP_OPTIONS = [
96
- { key: "status", label: "Status" },
97
- { key: "role", label: "Role" },
98
- ] as const
99
-
100
- export function TeamBoardView({
101
- members,
102
- groupByColumnKey,
103
- onRowActivate,
104
- }: {
105
- members: TeamMember[]
106
- groupByColumnKey: string
107
- onRowActivate?: (member: TeamMember) => void
108
- }) {
109
- const key = groupByColumnKey === "role" ? "role" : "status"
110
- const { columns, badgeMap } = useTeamBoardModel(members, key)
111
-
112
- return (
113
- <ListPageBoardTemplate
114
- columns={columns}
115
- rows={members}
116
- getRowKey={m => m.id}
117
- columnCountBadgeClassName={badgeMap}
118
- emptyColumnLabel="No members"
119
- renderCard={member => <TeamBoardCard member={member} onRowActivate={onRowActivate} />}
120
- />
121
- )
122
- }