@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Centralized localStorage for **Data** view dashboard canvas (
|
|
2
|
+
* Centralized localStorage for **Data** view dashboard canvas (list hub, question bank).
|
|
3
3
|
* Single bundle key; per-scope slices. Migrates legacy per-hub keys when a scope is missing.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -9,14 +9,16 @@ const BUNDLE_KEY = "exxat-ds:data-view-dashboards:v1"
|
|
|
9
9
|
|
|
10
10
|
/** Legacy keys (pre-bundle) — read when that scope is absent from the bundle. */
|
|
11
11
|
const LEGACY_KEYS: Record<DataViewScope, string> = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
compliance: "exxat-compliance-dashboard-cards",
|
|
12
|
+
"list-hub": "exxat-dashboard-cards",
|
|
13
|
+
"question-bank": "exxat-question-bank-dashboard-cards",
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
/** @deprecated Legacy scopes still migrated from the bundle. */
|
|
17
|
+
const LEGACY_SCOPES = ["placements", "team", "compliance"] as const
|
|
18
18
|
|
|
19
|
-
type
|
|
19
|
+
export type DataViewScope = "list-hub" | "question-bank"
|
|
20
|
+
|
|
21
|
+
type LayoutBundle = Partial<Record<DataViewScope | (typeof LEGACY_SCOPES)[number], DashboardLayoutV1>>
|
|
20
22
|
|
|
21
23
|
function parseLayout(raw: unknown): DashboardLayoutV1 | null {
|
|
22
24
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null
|
|
@@ -51,51 +53,52 @@ function readBundleRaw(): LayoutBundle {
|
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
function
|
|
56
|
+
function writeBundleRaw(bundle: LayoutBundle) {
|
|
55
57
|
if (typeof window === "undefined") return
|
|
56
58
|
try {
|
|
57
59
|
localStorage.setItem(BUNDLE_KEY, JSON.stringify(bundle))
|
|
58
60
|
} catch {
|
|
59
|
-
/*
|
|
61
|
+
/* quota / private mode */
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
65
|
+
function migrateLegacyScope(scope: DataViewScope): DashboardLayoutV1 | null {
|
|
66
|
+
const legacyKey = LEGACY_KEYS[scope]
|
|
67
|
+
if (typeof window === "undefined") return null
|
|
68
|
+
try {
|
|
69
|
+
const raw = localStorage.getItem(legacyKey)
|
|
70
|
+
if (!raw) return null
|
|
71
|
+
return parseLayout(JSON.parse(raw))
|
|
72
|
+
} catch {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One-time migration: copy legacy placements/team/compliance slices into list-hub when empty. */
|
|
78
|
+
function migrateRemovedHubScopes(bundle: LayoutBundle): LayoutBundle {
|
|
79
|
+
if (bundle["list-hub"]) return bundle
|
|
80
|
+
for (const legacy of LEGACY_SCOPES) {
|
|
81
|
+
const layout = bundle[legacy]
|
|
82
|
+
if (layout) {
|
|
83
|
+
return { ...bundle, "list-hub": layout }
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
|
-
|
|
86
|
+
const fromPlacementsKey = migrateLegacyScope("list-hub")
|
|
87
|
+
if (fromPlacementsKey) return { ...bundle, "list-hub": fromPlacementsKey }
|
|
84
88
|
return bundle
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
/**
|
|
88
|
-
* Load persisted layout for a hub
|
|
92
|
+
* Load persisted layout for a hub Data view.
|
|
89
93
|
*/
|
|
90
94
|
export function loadDataViewLayout(scope: DataViewScope): DashboardLayoutV1 | null {
|
|
91
|
-
const bundle =
|
|
92
|
-
return bundle[scope] ??
|
|
95
|
+
const bundle = migrateRemovedHubScopes(readBundleRaw())
|
|
96
|
+
return bundle[scope] ?? migrateLegacyScope(scope)
|
|
93
97
|
}
|
|
94
98
|
|
|
95
|
-
/**
|
|
96
|
-
* Save layout for one hub; updates the shared bundle atomically.
|
|
97
|
-
*/
|
|
99
|
+
/** Persist layout for a hub Data view. */
|
|
98
100
|
export function saveDataViewLayout(scope: DataViewScope, layout: DashboardLayoutV1) {
|
|
99
|
-
const bundle =
|
|
100
|
-
|
|
101
|
+
const bundle = migrateRemovedHubScopes(readBundleRaw())
|
|
102
|
+
bundle[scope] = layout
|
|
103
|
+
writeBundleRaw(bundle)
|
|
101
104
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed `ListPageConnectedViewBody` renderers aligned with `supportedViewTypes`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type * as React from "react"
|
|
6
|
+
import type { DataListViewType } from "@/lib/data-list-view"
|
|
7
|
+
import {
|
|
8
|
+
getDataListViewRenderKind,
|
|
9
|
+
type DataListViewRenderKind,
|
|
10
|
+
} from "@/lib/data-list-view-registry"
|
|
11
|
+
import type { ListPageConnectedViewRenderers } from "@/components/data-views/list-page-connected-view-body"
|
|
12
|
+
|
|
13
|
+
/** Maps each `DataListViewType` to its `DataListViewRenderKind` (compile-time). */
|
|
14
|
+
export type DataListViewRenderKindMap = {
|
|
15
|
+
table: "data-table"
|
|
16
|
+
list: "list-with-toolbar"
|
|
17
|
+
board: "board-with-toolbar"
|
|
18
|
+
dashboard: "dashboard-with-toolbar"
|
|
19
|
+
calendar: "calendar-with-toolbar"
|
|
20
|
+
folder: "folder-with-toolbar"
|
|
21
|
+
panel: "panel-with-toolbar"
|
|
22
|
+
"tree-panel": "tree-panel-with-toolbar"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type HubRenderKindForViews<Supported extends readonly DataListViewType[]> =
|
|
26
|
+
DataListViewRenderKindMap[Supported[number]]
|
|
27
|
+
|
|
28
|
+
export type HubConnectedViewRenderers<Supported extends readonly DataListViewType[]> = Partial<
|
|
29
|
+
Record<HubRenderKindForViews<Supported>, React.ReactNode | (() => React.ReactNode)>
|
|
30
|
+
>
|
|
31
|
+
|
|
32
|
+
/** Render kinds required for a hub's `supportedViewTypes` array. */
|
|
33
|
+
export function hubRenderKindsForSupported(
|
|
34
|
+
supported: readonly DataListViewType[],
|
|
35
|
+
): DataListViewRenderKind[] {
|
|
36
|
+
return supported.map(v => getDataListViewRenderKind(v))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build renderers for `ListPageConnectedViewBody` and warn in dev when a supported view has no body.
|
|
41
|
+
*/
|
|
42
|
+
export function defineHubViewRenderers<Supported extends readonly DataListViewType[]>(
|
|
43
|
+
supported: Supported,
|
|
44
|
+
renderers: HubConnectedViewRenderers<Supported>,
|
|
45
|
+
): ListPageConnectedViewRenderers {
|
|
46
|
+
if (process.env.NODE_ENV !== "production") {
|
|
47
|
+
for (const viewType of supported) {
|
|
48
|
+
const kind = getDataListViewRenderKind(viewType)
|
|
49
|
+
if (renderers[kind as HubRenderKindForViews<Supported>] == null) {
|
|
50
|
+
console.warn(
|
|
51
|
+
`[Exxat DS] Missing ListPageConnectedViewBody renderer for view "${viewType}" (${kind}). ` +
|
|
52
|
+
"Add it to defineHubViewRenderers or remove the view from supportedViewTypes.",
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return renderers as ListPageConnectedViewRenderers
|
|
58
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List hub secondary panel + URL scope — demo filters on {@link LIST_HUB_DIRECTORY}.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ListHubRecord } from "@/lib/mock/list-hub-directory"
|
|
6
|
+
|
|
7
|
+
export const LIST_HUB_PATH = "/data-list"
|
|
8
|
+
|
|
9
|
+
export type ListHubNavScope = "all" | "upcoming" | "past" | "category"
|
|
10
|
+
|
|
11
|
+
export interface ListHubNavState {
|
|
12
|
+
scope: ListHubNavScope
|
|
13
|
+
/** Set when `scope === "category"` */
|
|
14
|
+
category: string | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const LIST_HUB_DEFAULT_NAV: ListHubNavState = {
|
|
18
|
+
scope: "all",
|
|
19
|
+
category: null,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const LIST_HUB_CATEGORY_SCOPES = ["Training", "Field", "Clinical", "Admin"] as const
|
|
23
|
+
|
|
24
|
+
export type ListHubCategoryScope = (typeof LIST_HUB_CATEGORY_SCOPES)[number]
|
|
25
|
+
|
|
26
|
+
export function parseListHubNav(searchParams: URLSearchParams): ListHubNavState {
|
|
27
|
+
const raw = (searchParams.get("scope") ?? "all").toLowerCase()
|
|
28
|
+
if (raw === "upcoming") return { scope: "upcoming", category: null }
|
|
29
|
+
if (raw === "past") return { scope: "past", category: null }
|
|
30
|
+
if (raw === "category") {
|
|
31
|
+
const category = searchParams.get("category")?.trim() || null
|
|
32
|
+
return { scope: "category", category }
|
|
33
|
+
}
|
|
34
|
+
return { ...LIST_HUB_DEFAULT_NAV }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isListHubDefaultNav(nav: ListHubNavState): boolean {
|
|
38
|
+
return nav.scope === "all" && nav.category === null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isListHubNavActive(
|
|
42
|
+
pathname: string,
|
|
43
|
+
nav: ListHubNavState,
|
|
44
|
+
scope: ListHubNavScope,
|
|
45
|
+
category?: string | null,
|
|
46
|
+
): boolean {
|
|
47
|
+
const p = pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname
|
|
48
|
+
if (p !== LIST_HUB_PATH) return false
|
|
49
|
+
if (scope === "all") return nav.scope === "all"
|
|
50
|
+
if (scope === "upcoming") return nav.scope === "upcoming"
|
|
51
|
+
if (scope === "past") return nav.scope === "past"
|
|
52
|
+
if (scope === "category" && category) {
|
|
53
|
+
return nav.scope === "category" && nav.category === category
|
|
54
|
+
}
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function listHubNavHref(opts: {
|
|
59
|
+
scope: ListHubNavScope
|
|
60
|
+
category?: string | null
|
|
61
|
+
hash?: string
|
|
62
|
+
}): string {
|
|
63
|
+
const params = new URLSearchParams()
|
|
64
|
+
if (opts.scope !== "all") params.set("scope", opts.scope)
|
|
65
|
+
if (opts.scope === "category" && opts.category) params.set("category", opts.category)
|
|
66
|
+
const q = params.toString()
|
|
67
|
+
const hash = opts.hash ?? ""
|
|
68
|
+
return `${LIST_HUB_PATH}${q ? `?${q}` : ""}${hash}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function listHubHubScopeHref(
|
|
72
|
+
pathname: string,
|
|
73
|
+
currentSearch: URLSearchParams,
|
|
74
|
+
patch: { scope: ListHubNavScope; category?: string | null },
|
|
75
|
+
): string {
|
|
76
|
+
void pathname
|
|
77
|
+
void currentSearch
|
|
78
|
+
return listHubNavHref({
|
|
79
|
+
scope: patch.scope,
|
|
80
|
+
category: patch.scope === "category" ? patch.category : null,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function todayIsoDate(): string {
|
|
85
|
+
return new Date().toISOString().slice(0, 10)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function filterListHubRows(rows: ListHubRecord[], nav: ListHubNavState): ListHubRecord[] {
|
|
89
|
+
const today = todayIsoDate()
|
|
90
|
+
switch (nav.scope) {
|
|
91
|
+
case "upcoming":
|
|
92
|
+
return rows.filter(r => r.eventDate >= today)
|
|
93
|
+
case "past":
|
|
94
|
+
return rows.filter(r => r.eventDate < today)
|
|
95
|
+
case "category":
|
|
96
|
+
if (!nav.category) return rows
|
|
97
|
+
return rows.filter(r => r.category === nav.category)
|
|
98
|
+
case "all":
|
|
99
|
+
default:
|
|
100
|
+
return rows
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function listHubScopeLabel(nav: ListHubNavState): string {
|
|
105
|
+
switch (nav.scope) {
|
|
106
|
+
case "upcoming":
|
|
107
|
+
return "Upcoming"
|
|
108
|
+
case "past":
|
|
109
|
+
return "Past"
|
|
110
|
+
case "category":
|
|
111
|
+
return nav.category ?? "Category"
|
|
112
|
+
case "all":
|
|
113
|
+
default:
|
|
114
|
+
return "All records"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function listHubHeaderSubtitle(nav: ListHubNavState, count: number): string {
|
|
119
|
+
const noun = count === 1 ? "record" : "records"
|
|
120
|
+
return `${count} ${noun} · ${listHubScopeLabel(nav)} · One dataset across table, calendar, and board`
|
|
121
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DataListViewType } from "@/lib/data-list-view"
|
|
2
|
+
|
|
3
|
+
/** Views implemented in `ListHubTable` — keep in sync with `ListPageConnectedViewBody` renderers. */
|
|
4
|
+
export const LIST_HUB_SUPPORTED_VIEWS = [
|
|
5
|
+
"table",
|
|
6
|
+
"list",
|
|
7
|
+
"board",
|
|
8
|
+
"calendar",
|
|
9
|
+
"panel",
|
|
10
|
+
] as const satisfies readonly DataListViewType[]
|
|
@@ -10,19 +10,15 @@
|
|
|
10
10
|
import * as React from "react"
|
|
11
11
|
|
|
12
12
|
import { dataListViewIcon, type DataListViewType } from "@/lib/data-list-view"
|
|
13
|
+
import { isDataListSurfaceViewType } from "@/lib/data-list-view-registry"
|
|
14
|
+
|
|
15
|
+
export { isDataListSurfaceViewType }
|
|
13
16
|
|
|
14
17
|
/** Minimal ref API any list/table surface exposes for the shared Properties drawer. */
|
|
15
18
|
export interface OpenTablePropertiesHandle {
|
|
16
19
|
openPropertiesDrawer: () => void
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
const SURFACE_VIEW_TYPES = new Set<DataListViewType>(["table", "list", "board", "dashboard"])
|
|
20
|
-
|
|
21
|
-
/** True when `viewType` is one of the data-list surfaces that support TablePropertiesDrawer. */
|
|
22
|
-
export function isDataListSurfaceViewType(viewType: string): viewType is DataListViewType {
|
|
23
|
-
return SURFACE_VIEW_TYPES.has(viewType as DataListViewType)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
22
|
export interface CreateListPageEditViewHandlerOptions {
|
|
27
23
|
/** Delay before opening Properties after switching to table (ms). Default 160. */
|
|
28
24
|
switchDelayMs?: number
|
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared status chip labels, tint classes, and FA icon names for product list hubs
|
|
3
|
-
* (
|
|
4
|
-
* (dashboard **task priority**, placement **readiness** on the detail drawer).
|
|
3
|
+
* (Question bank, list hub, future entities), plus related chips (dashboard **task priority**).
|
|
5
4
|
*
|
|
6
5
|
* Labels use **sentence / title case** (e.g. "Due soon", "Under Review"). Do **not** add **`uppercase`**.
|
|
7
6
|
*
|
|
8
|
-
* **Rendering:** Use **`ListHubStatusBadge`** from `@/components/list-hub-status-badge
|
|
9
|
-
*
|
|
10
|
-
* around **`ListHubStatusBadge`** + **`PLACEMENT_STATUS_*`** below). Task priority → **`TaskPriorityBadge`**.
|
|
7
|
+
* **Rendering:** Use **`ListHubStatusBadge`** from `@/components/list-hub-status-badge`.
|
|
8
|
+
* Task priority → **`TaskPriorityBadge`**.
|
|
11
9
|
*
|
|
12
10
|
* **Semantic tints:** Map domain statuses onto **`LIST_HUB_STATUS_TINT_*`** before inventing new colors.
|
|
13
11
|
* **Icon-on-tinted-disc** (insights / activity): **`TintedIconDisc`** + **`--icon-disc-*`** in **`app/globals.css`**.
|
|
14
12
|
*/
|
|
15
13
|
|
|
16
|
-
import type { ComplianceStatus } from "@/lib/mock/compliance"
|
|
17
|
-
import type { Status as PlacementStatus } from "@/lib/mock/placements"
|
|
18
|
-
import type { TeamMember } from "@/lib/mock/team"
|
|
19
|
-
|
|
20
14
|
// ─── Semantic variants (reuse for new entities) ─────────────────────────────
|
|
21
15
|
//
|
|
22
16
|
// **Light washes** (same visual weight as before) + **darker ink** via `--chip-*` for WCAG 1.4.3.
|
|
@@ -34,24 +28,10 @@ export const LIST_HUB_STATUS_TINT_NEUTRAL =
|
|
|
34
28
|
export const LIST_HUB_STATUS_TINT_DANGER =
|
|
35
29
|
"bg-destructive/15 text-[var(--chip-destructive)] border-destructive/20 dark:bg-destructive/15 dark:text-red-200"
|
|
36
30
|
|
|
37
|
-
/** In-progress / review (distinct from warning where both appear
|
|
31
|
+
/** In-progress / review (distinct from warning where both appear). */
|
|
38
32
|
export const LIST_HUB_STATUS_TINT_INFO =
|
|
39
33
|
"bg-sky-500/15 text-[var(--chip-1)] border-sky-500/20 dark:bg-sky-500/15 dark:text-sky-100"
|
|
40
34
|
|
|
41
|
-
// ─── Placement detail — readiness row (string labels from mock) ─────────────
|
|
42
|
-
|
|
43
|
-
const PLACEMENT_READINESS_BADGE_CLASS: Record<string, string> = {
|
|
44
|
-
Ready: LIST_HUB_STATUS_TINT_SUCCESS,
|
|
45
|
-
"At risk": LIST_HUB_STATUS_TINT_DANGER,
|
|
46
|
-
Blocked: LIST_HUB_STATUS_TINT_DANGER,
|
|
47
|
-
"In review": LIST_HUB_STATUS_TINT_INFO,
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Badge `className` tail for placement readiness labels; unknown → neutral. */
|
|
51
|
-
export function placementReadinessBadgeClass(readiness: string): string {
|
|
52
|
-
return PLACEMENT_READINESS_BADGE_CLASS[readiness] ?? LIST_HUB_STATUS_TINT_NEUTRAL
|
|
53
|
-
}
|
|
54
|
-
|
|
55
35
|
// ─── Dashboard task priority (shared chip system) ──────────────────────────
|
|
56
36
|
|
|
57
37
|
export type TaskPriorityLevel = "high" | "medium" | "low"
|
|
@@ -73,76 +53,3 @@ export function normalizeTaskPriority(priority: string): TaskPriorityLevel | nul
|
|
|
73
53
|
if (k === "high" || k === "medium" || k === "low") return k
|
|
74
54
|
return null
|
|
75
55
|
}
|
|
76
|
-
|
|
77
|
-
// ─── Placements (lifecycle status) ───────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
export const PLACEMENT_STATUS_LABEL: Record<PlacementStatus, string> = {
|
|
80
|
-
confirmed: "Confirmed",
|
|
81
|
-
pending: "Pending",
|
|
82
|
-
"under-review": "Under Review",
|
|
83
|
-
rejected: "Rejected",
|
|
84
|
-
completed: "Completed",
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export const PLACEMENT_STATUS_BADGE_CLASS: Record<PlacementStatus, string> = {
|
|
88
|
-
confirmed: LIST_HUB_STATUS_TINT_SUCCESS,
|
|
89
|
-
pending: LIST_HUB_STATUS_TINT_WARNING,
|
|
90
|
-
"under-review": LIST_HUB_STATUS_TINT_INFO,
|
|
91
|
-
rejected: LIST_HUB_STATUS_TINT_DANGER,
|
|
92
|
-
completed: LIST_HUB_STATUS_TINT_NEUTRAL,
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export const PLACEMENT_STATUS_ICON: Record<PlacementStatus, string> = {
|
|
96
|
-
confirmed: "fa-circle-check",
|
|
97
|
-
pending: "fa-hourglass-half",
|
|
98
|
-
"under-review": "fa-eye",
|
|
99
|
-
rejected: "fa-circle-xmark",
|
|
100
|
-
completed: "fa-clipboard-check",
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ─── Team ─────────────────────────────────────────────────────────────────
|
|
104
|
-
|
|
105
|
-
export type TeamMemberStatus = TeamMember["status"]
|
|
106
|
-
|
|
107
|
-
export const TEAM_MEMBER_STATUS_LABEL: Record<TeamMemberStatus, string> = {
|
|
108
|
-
active: "Active",
|
|
109
|
-
away: "Away",
|
|
110
|
-
invited: "Invited",
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export const TEAM_MEMBER_STATUS_BADGE_CLASS: Record<TeamMemberStatus, string> = {
|
|
114
|
-
active: LIST_HUB_STATUS_TINT_SUCCESS,
|
|
115
|
-
away: LIST_HUB_STATUS_TINT_WARNING,
|
|
116
|
-
invited: LIST_HUB_STATUS_TINT_NEUTRAL,
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Font Awesome icon per status — shape + label, not colour alone (WCAG 1.4.1). */
|
|
120
|
-
export const TEAM_MEMBER_STATUS_ICON: Record<TeamMemberStatus, string> = {
|
|
121
|
-
active: "fa-circle-check",
|
|
122
|
-
away: "fa-moon",
|
|
123
|
-
invited: "fa-envelope",
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ─── Compliance ───────────────────────────────────────────────────────────
|
|
127
|
-
|
|
128
|
-
export const COMPLIANCE_STATUS_LABEL: Record<ComplianceStatus, string> = {
|
|
129
|
-
compliant: "Compliant",
|
|
130
|
-
due_soon: "Due soon",
|
|
131
|
-
overdue: "Overdue",
|
|
132
|
-
pending: "Pending",
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export const COMPLIANCE_STATUS_BADGE_CLASS: Record<ComplianceStatus, string> = {
|
|
136
|
-
compliant: LIST_HUB_STATUS_TINT_SUCCESS,
|
|
137
|
-
due_soon: LIST_HUB_STATUS_TINT_WARNING,
|
|
138
|
-
overdue: LIST_HUB_STATUS_TINT_DANGER,
|
|
139
|
-
pending: LIST_HUB_STATUS_TINT_NEUTRAL,
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export const COMPLIANCE_STATUS_ICON: Record<ComplianceStatus, string> = {
|
|
143
|
-
compliant: "fa-shield-check",
|
|
144
|
-
due_soon: "fa-clock",
|
|
145
|
-
overdue: "fa-triangle-exclamation",
|
|
146
|
-
pending: "fa-hourglass-half",
|
|
147
|
-
}
|
|
148
|
-
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Row shape for the List hub (`/data-list`) — one dataset for table, calendar, board, etc. */
|
|
2
|
+
export interface ListHubRecord extends Record<string, unknown> {
|
|
3
|
+
id: string
|
|
4
|
+
title: string
|
|
5
|
+
category: string
|
|
6
|
+
/** ISO date (YYYY-MM-DD) for calendar view */
|
|
7
|
+
eventDate: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function addDays(base: Date, days: number) {
|
|
11
|
+
const d = new Date(base)
|
|
12
|
+
d.setDate(d.getDate() + days)
|
|
13
|
+
return d.toISOString().slice(0, 10)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const BASE = new Date()
|
|
17
|
+
|
|
18
|
+
export const LIST_HUB_DIRECTORY: ListHubRecord[] = [
|
|
19
|
+
{ id: "LH-2401", title: "Orientation workshop", category: "Training", eventDate: addDays(BASE, 2) },
|
|
20
|
+
{ id: "LH-2402", title: "Site visit — Metro campus", category: "Field", eventDate: addDays(BASE, 5) },
|
|
21
|
+
{ id: "LH-2403", title: "Compliance review", category: "Admin", eventDate: addDays(BASE, 9) },
|
|
22
|
+
{ id: "LH-2404", title: "Preceptor check-in", category: "Clinical", eventDate: addDays(BASE, 12) },
|
|
23
|
+
{ id: "LH-2405", title: "Skills lab session", category: "Training", eventDate: addDays(BASE, 18) },
|
|
24
|
+
{ id: "LH-2406", title: "Cohort debrief", category: "Admin", eventDate: addDays(BASE, 22) },
|
|
25
|
+
{ id: "LH-2407", title: "Documentation audit", category: "Admin", eventDate: addDays(BASE, -3) },
|
|
26
|
+
{ id: "LH-2408", title: "Weekend rotation", category: "Clinical", eventDate: addDays(BASE, -7) },
|
|
27
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { MetricInsight, MetricItem } from "@/components/key-metrics"
|
|
2
|
+
import type { ListHubRecord } from "@/lib/mock/list-hub-directory"
|
|
3
|
+
|
|
4
|
+
export function listHubKpiMetrics(countOrRows: number | ListHubRecord[]): MetricItem[] {
|
|
5
|
+
const count = typeof countOrRows === "number" ? countOrRows : countOrRows.length
|
|
6
|
+
const scheduled = Math.max(0, count - 2)
|
|
7
|
+
const thisWeek = Math.min(count, 4)
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
id: "total",
|
|
11
|
+
label: "Total records",
|
|
12
|
+
value: count,
|
|
13
|
+
delta: "+2",
|
|
14
|
+
trend: "up",
|
|
15
|
+
href: "#",
|
|
16
|
+
metricVariant: "hero",
|
|
17
|
+
},
|
|
18
|
+
{ id: "scheduled", label: "Scheduled", value: scheduled, delta: "+1", trend: "up", href: "#" },
|
|
19
|
+
{ id: "this-week", label: "This week", value: thisWeek, delta: "—", trend: "neutral", href: "#" },
|
|
20
|
+
{ id: "completed", label: "Completed", value: 2, delta: "—", trend: "neutral", href: "#" },
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const LIST_HUB_KPI_INSIGHT: MetricInsight = {
|
|
25
|
+
title: "3 events land this week",
|
|
26
|
+
description: "Filtered calendar and table views share the same row set after search and filters.",
|
|
27
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps app routes to loading skeleton layouts — keep in sync with page templates
|
|
3
|
+
* (`PrimaryPageTemplate`, `FocusedWorkflowPageTemplate`, dedicated search, hub landing).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type PageLoadingVariant =
|
|
7
|
+
| "dashboard"
|
|
8
|
+
| "primary-list-hub"
|
|
9
|
+
| "question-bank-hub"
|
|
10
|
+
| "dedicated-search"
|
|
11
|
+
| "focused-workflow"
|
|
12
|
+
| "focused-workflow-sidebar"
|
|
13
|
+
| "simple"
|
|
14
|
+
|
|
15
|
+
function normalizePathname(pathname: string): string {
|
|
16
|
+
if (!pathname || pathname === "/") return "/"
|
|
17
|
+
return pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Pick the loading skeleton for the destination route (client-safe). */
|
|
21
|
+
export function resolvePageLoadingVariant(pathname: string): PageLoadingVariant {
|
|
22
|
+
const path = normalizePathname(pathname)
|
|
23
|
+
|
|
24
|
+
if (path === "/dashboard") return "dashboard"
|
|
25
|
+
if (path === "/settings") return "focused-workflow-sidebar"
|
|
26
|
+
if (path === "/help" || path === "/examples" || path.startsWith("/examples/")) {
|
|
27
|
+
return path === "/examples/focused-workflow" ? "focused-workflow-sidebar" : "simple"
|
|
28
|
+
}
|
|
29
|
+
if (path === "/question-bank/new" || path.startsWith("/question-bank/new/")) {
|
|
30
|
+
return "focused-workflow"
|
|
31
|
+
}
|
|
32
|
+
if (path === "/question-bank/find" || path === "/question-bank/list") {
|
|
33
|
+
return "dedicated-search"
|
|
34
|
+
}
|
|
35
|
+
if (path === "/question-bank") return "question-bank-hub"
|
|
36
|
+
if (path.startsWith("/question-bank/")) return "primary-list-hub"
|
|
37
|
+
if (path === "/data-list") return "primary-list-hub"
|
|
38
|
+
|
|
39
|
+
return "primary-list-hub"
|
|
40
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DataListViewType } from "@/lib/data-list-view"
|
|
2
|
+
|
|
3
|
+
/** Views implemented in `QuestionBankTable` — keep in sync with `ListPageConnectedViewBody` renderers. */
|
|
4
|
+
export const QUESTION_BANK_SUPPORTED_VIEWS = [
|
|
5
|
+
"table",
|
|
6
|
+
"list",
|
|
7
|
+
"board",
|
|
8
|
+
"dashboard",
|
|
9
|
+
"calendar",
|
|
10
|
+
"folder",
|
|
11
|
+
"panel",
|
|
12
|
+
"tree-panel",
|
|
13
|
+
] as const satisfies readonly DataListViewType[]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Cookie name persisted by `@exxatdesignux/ui` `SidebarProvider` (`setOpen`). */
|
|
2
|
+
export const SIDEBAR_STATE_COOKIE_NAME = "sidebar_state"
|
|
3
|
+
|
|
4
|
+
/** Read desktop sidebar expanded state for SSR `defaultOpen` (matches client cookie restore). */
|
|
5
|
+
export function sidebarDefaultOpenFromCookie(
|
|
6
|
+
value: string | undefined,
|
|
7
|
+
): boolean {
|
|
8
|
+
return value !== "false"
|
|
9
|
+
}
|