@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.
- package/CHANGELOG.md +30 -0
- package/consumer-extras/AGENTS.md +76 -0
- package/consumer-extras/README.md +5 -1
- package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
- package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +22 -7
- package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
- package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +38 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +10 -3
- package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
- package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
- package/consumer-extras/patterns/data-views-pattern.md +42 -3
- package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
- package/consumer-extras/patterns/kpi-flat-band-pattern.md +57 -0
- package/consumer-extras/patterns/shell-surface-elevation-pattern.md +54 -0
- package/package.json +2 -1
- package/src/components/ui/button-group.tsx +81 -0
- package/src/components/ui/button.tsx +4 -4
- package/src/components/ui/sidebar.tsx +2 -2
- package/src/globals.css +7 -1807
- package/src/theme.css +10 -1126
- package/src/tokens/README.md +15 -0
- package/src/tokens/base.css +337 -0
- package/src/tokens/high-contrast.css +1195 -0
- package/src/tokens/layers.css +224 -0
- package/src/tokens/tailwind-bridge.css +118 -0
- package/src/tokens/themes.css +201 -0
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -1
- package/template/AGENTS.md +66 -21
- package/template/app/(app)/dashboard/loading.tsx +3 -15
- package/template/app/(app)/dashboard/page.tsx +2 -14
- package/template/app/(app)/data-list/layout.tsx +43 -0
- package/template/app/(app)/data-list/page.tsx +2 -2
- package/template/app/(app)/error.tsx +22 -6
- package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
- package/template/app/(app)/examples/page.tsx +1 -0
- package/template/app/(app)/layout.tsx +13 -6
- package/template/app/(app)/loading.tsx +1 -18
- package/template/app/(app)/question-bank/find/page.tsx +2 -1
- package/template/app/(app)/question-bank/library/page.tsx +2 -1
- package/template/app/(app)/question-bank/list/page.tsx +2 -1
- package/template/app/(app)/question-bank/new/page.tsx +15 -23
- package/template/app/(app)/question-bank/page.tsx +2 -1
- package/template/app/(app)/settings/page.tsx +4 -5
- package/template/app/global-error.tsx +63 -0
- package/template/app/globals.css +7 -1934
- package/template/app/layout.tsx +2 -0
- package/template/components/app-route-loading.tsx +14 -0
- package/template/components/app-sidebar.tsx +71 -55
- package/template/components/data-table/index.tsx +31 -67
- package/template/components/data-table/use-table-state.ts +33 -6
- package/template/components/data-views/index.ts +37 -9
- package/template/components/data-views/list-page-calendar-view.tsx +593 -0
- package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
- package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
- package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
- package/template/components/dev-chunk-load-recovery.tsx +41 -0
- package/template/components/examples/focused-workflow-showcase.tsx +183 -0
- package/template/components/exxat-product-logo.tsx +2 -6
- package/template/components/key-metrics.tsx +54 -22
- package/template/components/list-hub-board-view.tsx +68 -0
- package/template/components/list-hub-client.tsx +186 -0
- package/template/components/list-hub-list-view.tsx +36 -0
- package/template/components/list-hub-panel-activator.tsx +8 -0
- package/template/components/list-hub-secondary-nav.tsx +121 -0
- package/template/components/list-hub-table.tsx +336 -0
- package/template/components/new-question-composer.tsx +6 -24
- package/template/components/product-switcher.tsx +5 -5
- package/template/components/product-wordmark.tsx +4 -7
- package/template/components/question-bank-client.tsx +4 -1
- package/template/components/question-bank-folder-columns-panel.tsx +104 -0
- package/template/components/question-bank-hub-client.tsx +2 -5
- package/template/components/question-bank-table.tsx +155 -509
- package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
- package/template/components/secondary-panel.tsx +4 -44
- package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
- package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
- package/template/components/secondary-panels/registry.tsx +15 -0
- package/template/components/settings-appearance-card.tsx +3 -2
- package/template/components/settings-client.tsx +59 -15
- package/template/components/settings-form-row.tsx +9 -4
- package/template/components/sidebar-shell.tsx +2 -1
- package/template/components/table-properties/drawer-button.tsx +51 -20
- package/template/components/table-properties/drawer.tsx +81 -17
- package/template/components/templates/focused-workflow-layouts.tsx +448 -0
- package/template/components/templates/focused-workflow-page-template.tsx +69 -0
- package/template/components/templates/list-page.tsx +40 -13
- package/template/components/templates/nested-secondary-panel-shell.tsx +3 -2
- package/template/components/templates/page-loading-shell.tsx +262 -0
- package/template/components/ui/button-group.tsx +1 -0
- package/template/contexts/product-context.tsx +21 -2
- package/template/docs/consumer-app-pattern.md +39 -0
- package/template/docs/data-views-pattern.md +42 -3
- package/template/docs/drawer-vs-dialog-pattern.md +3 -1
- package/template/docs/focused-workflow-page-pattern.md +84 -0
- package/template/docs/kpi-flat-band-pattern.md +57 -0
- package/template/docs/kpi-strip-max-four-pattern.md +1 -0
- package/template/docs/shell-surface-elevation-pattern.md +54 -0
- package/template/lib/chunk-load-error.ts +13 -0
- package/template/lib/command-menu-search-data.ts +11 -27
- package/template/lib/conditional-rule-match.ts +87 -22
- package/template/lib/data-list-display-options.ts +16 -2
- package/template/lib/data-list-view-registry.ts +104 -0
- package/template/lib/data-list-view-surface.ts +15 -1
- package/template/lib/data-list-view.ts +16 -1
- package/template/lib/data-view-dashboard-storage.ts +38 -35
- package/template/lib/hub-connected-view-renderers.ts +58 -0
- package/template/lib/list-hub-nav.ts +121 -0
- package/template/lib/list-hub-supported-views.ts +10 -0
- package/template/lib/list-page-table-properties.ts +3 -7
- package/template/lib/list-status-badges.ts +4 -97
- package/template/lib/mock/list-hub-directory.ts +27 -0
- package/template/lib/mock/list-hub-kpi.ts +27 -0
- package/template/lib/mock/navigation.tsx +1 -0
- package/template/lib/page-loading-variant.ts +40 -0
- package/template/lib/question-bank-supported-views.ts +13 -0
- package/template/lib/sidebar-state-cookie.ts +9 -0
- package/template/lib/table-state-lifecycle.ts +60 -13
- package/template/app/(app)/data-list/[id]/page.tsx +0 -44
- package/template/app/(app)/data-list/new/page.tsx +0 -34
- package/template/components/compliance-board-view.tsx +0 -142
- package/template/components/compliance-client.tsx +0 -92
- package/template/components/compliance-list-view.tsx +0 -54
- package/template/components/compliance-page-header.tsx +0 -89
- package/template/components/compliance-table.tsx +0 -632
- 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 -1068
- package/template/components/placement-board-card.tsx +0 -262
- package/template/components/placement-detail.tsx +0 -438
- package/template/components/placements-board-view.tsx +0 -404
- package/template/components/placements-client.tsx +0 -252
- package/template/components/placements-list-view.tsx +0 -171
- 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 -640
- package/template/components/placements-table.tsx +0 -1675
- package/template/components/rotations-empty-state.tsx +0 -50
- package/template/components/rotations-panel-activator.tsx +0 -8
- package/template/components/sites-all-client.tsx +0 -154
- package/template/components/sites-board-view.tsx +0 -67
- package/template/components/sites-list-view.tsx +0 -42
- package/template/components/sites-table.tsx +0 -402
- package/template/components/team-board-view.tsx +0 -122
- package/template/components/team-client.tsx +0 -100
- package/template/components/team-list-view.tsx +0 -59
- package/template/components/team-page-header.tsx +0 -92
- package/template/components/team-table.tsx +0 -714
- 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 -183
- 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/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
|
-
?
|
|
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
|
-
|
|
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:
|
|
697
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
|
943
|
+
/** List-page KPI band — transparent; only `--key-metrics-flat-band-radial` glow. */
|
|
909
944
|
const flatBandStyle: React.CSSProperties = {
|
|
910
|
-
background:
|
|
911
|
-
|
|
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 —
|
|
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-
|
|
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
|
+
}
|