@exxatdesignux/ui 0.2.17 → 0.2.19

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 (162) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/consumer-extras/AGENTS.md +76 -0
  3. package/consumer-extras/README.md +5 -1
  4. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
  5. package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
  6. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +22 -7
  7. package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
  8. package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +38 -0
  9. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +10 -3
  10. package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
  11. package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
  12. package/consumer-extras/patterns/data-views-pattern.md +42 -3
  13. package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
  14. package/consumer-extras/patterns/kpi-flat-band-pattern.md +57 -0
  15. package/consumer-extras/patterns/shell-surface-elevation-pattern.md +54 -0
  16. package/package.json +2 -1
  17. package/src/components/ui/button-group.tsx +81 -0
  18. package/src/components/ui/button.tsx +4 -4
  19. package/src/components/ui/sidebar.tsx +2 -2
  20. package/src/globals.css +7 -1807
  21. package/src/theme.css +10 -1126
  22. package/src/tokens/README.md +15 -0
  23. package/src/tokens/base.css +337 -0
  24. package/src/tokens/high-contrast.css +1195 -0
  25. package/src/tokens/layers.css +224 -0
  26. package/src/tokens/tailwind-bridge.css +118 -0
  27. package/src/tokens/themes.css +201 -0
  28. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -1
  29. package/template/AGENTS.md +66 -21
  30. package/template/app/(app)/dashboard/loading.tsx +3 -15
  31. package/template/app/(app)/dashboard/page.tsx +2 -14
  32. package/template/app/(app)/data-list/layout.tsx +43 -0
  33. package/template/app/(app)/data-list/page.tsx +2 -2
  34. package/template/app/(app)/error.tsx +22 -6
  35. package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
  36. package/template/app/(app)/examples/page.tsx +1 -0
  37. package/template/app/(app)/layout.tsx +13 -6
  38. package/template/app/(app)/loading.tsx +1 -18
  39. package/template/app/(app)/question-bank/find/page.tsx +2 -1
  40. package/template/app/(app)/question-bank/library/page.tsx +2 -1
  41. package/template/app/(app)/question-bank/list/page.tsx +2 -1
  42. package/template/app/(app)/question-bank/new/page.tsx +15 -23
  43. package/template/app/(app)/question-bank/page.tsx +2 -1
  44. package/template/app/(app)/settings/page.tsx +4 -5
  45. package/template/app/global-error.tsx +63 -0
  46. package/template/app/globals.css +7 -1934
  47. package/template/app/layout.tsx +2 -0
  48. package/template/components/app-route-loading.tsx +14 -0
  49. package/template/components/app-sidebar.tsx +71 -55
  50. package/template/components/data-table/index.tsx +31 -67
  51. package/template/components/data-table/use-table-state.ts +33 -6
  52. package/template/components/data-views/index.ts +37 -9
  53. package/template/components/data-views/list-page-calendar-view.tsx +593 -0
  54. package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
  55. package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
  56. package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
  57. package/template/components/dev-chunk-load-recovery.tsx +41 -0
  58. package/template/components/examples/focused-workflow-showcase.tsx +183 -0
  59. package/template/components/exxat-product-logo.tsx +2 -6
  60. package/template/components/key-metrics.tsx +54 -22
  61. package/template/components/list-hub-board-view.tsx +68 -0
  62. package/template/components/list-hub-client.tsx +186 -0
  63. package/template/components/list-hub-list-view.tsx +36 -0
  64. package/template/components/list-hub-panel-activator.tsx +8 -0
  65. package/template/components/list-hub-secondary-nav.tsx +121 -0
  66. package/template/components/list-hub-table.tsx +336 -0
  67. package/template/components/new-question-composer.tsx +6 -24
  68. package/template/components/product-switcher.tsx +5 -5
  69. package/template/components/product-wordmark.tsx +4 -7
  70. package/template/components/question-bank-client.tsx +4 -1
  71. package/template/components/question-bank-folder-columns-panel.tsx +104 -0
  72. package/template/components/question-bank-hub-client.tsx +2 -5
  73. package/template/components/question-bank-table.tsx +155 -509
  74. package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
  75. package/template/components/secondary-panel.tsx +4 -44
  76. package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
  77. package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
  78. package/template/components/secondary-panels/registry.tsx +15 -0
  79. package/template/components/settings-appearance-card.tsx +3 -2
  80. package/template/components/settings-client.tsx +59 -15
  81. package/template/components/settings-form-row.tsx +9 -4
  82. package/template/components/sidebar-shell.tsx +2 -1
  83. package/template/components/table-properties/drawer-button.tsx +51 -20
  84. package/template/components/table-properties/drawer.tsx +81 -17
  85. package/template/components/templates/focused-workflow-layouts.tsx +448 -0
  86. package/template/components/templates/focused-workflow-page-template.tsx +69 -0
  87. package/template/components/templates/list-page.tsx +40 -13
  88. package/template/components/templates/nested-secondary-panel-shell.tsx +3 -2
  89. package/template/components/templates/page-loading-shell.tsx +262 -0
  90. package/template/components/ui/button-group.tsx +1 -0
  91. package/template/contexts/product-context.tsx +21 -2
  92. package/template/docs/consumer-app-pattern.md +39 -0
  93. package/template/docs/data-views-pattern.md +42 -3
  94. package/template/docs/drawer-vs-dialog-pattern.md +3 -1
  95. package/template/docs/focused-workflow-page-pattern.md +84 -0
  96. package/template/docs/kpi-flat-band-pattern.md +57 -0
  97. package/template/docs/kpi-strip-max-four-pattern.md +1 -0
  98. package/template/docs/shell-surface-elevation-pattern.md +54 -0
  99. package/template/lib/chunk-load-error.ts +13 -0
  100. package/template/lib/command-menu-search-data.ts +11 -27
  101. package/template/lib/conditional-rule-match.ts +87 -22
  102. package/template/lib/data-list-display-options.ts +16 -2
  103. package/template/lib/data-list-view-registry.ts +104 -0
  104. package/template/lib/data-list-view-surface.ts +15 -1
  105. package/template/lib/data-list-view.ts +16 -1
  106. package/template/lib/data-view-dashboard-storage.ts +38 -35
  107. package/template/lib/hub-connected-view-renderers.ts +58 -0
  108. package/template/lib/list-hub-nav.ts +121 -0
  109. package/template/lib/list-hub-supported-views.ts +10 -0
  110. package/template/lib/list-page-table-properties.ts +3 -7
  111. package/template/lib/list-status-badges.ts +4 -97
  112. package/template/lib/mock/list-hub-directory.ts +27 -0
  113. package/template/lib/mock/list-hub-kpi.ts +27 -0
  114. package/template/lib/mock/navigation.tsx +1 -0
  115. package/template/lib/page-loading-variant.ts +40 -0
  116. package/template/lib/question-bank-supported-views.ts +13 -0
  117. package/template/lib/sidebar-state-cookie.ts +9 -0
  118. package/template/lib/table-state-lifecycle.ts +60 -13
  119. package/template/app/(app)/data-list/[id]/page.tsx +0 -44
  120. package/template/app/(app)/data-list/new/page.tsx +0 -34
  121. package/template/components/compliance-board-view.tsx +0 -142
  122. package/template/components/compliance-client.tsx +0 -92
  123. package/template/components/compliance-list-view.tsx +0 -54
  124. package/template/components/compliance-page-header.tsx +0 -89
  125. package/template/components/compliance-table.tsx +0 -632
  126. package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
  127. package/template/components/data-view-dashboard-charts-team.tsx +0 -971
  128. package/template/components/data-view-dashboard-charts.tsx +0 -1503
  129. package/template/components/new-placement-back-btn.tsx +0 -28
  130. package/template/components/new-placement-form.tsx +0 -1068
  131. package/template/components/placement-board-card.tsx +0 -262
  132. package/template/components/placement-detail.tsx +0 -438
  133. package/template/components/placements-board-view.tsx +0 -404
  134. package/template/components/placements-client.tsx +0 -252
  135. package/template/components/placements-list-view.tsx +0 -171
  136. package/template/components/placements-page-header.tsx +0 -166
  137. package/template/components/placements-table-cells.test.tsx +0 -22
  138. package/template/components/placements-table-cells.tsx +0 -173
  139. package/template/components/placements-table-columns.tsx +0 -640
  140. package/template/components/placements-table.tsx +0 -1675
  141. package/template/components/rotations-empty-state.tsx +0 -50
  142. package/template/components/rotations-panel-activator.tsx +0 -8
  143. package/template/components/sites-all-client.tsx +0 -154
  144. package/template/components/sites-board-view.tsx +0 -67
  145. package/template/components/sites-list-view.tsx +0 -42
  146. package/template/components/sites-table.tsx +0 -402
  147. package/template/components/team-board-view.tsx +0 -122
  148. package/template/components/team-client.tsx +0 -100
  149. package/template/components/team-list-view.tsx +0 -59
  150. package/template/components/team-page-header.tsx +0 -92
  151. package/template/components/team-table.tsx +0 -714
  152. package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
  153. package/template/lib/mock/compliance-kpi.ts +0 -61
  154. package/template/lib/mock/compliance.ts +0 -146
  155. package/template/lib/mock/placements-kpi.ts +0 -134
  156. package/template/lib/mock/placements.ts +0 -183
  157. package/template/lib/mock/sites-directory.ts +0 -16
  158. package/template/lib/mock/sites-kpi.ts +0 -25
  159. package/template/lib/mock/team-kpi.ts +0 -60
  160. package/template/lib/mock/team.ts +0 -118
  161. package/template/lib/placement-board-card-layout.ts +0 -79
  162. package/template/lib/placement-lifecycle.ts +0 -5
@@ -216,6 +216,41 @@ export interface KeyMetricsProps {
216
216
  * (3fr / 2fr split). Tighter breakpoints because available width is ~60%
217
217
  * of the section.
218
218
  */
219
+ /**
220
+ * Flat KPI hairlines — cell borders only (no grid gap fill / no surface).
221
+ * Four tiles: default 4-across verticals; 2×2 hairlines only when @container is narrow.
222
+ */
223
+ function flatMetricsHairlineClass(
224
+ itemCount: number,
225
+ metricsHalfWidthLayout: boolean,
226
+ ): string {
227
+ if (itemCount <= 1) return "gap-0"
228
+
229
+ const childBorder = "[&>*]:border-[color:var(--key-metrics-flat-divider)]"
230
+
231
+ if (itemCount === 2) {
232
+ return cn("gap-0", childBorder, "[&>*:first-child]:border-r")
233
+ }
234
+
235
+ if (itemCount === 4) {
236
+ const narrow2x2 = metricsHalfWidthLayout
237
+ ? "@[max-width:25.99rem]"
238
+ : "@[max-width:29.99rem]"
239
+ return cn(
240
+ "gap-0",
241
+ childBorder,
242
+ /* Wide strip (matches `@[30rem]:grid-cols-4`) — verticals between all tiles, no horizontal */
243
+ "[&>*:not(:last-child)]:border-r",
244
+ /* Narrow strip (`@[18rem]`–`@[30rem]` 2×2) */
245
+ `${narrow2x2}:[&>*:not(:last-child)]:border-r-0`,
246
+ `${narrow2x2}:[&>*:nth-child(odd)]:border-r`,
247
+ `${narrow2x2}:[&>*:not(:nth-last-child(-n+2))]:border-b`,
248
+ )
249
+ }
250
+
251
+ return cn("gap-0", childBorder, "[&>*:not(:last-child)]:border-r")
252
+ }
253
+
219
254
  function metricsRowColumnsClass(rowLength: number, metricsHalfWidthLayout: boolean): string {
220
255
  const half = metricsHalfWidthLayout
221
256
  switch (rowLength) {
@@ -538,7 +573,7 @@ function KeyMetricsInner({
538
573
  }: InnerProps) {
539
574
  const isFlatBand = surfaceVariant === "flat"
540
575
  const metricsGridClassName = isFlatBand
541
- ? "gap-0 bg-transparent [&>*:not(:last-child)]:border-r [&>*:not(:last-child)]:border-foreground/[0.055]"
576
+ ? flatMetricsHairlineClass(metrics.length, metricsHalfWidthLayout)
542
577
  : "gap-px bg-border"
543
578
  /** Side-by-side KPI + insight rail (md+). Disabled for half-width dashboard cards — insight stacks below. */
544
579
  const insightSideBySide = insight && !insightFullWidth && !metricsHalfWidthLayout
@@ -642,17 +677,16 @@ function KeyMetricsInner({
642
677
  <div className="@container/metrics-strip hidden lg:block">
643
678
  {rows.map((row, rowIdx) => (
644
679
  <React.Fragment key={rowIdx}>
645
- {rowIdx > 0 && (
646
- <Separator
647
- aria-hidden="true"
648
- className={cn("my-1", isFlatBand && "bg-foreground/[0.055]")}
649
- />
680
+ {rowIdx > 0 && !isFlatBand && (
681
+ <Separator aria-hidden="true" className="my-1" />
650
682
  )}
651
683
  <div
652
684
  className={cn(
653
685
  "grid",
654
686
  metricsRowColumnsClass(row.length, metricsHalfWidthLayout),
655
- metricsGridClassName,
687
+ isFlatBand
688
+ ? flatMetricsHairlineClass(row.length, metricsHalfWidthLayout)
689
+ : metricsGridClassName,
656
690
  )}
657
691
  >
658
692
  {row.map((m) => (
@@ -693,8 +727,9 @@ function KeyMetricsInner({
693
727
  insightSideBySide &&
694
728
  !insightFullWidth &&
695
729
  cn(
696
- "lg:h-full lg:border-l lg:pl-6",
697
- isFlatBand ? "lg:border-border/40" : "lg:border-border",
730
+ "lg:h-full lg:pl-6",
731
+ /* Flat band: insight card ring is the divider — skip `border-l` (double line). */
732
+ !isFlatBand && "lg:border-l lg:border-border",
698
733
  )
699
734
  )}
700
735
  >
@@ -856,7 +891,9 @@ export function KeyMetrics({
856
891
  })()
857
892
 
858
893
  const metricsCellSurfaceClassName =
859
- variant === "flat" ? "bg-transparent" : "bg-card"
894
+ variant === "flat"
895
+ ? "bg-transparent"
896
+ : "bg-card dark:bg-transparent"
860
897
 
861
898
  const innerProps: InnerProps = {
862
899
  title,
@@ -900,18 +937,13 @@ export function KeyMetrics({
900
937
  * ─────────────────────────────────────────────────────────────────────────
901
938
  */
902
939
  const glowStyle: React.CSSProperties = {
903
- /* oklch relative color: inherit brand hue/chroma/lightness, set alpha only */
904
- background:
905
- "radial-gradient(ellipse 110% 90% at 50% 100%, oklch(from var(--brand-color) l c h / 0.13) 0%, transparent 65%)",
940
+ background: "var(--key-metrics-card-glow-radial)",
906
941
  }
907
942
 
908
- /** List-page KPI band: soft tint page bg + gentle lift (avoids a hard line into the toolbar). */
943
+ /** List-page KPI band transparent; only `--key-metrics-flat-band-radial` glow. */
909
944
  const flatBandStyle: React.CSSProperties = {
910
- background: [
911
- "radial-gradient(ellipse 118% 70% at 50% 100%, oklch(from var(--brand-color) l c h / 0.11) 0%, transparent 60%)",
912
- "linear-gradient(180deg, var(--key-metrics-flat-grad-top) 0%, var(--key-metrics-flat-grad-mid) 48%, var(--background) 100%)",
913
- ].join(", "),
914
- boxShadow: "0 18px 42px -26px color-mix(in oklch, var(--brand-color) 10%, transparent)",
945
+ background: "var(--key-metrics-flat-band-radial)",
946
+ boxShadow: "var(--key-metrics-flat-band-shadow)",
915
947
  }
916
948
 
917
949
  /* ── Card variant — ChartCard-style chrome ───────────────────────────── */
@@ -982,11 +1014,11 @@ export function KeyMetrics({
982
1014
  )
983
1015
  }
984
1016
 
985
- /* ── Flat variant — soft tint band + bottom glow (no sharp cut to content below) ── */
1017
+ /* ── Flat variant — no surface; bottom brand glow only ── */
986
1018
  return (
987
1019
  <section
988
1020
  aria-label={title}
989
- className={cn("relative w-full overflow-hidden pt-5 pb-6", className)}
1021
+ className={cn("relative w-full overflow-hidden pt-5 pb-8", className)}
990
1022
  style={flatBandStyle}
991
1023
  >
992
1024
  <KeyMetricsInner
@@ -1025,7 +1057,7 @@ export function KeyMetricsContent({
1025
1057
  showHeader={false}
1026
1058
  insightCompact={insightCompact}
1027
1059
  insightFullWidth={insightFullWidth}
1028
- metricsCellSurfaceClassName="bg-card"
1060
+ metricsCellSurfaceClassName="bg-card dark:bg-transparent"
1029
1061
  />
1030
1062
  )
1031
1063
  }
@@ -0,0 +1,68 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import type { ListHubRecord } from "@/lib/mock/list-hub-directory"
5
+ import { formatDateUS } from "@/lib/date-filter"
6
+ import { BoardCardIconRow, BoardCardTwoLineBlock } from "@/components/data-views/board-card-primitives"
7
+ import {
8
+ HubRecordCard,
9
+ ListPageBoardCardBody,
10
+ ListPageBoardCardHeader,
11
+ ListPageBoardCardTitleRow,
12
+ } from "@/components/data-views/list-page-board-card"
13
+ import {
14
+ ListPageBoardTemplate,
15
+ type ListPageBoardColumnDef,
16
+ } from "@/components/data-views/list-page-board-template"
17
+
18
+ const CATEGORY_COUNT_BADGE = "bg-muted/90 text-foreground"
19
+
20
+ function ListHubBoardCard({ row }: { row: ListHubRecord }) {
21
+ return (
22
+ <HubRecordCard interactive className="h-full w-full">
23
+ <ListPageBoardCardHeader>
24
+ <ListPageBoardCardTitleRow title={row.title} titleClassName="truncate" />
25
+ <ListPageBoardCardBody>
26
+ <BoardCardTwoLineBlock
27
+ iconClass="fa-tag"
28
+ line1={row.category}
29
+ line2={formatDateUS(row.eventDate)}
30
+ />
31
+ <BoardCardIconRow iconClass="fa-hashtag">
32
+ <span className="font-mono tabular-nums">{row.id}</span>
33
+ </BoardCardIconRow>
34
+ </ListPageBoardCardBody>
35
+ </ListPageBoardCardHeader>
36
+ </HubRecordCard>
37
+ )
38
+ }
39
+
40
+ function useListHubBoardColumns(rows: ListHubRecord[]) {
41
+ return React.useMemo(() => {
42
+ const categories = [...new Set(rows.map(r => r.category))].sort((a, b) => a.localeCompare(b))
43
+ const columns: ListPageBoardColumnDef<ListHubRecord>[] = categories.map(category => ({
44
+ id: category,
45
+ label: category,
46
+ filter: (r: ListHubRecord) => r.category === category,
47
+ }))
48
+ const badgeMap = Object.fromEntries(
49
+ categories.map(c => [c, CATEGORY_COUNT_BADGE]),
50
+ )
51
+ return { columns, badgeMap }
52
+ }, [rows])
53
+ }
54
+
55
+ export function ListHubCardGrid({ rows }: { rows: ListHubRecord[] }) {
56
+ const { columns, badgeMap } = useListHubBoardColumns(rows)
57
+
58
+ return (
59
+ <ListPageBoardTemplate
60
+ columns={columns}
61
+ rows={rows}
62
+ getRowKey={r => r.id}
63
+ columnCountBadgeClassName={badgeMap}
64
+ emptyColumnLabel="No records"
65
+ renderCard={row => <ListHubBoardCard row={row} />}
66
+ />
67
+ )
68
+ }
@@ -0,0 +1,186 @@
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 { ListHubPanelActivator } from "@/components/list-hub-panel-activator"
7
+ import { PageHeader } from "@/components/page-header"
8
+ import { Button } from "@/components/ui/button"
9
+ import { Tip } from "@/components/ui/tip"
10
+ import {
11
+ DropdownMenu,
12
+ DropdownMenuContent,
13
+ DropdownMenuItem,
14
+ DropdownMenuSeparator,
15
+ DropdownMenuTrigger,
16
+ } from "@/components/ui/dropdown-menu"
17
+ import { KeyMetrics } from "@/components/key-metrics"
18
+ import { ListHubTable, type ListHubTableHandle } from "@/components/list-hub-table"
19
+ import { useAskLeoPageContext } from "@/components/ask-leo-sidebar"
20
+ import { useSecondaryPanelHubNav } from "@/hooks/use-secondary-panel-hub-nav"
21
+ import { LIST_HUB_DIRECTORY } from "@/lib/mock/list-hub-directory"
22
+ import { LIST_HUB_KPI_INSIGHT, listHubKpiMetrics } from "@/lib/mock/list-hub-kpi"
23
+ import { LIST_HUB_SUPPORTED_VIEWS } from "@/lib/list-hub-supported-views"
24
+ import {
25
+ filterListHubRows,
26
+ isListHubDefaultNav,
27
+ listHubHeaderSubtitle,
28
+ listHubScopeLabel,
29
+ LIST_HUB_PATH,
30
+ parseListHubNav,
31
+ } from "@/lib/list-hub-nav"
32
+
33
+ const DEFAULT_TABS: ViewTab[] = [
34
+ { id: "table", label: "Directory", viewType: "table", icon: "fa-table", filterId: "all" },
35
+ { id: "calendar", label: "Schedule", viewType: "calendar", icon: "fa-calendar-days", filterId: "schedule" },
36
+ { id: "board", label: "Board", viewType: "board", icon: "fa-grid-2", filterId: "board" },
37
+ ]
38
+
39
+ function ListHubPageHeader({
40
+ title,
41
+ subtitle,
42
+ onExport,
43
+ showMetrics,
44
+ onToggleMetrics,
45
+ }: {
46
+ title: string
47
+ subtitle: string
48
+ onExport: () => void
49
+ showMetrics: boolean
50
+ onToggleMetrics: () => void
51
+ }) {
52
+ const [moreOpen, setMoreOpen] = React.useState(false)
53
+ return (
54
+ <PageHeader
55
+ title={title}
56
+ subtitle={subtitle}
57
+ actions={
58
+ <div className="flex items-center gap-2" role="group" aria-label="List hub actions">
59
+ <DropdownMenu open={moreOpen} onOpenChange={setMoreOpen}>
60
+ <Tip side="bottom" label="More actions">
61
+ <DropdownMenuTrigger asChild>
62
+ <Button
63
+ type="button"
64
+ size="lg"
65
+ variant="outline"
66
+ className="aspect-square px-0"
67
+ aria-label="More actions"
68
+ >
69
+ <i className="fa-light fa-ellipsis text-base" aria-hidden="true" />
70
+ </Button>
71
+ </DropdownMenuTrigger>
72
+ </Tip>
73
+ <DropdownMenuContent align="end">
74
+ <DropdownMenuItem
75
+ onSelect={() => {
76
+ window.setTimeout(() => onExport(), 0)
77
+ }}
78
+ >
79
+ <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
80
+ Export
81
+ </DropdownMenuItem>
82
+ <DropdownMenuSeparator />
83
+ <DropdownMenuItem
84
+ onSelect={() => {
85
+ window.setTimeout(() => onToggleMetrics(), 0)
86
+ }}
87
+ >
88
+ <i
89
+ className={`fa-light ${showMetrics ? "fa-eye-slash" : "fa-eye"}`}
90
+ aria-hidden="true"
91
+ />
92
+ {showMetrics ? "Hide metric section" : "Show metric section"}
93
+ </DropdownMenuItem>
94
+ </DropdownMenuContent>
95
+ </DropdownMenu>
96
+ </div>
97
+ }
98
+ />
99
+ )
100
+ }
101
+
102
+ export function ListHubClient() {
103
+ const [exportOpen, setExportOpen] = React.useState(false)
104
+ const [showMetrics, setShowMetrics] = React.useState(true)
105
+ const [tabs, setTabs] = React.useState<ViewTab[]>(DEFAULT_TABS)
106
+ const [activeTabId, setActiveTabId] = React.useState(DEFAULT_TABS[0]!.id)
107
+ const tableRef = React.useRef<ListHubTableHandle>(null)
108
+
109
+ const { navState } = useSecondaryPanelHubNav({
110
+ hubPathname: LIST_HUB_PATH,
111
+ panelId: "list-hub",
112
+ parseNav: parseListHubNav,
113
+ shouldReopenPanel: nav => isListHubDefaultNav(nav),
114
+ })
115
+
116
+ const scopedRows = React.useMemo(
117
+ () => filterListHubRows(LIST_HUB_DIRECTORY, navState),
118
+ [navState],
119
+ )
120
+ const count = scopedRows.length
121
+ const metrics = React.useMemo(() => listHubKpiMetrics(count), [count])
122
+ const pageTitle = listHubScopeLabel(navState)
123
+ const subtitle = listHubHeaderSubtitle(navState, count)
124
+
125
+ useAskLeoPageContext(
126
+ React.useMemo(
127
+ () => ({
128
+ title: pageTitle,
129
+ description: `${count} records in the active scope — table, calendar, and board read the same filtered rows.`,
130
+ suggestions: [
131
+ "Which events are scheduled this week?",
132
+ "How do I show or hide the calendar summary panel?",
133
+ ],
134
+ }),
135
+ [count, pageTitle],
136
+ ),
137
+ )
138
+
139
+ return (
140
+ <>
141
+ <ListHubPanelActivator />
142
+ <ListPageTemplate
143
+ defaultTabs={DEFAULT_TABS}
144
+ tabs={tabs}
145
+ onTabsChange={setTabs}
146
+ activeTabId={activeTabId}
147
+ onActiveTabChange={setActiveTabId}
148
+ getTabCount={() => count}
149
+ showMetrics={showMetrics}
150
+ supportedViewTypes={LIST_HUB_SUPPORTED_VIEWS}
151
+ tablePropertiesRef={tableRef}
152
+ metrics={
153
+ <KeyMetrics
154
+ variant="flat"
155
+ metrics={metrics}
156
+ insight={LIST_HUB_KPI_INSIGHT}
157
+ showHeader={false}
158
+ metricsSingleRow
159
+ />
160
+ }
161
+ exportOpen={exportOpen}
162
+ onExportOpenChange={setExportOpen}
163
+ exportTotalRows={count}
164
+ header={
165
+ <ListHubPageHeader
166
+ title={pageTitle}
167
+ subtitle={subtitle}
168
+ onExport={() => setExportOpen(true)}
169
+ showMetrics={showMetrics}
170
+ onToggleMetrics={() => setShowMetrics(v => !v)}
171
+ />
172
+ }
173
+ renderContent={(tab, updateTab) => (
174
+ <ListHubTable
175
+ key={`${tab.id}-${navState.scope}-${navState.category ?? ""}`}
176
+ ref={tableRef}
177
+ rows={scopedRows}
178
+ view={tab.viewType}
179
+ supportedViewTypes={LIST_HUB_SUPPORTED_VIEWS}
180
+ onViewChange={(v: DataListViewType) => updateTab({ viewType: v, icon: dataListViewIcon(v) })}
181
+ />
182
+ )}
183
+ />
184
+ </>
185
+ )
186
+ }
@@ -0,0 +1,36 @@
1
+ "use client"
2
+
3
+ import type { ListHubRecord } from "@/lib/mock/list-hub-directory"
4
+ import { formatDateUS } from "@/lib/date-filter"
5
+ import { ListPageBoardCard } from "@/components/data-views/list-page-board-card"
6
+ import { DataRowList } from "@/components/data-views/data-row-list"
7
+
8
+ export function ListHubListView({ rows }: { rows: ListHubRecord[] }) {
9
+ return (
10
+ <DataRowList<ListHubRecord>
11
+ rows={rows}
12
+ getRowId={row => row.id}
13
+ emptyState="No records match your filters."
14
+ ariaLabel="List hub records"
15
+ renderRow={row => (
16
+ <ListPageBoardCard
17
+ layout="row"
18
+ interactive
19
+ rowContainerClassName="flex flex-row items-center gap-3"
20
+ leading={
21
+ <span className="inline-flex size-9 shrink-0 items-center justify-center rounded-md bg-brand/10 text-brand">
22
+ <i className="fa-light fa-calendar-days text-sm" aria-hidden="true" />
23
+ </span>
24
+ }
25
+ >
26
+ <div className="space-y-0.5">
27
+ <p className="truncate text-sm font-semibold text-foreground">{row.title}</p>
28
+ <p className="truncate text-xs text-muted-foreground">
29
+ {row.category} · <span className="tabular-nums">{formatDateUS(row.eventDate)}</span>
30
+ </p>
31
+ </div>
32
+ </ListPageBoardCard>
33
+ )}
34
+ />
35
+ )
36
+ }
@@ -0,0 +1,8 @@
1
+ "use client"
2
+
3
+ import { SecondaryPanelHubActivator } from "@/components/templates/secondary-panel-hub-template"
4
+
5
+ /** Opens the List hub secondary panel while this route is mounted. */
6
+ export function ListHubPanelActivator() {
7
+ return <SecondaryPanelHubActivator panelId="list-hub" />
8
+ }
@@ -0,0 +1,121 @@
1
+ "use client"
2
+
3
+ /**
4
+ * List hub secondary nav — time + category scopes via `?scope=` (`lib/list-hub-nav.ts`).
5
+ */
6
+
7
+ import * as React from "react"
8
+ import { usePathname, useSearchParams } from "next/navigation"
9
+ import { Button } from "@/components/ui/button"
10
+ import { Tip } from "@/components/ui/tip"
11
+ import {
12
+ SecondaryPanelIconNavRow,
13
+ SecondaryPanelNavRow,
14
+ } from "@/components/secondary-panel/nav-link-rows"
15
+ import { useSecondaryPanel } from "@/components/secondary-panel"
16
+ import {
17
+ isListHubNavActive,
18
+ listHubHubScopeHref,
19
+ LIST_HUB_CATEGORY_SCOPES,
20
+ parseListHubNav,
21
+ } from "@/lib/list-hub-nav"
22
+
23
+ export function ListHubSecondaryNav() {
24
+ const pathname = usePathname()
25
+ const searchParams = useSearchParams()
26
+ const searchParamsKey = searchParams.toString()
27
+ const { openPanel, secondaryPanelCompact } = useSecondaryPanel()
28
+
29
+ const nav = React.useMemo(
30
+ () => parseListHubNav(new URLSearchParams(searchParamsKey)),
31
+ [searchParamsKey],
32
+ )
33
+
34
+ const reopenPanel = React.useCallback(() => openPanel("list-hub"), [openPanel])
35
+
36
+ if (secondaryPanelCompact) {
37
+ return (
38
+ <nav className="flex min-h-0 flex-1 flex-col" role="navigation" aria-label="List hub">
39
+ <div className="flex flex-col items-center border-b border-sidebar-border/60 px-1 py-2">
40
+ <Tip label="Show labels" side="right">
41
+ <Button
42
+ type="button"
43
+ size="icon"
44
+ variant="ghost"
45
+ className="size-9 shrink-0"
46
+ aria-label="Show labels"
47
+ onClick={reopenPanel}
48
+ >
49
+ <i className="fa-light fa-angles-right text-[15px]" aria-hidden="true" />
50
+ </Button>
51
+ </Tip>
52
+ </div>
53
+ <ul className="flex flex-1 flex-col items-center gap-1 overflow-y-auto px-1 py-2" role="list">
54
+ <SecondaryPanelIconNavRow
55
+ href={listHubHubScopeHref(pathname, searchParams, { scope: "all" })}
56
+ active={isListHubNavActive(pathname, nav, "all")}
57
+ iconClass="fa-table-list"
58
+ label="All records"
59
+ onClick={reopenPanel}
60
+ />
61
+ <SecondaryPanelIconNavRow
62
+ href={listHubHubScopeHref(pathname, searchParams, { scope: "upcoming" })}
63
+ active={isListHubNavActive(pathname, nav, "upcoming")}
64
+ iconClass="fa-calendar-arrow-up"
65
+ label="Upcoming"
66
+ onClick={reopenPanel}
67
+ />
68
+ <SecondaryPanelIconNavRow
69
+ href={listHubHubScopeHref(pathname, searchParams, { scope: "past" })}
70
+ active={isListHubNavActive(pathname, nav, "past")}
71
+ iconClass="fa-calendar-arrow-down"
72
+ label="Past"
73
+ onClick={reopenPanel}
74
+ />
75
+ </ul>
76
+ </nav>
77
+ )
78
+ }
79
+
80
+ return (
81
+ <div className="min-h-0 flex-1 overflow-y-auto px-3 pb-4" role="navigation" aria-label="List hub">
82
+ <ul className="space-y-0.5" role="list">
83
+ <SecondaryPanelNavRow
84
+ href={listHubHubScopeHref(pathname, searchParams, { scope: "all" })}
85
+ active={isListHubNavActive(pathname, nav, "all")}
86
+ iconClass="fa-table-list"
87
+ label="All records"
88
+ onClick={reopenPanel}
89
+ />
90
+ <SecondaryPanelNavRow
91
+ href={listHubHubScopeHref(pathname, searchParams, { scope: "upcoming" })}
92
+ active={isListHubNavActive(pathname, nav, "upcoming")}
93
+ iconClass="fa-calendar-arrow-up"
94
+ label="Upcoming"
95
+ />
96
+ <SecondaryPanelNavRow
97
+ href={listHubHubScopeHref(pathname, searchParams, { scope: "past" })}
98
+ active={isListHubNavActive(pathname, nav, "past")}
99
+ iconClass="fa-calendar-arrow-down"
100
+ label="Past"
101
+ />
102
+ <li role="presentation" className="select-none">
103
+ <div className="px-2 pt-3 pb-1">
104
+ <span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground/70">
105
+ Categories
106
+ </span>
107
+ </div>
108
+ </li>
109
+ {LIST_HUB_CATEGORY_SCOPES.map(category => (
110
+ <SecondaryPanelNavRow
111
+ key={category}
112
+ href={listHubHubScopeHref(pathname, searchParams, { scope: "category", category })}
113
+ active={isListHubNavActive(pathname, nav, "category", category)}
114
+ iconClass="fa-folder"
115
+ label={category}
116
+ />
117
+ ))}
118
+ </ul>
119
+ </div>
120
+ )
121
+ }