@exxatdesignux/ui 0.2.17 → 0.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/consumer-extras/AGENTS.md +76 -0
  3. package/consumer-extras/README.md +5 -1
  4. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
  5. package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
  6. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +22 -7
  7. package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
  8. package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +38 -0
  9. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +10 -3
  10. package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
  11. package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
  12. package/consumer-extras/patterns/data-views-pattern.md +42 -3
  13. package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
  14. package/consumer-extras/patterns/kpi-flat-band-pattern.md +57 -0
  15. package/consumer-extras/patterns/shell-surface-elevation-pattern.md +54 -0
  16. package/package.json +2 -1
  17. package/src/components/ui/button-group.tsx +81 -0
  18. package/src/components/ui/button.tsx +4 -4
  19. package/src/components/ui/sidebar.tsx +2 -2
  20. package/src/globals.css +7 -1807
  21. package/src/theme.css +10 -1126
  22. package/src/tokens/README.md +15 -0
  23. package/src/tokens/base.css +337 -0
  24. package/src/tokens/high-contrast.css +1195 -0
  25. package/src/tokens/layers.css +224 -0
  26. package/src/tokens/tailwind-bridge.css +118 -0
  27. package/src/tokens/themes.css +201 -0
  28. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -1
  29. package/template/AGENTS.md +66 -21
  30. package/template/app/(app)/dashboard/loading.tsx +3 -15
  31. package/template/app/(app)/dashboard/page.tsx +2 -14
  32. package/template/app/(app)/data-list/layout.tsx +43 -0
  33. package/template/app/(app)/data-list/page.tsx +2 -2
  34. package/template/app/(app)/error.tsx +22 -6
  35. package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
  36. package/template/app/(app)/examples/page.tsx +1 -0
  37. package/template/app/(app)/layout.tsx +13 -6
  38. package/template/app/(app)/loading.tsx +1 -18
  39. package/template/app/(app)/question-bank/find/page.tsx +2 -1
  40. package/template/app/(app)/question-bank/library/page.tsx +2 -1
  41. package/template/app/(app)/question-bank/list/page.tsx +2 -1
  42. package/template/app/(app)/question-bank/new/page.tsx +15 -23
  43. package/template/app/(app)/question-bank/page.tsx +2 -1
  44. package/template/app/(app)/settings/page.tsx +4 -5
  45. package/template/app/global-error.tsx +63 -0
  46. package/template/app/globals.css +7 -1934
  47. package/template/app/layout.tsx +2 -0
  48. package/template/components/app-route-loading.tsx +14 -0
  49. package/template/components/app-sidebar.tsx +71 -55
  50. package/template/components/data-table/index.tsx +31 -67
  51. package/template/components/data-table/use-table-state.ts +33 -6
  52. package/template/components/data-views/index.ts +37 -9
  53. package/template/components/data-views/list-page-calendar-view.tsx +593 -0
  54. package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
  55. package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
  56. package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
  57. package/template/components/dev-chunk-load-recovery.tsx +41 -0
  58. package/template/components/examples/focused-workflow-showcase.tsx +183 -0
  59. package/template/components/exxat-product-logo.tsx +2 -6
  60. package/template/components/key-metrics.tsx +54 -22
  61. package/template/components/list-hub-board-view.tsx +68 -0
  62. package/template/components/list-hub-client.tsx +186 -0
  63. package/template/components/list-hub-list-view.tsx +36 -0
  64. package/template/components/list-hub-panel-activator.tsx +8 -0
  65. package/template/components/list-hub-secondary-nav.tsx +121 -0
  66. package/template/components/list-hub-table.tsx +336 -0
  67. package/template/components/new-question-composer.tsx +6 -24
  68. package/template/components/product-switcher.tsx +5 -5
  69. package/template/components/product-wordmark.tsx +4 -7
  70. package/template/components/question-bank-client.tsx +4 -1
  71. package/template/components/question-bank-folder-columns-panel.tsx +104 -0
  72. package/template/components/question-bank-hub-client.tsx +2 -5
  73. package/template/components/question-bank-table.tsx +155 -509
  74. package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
  75. package/template/components/secondary-panel.tsx +4 -44
  76. package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
  77. package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
  78. package/template/components/secondary-panels/registry.tsx +15 -0
  79. package/template/components/settings-appearance-card.tsx +3 -2
  80. package/template/components/settings-client.tsx +59 -15
  81. package/template/components/settings-form-row.tsx +9 -4
  82. package/template/components/sidebar-shell.tsx +2 -1
  83. package/template/components/table-properties/drawer-button.tsx +51 -20
  84. package/template/components/table-properties/drawer.tsx +81 -17
  85. package/template/components/templates/focused-workflow-layouts.tsx +448 -0
  86. package/template/components/templates/focused-workflow-page-template.tsx +69 -0
  87. package/template/components/templates/list-page.tsx +40 -13
  88. package/template/components/templates/nested-secondary-panel-shell.tsx +3 -2
  89. package/template/components/templates/page-loading-shell.tsx +262 -0
  90. package/template/components/ui/button-group.tsx +1 -0
  91. package/template/contexts/product-context.tsx +21 -2
  92. package/template/docs/consumer-app-pattern.md +39 -0
  93. package/template/docs/data-views-pattern.md +42 -3
  94. package/template/docs/drawer-vs-dialog-pattern.md +3 -1
  95. package/template/docs/focused-workflow-page-pattern.md +84 -0
  96. package/template/docs/kpi-flat-band-pattern.md +57 -0
  97. package/template/docs/kpi-strip-max-four-pattern.md +1 -0
  98. package/template/docs/shell-surface-elevation-pattern.md +54 -0
  99. package/template/lib/chunk-load-error.ts +13 -0
  100. package/template/lib/command-menu-search-data.ts +11 -27
  101. package/template/lib/conditional-rule-match.ts +87 -22
  102. package/template/lib/data-list-display-options.ts +16 -2
  103. package/template/lib/data-list-view-registry.ts +104 -0
  104. package/template/lib/data-list-view-surface.ts +15 -1
  105. package/template/lib/data-list-view.ts +16 -1
  106. package/template/lib/data-view-dashboard-storage.ts +38 -35
  107. package/template/lib/hub-connected-view-renderers.ts +58 -0
  108. package/template/lib/list-hub-nav.ts +121 -0
  109. package/template/lib/list-hub-supported-views.ts +10 -0
  110. package/template/lib/list-page-table-properties.ts +3 -7
  111. package/template/lib/list-status-badges.ts +4 -97
  112. package/template/lib/mock/list-hub-directory.ts +27 -0
  113. package/template/lib/mock/list-hub-kpi.ts +27 -0
  114. package/template/lib/mock/navigation.tsx +1 -0
  115. package/template/lib/page-loading-variant.ts +40 -0
  116. package/template/lib/question-bank-supported-views.ts +13 -0
  117. package/template/lib/sidebar-state-cookie.ts +9 -0
  118. package/template/lib/table-state-lifecycle.ts +60 -13
  119. package/template/app/(app)/data-list/[id]/page.tsx +0 -44
  120. package/template/app/(app)/data-list/new/page.tsx +0 -34
  121. package/template/components/compliance-board-view.tsx +0 -142
  122. package/template/components/compliance-client.tsx +0 -92
  123. package/template/components/compliance-list-view.tsx +0 -54
  124. package/template/components/compliance-page-header.tsx +0 -89
  125. package/template/components/compliance-table.tsx +0 -632
  126. package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
  127. package/template/components/data-view-dashboard-charts-team.tsx +0 -971
  128. package/template/components/data-view-dashboard-charts.tsx +0 -1503
  129. package/template/components/new-placement-back-btn.tsx +0 -28
  130. package/template/components/new-placement-form.tsx +0 -1068
  131. package/template/components/placement-board-card.tsx +0 -262
  132. package/template/components/placement-detail.tsx +0 -438
  133. package/template/components/placements-board-view.tsx +0 -404
  134. package/template/components/placements-client.tsx +0 -252
  135. package/template/components/placements-list-view.tsx +0 -171
  136. package/template/components/placements-page-header.tsx +0 -166
  137. package/template/components/placements-table-cells.test.tsx +0 -22
  138. package/template/components/placements-table-cells.tsx +0 -173
  139. package/template/components/placements-table-columns.tsx +0 -640
  140. package/template/components/placements-table.tsx +0 -1675
  141. package/template/components/rotations-empty-state.tsx +0 -50
  142. package/template/components/rotations-panel-activator.tsx +0 -8
  143. package/template/components/sites-all-client.tsx +0 -154
  144. package/template/components/sites-board-view.tsx +0 -67
  145. package/template/components/sites-list-view.tsx +0 -42
  146. package/template/components/sites-table.tsx +0 -402
  147. package/template/components/team-board-view.tsx +0 -122
  148. package/template/components/team-client.tsx +0 -100
  149. package/template/components/team-list-view.tsx +0 -59
  150. package/template/components/team-page-header.tsx +0 -92
  151. package/template/components/team-table.tsx +0 -714
  152. package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
  153. package/template/lib/mock/compliance-kpi.ts +0 -61
  154. package/template/lib/mock/compliance.ts +0 -146
  155. package/template/lib/mock/placements-kpi.ts +0 -134
  156. package/template/lib/mock/placements.ts +0 -183
  157. package/template/lib/mock/sites-directory.ts +0 -16
  158. package/template/lib/mock/sites-kpi.ts +0 -25
  159. package/template/lib/mock/team-kpi.ts +0 -60
  160. package/template/lib/mock/team.ts +0 -118
  161. package/template/lib/placement-board-card-layout.ts +0 -79
  162. package/template/lib/placement-lifecycle.ts +0 -5
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Centralized localStorage for **Data** view dashboard canvas (Placements, Team, Compliance).
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
- placements: "exxat-dashboard-cards",
13
- team: "exxat-team-dashboard-cards",
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
- export type DataViewScope = "placements" | "team" | "compliance"
16
+ /** @deprecated Legacy scopes still migrated from the bundle. */
17
+ const LEGACY_SCOPES = ["placements", "team", "compliance"] as const
18
18
 
19
- type LayoutBundle = Partial<Record<DataViewScope, DashboardLayoutV1>>
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 writeBundle(bundle: LayoutBundle) {
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
- /* ignore quota */
61
+ /* quota / private mode */
60
62
  }
61
63
  }
62
64
 
63
- /**
64
- * Merge any missing scopes from legacy keys into the bundle (one-time per scope per session edge cases OK).
65
- */
66
- function ensureBundleWithLegacy(): LayoutBundle {
67
- let bundle = readBundleRaw()
68
- let changed = false
69
- for (const scope of ["placements", "team", "compliance"] as const) {
70
- if (bundle[scope]) continue
71
- try {
72
- const raw = localStorage.getItem(LEGACY_KEYS[scope])
73
- if (!raw) continue
74
- const layout = parseLayout(JSON.parse(raw) as unknown)
75
- if (layout) {
76
- bundle = { ...bundle, [scope]: layout }
77
- changed = true
78
- }
79
- } catch {
80
- /* ignore */
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
- if (changed) writeBundle(bundle)
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 (Placements / Team / Compliance Data view).
92
+ * Load persisted layout for a hub Data view.
89
93
  */
90
94
  export function loadDataViewLayout(scope: DataViewScope): DashboardLayoutV1 | null {
91
- const bundle = ensureBundleWithLegacy()
92
- return bundle[scope] ?? null
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 = ensureBundleWithLegacy()
100
- writeBundle({ ...bundle, [scope]: layout })
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
- * (Placements, Team, Compliance — table, list, board), plus related chips
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`, or
9
- * **`StatusBadge`** from **`components/placements-table-cells.tsx`** for placement rows (wrapper
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 — e.g. Placements “Under review”). */
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
+ }
@@ -138,6 +138,7 @@ export const NAV_PRIMARY: NavLinkItem[] = [
138
138
  icon: <i className="fa-light fa-table" aria-hidden="true" />,
139
139
  iconActive: <i className="fa-solid fa-table" aria-hidden="true" />,
140
140
  badge: 24,
141
+ secondaryPanel: "list-hub",
141
142
  },
142
143
  ]
143
144
 
@@ -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
+ }