@exxatdesignux/ui 0.2.18 → 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 (140) hide show
  1. package/CHANGELOG.md +15 -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 +21 -6
  7. package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
  8. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +4 -2
  9. package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
  10. package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
  11. package/consumer-extras/patterns/data-views-pattern.md +40 -3
  12. package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
  13. package/consumer-extras/patterns/shell-surface-elevation-pattern.md +5 -3
  14. package/package.json +2 -1
  15. package/src/components/ui/button-group.tsx +81 -0
  16. package/src/components/ui/button.tsx +4 -4
  17. package/src/globals.css +7 -1858
  18. package/src/theme.css +10 -1126
  19. package/src/tokens/README.md +15 -0
  20. package/src/tokens/base.css +337 -0
  21. package/src/tokens/high-contrast.css +1195 -0
  22. package/src/tokens/layers.css +224 -0
  23. package/src/tokens/tailwind-bridge.css +118 -0
  24. package/src/tokens/themes.css +201 -0
  25. package/template/AGENTS.md +60 -22
  26. package/template/app/(app)/dashboard/loading.tsx +3 -15
  27. package/template/app/(app)/dashboard/page.tsx +2 -14
  28. package/template/app/(app)/data-list/layout.tsx +43 -0
  29. package/template/app/(app)/data-list/page.tsx +2 -2
  30. package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
  31. package/template/app/(app)/examples/page.tsx +1 -0
  32. package/template/app/(app)/loading.tsx +1 -18
  33. package/template/app/(app)/question-bank/find/page.tsx +2 -1
  34. package/template/app/(app)/question-bank/library/page.tsx +2 -1
  35. package/template/app/(app)/question-bank/list/page.tsx +2 -1
  36. package/template/app/(app)/question-bank/new/page.tsx +15 -23
  37. package/template/app/(app)/question-bank/page.tsx +2 -1
  38. package/template/app/(app)/settings/page.tsx +4 -5
  39. package/template/app/globals.css +7 -1964
  40. package/template/components/app-route-loading.tsx +14 -0
  41. package/template/components/app-sidebar.tsx +70 -55
  42. package/template/components/data-views/index.ts +37 -9
  43. package/template/components/data-views/list-page-calendar-view.tsx +593 -0
  44. package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
  45. package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
  46. package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
  47. package/template/components/examples/focused-workflow-showcase.tsx +183 -0
  48. package/template/components/list-hub-board-view.tsx +68 -0
  49. package/template/components/list-hub-client.tsx +186 -0
  50. package/template/components/list-hub-list-view.tsx +36 -0
  51. package/template/components/list-hub-panel-activator.tsx +8 -0
  52. package/template/components/list-hub-secondary-nav.tsx +121 -0
  53. package/template/components/list-hub-table.tsx +336 -0
  54. package/template/components/new-question-composer.tsx +6 -24
  55. package/template/components/product-switcher.tsx +3 -2
  56. package/template/components/question-bank-client.tsx +4 -1
  57. package/template/components/question-bank-folder-columns-panel.tsx +104 -0
  58. package/template/components/question-bank-table.tsx +143 -485
  59. package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
  60. package/template/components/secondary-panel.tsx +4 -44
  61. package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
  62. package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
  63. package/template/components/secondary-panels/registry.tsx +15 -0
  64. package/template/components/settings-appearance-card.tsx +3 -2
  65. package/template/components/settings-client.tsx +59 -15
  66. package/template/components/settings-form-row.tsx +9 -4
  67. package/template/components/table-properties/drawer-button.tsx +13 -0
  68. package/template/components/table-properties/drawer.tsx +65 -4
  69. package/template/components/templates/focused-workflow-layouts.tsx +448 -0
  70. package/template/components/templates/focused-workflow-page-template.tsx +69 -0
  71. package/template/components/templates/list-page.tsx +29 -5
  72. package/template/components/templates/nested-secondary-panel-shell.tsx +2 -1
  73. package/template/components/templates/page-loading-shell.tsx +262 -0
  74. package/template/components/ui/button-group.tsx +1 -0
  75. package/template/docs/consumer-app-pattern.md +39 -0
  76. package/template/docs/data-views-pattern.md +40 -3
  77. package/template/docs/drawer-vs-dialog-pattern.md +3 -1
  78. package/template/docs/focused-workflow-page-pattern.md +84 -0
  79. package/template/docs/shell-surface-elevation-pattern.md +5 -3
  80. package/template/lib/command-menu-search-data.ts +11 -27
  81. package/template/lib/data-list-display-options.ts +16 -2
  82. package/template/lib/data-list-view-registry.ts +104 -0
  83. package/template/lib/data-list-view-surface.ts +15 -1
  84. package/template/lib/data-list-view.ts +10 -1
  85. package/template/lib/data-view-dashboard-storage.ts +38 -35
  86. package/template/lib/hub-connected-view-renderers.ts +58 -0
  87. package/template/lib/list-hub-nav.ts +121 -0
  88. package/template/lib/list-hub-supported-views.ts +10 -0
  89. package/template/lib/list-page-table-properties.ts +3 -7
  90. package/template/lib/list-status-badges.ts +4 -97
  91. package/template/lib/mock/list-hub-directory.ts +27 -0
  92. package/template/lib/mock/list-hub-kpi.ts +27 -0
  93. package/template/lib/mock/navigation.tsx +1 -0
  94. package/template/lib/page-loading-variant.ts +40 -0
  95. package/template/lib/question-bank-supported-views.ts +13 -0
  96. package/template/lib/table-state-lifecycle.ts +2 -2
  97. package/template/app/(app)/data-list/[id]/page.tsx +0 -44
  98. package/template/app/(app)/data-list/new/page.tsx +0 -34
  99. package/template/components/compliance-board-view.tsx +0 -142
  100. package/template/components/compliance-client.tsx +0 -92
  101. package/template/components/compliance-list-view.tsx +0 -54
  102. package/template/components/compliance-page-header.tsx +0 -89
  103. package/template/components/compliance-table.tsx +0 -612
  104. package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
  105. package/template/components/data-view-dashboard-charts-team.tsx +0 -971
  106. package/template/components/data-view-dashboard-charts.tsx +0 -1503
  107. package/template/components/new-placement-back-btn.tsx +0 -28
  108. package/template/components/new-placement-form.tsx +0 -1068
  109. package/template/components/placement-board-card.tsx +0 -262
  110. package/template/components/placement-detail.tsx +0 -438
  111. package/template/components/placements-board-view.tsx +0 -404
  112. package/template/components/placements-client.tsx +0 -252
  113. package/template/components/placements-list-view.tsx +0 -171
  114. package/template/components/placements-page-header.tsx +0 -166
  115. package/template/components/placements-table-cells.test.tsx +0 -22
  116. package/template/components/placements-table-cells.tsx +0 -173
  117. package/template/components/placements-table-columns.tsx +0 -640
  118. package/template/components/placements-table.tsx +0 -1642
  119. package/template/components/rotations-empty-state.tsx +0 -50
  120. package/template/components/rotations-panel-activator.tsx +0 -8
  121. package/template/components/sites-all-client.tsx +0 -154
  122. package/template/components/sites-board-view.tsx +0 -67
  123. package/template/components/sites-list-view.tsx +0 -42
  124. package/template/components/sites-table.tsx +0 -382
  125. package/template/components/team-board-view.tsx +0 -122
  126. package/template/components/team-client.tsx +0 -100
  127. package/template/components/team-list-view.tsx +0 -59
  128. package/template/components/team-page-header.tsx +0 -92
  129. package/template/components/team-table.tsx +0 -693
  130. package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
  131. package/template/lib/mock/compliance-kpi.ts +0 -61
  132. package/template/lib/mock/compliance.ts +0 -146
  133. package/template/lib/mock/placements-kpi.ts +0 -134
  134. package/template/lib/mock/placements.ts +0 -183
  135. package/template/lib/mock/sites-directory.ts +0 -16
  136. package/template/lib/mock/sites-kpi.ts +0 -25
  137. package/template/lib/mock/team-kpi.ts +0 -60
  138. package/template/lib/mock/team.ts +0 -118
  139. package/template/lib/placement-board-card-layout.ts +0 -79
  140. package/template/lib/placement-lifecycle.ts +0 -5
@@ -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
+ }
@@ -0,0 +1,336 @@
1
+ "use client"
2
+
3
+ /**
4
+ * List hub table — `DataTable` + `useTableState`; table | list | board | calendar | dashboard
5
+ * share `tableState.rows` (centralized dataset).
6
+ */
7
+
8
+ import * as React from "react"
9
+ import type { ListHubRecord } from "@/lib/mock/list-hub-directory"
10
+ import { DataTable, DataTableToolbar } from "@/components/data-table"
11
+ import { useTableState } from "@/components/data-table/use-table-state"
12
+ import type { ColumnDef } from "@/components/data-table/types"
13
+ import type { DataListViewType } from "@/lib/data-list-view"
14
+ import type { OpenTablePropertiesHandle } from "@/lib/list-page-table-properties"
15
+ import { LIST_HUB_SUPPORTED_VIEWS } from "@/lib/list-hub-supported-views"
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 { Tip } from "@/components/ui/tip"
20
+ import { FinderPanelView, type FinderGroup } from "@/components/data-views/finder-panel-view"
21
+ import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
22
+ import { ListPageConnectedViewBody } from "@/components/data-views/list-page-connected-view-body"
23
+ import { defineHubViewRenderers } from "@/lib/hub-connected-view-renderers"
24
+ import { ListPageCalendarView } from "@/components/data-views/list-page-calendar-view"
25
+ import { ListHubCardGrid } from "@/components/list-hub-board-view"
26
+ import { ListHubListView } from "@/components/list-hub-list-view"
27
+ import {
28
+ DEFAULT_DATA_LIST_DISPLAY_OPTIONS,
29
+ type DataListDisplayOptions,
30
+ } from "@/lib/data-list-display-options"
31
+
32
+ function columnToFilterFieldDef(c: ColumnDef<ListHubRecord>): FilterFieldDef | null {
33
+ if (!c.filter) return null
34
+ const f = c.filter
35
+ const defaultOps: FilterOperator[] =
36
+ f.type === "select" || f.type === "date" ? ["is", "is_not"] : ["contains", "not_contains"]
37
+ return {
38
+ key: c.key,
39
+ label: c.label,
40
+ icon: f.icon ?? "fa-filter",
41
+ type: f.type,
42
+ operators: (f.operators ?? defaultOps) as FilterOperator[],
43
+ options: f.options,
44
+ ...(f.textMask ? { textMask: f.textMask } : {}),
45
+ }
46
+ }
47
+
48
+ function columnsToFilterFields(cols: ColumnDef<ListHubRecord>[]) {
49
+ return cols.map(columnToFilterFieldDef).filter((x): x is FilterFieldDef => x !== null)
50
+ }
51
+
52
+ function buildListHubColumns(): ColumnDef<ListHubRecord>[] {
53
+ return [
54
+ {
55
+ key: "select",
56
+ label: "",
57
+ width: 40,
58
+ minWidth: 40,
59
+ defaultPin: "left",
60
+ lockPin: true,
61
+ },
62
+ {
63
+ key: "title",
64
+ label: "Title",
65
+ width: 280,
66
+ minWidth: 160,
67
+ sortable: true,
68
+ sortKey: "title",
69
+ filter: {
70
+ type: "text",
71
+ icon: "fa-file-lines",
72
+ operators: ["contains", "not_contains"],
73
+ },
74
+ cell: row => <span className="truncate text-sm font-medium text-foreground">{row.title}</span>,
75
+ },
76
+ {
77
+ key: "id",
78
+ label: "ID",
79
+ width: 120,
80
+ minWidth: 100,
81
+ sortable: true,
82
+ sortKey: "id",
83
+ filter: {
84
+ type: "text",
85
+ icon: "fa-hashtag",
86
+ operators: ["contains", "not_contains"],
87
+ },
88
+ cell: row => <span className="font-mono text-sm tabular-nums text-foreground/90">{row.id}</span>,
89
+ },
90
+ {
91
+ key: "eventDate",
92
+ label: "Event date",
93
+ width: 140,
94
+ minWidth: 120,
95
+ sortable: true,
96
+ sortKey: "eventDate",
97
+ filter: {
98
+ type: "date",
99
+ icon: "fa-calendar-days",
100
+ operators: ["is", "is_not"],
101
+ },
102
+ cell: row => <span className="text-sm tabular-nums text-foreground">{row.eventDate}</span>,
103
+ },
104
+ {
105
+ key: "category",
106
+ label: "Category",
107
+ width: 140,
108
+ minWidth: 100,
109
+ sortable: true,
110
+ sortKey: "category",
111
+ filter: {
112
+ type: "text",
113
+ icon: "fa-tag",
114
+ operators: ["contains", "not_contains"],
115
+ },
116
+ cell: row => <span className="text-sm text-muted-foreground">{row.category}</span>,
117
+ },
118
+ {
119
+ key: "actions",
120
+ label: "",
121
+ width: 48,
122
+ minWidth: 48,
123
+ defaultPin: "right",
124
+ lockPin: true,
125
+ cell: () => null,
126
+ },
127
+ ]
128
+ }
129
+
130
+ export type ListHubTableHandle = OpenTablePropertiesHandle
131
+
132
+ export const ListHubTable = React.forwardRef<
133
+ ListHubTableHandle,
134
+ {
135
+ rows: ListHubRecord[]
136
+ view?: DataListViewType
137
+ onViewChange?: (v: DataListViewType) => void
138
+ /** Aligns Properties view tiles with `ListPageTemplate` `supportedViewTypes`. */
139
+ supportedViewTypes?: readonly DataListViewType[]
140
+ }
141
+ >(function ListHubTable(
142
+ { rows, view = "board", onViewChange, supportedViewTypes = LIST_HUB_SUPPORTED_VIEWS },
143
+ ref,
144
+ ) {
145
+ const columns = React.useMemo(() => buildListHubColumns(), [])
146
+ const filterFields = React.useMemo(() => columnsToFilterFields(columns), [columns])
147
+ const fieldDefinitionsForDrawer = React.useMemo(
148
+ () =>
149
+ columns
150
+ .filter(c => c.key !== "select" && c.key !== "actions")
151
+ .map(c => ({ key: c.key, label: c.label, sortable: !!(c.sortable && (c.sortKey ?? c.key)) })),
152
+ [columns],
153
+ )
154
+
155
+ const resolveColumnLabel = React.useCallback(
156
+ (key: string) => columns.find(c => c.key === key)?.label ?? key,
157
+ [columns],
158
+ )
159
+
160
+ const [displayOptions, setDisplayOptions] = React.useState<DataListDisplayOptions>(
161
+ DEFAULT_DATA_LIST_DISPLAY_OPTIONS,
162
+ )
163
+ const patchDisplay = React.useCallback((patch: Partial<DataListDisplayOptions>) => {
164
+ setDisplayOptions(prev => ({ ...prev, ...patch }))
165
+ }, [])
166
+
167
+ const [conditionalRules, setConditionalRules] = React.useState<ConditionalRule[]>([])
168
+
169
+ const addConditionalRule = React.useCallback((rule: Omit<ConditionalRule, "id">) => {
170
+ setConditionalRules(prev => [...prev, { ...rule, id: `cr-${Date.now()}` }])
171
+ }, [])
172
+ const removeConditionalRule = React.useCallback((id: string) => {
173
+ setConditionalRules(prev => prev.filter(r => r.id !== id))
174
+ }, [])
175
+ const updateConditionalRule = React.useCallback((id: string, patch: Partial<ConditionalRule>) => {
176
+ setConditionalRules(prev => prev.map(r => (r.id === id ? { ...r, ...patch } : r)))
177
+ }, [])
178
+
179
+ const tableState = useTableState<ListHubRecord>(rows, columns, { key: "title", dir: "asc" })
180
+
181
+ React.useImperativeHandle(
182
+ ref,
183
+ () => ({
184
+ openPropertiesDrawer: () => {
185
+ tableState.setSheetOpen(true)
186
+ },
187
+ }),
188
+ [tableState.setSheetOpen],
189
+ )
190
+
191
+ const panelGroupsBuilder = (filtered: ListHubRecord[]): FinderGroup[] => [
192
+ { id: "all", label: `All records (${filtered.length})`, count: filtered.length },
193
+ ]
194
+
195
+ const panelRenderListRow = (row: ListHubRecord) => (
196
+ <div className="min-w-0 flex-1">
197
+ <p className="truncate text-sm font-medium text-foreground">{row.title}</p>
198
+ <p className="truncate text-xs text-muted-foreground">{row.category}</p>
199
+ </div>
200
+ )
201
+
202
+ const panelRenderDetail = (row: ListHubRecord) => (
203
+ <div className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-4">
204
+ <div>
205
+ <h3 className="mb-1 text-sm font-semibold text-foreground">{row.title}</h3>
206
+ <p className="text-xs text-muted-foreground">{row.category}</p>
207
+ </div>
208
+ <dl className="grid gap-2 text-sm">
209
+ <div>
210
+ <dt className="text-xs font-medium text-muted-foreground">ID</dt>
211
+ <dd className="font-mono tabular-nums">{row.id}</dd>
212
+ </div>
213
+ <div>
214
+ <dt className="text-xs font-medium text-muted-foreground">Event date</dt>
215
+ <dd className="tabular-nums">{row.eventDate}</dd>
216
+ </div>
217
+ </dl>
218
+ </div>
219
+ )
220
+
221
+ const drawerToolbarProps = {
222
+ state: tableState,
223
+ totalRows: rows.length,
224
+ filterFields,
225
+ fieldDefinitions: fieldDefinitionsForDrawer,
226
+ resolveColumnLabel,
227
+ displayOptions,
228
+ onDisplayOptionsChange: patchDisplay,
229
+ conditionalRules,
230
+ onAddConditionalRule: addConditionalRule,
231
+ onRemoveConditionalRule: removeConditionalRule,
232
+ onUpdateConditionalRule: updateConditionalRule,
233
+ currentView: view,
234
+ onViewChange,
235
+ supportedViewTypes,
236
+ lifecycleTabLabel: "List hub",
237
+ }
238
+
239
+ const tableProps = {
240
+ data: rows,
241
+ columns,
242
+ getRowId: (row: ListHubRecord) => row.id,
243
+ getRowSelectionLabel: (row: ListHubRecord) => row.title,
244
+ selectable: true,
245
+ searchable: displayOptions.showToolbarSearch,
246
+ showColumnHeaders: displayOptions.showColumnLabels,
247
+ groupable: true,
248
+ defaultSort: { key: "title", dir: "asc" as const },
249
+ emptyState: <p className="text-sm text-muted-foreground">No records match your filters.</p>,
250
+ conditionalRules,
251
+ state: tableState,
252
+ toolbarSlot: (s: ReturnType<typeof useTableState<ListHubRecord>>) => (
253
+ <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />
254
+ ),
255
+ bulkActionsSlot: (selected: Set<string | number>) => {
256
+ const n = selected.size
257
+ if (n === 0) return null
258
+ return (
259
+ <>
260
+ <span className="sr-only">{n} selected</span>
261
+ <Tip label="Export selection (demo)">
262
+ <Button size="sm" variant="outline" type="button">
263
+ <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
264
+ Export
265
+ </Button>
266
+ </Tip>
267
+ </>
268
+ )
269
+ },
270
+ }
271
+
272
+ const sharedToolbar = (
273
+ <DataTableToolbar
274
+ state={tableState}
275
+ columns={columns}
276
+ searchable={displayOptions.showToolbarSearch}
277
+ searchAriaLabel="Search records"
278
+ toolbarSlot={s => <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />}
279
+ />
280
+ )
281
+
282
+ const toolbarShell = (body: React.ReactNode) => (
283
+ <div className="flex min-h-0 flex-1 flex-col">
284
+ {sharedToolbar}
285
+ {body}
286
+ </div>
287
+ )
288
+
289
+ return (
290
+ <ListPageConnectedViewBody
291
+ view={view}
292
+ hubLabel="List hub"
293
+ renderers={defineHubViewRenderers(LIST_HUB_SUPPORTED_VIEWS, {
294
+ "data-table": (
295
+ <div className="pb-6">
296
+ <DataTable<ListHubRecord> {...tableProps} />
297
+ </div>
298
+ ),
299
+ "list-with-toolbar": toolbarShell(<ListHubListView rows={tableState.rows} />),
300
+ "board-with-toolbar": toolbarShell(<ListHubCardGrid rows={tableState.rows} />),
301
+ "calendar-with-toolbar": toolbarShell(
302
+ <ListPageCalendarView
303
+ rows={tableState.rows}
304
+ getRowId={row => row.id}
305
+ getEventDate={row => row.eventDate}
306
+ getEventLabel={row => row.title}
307
+ getEventMeta={row => row.category}
308
+ emptyMonthLabel="No events on this day."
309
+ ariaLabel="List hub calendar"
310
+ showSummaryPanel={displayOptions.showCalendarSummaryPanel}
311
+ calendarMainView={displayOptions.calendarMainView}
312
+ onCalendarMainViewChange={v => patchDisplay({ calendarMainView: v })}
313
+ />,
314
+ ),
315
+ "panel-with-toolbar": toolbarShell(
316
+ <ListPageSplitHubChrome aria-label="List hub panel view">
317
+ <FinderPanelView<ListHubRecord>
318
+ embedded
319
+ groupsColumnTitle="Records"
320
+ groups={panelGroupsBuilder(tableState.rows)}
321
+ rows={tableState.rows}
322
+ getRowId={row => row.id}
323
+ getRowGroupId={() => "all"}
324
+ autoSaveId="list-hub-panel-view"
325
+ renderListRow={panelRenderListRow}
326
+ renderDetail={panelRenderDetail}
327
+ emptyList={<p className="text-sm text-muted-foreground">No records found.</p>}
328
+ />
329
+ </ListPageSplitHubChrome>
330
+ ),
331
+ })}
332
+ />
333
+ )
334
+ })
335
+
336
+ ListHubTable.displayName = "ListHubTable"