@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.
- package/CHANGELOG.md +701 -6
- package/README.md +138 -0
- package/bin/init.mjs +134 -31
- package/consumer-extras/cursor-rules/exxat-board-cards.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-centralized-list-dataset.mdc +2 -2
- package/consumer-extras/cursor-rules/exxat-collaboration-access.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-data-tables.mdc +2 -0
- package/consumer-extras/cursor-rules/exxat-dedicated-search-surfaces.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +3 -3
- package/consumer-extras/cursor-rules/exxat-library-hub-header.mdc +28 -0
- package/consumer-extras/cursor-rules/exxat-mono-ids.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-person-identity-display.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-primary-nav-secondary-panel.mdc +6 -6
- package/consumer-extras/cursor-rules/exxat-reuse-before-custom.mdc +1 -1
- package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +2 -2
- package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -3
- package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +2 -2
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +7 -7
- package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-list-page-view-shells/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +4 -4
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +8 -8
- package/consumer-extras/cursor-skills/exxat-token-economy/SKILL.md +277 -0
- package/consumer-extras/handbook/HANDBOOK.md +2 -0
- package/consumer-extras/handbook/glossary.md +2 -1
- package/consumer-extras/handbook/reference-implementations.md +31 -4
- package/consumer-extras/patterns/collaboration-access-pattern.md +7 -7
- package/consumer-extras/patterns/data-views-pattern.md +18 -16
- package/consumer-extras/patterns/kpi-flat-band-pattern.md +2 -2
- package/dist/components/data-table/index.js +2 -2
- package/dist/components/data-table/index.js.map +1 -1
- package/dist/components/data-table/pagination.js +3 -3
- package/dist/components/data-table/pagination.js.map +1 -1
- package/dist/components/data-table/use-table-state.d.ts +1 -1
- package/dist/components/data-table/use-table-state.js.map +1 -1
- package/dist/components/data-views/data-row-list.js.map +1 -1
- package/dist/components/data-views/finder-panel-view.d.ts +1 -1
- package/dist/components/data-views/finder-panel-view.js.map +1 -1
- package/dist/components/data-views/hub-table.d.ts +9 -3
- package/dist/components/data-views/hub-table.js +262 -40
- package/dist/components/data-views/hub-table.js.map +1 -1
- package/dist/components/data-views/index.js +262 -40
- package/dist/components/data-views/index.js.map +1 -1
- package/dist/components/data-views/list-page-split-hub-tokens.d.ts +2 -2
- package/dist/components/data-views/list-page-split-hub-tokens.js.map +1 -1
- package/dist/components/data-views/list-page-tree-column-header.d.ts +1 -1
- package/dist/components/data-views/list-page-tree-column-header.js.map +1 -1
- package/dist/components/data-views/list-page-tree-panel-shell.js.map +1 -1
- package/dist/components/data-views/os-folder-glyph.d.ts +1 -1
- package/dist/components/data-views/os-folder-glyph.js.map +1 -1
- package/dist/components/ui/avatar.d.ts +1 -1
- package/dist/components/ui/key-metrics.js.map +1 -1
- package/dist/index.js +136 -39
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/components/data-table/index.tsx +2 -2
- package/src/components/data-table/pagination.tsx +5 -1
- package/src/components/data-table/use-table-state.ts +1 -1
- package/src/components/data-views/data-row-list.tsx +1 -1
- package/src/components/data-views/finder-panel-view.tsx +2 -2
- package/src/components/data-views/hub-table.tsx +149 -41
- package/src/components/data-views/list-page-split-hub-tokens.ts +2 -2
- package/src/components/data-views/list-page-tree-column-header.tsx +1 -1
- package/src/components/data-views/os-folder-glyph.tsx +1 -1
- package/src/components/ui/key-metrics.tsx +1 -1
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +8 -7
- package/template/.cursor/rules/exxat-accessibility.mdc +1 -1
- package/template/.cursor/rules/exxat-command-menu.mdc +1 -1
- package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +6 -6
- package/template/.cursor/rules/exxat-data-tables.mdc +3 -3
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +5 -5
- package/template/.cursor/rules/exxat-mono-ids.mdc +1 -1
- package/template/.cursor/rules/exxat-page-vs-drawer.mdc +1 -1
- package/template/.cursor/rules/exxat-table-properties-drawer.mdc +1 -1
- package/template/AGENTS.md +43 -37
- package/template/app/(app)/columns/page.tsx +11 -0
- package/template/app/(app)/library/all/page.tsx +11 -0
- package/template/app/(app)/library/find/page.tsx +12 -0
- package/template/app/(app)/{question-bank → library}/layout.tsx +16 -16
- package/template/app/(app)/library/list/page.tsx +12 -0
- package/template/app/(app)/{question-bank → library}/new/page.tsx +10 -10
- package/template/app/(app)/library/page.tsx +11 -0
- package/template/app/(app)/tokens-themes/page.tsx +11 -0
- package/template/components/ask-leo-composer.tsx +2 -2
- package/template/components/columns-client.tsx +158 -0
- package/template/components/columns-showcase.tsx +541 -0
- package/template/components/data-views/index.ts +32 -6
- package/template/components/data-views/{question-bank-folder-tree-branch.tsx → library-folder-tree-branch.tsx} +19 -19
- package/template/components/data-views/table-cells.tsx +673 -0
- package/template/components/folder-details-shell.tsx +11 -11
- package/template/components/hub-tree-panel-view.tsx +24 -24
- package/template/components/{question-bank-board-view.tsx → library-board-view.tsx} +44 -44
- package/template/components/{question-bank-client.tsx → library-client.tsx} +82 -82
- package/template/components/{question-bank-dashboard-charts.tsx → library-dashboard-charts.tsx} +14 -14
- package/template/components/{question-bank-favorite-button.tsx → library-favorite-button.tsx} +7 -7
- package/template/components/{question-bank-hub-client.tsx → library-hub-client.tsx} +43 -43
- package/template/components/{question-bank-new-folder-sheet.tsx → library-new-folder-sheet.tsx} +14 -14
- package/template/components/{question-bank-os-folder-view.tsx → library-os-folder-view.tsx} +31 -31
- package/template/components/{question-bank-page-header.tsx → library-page-header.tsx} +6 -6
- package/template/components/library-panel-activator.tsx +8 -0
- package/template/components/{question-bank-secondary-nav.tsx → library-secondary-nav.tsx} +60 -60
- package/template/components/{question-bank-table.tsx → library-table.tsx} +97 -97
- package/template/components/list-hub-status-badge.tsx +2 -2
- package/template/components/{new-question-composer.tsx → new-library-item-form.tsx} +37 -37
- package/template/components/sidebar/app-sidebar.tsx +61 -5
- package/template/components/sidebar/secondary-panel.tsx +109 -56
- package/template/components/sidebar/sidebar-auto-collapse.tsx +2 -2
- package/template/components/sidebar/sidebar-auto-open.tsx +2 -1
- package/template/components/table-properties/types.ts +1 -1
- package/template/components/templates/discovery-hub-template.tsx +1 -1
- package/template/components/templates/new-focus-template.tsx +2 -2
- package/template/components/templates/secondary-panel-hub-template.tsx +1 -1
- package/template/components/tokens-secondary-nav.tsx +192 -0
- package/template/components/tokens-themes-client.tsx +476 -0
- package/template/components/tokens-themes-section.tsx +386 -0
- package/template/docs/HANDBOOK.md +187 -0
- package/template/docs/blueprints/README.md +1 -1
- package/template/docs/blueprints/board-card.md +1 -1
- package/template/docs/blueprints/data-table.md +2 -2
- package/template/docs/blueprints/list-page-template.md +3 -3
- package/template/docs/blueprints/page-header.md +4 -4
- package/template/docs/collaboration-access-pattern.md +7 -7
- package/template/docs/component-selection-guide.md +1 -1
- package/template/docs/data-views-pattern.md +18 -16
- package/template/docs/glossary.md +58 -0
- package/template/docs/kpi-flat-band-pattern.md +3 -3
- package/template/docs/kpi-trend-pattern.md +18 -3
- package/template/docs/large-dataset-strategy.md +155 -0
- package/template/docs/library-hub-header-pattern.md +25 -0
- package/template/docs/migrations/_template.md +1 -1
- package/template/docs/reference-implementations.md +151 -0
- package/template/docs/token-taxonomy.md +1 -1
- package/template/docs/voice-and-tone.md +262 -0
- package/template/eslint.config.mjs +9 -39
- package/template/hooks/use-secondary-panel-hub-nav.ts +10 -10
- package/template/lib/ask-leo-route-context.ts +6 -18
- package/template/lib/coach-mark-registry.ts +0 -16
- package/template/lib/command-menu-config.ts +5 -12
- package/template/lib/command-menu-search-data.ts +8 -39
- package/template/lib/{question-bank-authoring.ts → library-authoring.ts} +89 -88
- package/template/lib/library-dedicated-search.ts +19 -0
- package/template/lib/library-hub-search.ts +90 -0
- package/template/lib/library-nav.ts +477 -0
- package/template/lib/library-recent-searches.ts +22 -0
- package/template/lib/{placements-supported-views.ts → library-supported-views.ts} +2 -2
- package/template/lib/list-status-badges.ts +16 -104
- package/template/lib/mock/dashboard.ts +1 -1
- package/template/lib/mock/{question-bank-folders.ts → library-folders.ts} +30 -30
- package/template/lib/mock/library-header-collaborators.ts +54 -0
- package/template/lib/mock/{question-bank-inspector.ts → library-inspector.ts} +29 -29
- package/template/lib/mock/{question-bank-kpi.ts → library-kpi.ts} +20 -20
- package/template/lib/mock/library.ts +249 -0
- package/template/lib/mock/navigation.tsx +32 -26
- package/template/lib/table-state-lifecycle.ts +1 -1
- package/template/next.config.mjs +7 -4
- package/template/package.json +0 -1
- package/tokens/hooks-index.json +2874 -0
- package/consumer-extras/cursor-rules/exxat-question-bank-hub-header.mdc +0 -28
- package/template/app/(app)/examples/page.tsx +0 -41
- package/template/app/(app)/question-bank/find/page.tsx +0 -12
- package/template/app/(app)/question-bank/library/page.tsx +0 -11
- package/template/app/(app)/question-bank/list/page.tsx +0 -12
- package/template/app/(app)/question-bank/page.tsx +0 -11
- package/template/components/compliance-board-view.tsx +0 -142
- package/template/components/compliance-client.tsx +0 -92
- package/template/components/compliance-page-header.tsx +0 -89
- package/template/components/compliance-table.tsx +0 -468
- package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
- package/template/components/data-view-dashboard-charts-team.tsx +0 -971
- package/template/components/data-view-dashboard-charts.tsx +0 -1503
- package/template/components/new-placement-back-btn.tsx +0 -28
- package/template/components/new-placement-form.tsx +0 -942
- package/template/components/placement-board-card.tsx +0 -250
- package/template/components/placement-detail.tsx +0 -438
- package/template/components/placements-board-view.tsx +0 -397
- package/template/components/placements-client.tsx +0 -220
- package/template/components/placements-list-view.tsx +0 -124
- package/template/components/placements-page-header.tsx +0 -166
- package/template/components/placements-table-cells.test.tsx +0 -22
- package/template/components/placements-table-cells.tsx +0 -173
- package/template/components/placements-table-columns.tsx +0 -210
- package/template/components/placements-table.tsx +0 -934
- package/template/components/question-bank-panel-activator.tsx +0 -8
- package/template/components/rotations-empty-state.tsx +0 -50
- package/template/components/rotations-panel-activator.tsx +0 -8
- package/template/components/sites-board-view.tsx +0 -67
- package/template/components/sites-client.tsx +0 -154
- package/template/components/sites-table.tsx +0 -249
- package/template/components/team-board-view.tsx +0 -122
- package/template/components/team-client.tsx +0 -100
- package/template/components/team-page-header.tsx +0 -92
- package/template/components/team-table.tsx +0 -553
- package/template/docs/question-bank-hub-header-pattern.md +0 -25
- package/template/lib/compliance-supported-views.ts +0 -10
- package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
- package/template/lib/mock/compliance-kpi.ts +0 -61
- package/template/lib/mock/compliance.ts +0 -146
- package/template/lib/mock/placements-kpi.ts +0 -134
- package/template/lib/mock/placements.ts +0 -176
- package/template/lib/mock/question-bank-header-collaborators.ts +0 -54
- package/template/lib/mock/question-bank.ts +0 -249
- package/template/lib/mock/sites-directory.ts +0 -16
- package/template/lib/mock/sites-kpi.ts +0 -25
- package/template/lib/mock/team-kpi.ts +0 -60
- package/template/lib/mock/team.ts +0 -118
- package/template/lib/placement-board-card-layout.ts +0 -79
- package/template/lib/question-bank-dedicated-search.ts +0 -19
- package/template/lib/question-bank-hub-search.ts +0 -90
- package/template/lib/question-bank-nav.ts +0 -477
- package/template/lib/question-bank-recent-searches.ts +0 -22
- package/template/lib/question-bank-supported-views.ts +0 -12
- package/template/lib/sites-supported-views.ts +0 -10
- package/template/lib/team-supported-views.ts +0 -10
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Column types — hub client.
|
|
5
|
+
*
|
|
6
|
+
* Same composition as Placements / Library
|
|
7
|
+
* (`PrimaryPageTemplate` + `ListPageTemplate`):
|
|
8
|
+
* - `header` : `PageHeader` with title + one-line subtitle describing the demo.
|
|
9
|
+
* - `metrics` : `KeyMetrics` `variant="flat"` — patterns, pinned, sortable, demo rows.
|
|
10
|
+
* - tabs : single `table` view tab (one demo table — no list / board variants).
|
|
11
|
+
* - `renderContent` : the `<ColumnsShowcase />` DataTable surface.
|
|
12
|
+
*
|
|
13
|
+
* Cell patterns are exercised inside `columns-showcase.tsx` so the rendered
|
|
14
|
+
* DataTable mirrors what real product hubs ship (favorite star, mono IDs,
|
|
15
|
+
* `ListHubStatusBadge`, `AvatarGroup` + `+N`, etc.).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as React from "react"
|
|
19
|
+
|
|
20
|
+
import { PrimaryPageTemplate } from "@/components/templates/primary-page-template"
|
|
21
|
+
import { PageHeader } from "@/components/page-header"
|
|
22
|
+
import {
|
|
23
|
+
KeyMetrics,
|
|
24
|
+
type MetricInsight,
|
|
25
|
+
type MetricItem,
|
|
26
|
+
} from "@/components/key-metrics"
|
|
27
|
+
import {
|
|
28
|
+
ListPageTemplate,
|
|
29
|
+
type ViewTab,
|
|
30
|
+
} from "@/components/data-views"
|
|
31
|
+
import {
|
|
32
|
+
ColumnsShowcase,
|
|
33
|
+
COLUMNS_SHOWCASE_PATTERN_COUNT,
|
|
34
|
+
COLUMNS_SHOWCASE_PINNED_COUNT,
|
|
35
|
+
COLUMNS_SHOWCASE_SORTABLE_COUNT,
|
|
36
|
+
} from "@/components/columns-showcase"
|
|
37
|
+
|
|
38
|
+
const COLUMNS_DEFAULT_TABS: ViewTab[] = [
|
|
39
|
+
{
|
|
40
|
+
id: "columns-all",
|
|
41
|
+
label: "All columns",
|
|
42
|
+
viewType: "table",
|
|
43
|
+
icon: "fa-table",
|
|
44
|
+
filterId: "all",
|
|
45
|
+
},
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
const COLUMNS_SUBTITLE =
|
|
49
|
+
"Every cell pattern the design system ships — checkbox select, primary identity, avatar group, status chip, inline toggle, tag overflow, rating stars, progress bar, currency, attachments, external link, relative time, absolute date, and row actions overflow."
|
|
50
|
+
|
|
51
|
+
const COLUMNS_TABLE_ANCHOR = "columns-table"
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Canonical KPI shape (matches `placement-kpi.ts` precedent):
|
|
55
|
+
* - every `MetricItem` is clickable (`href` anchor-jumps the table region),
|
|
56
|
+
* - a `MetricInsight` provides the narrative on the right side.
|
|
57
|
+
* See `apps/web/docs/kpi-flat-band-pattern.md` + `exxat-kpi-trends.mdc`.
|
|
58
|
+
*/
|
|
59
|
+
const COLUMNS_KPIS: MetricItem[] = [
|
|
60
|
+
{
|
|
61
|
+
id: "patterns",
|
|
62
|
+
label: "Cell patterns",
|
|
63
|
+
value: COLUMNS_SHOWCASE_PATTERN_COUNT,
|
|
64
|
+
delta: "",
|
|
65
|
+
trend: "neutral",
|
|
66
|
+
trendPolarity: "informational",
|
|
67
|
+
metricVariant: "hero",
|
|
68
|
+
description: "every SaaS-grid pattern, in one HubTable",
|
|
69
|
+
href: `#${COLUMNS_TABLE_ANCHOR}`,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "pinned",
|
|
73
|
+
label: "Pinned columns",
|
|
74
|
+
value: COLUMNS_SHOWCASE_PINNED_COUNT,
|
|
75
|
+
delta: "",
|
|
76
|
+
trend: "neutral",
|
|
77
|
+
trendPolarity: "informational",
|
|
78
|
+
description: "select + question on the left, actions on the right",
|
|
79
|
+
href: `#${COLUMNS_TABLE_ANCHOR}`,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "sortable",
|
|
83
|
+
label: "Sortable",
|
|
84
|
+
value: COLUMNS_SHOWCASE_SORTABLE_COUNT,
|
|
85
|
+
delta: "",
|
|
86
|
+
trend: "neutral",
|
|
87
|
+
trendPolarity: "informational",
|
|
88
|
+
description: "click any sortable header",
|
|
89
|
+
href: `#${COLUMNS_TABLE_ANCHOR}`,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "rows",
|
|
93
|
+
label: "Demo rows",
|
|
94
|
+
value: 12,
|
|
95
|
+
delta: "",
|
|
96
|
+
trend: "neutral",
|
|
97
|
+
trendPolarity: "informational",
|
|
98
|
+
description: "real library mocks + demo augmentations",
|
|
99
|
+
href: `#${COLUMNS_TABLE_ANCHOR}`,
|
|
100
|
+
},
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
const COLUMNS_INSIGHT: MetricInsight = {
|
|
104
|
+
title: "Catalog, not playground",
|
|
105
|
+
description:
|
|
106
|
+
"Every cell pattern below is an importable named export from `@/components/data-views` — `ProgressCell`, `CurrencyCell`, `RatingCell`, `RowActionsCell`, and ten more. On a real hub, do not inline-implement these; import the named cell and pass the value.",
|
|
107
|
+
severity: "info",
|
|
108
|
+
actionLabel: "Ask Leo",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function ColumnsClient() {
|
|
112
|
+
const [tabs, setTabs] = React.useState<ViewTab[]>(COLUMNS_DEFAULT_TABS)
|
|
113
|
+
const [activeTabId, setActiveTabId] = React.useState<string>(COLUMNS_DEFAULT_TABS[0]!.id)
|
|
114
|
+
|
|
115
|
+
const getTabCount = React.useCallback(() => 12, [])
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<PrimaryPageTemplate
|
|
119
|
+
siteHeader={{
|
|
120
|
+
breadcrumbs: [{ label: "Dashboard", href: "/dashboard" }],
|
|
121
|
+
title: "Column types",
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<ListPageTemplate
|
|
125
|
+
defaultTabs={COLUMNS_DEFAULT_TABS}
|
|
126
|
+
tabs={tabs}
|
|
127
|
+
onTabsChange={setTabs}
|
|
128
|
+
activeTabId={activeTabId}
|
|
129
|
+
onActiveTabChange={setActiveTabId}
|
|
130
|
+
supportedViewTypes={["table"]}
|
|
131
|
+
getTabCount={getTabCount}
|
|
132
|
+
header={
|
|
133
|
+
<PageHeader
|
|
134
|
+
title="Column types"
|
|
135
|
+
subtitle={COLUMNS_SUBTITLE}
|
|
136
|
+
/>
|
|
137
|
+
}
|
|
138
|
+
metrics={
|
|
139
|
+
<KeyMetrics
|
|
140
|
+
variant="flat"
|
|
141
|
+
metrics={COLUMNS_KPIS}
|
|
142
|
+
insight={COLUMNS_INSIGHT}
|
|
143
|
+
showHeader={false}
|
|
144
|
+
metricsSingleRow
|
|
145
|
+
/>
|
|
146
|
+
}
|
|
147
|
+
renderContent={(tab, updateTab) => (
|
|
148
|
+
<div id={COLUMNS_TABLE_ANCHOR}>
|
|
149
|
+
<ColumnsShowcase
|
|
150
|
+
view={tab.viewType}
|
|
151
|
+
onViewChange={(v) => updateTab({ viewType: v })}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
/>
|
|
156
|
+
</PrimaryPageTemplate>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ColumnsShowcase — a single `HubTable` exercising every cell pattern the
|
|
5
|
+
* design system already ships. Lives at `/columns` under Resources.
|
|
6
|
+
*
|
|
7
|
+
* Hosted inside `columns-client.tsx` so the page client owns `ListPageTemplate`,
|
|
8
|
+
* `PageHeader`, and `KeyMetrics`. `HubTable` (NOT raw `<DataTable>`) is the
|
|
9
|
+
* canonical primitive for a hub view body — it wires `useTableState`, search,
|
|
10
|
+
* filter chips, the filter dropdown, sort, the **Table properties** drawer,
|
|
11
|
+
* bulk-actions, and conditional rules. Pages that drop down to raw `<DataTable>`
|
|
12
|
+
* silently lose filters and Properties; do not do that.
|
|
13
|
+
*
|
|
14
|
+
* **All cell renderers come from `@/components/data-views`** (re-exported from
|
|
15
|
+
* `components/data-views/table-cells.tsx`). This file is the **catalog page** —
|
|
16
|
+
* if you need any of these cells in a real hub, **import them**, do not
|
|
17
|
+
* re-implement. The token-economy skill (`.cursor/skills/exxat-token-economy/SKILL.md`
|
|
18
|
+
* §3) lists each one by name so the AI imports directly.
|
|
19
|
+
*
|
|
20
|
+
* Rows are real `LibraryItem` mocks (so the favorite/star pattern lights
|
|
21
|
+
* up out of the box), augmented with demo-only fields — `reviewStatus`,
|
|
22
|
+
* `reviewers`, `attempts`, `progress`, `cost`, `rating`, `lastActivityAt`,
|
|
23
|
+
* `sourceUrl`, `attachmentCount`, `published` — courtesy of the row type's
|
|
24
|
+
* `Record<string, unknown>` extension.
|
|
25
|
+
*
|
|
26
|
+
* Patterns in column order (mirrors what Linear / Notion / Airtable / Asana /
|
|
27
|
+
* Salesforce / Stripe / Jira / Monday all ship for grid surfaces):
|
|
28
|
+
*
|
|
29
|
+
* 1. Row select — explicit `key: "select"`, pinned-left, locked
|
|
30
|
+
* 2. Stem + ID + ⭐ — primary identity (QB favorite-button pattern)
|
|
31
|
+
* 3. Author identity — avatar + name + mailto email (two-line cell)
|
|
32
|
+
* 4. Reviewers face rail — `PeopleAvatarRailCell` (+N more overflow)
|
|
33
|
+
* 5. Type pill w/ icon — `PillCell` + leading FA icon
|
|
34
|
+
* 6. Difficulty signal — `SignalBarsCell` (Wi-Fi-style ordinal)
|
|
35
|
+
* 7. Status (chip+icon) — `ListHubStatusBadge` (color + icon, never alone)
|
|
36
|
+
* 8. Published toggle — `BooleanToggleCell` (inline `ToggleSwitch`)
|
|
37
|
+
* 9. Tag list +N — `TagListCell` (soft `Badge`s with overflow tip)
|
|
38
|
+
* 10. Rating — `RatingCell` (1–5 FA stars + value)
|
|
39
|
+
* 11. Progress — `ProgressCell` (track + filled bar + label)
|
|
40
|
+
* 12. Cost — `CurrencyCell` (right-aligned `tabular-nums`)
|
|
41
|
+
* 13. Attempts — `NumericCell` (right-aligned `tabular-nums`)
|
|
42
|
+
* 14. Files — `AttachmentCountCell` (paperclip + count)
|
|
43
|
+
* 15. Source — `ExternalLinkCell` (host + new-tab icon)
|
|
44
|
+
* 16. Last activity — `RelativeTimeCell` (+ absolute on hover)
|
|
45
|
+
* 17. Updated — absolute date (matches QB column)
|
|
46
|
+
* 18. Row actions ⋯ — `RowActionsCell<LibraryItem>` (generic)
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import * as React from "react"
|
|
50
|
+
import {
|
|
51
|
+
HubTable,
|
|
52
|
+
AttachmentCountCell,
|
|
53
|
+
BooleanToggleCell,
|
|
54
|
+
CurrencyCell,
|
|
55
|
+
ExternalLinkCell,
|
|
56
|
+
NumericCell,
|
|
57
|
+
PeopleAvatarRailCell,
|
|
58
|
+
PillCell,
|
|
59
|
+
ProgressCell,
|
|
60
|
+
RatingCell,
|
|
61
|
+
RelativeTimeCell,
|
|
62
|
+
RowActionsCell,
|
|
63
|
+
SignalBarsCell,
|
|
64
|
+
TagListCell,
|
|
65
|
+
type PersonStub,
|
|
66
|
+
type RowActionDef,
|
|
67
|
+
} from "@/components/data-views"
|
|
68
|
+
import type { DataListViewType } from "@/lib/data-list-view"
|
|
69
|
+
import { AvatarInitials } from "@/components/ui/avatar"
|
|
70
|
+
import { cn } from "@/lib/utils"
|
|
71
|
+
import {
|
|
72
|
+
LibraryFavoriteButton,
|
|
73
|
+
LIBRARY_FAVORITE_HOVER_GROUP,
|
|
74
|
+
} from "@/components/library-favorite-button"
|
|
75
|
+
import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
|
|
76
|
+
import {
|
|
77
|
+
LIST_HUB_STATUS_TINT_DANGER,
|
|
78
|
+
LIST_HUB_STATUS_TINT_INFO,
|
|
79
|
+
LIST_HUB_STATUS_TINT_NEUTRAL,
|
|
80
|
+
LIST_HUB_STATUS_TINT_SUCCESS,
|
|
81
|
+
LIST_HUB_STATUS_TINT_WARNING,
|
|
82
|
+
} from "@/lib/list-status-badges"
|
|
83
|
+
import { initialsFromDisplayName } from "@/lib/initials-from-name"
|
|
84
|
+
import { formatDateUS } from "@/lib/date-filter"
|
|
85
|
+
import { mailtoHref } from "@/lib/mailto"
|
|
86
|
+
import {
|
|
87
|
+
LIBRARY_ITEMS,
|
|
88
|
+
type LibraryItem,
|
|
89
|
+
type LibraryItemType,
|
|
90
|
+
type LibraryLevel,
|
|
91
|
+
} from "@/lib/mock/library"
|
|
92
|
+
import type { ColumnDef } from "@/components/data-table/types"
|
|
93
|
+
|
|
94
|
+
/* ── Demo-only row augmentation ────────────────────────────────────────── */
|
|
95
|
+
|
|
96
|
+
type ReviewStatus = "draft" | "in_review" | "approved" | "needs_update" | "archived"
|
|
97
|
+
|
|
98
|
+
const STATUS_LABEL: Record<ReviewStatus, string> = {
|
|
99
|
+
draft: "Draft",
|
|
100
|
+
in_review: "In review",
|
|
101
|
+
approved: "Approved",
|
|
102
|
+
needs_update: "Needs update",
|
|
103
|
+
archived: "Archived",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const STATUS_TINT: Record<ReviewStatus, string> = {
|
|
107
|
+
draft: LIST_HUB_STATUS_TINT_NEUTRAL,
|
|
108
|
+
in_review: LIST_HUB_STATUS_TINT_INFO,
|
|
109
|
+
approved: LIST_HUB_STATUS_TINT_SUCCESS,
|
|
110
|
+
needs_update: LIST_HUB_STATUS_TINT_WARNING,
|
|
111
|
+
archived: LIST_HUB_STATUS_TINT_DANGER,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const STATUS_ICON: Record<ReviewStatus, string> = {
|
|
115
|
+
draft: "fa-pen-to-square",
|
|
116
|
+
in_review: "fa-eye",
|
|
117
|
+
approved: "fa-circle-check",
|
|
118
|
+
needs_update: "fa-triangle-exclamation",
|
|
119
|
+
archived: "fa-box-archive",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const TYPE_LABEL: Record<LibraryItemType, string> = {
|
|
123
|
+
multiple_choice: "Multiple choice",
|
|
124
|
+
true_false: "True / false",
|
|
125
|
+
short_answer: "Short answer",
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const TYPE_ICON: Record<LibraryItemType, string> = {
|
|
129
|
+
multiple_choice: "fa-list-check",
|
|
130
|
+
true_false: "fa-toggle-on",
|
|
131
|
+
short_answer: "fa-pen-line",
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const DIFFICULTY_LEVEL: Record<LibraryLevel, number> = {
|
|
135
|
+
easy: 1, medium: 2, hard: 3,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const DIFFICULTY_TONE: Record<LibraryLevel, "success" | "warning" | "danger"> = {
|
|
139
|
+
easy: "success", medium: "warning", hard: "danger",
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const REVIEWER_POOL: PersonStub[] = [
|
|
143
|
+
{ name: "Aisha Khan", initials: "AK" },
|
|
144
|
+
{ name: "Marcus Patel", initials: "MP" },
|
|
145
|
+
{ name: "Sofia Rinaldi", initials: "SR" },
|
|
146
|
+
{ name: "Jamal Brooks", initials: "JB" },
|
|
147
|
+
{ name: "Priya Iyer", initials: "PI" },
|
|
148
|
+
{ name: "Diego Suarez", initials: "DS" },
|
|
149
|
+
{ name: "Hannah Reed", initials: "HR" },
|
|
150
|
+
{ name: "Mei Lin", initials: "ML" },
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
const REVIEW_STATUSES: ReviewStatus[] = [
|
|
154
|
+
"draft", "in_review", "approved", "needs_update", "approved", "in_review",
|
|
155
|
+
"draft", "archived", "approved", "in_review", "needs_update", "approved",
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
const SOURCE_URLS: string[] = [
|
|
159
|
+
"https://nlm.nih.gov/medlineplus",
|
|
160
|
+
"https://merckmanuals.com/professional",
|
|
161
|
+
"https://uptodate.com/contents/diabetes",
|
|
162
|
+
"https://cdc.gov/asthma/clinical-care",
|
|
163
|
+
"https://ada.org/resources/research",
|
|
164
|
+
"https://heart.org/health-topics",
|
|
165
|
+
"https://aap.org/en/practice-management",
|
|
166
|
+
"https://nice.org.uk/guidance/ng17",
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
const PUBLISHED_BY_INDEX = [
|
|
170
|
+
true, true, false, true, false, true, true, false, true, true, false, true,
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
/** Build the showcase dataset once. Keeps `LibraryItem` as the row type so
|
|
174
|
+
* `LibraryFavoriteButton` plugs in with zero adaptation. The demo
|
|
175
|
+
* augmentations exercise the long tail of SaaS-grid cell patterns. */
|
|
176
|
+
function buildRows(): LibraryItem[] {
|
|
177
|
+
const NOW = Date.UTC(2026, 4, 21, 10, 30, 0)
|
|
178
|
+
return LIBRARY_ITEMS.slice(0, 12).map((item, i) => {
|
|
179
|
+
const lastActivityAt = new Date(
|
|
180
|
+
NOW - i * 1000 * 60 * 60 * 17 - 1000 * 60 * 13,
|
|
181
|
+
).toISOString()
|
|
182
|
+
return {
|
|
183
|
+
...item,
|
|
184
|
+
reviewStatus: REVIEW_STATUSES[i % REVIEW_STATUSES.length],
|
|
185
|
+
reviewers: REVIEWER_POOL.slice(i % 3, (i % 3) + 3 + (i % 3)),
|
|
186
|
+
attempts: 27 + ((i * 11) % 96),
|
|
187
|
+
isStarred: i % 4 === 0,
|
|
188
|
+
progress: 8 + ((i * 17) % 92),
|
|
189
|
+
cost: 12 + ((i * 91) % 488) + ((i * 31) % 100) / 100,
|
|
190
|
+
rating: 1 + ((i * 7) % 5),
|
|
191
|
+
attachmentCount: i === 1 ? 0 : 1 + ((i * 5) % 7),
|
|
192
|
+
sourceUrl: SOURCE_URLS[i % SOURCE_URLS.length],
|
|
193
|
+
lastActivityAt,
|
|
194
|
+
published: PUBLISHED_BY_INDEX[i % PUBLISHED_BY_INDEX.length],
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* ── Row actions definition ────────────────────────────────────────────── */
|
|
200
|
+
|
|
201
|
+
const ROW_ACTIONS: RowActionDef<LibraryItem>[] = [
|
|
202
|
+
{ label: "Open", icon: "fa-arrow-up-right", onSelect: () => {} },
|
|
203
|
+
{ label: "Edit", icon: "fa-pen-to-square", onSelect: () => {} },
|
|
204
|
+
{ label: "Duplicate", icon: "fa-clone", onSelect: () => {} },
|
|
205
|
+
{ label: "Archive", icon: "fa-box-archive", onSelect: () => {}, variant: "destructive" },
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
/* ── Column definitions ────────────────────────────────────────────────── */
|
|
209
|
+
|
|
210
|
+
function useColumns(
|
|
211
|
+
onToggleFavorite: (row: LibraryItem) => void,
|
|
212
|
+
onTogglePublished: (row: LibraryItem) => void,
|
|
213
|
+
): ColumnDef<LibraryItem>[] {
|
|
214
|
+
return React.useMemo<ColumnDef<LibraryItem>[]>(() => [
|
|
215
|
+
// 1. Select — explicit checkbox column. DataTable renders the checkbox cell
|
|
216
|
+
// automatically; declaring it here makes it visible in the Properties
|
|
217
|
+
// drawer column list and pins it left.
|
|
218
|
+
{
|
|
219
|
+
key: "select",
|
|
220
|
+
label: "",
|
|
221
|
+
width: 40,
|
|
222
|
+
minWidth: 40,
|
|
223
|
+
defaultPin: "left",
|
|
224
|
+
lockPin: true,
|
|
225
|
+
},
|
|
226
|
+
// 2. Primary identity — name + mono ID + favorite star.
|
|
227
|
+
{
|
|
228
|
+
key: "stem",
|
|
229
|
+
label: "Name",
|
|
230
|
+
width: 320,
|
|
231
|
+
minWidth: 220,
|
|
232
|
+
defaultPin: "left",
|
|
233
|
+
sortable: true,
|
|
234
|
+
sortKey: "stem",
|
|
235
|
+
cell: (row) => (
|
|
236
|
+
<div className={cn(LIBRARY_FAVORITE_HOVER_GROUP, "flex min-w-0 items-start gap-2")}>
|
|
237
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5 pe-1">
|
|
238
|
+
<span className="line-clamp-2 text-sm font-medium text-foreground">{row.stem}</span>
|
|
239
|
+
<span className="font-mono text-xs text-muted-foreground tabular-nums">{row.questionId}</span>
|
|
240
|
+
</div>
|
|
241
|
+
<LibraryFavoriteButton row={row} onToggleFavorite={onToggleFavorite} />
|
|
242
|
+
</div>
|
|
243
|
+
),
|
|
244
|
+
},
|
|
245
|
+
// 3. Person identity — avatar + name + email (two-line cell).
|
|
246
|
+
{
|
|
247
|
+
key: "author",
|
|
248
|
+
label: "Owner",
|
|
249
|
+
width: 260,
|
|
250
|
+
minWidth: 200,
|
|
251
|
+
sortable: true,
|
|
252
|
+
sortKey: "author",
|
|
253
|
+
filter: { type: "text", icon: "fa-user", operators: ["contains", "not_contains"] },
|
|
254
|
+
cell: (row) => {
|
|
255
|
+
const initials = initialsFromDisplayName(row.author)
|
|
256
|
+
return (
|
|
257
|
+
<div className="flex min-w-0 items-center gap-2.5">
|
|
258
|
+
<AvatarInitials initials={initials} className="size-8 shrink-0 text-xs" />
|
|
259
|
+
<div className="flex min-w-0 flex-col gap-0.5">
|
|
260
|
+
<span className="truncate text-sm font-medium text-foreground">{row.author}</span>
|
|
261
|
+
{row.authorEmail ? (
|
|
262
|
+
<a
|
|
263
|
+
href={mailtoHref(row.authorEmail)}
|
|
264
|
+
className="truncate text-xs text-muted-foreground transition-colors hover:text-foreground hover:underline"
|
|
265
|
+
onClick={(e) => e.stopPropagation()}
|
|
266
|
+
>
|
|
267
|
+
{row.authorEmail}
|
|
268
|
+
</a>
|
|
269
|
+
) : null}
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
)
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
// 4. Face rail (+N overflow) — `PeopleAvatarRailCell`.
|
|
276
|
+
{
|
|
277
|
+
key: "reviewers",
|
|
278
|
+
label: "Reviewers",
|
|
279
|
+
width: 160,
|
|
280
|
+
minWidth: 140,
|
|
281
|
+
cell: (row) => (
|
|
282
|
+
<PeopleAvatarRailCell people={row.reviewers as PersonStub[] | undefined} />
|
|
283
|
+
),
|
|
284
|
+
},
|
|
285
|
+
// 5. Single-select pill with icon — `PillCell`.
|
|
286
|
+
{
|
|
287
|
+
key: "type",
|
|
288
|
+
label: "Type",
|
|
289
|
+
width: 170,
|
|
290
|
+
minWidth: 150,
|
|
291
|
+
sortable: true,
|
|
292
|
+
sortKey: "type",
|
|
293
|
+
filter: {
|
|
294
|
+
type: "select",
|
|
295
|
+
icon: "fa-list-check",
|
|
296
|
+
options: (Object.keys(TYPE_LABEL) as LibraryItemType[]).map((k) => ({ value: k, label: TYPE_LABEL[k] })),
|
|
297
|
+
},
|
|
298
|
+
cell: (row) => <PillCell label={TYPE_LABEL[row.type]} icon={TYPE_ICON[row.type]} />,
|
|
299
|
+
},
|
|
300
|
+
// 6. Level signal — `SignalBarsCell` (Wi-Fi metaphor).
|
|
301
|
+
{
|
|
302
|
+
key: "difficulty",
|
|
303
|
+
label: "Level",
|
|
304
|
+
width: 100,
|
|
305
|
+
minWidth: 90,
|
|
306
|
+
sortable: true,
|
|
307
|
+
sortKey: "difficulty",
|
|
308
|
+
filter: {
|
|
309
|
+
type: "select",
|
|
310
|
+
icon: "fa-signal-bars",
|
|
311
|
+
options: [
|
|
312
|
+
{ value: "easy", label: "Low" },
|
|
313
|
+
{ value: "medium", label: "Normal" },
|
|
314
|
+
{ value: "hard", label: "High" },
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
cell: (row) => (
|
|
318
|
+
<SignalBarsCell
|
|
319
|
+
level={DIFFICULTY_LEVEL[row.difficulty]}
|
|
320
|
+
tone={DIFFICULTY_TONE[row.difficulty]}
|
|
321
|
+
label={`Difficulty: ${row.difficulty}`}
|
|
322
|
+
/>
|
|
323
|
+
),
|
|
324
|
+
},
|
|
325
|
+
// 7. Status — chip + icon (color + glyph; never color alone).
|
|
326
|
+
{
|
|
327
|
+
key: "reviewStatus",
|
|
328
|
+
label: "Status",
|
|
329
|
+
width: 150,
|
|
330
|
+
minWidth: 130,
|
|
331
|
+
filter: {
|
|
332
|
+
type: "select",
|
|
333
|
+
icon: "fa-circle-check",
|
|
334
|
+
options: (Object.keys(STATUS_LABEL) as ReviewStatus[]).map((k) => ({ value: k, label: STATUS_LABEL[k] })),
|
|
335
|
+
},
|
|
336
|
+
cell: (row) => {
|
|
337
|
+
const s = (row.reviewStatus as ReviewStatus | undefined) ?? "draft"
|
|
338
|
+
return (
|
|
339
|
+
<ListHubStatusBadge
|
|
340
|
+
label={STATUS_LABEL[s]}
|
|
341
|
+
tintClassName={STATUS_TINT[s]}
|
|
342
|
+
icon={STATUS_ICON[s]}
|
|
343
|
+
/>
|
|
344
|
+
)
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
// 8. Inline toggle — `BooleanToggleCell` for a boolean lifecycle field.
|
|
348
|
+
{
|
|
349
|
+
key: "published",
|
|
350
|
+
label: "Active",
|
|
351
|
+
width: 110,
|
|
352
|
+
minWidth: 100,
|
|
353
|
+
cell: (row) => (
|
|
354
|
+
<BooleanToggleCell
|
|
355
|
+
checked={Boolean((row as Record<string, unknown>).published)}
|
|
356
|
+
onChange={() => onTogglePublished(row)}
|
|
357
|
+
labelOn="Active — click to disable"
|
|
358
|
+
labelOff="Inactive — click to activate"
|
|
359
|
+
/>
|
|
360
|
+
),
|
|
361
|
+
},
|
|
362
|
+
// 9. Tag list +N — `TagListCell`.
|
|
363
|
+
{
|
|
364
|
+
key: "tags",
|
|
365
|
+
label: "Tags",
|
|
366
|
+
width: 180,
|
|
367
|
+
minWidth: 140,
|
|
368
|
+
cell: (row) => <TagListCell tags={row.tags} />,
|
|
369
|
+
},
|
|
370
|
+
// 10. Rating — `RatingCell` (5 stars + value).
|
|
371
|
+
{
|
|
372
|
+
key: "rating",
|
|
373
|
+
label: "Rating",
|
|
374
|
+
width: 130,
|
|
375
|
+
minWidth: 110,
|
|
376
|
+
sortable: true,
|
|
377
|
+
sortKey: "rating",
|
|
378
|
+
cell: (row) => <RatingCell value={(row.rating as number | undefined) ?? 0} />,
|
|
379
|
+
},
|
|
380
|
+
// 11. Progress — `ProgressCell` (track + filled + label).
|
|
381
|
+
{
|
|
382
|
+
key: "progress",
|
|
383
|
+
label: "Progress",
|
|
384
|
+
width: 180,
|
|
385
|
+
minWidth: 150,
|
|
386
|
+
sortable: true,
|
|
387
|
+
sortKey: "progress",
|
|
388
|
+
cell: (row) => <ProgressCell value={(row.progress as number | undefined) ?? 0} />,
|
|
389
|
+
},
|
|
390
|
+
// 12. Currency — `CurrencyCell` (right-aligned tabular-nums USD).
|
|
391
|
+
{
|
|
392
|
+
key: "cost",
|
|
393
|
+
label: "Cost",
|
|
394
|
+
width: 110,
|
|
395
|
+
minWidth: 90,
|
|
396
|
+
sortable: true,
|
|
397
|
+
sortKey: "cost",
|
|
398
|
+
cell: (row) => <CurrencyCell value={(row.cost as number | undefined) ?? 0} />,
|
|
399
|
+
},
|
|
400
|
+
// 13. Plain numeric — `NumericCell` (right-aligned).
|
|
401
|
+
{
|
|
402
|
+
key: "attempts",
|
|
403
|
+
label: "Count",
|
|
404
|
+
width: 100,
|
|
405
|
+
minWidth: 80,
|
|
406
|
+
sortable: true,
|
|
407
|
+
sortKey: "attempts",
|
|
408
|
+
cell: (row) => <NumericCell value={(row.attempts as number | undefined) ?? 0} />,
|
|
409
|
+
},
|
|
410
|
+
// 14. Attachment count — `AttachmentCountCell`.
|
|
411
|
+
{
|
|
412
|
+
key: "attachmentCount",
|
|
413
|
+
label: "Files",
|
|
414
|
+
width: 80,
|
|
415
|
+
minWidth: 70,
|
|
416
|
+
sortable: true,
|
|
417
|
+
sortKey: "attachmentCount",
|
|
418
|
+
cell: (row) => (
|
|
419
|
+
<AttachmentCountCell count={(row.attachmentCount as number | undefined) ?? 0} />
|
|
420
|
+
),
|
|
421
|
+
},
|
|
422
|
+
// 15. External link — `ExternalLinkCell` (host + new-tab icon).
|
|
423
|
+
{
|
|
424
|
+
key: "sourceUrl",
|
|
425
|
+
label: "Link",
|
|
426
|
+
width: 200,
|
|
427
|
+
minWidth: 160,
|
|
428
|
+
cell: (row) => <ExternalLinkCell url={(row.sourceUrl as string | undefined) ?? ""} />,
|
|
429
|
+
},
|
|
430
|
+
// 16. Relative time + absolute on hover — `RelativeTimeCell`.
|
|
431
|
+
{
|
|
432
|
+
key: "lastActivityAt",
|
|
433
|
+
label: "Last activity",
|
|
434
|
+
width: 150,
|
|
435
|
+
minWidth: 130,
|
|
436
|
+
sortable: true,
|
|
437
|
+
sortKey: "lastActivityAt",
|
|
438
|
+
cell: (row) => (
|
|
439
|
+
<RelativeTimeCell iso={(row.lastActivityAt as string | undefined) ?? ""} />
|
|
440
|
+
),
|
|
441
|
+
},
|
|
442
|
+
// 17. Absolute date.
|
|
443
|
+
{
|
|
444
|
+
key: "updatedAt",
|
|
445
|
+
label: "Updated",
|
|
446
|
+
width: 120,
|
|
447
|
+
minWidth: 100,
|
|
448
|
+
sortable: true,
|
|
449
|
+
sortKey: "updatedAt",
|
|
450
|
+
cell: (row) => (
|
|
451
|
+
<span className="text-sm tabular-nums text-foreground/90 whitespace-nowrap">
|
|
452
|
+
{formatDateUS(row.updatedAt)}
|
|
453
|
+
</span>
|
|
454
|
+
),
|
|
455
|
+
},
|
|
456
|
+
// 18. Row actions overflow — `RowActionsCell<LibraryItem>`.
|
|
457
|
+
{
|
|
458
|
+
key: "actions",
|
|
459
|
+
label: "",
|
|
460
|
+
width: 48,
|
|
461
|
+
minWidth: 48,
|
|
462
|
+
defaultPin: "right",
|
|
463
|
+
lockPin: true,
|
|
464
|
+
cell: (row) => (
|
|
465
|
+
<div className="flex items-center justify-center">
|
|
466
|
+
<RowActionsCell row={row} actions={ROW_ACTIONS} triggerLabel={`Actions for item ${row.questionId}`} />
|
|
467
|
+
</div>
|
|
468
|
+
),
|
|
469
|
+
},
|
|
470
|
+
], [onToggleFavorite, onTogglePublished])
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* ── Public ───────────────────────────────────────────────────────────── */
|
|
474
|
+
|
|
475
|
+
/** Column patterns showcased in this HubTable — surfaced as KPIs by the page client. */
|
|
476
|
+
export const COLUMNS_SHOWCASE_PATTERN_COUNT = 18
|
|
477
|
+
export const COLUMNS_SHOWCASE_PINNED_COUNT = 3 // select + name + actions
|
|
478
|
+
export const COLUMNS_SHOWCASE_SORTABLE_COUNT = 11 // name, owner, type, level, rating, progress, cost, count, files, lastActivityAt, updatedAt
|
|
479
|
+
|
|
480
|
+
const COLUMNS_SUPPORTED_VIEWS: readonly DataListViewType[] = ["table"] as const
|
|
481
|
+
|
|
482
|
+
export interface ColumnsShowcaseProps {
|
|
483
|
+
/** Active view from `ListPageTemplate.renderContent`. */
|
|
484
|
+
view: DataListViewType
|
|
485
|
+
/** Tab update callback from `ListPageTemplate.renderContent`. */
|
|
486
|
+
onViewChange: (v: DataListViewType) => void
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* The actual hub surface — wrapped by `columns-client.tsx` inside
|
|
491
|
+
* `ListPageTemplate.renderContent`. No outer card chrome — keep this lean so
|
|
492
|
+
* the host template owns header / KPIs / view tabs.
|
|
493
|
+
*/
|
|
494
|
+
export function ColumnsShowcase({ view, onViewChange }: ColumnsShowcaseProps) {
|
|
495
|
+
const [rows, setRows] = React.useState<LibraryItem[]>(() => buildRows())
|
|
496
|
+
const [pagination, setPagination] = React.useState(false)
|
|
497
|
+
|
|
498
|
+
const toggleFavorite = React.useCallback((row: LibraryItem) => {
|
|
499
|
+
setRows((current) =>
|
|
500
|
+
current.map((r) => (r.id === row.id ? { ...r, isStarred: !r.isStarred } : r)),
|
|
501
|
+
)
|
|
502
|
+
}, [])
|
|
503
|
+
|
|
504
|
+
const togglePublished = React.useCallback((row: LibraryItem) => {
|
|
505
|
+
setRows((current) =>
|
|
506
|
+
current.map((r) =>
|
|
507
|
+
r.id === row.id
|
|
508
|
+
? { ...r, published: !(r as Record<string, unknown>).published }
|
|
509
|
+
: r,
|
|
510
|
+
),
|
|
511
|
+
)
|
|
512
|
+
}, [])
|
|
513
|
+
|
|
514
|
+
const columns = useColumns(toggleFavorite, togglePublished)
|
|
515
|
+
|
|
516
|
+
return (
|
|
517
|
+
<HubTable<LibraryItem>
|
|
518
|
+
rows={rows}
|
|
519
|
+
columns={columns}
|
|
520
|
+
view={view}
|
|
521
|
+
onViewChange={onViewChange}
|
|
522
|
+
supportedViewTypes={COLUMNS_SUPPORTED_VIEWS}
|
|
523
|
+
hubLabel="Column types"
|
|
524
|
+
lifecycleTabLabel="Column types"
|
|
525
|
+
searchAriaLabel="Search columns showcase"
|
|
526
|
+
getRowId={(r) => r.id}
|
|
527
|
+
getRowSelectionLabel={(r) => r.stem}
|
|
528
|
+
defaultSort={{ key: "stem", dir: "asc" }}
|
|
529
|
+
pagination={pagination}
|
|
530
|
+
onPaginationChange={setPagination}
|
|
531
|
+
paginationInitialPageSize={5}
|
|
532
|
+
paginationPageSizeOptions={[5, 10, 25]}
|
|
533
|
+
emptyState={
|
|
534
|
+
<p className="text-sm text-muted-foreground">
|
|
535
|
+
No rows match your filters.
|
|
536
|
+
</p>
|
|
537
|
+
}
|
|
538
|
+
renderers={{}}
|
|
539
|
+
/>
|
|
540
|
+
)
|
|
541
|
+
}
|