@exxatdesignux/ui 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +701 -6
- package/README.md +138 -0
- package/bin/init.mjs +134 -31
- package/consumer-extras/cursor-rules/exxat-board-cards.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-centralized-list-dataset.mdc +2 -2
- package/consumer-extras/cursor-rules/exxat-collaboration-access.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-data-tables.mdc +2 -0
- package/consumer-extras/cursor-rules/exxat-dedicated-search-surfaces.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +3 -3
- package/consumer-extras/cursor-rules/exxat-library-hub-header.mdc +28 -0
- package/consumer-extras/cursor-rules/exxat-mono-ids.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-person-identity-display.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-primary-nav-secondary-panel.mdc +6 -6
- package/consumer-extras/cursor-rules/exxat-reuse-before-custom.mdc +1 -1
- package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +2 -2
- package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -3
- package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +2 -2
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +7 -7
- package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-list-page-view-shells/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +4 -4
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +8 -8
- package/consumer-extras/cursor-skills/exxat-token-economy/SKILL.md +277 -0
- package/consumer-extras/handbook/HANDBOOK.md +2 -0
- package/consumer-extras/handbook/glossary.md +2 -1
- package/consumer-extras/handbook/reference-implementations.md +31 -4
- package/consumer-extras/patterns/collaboration-access-pattern.md +7 -7
- package/consumer-extras/patterns/data-views-pattern.md +18 -16
- package/consumer-extras/patterns/kpi-flat-band-pattern.md +2 -2
- package/dist/components/data-table/index.js +2 -2
- package/dist/components/data-table/index.js.map +1 -1
- package/dist/components/data-table/pagination.js +3 -3
- package/dist/components/data-table/pagination.js.map +1 -1
- package/dist/components/data-table/use-table-state.d.ts +1 -1
- package/dist/components/data-table/use-table-state.js.map +1 -1
- package/dist/components/data-views/data-row-list.js.map +1 -1
- package/dist/components/data-views/finder-panel-view.d.ts +1 -1
- package/dist/components/data-views/finder-panel-view.js.map +1 -1
- package/dist/components/data-views/hub-table.d.ts +9 -3
- package/dist/components/data-views/hub-table.js +262 -40
- package/dist/components/data-views/hub-table.js.map +1 -1
- package/dist/components/data-views/index.js +262 -40
- package/dist/components/data-views/index.js.map +1 -1
- package/dist/components/data-views/list-page-split-hub-tokens.d.ts +2 -2
- package/dist/components/data-views/list-page-split-hub-tokens.js.map +1 -1
- package/dist/components/data-views/list-page-tree-column-header.d.ts +1 -1
- package/dist/components/data-views/list-page-tree-column-header.js.map +1 -1
- package/dist/components/data-views/list-page-tree-panel-shell.js.map +1 -1
- package/dist/components/data-views/os-folder-glyph.d.ts +1 -1
- package/dist/components/data-views/os-folder-glyph.js.map +1 -1
- package/dist/components/ui/avatar.d.ts +1 -1
- package/dist/components/ui/key-metrics.js.map +1 -1
- package/dist/index.js +136 -39
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/components/data-table/index.tsx +2 -2
- package/src/components/data-table/pagination.tsx +5 -1
- package/src/components/data-table/use-table-state.ts +1 -1
- package/src/components/data-views/data-row-list.tsx +1 -1
- package/src/components/data-views/finder-panel-view.tsx +2 -2
- package/src/components/data-views/hub-table.tsx +149 -41
- package/src/components/data-views/list-page-split-hub-tokens.ts +2 -2
- package/src/components/data-views/list-page-tree-column-header.tsx +1 -1
- package/src/components/data-views/os-folder-glyph.tsx +1 -1
- package/src/components/ui/key-metrics.tsx +1 -1
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +8 -7
- package/template/.cursor/rules/exxat-accessibility.mdc +1 -1
- package/template/.cursor/rules/exxat-command-menu.mdc +1 -1
- package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +6 -6
- package/template/.cursor/rules/exxat-data-tables.mdc +3 -3
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +5 -5
- package/template/.cursor/rules/exxat-mono-ids.mdc +1 -1
- package/template/.cursor/rules/exxat-page-vs-drawer.mdc +1 -1
- package/template/.cursor/rules/exxat-table-properties-drawer.mdc +1 -1
- package/template/AGENTS.md +43 -37
- package/template/app/(app)/columns/page.tsx +11 -0
- package/template/app/(app)/library/all/page.tsx +11 -0
- package/template/app/(app)/library/find/page.tsx +12 -0
- package/template/app/(app)/{question-bank → library}/layout.tsx +16 -16
- package/template/app/(app)/library/list/page.tsx +12 -0
- package/template/app/(app)/{question-bank → library}/new/page.tsx +10 -10
- package/template/app/(app)/library/page.tsx +11 -0
- package/template/app/(app)/tokens-themes/page.tsx +11 -0
- package/template/components/ask-leo-composer.tsx +2 -2
- package/template/components/columns-client.tsx +158 -0
- package/template/components/columns-showcase.tsx +541 -0
- package/template/components/data-views/index.ts +32 -6
- package/template/components/data-views/{question-bank-folder-tree-branch.tsx → library-folder-tree-branch.tsx} +19 -19
- package/template/components/data-views/table-cells.tsx +673 -0
- package/template/components/folder-details-shell.tsx +11 -11
- package/template/components/hub-tree-panel-view.tsx +24 -24
- package/template/components/{question-bank-board-view.tsx → library-board-view.tsx} +44 -44
- package/template/components/{question-bank-client.tsx → library-client.tsx} +82 -82
- package/template/components/{question-bank-dashboard-charts.tsx → library-dashboard-charts.tsx} +14 -14
- package/template/components/{question-bank-favorite-button.tsx → library-favorite-button.tsx} +7 -7
- package/template/components/{question-bank-hub-client.tsx → library-hub-client.tsx} +43 -43
- package/template/components/{question-bank-new-folder-sheet.tsx → library-new-folder-sheet.tsx} +14 -14
- package/template/components/{question-bank-os-folder-view.tsx → library-os-folder-view.tsx} +31 -31
- package/template/components/{question-bank-page-header.tsx → library-page-header.tsx} +6 -6
- package/template/components/library-panel-activator.tsx +8 -0
- package/template/components/{question-bank-secondary-nav.tsx → library-secondary-nav.tsx} +60 -60
- package/template/components/{question-bank-table.tsx → library-table.tsx} +97 -97
- package/template/components/list-hub-status-badge.tsx +2 -2
- package/template/components/{new-question-composer.tsx → new-library-item-form.tsx} +37 -37
- package/template/components/sidebar/app-sidebar.tsx +61 -5
- package/template/components/sidebar/secondary-panel.tsx +109 -56
- package/template/components/sidebar/sidebar-auto-collapse.tsx +2 -2
- package/template/components/sidebar/sidebar-auto-open.tsx +2 -1
- package/template/components/table-properties/types.ts +1 -1
- package/template/components/templates/discovery-hub-template.tsx +1 -1
- package/template/components/templates/new-focus-template.tsx +2 -2
- package/template/components/templates/secondary-panel-hub-template.tsx +1 -1
- package/template/components/tokens-secondary-nav.tsx +192 -0
- package/template/components/tokens-themes-client.tsx +476 -0
- package/template/components/tokens-themes-section.tsx +386 -0
- package/template/docs/HANDBOOK.md +187 -0
- package/template/docs/blueprints/README.md +1 -1
- package/template/docs/blueprints/board-card.md +1 -1
- package/template/docs/blueprints/data-table.md +2 -2
- package/template/docs/blueprints/list-page-template.md +3 -3
- package/template/docs/blueprints/page-header.md +4 -4
- package/template/docs/collaboration-access-pattern.md +7 -7
- package/template/docs/component-selection-guide.md +1 -1
- package/template/docs/data-views-pattern.md +18 -16
- package/template/docs/glossary.md +58 -0
- package/template/docs/kpi-flat-band-pattern.md +3 -3
- package/template/docs/kpi-trend-pattern.md +18 -3
- package/template/docs/large-dataset-strategy.md +155 -0
- package/template/docs/library-hub-header-pattern.md +25 -0
- package/template/docs/migrations/_template.md +1 -1
- package/template/docs/reference-implementations.md +151 -0
- package/template/docs/token-taxonomy.md +1 -1
- package/template/docs/voice-and-tone.md +262 -0
- package/template/eslint.config.mjs +9 -39
- package/template/hooks/use-secondary-panel-hub-nav.ts +10 -10
- package/template/lib/ask-leo-route-context.ts +6 -18
- package/template/lib/coach-mark-registry.ts +0 -16
- package/template/lib/command-menu-config.ts +5 -12
- package/template/lib/command-menu-search-data.ts +8 -39
- package/template/lib/{question-bank-authoring.ts → library-authoring.ts} +89 -88
- package/template/lib/library-dedicated-search.ts +19 -0
- package/template/lib/library-hub-search.ts +90 -0
- package/template/lib/library-nav.ts +477 -0
- package/template/lib/library-recent-searches.ts +22 -0
- package/template/lib/{placements-supported-views.ts → library-supported-views.ts} +2 -2
- package/template/lib/list-status-badges.ts +16 -104
- package/template/lib/mock/dashboard.ts +1 -1
- package/template/lib/mock/{question-bank-folders.ts → library-folders.ts} +30 -30
- package/template/lib/mock/library-header-collaborators.ts +54 -0
- package/template/lib/mock/{question-bank-inspector.ts → library-inspector.ts} +29 -29
- package/template/lib/mock/{question-bank-kpi.ts → library-kpi.ts} +20 -20
- package/template/lib/mock/library.ts +249 -0
- package/template/lib/mock/navigation.tsx +32 -26
- package/template/lib/table-state-lifecycle.ts +1 -1
- package/template/next.config.mjs +7 -4
- package/template/package.json +0 -1
- package/tokens/hooks-index.json +2874 -0
- package/consumer-extras/cursor-rules/exxat-question-bank-hub-header.mdc +0 -28
- package/template/app/(app)/examples/page.tsx +0 -41
- package/template/app/(app)/question-bank/find/page.tsx +0 -12
- package/template/app/(app)/question-bank/library/page.tsx +0 -11
- package/template/app/(app)/question-bank/list/page.tsx +0 -12
- package/template/app/(app)/question-bank/page.tsx +0 -11
- package/template/components/compliance-board-view.tsx +0 -142
- package/template/components/compliance-client.tsx +0 -92
- package/template/components/compliance-page-header.tsx +0 -89
- package/template/components/compliance-table.tsx +0 -468
- package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
- package/template/components/data-view-dashboard-charts-team.tsx +0 -971
- package/template/components/data-view-dashboard-charts.tsx +0 -1503
- package/template/components/new-placement-back-btn.tsx +0 -28
- package/template/components/new-placement-form.tsx +0 -942
- package/template/components/placement-board-card.tsx +0 -250
- package/template/components/placement-detail.tsx +0 -438
- package/template/components/placements-board-view.tsx +0 -397
- package/template/components/placements-client.tsx +0 -220
- package/template/components/placements-list-view.tsx +0 -124
- package/template/components/placements-page-header.tsx +0 -166
- package/template/components/placements-table-cells.test.tsx +0 -22
- package/template/components/placements-table-cells.tsx +0 -173
- package/template/components/placements-table-columns.tsx +0 -210
- package/template/components/placements-table.tsx +0 -934
- package/template/components/question-bank-panel-activator.tsx +0 -8
- package/template/components/rotations-empty-state.tsx +0 -50
- package/template/components/rotations-panel-activator.tsx +0 -8
- package/template/components/sites-board-view.tsx +0 -67
- package/template/components/sites-client.tsx +0 -154
- package/template/components/sites-table.tsx +0 -249
- package/template/components/team-board-view.tsx +0 -122
- package/template/components/team-client.tsx +0 -100
- package/template/components/team-page-header.tsx +0 -92
- package/template/components/team-table.tsx +0 -553
- package/template/docs/question-bank-hub-header-pattern.md +0 -25
- package/template/lib/compliance-supported-views.ts +0 -10
- package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
- package/template/lib/mock/compliance-kpi.ts +0 -61
- package/template/lib/mock/compliance.ts +0 -146
- package/template/lib/mock/placements-kpi.ts +0 -134
- package/template/lib/mock/placements.ts +0 -176
- package/template/lib/mock/question-bank-header-collaborators.ts +0 -54
- package/template/lib/mock/question-bank.ts +0 -249
- package/template/lib/mock/sites-directory.ts +0 -16
- package/template/lib/mock/sites-kpi.ts +0 -25
- package/template/lib/mock/team-kpi.ts +0 -60
- package/template/lib/mock/team.ts +0 -118
- package/template/lib/placement-board-card-layout.ts +0 -79
- package/template/lib/question-bank-dedicated-search.ts +0 -19
- package/template/lib/question-bank-hub-search.ts +0 -90
- package/template/lib/question-bank-nav.ts +0 -477
- package/template/lib/question-bank-recent-searches.ts +0 -22
- package/template/lib/question-bank-supported-views.ts +0 -12
- package/template/lib/sites-supported-views.ts +0 -10
- package/template/lib/team-supported-views.ts +0 -10
|
@@ -1,477 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Question bank secondary nav + URL scope — demo “My” matches mock author rows.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { QuestionBankItem } from "@/lib/mock/question-bank"
|
|
6
|
-
import type { QuestionBankFolder } from "@/lib/mock/question-bank-folders"
|
|
7
|
-
import { collectFolderDescendantIds } from "@/lib/mock/question-bank-folders"
|
|
8
|
-
|
|
9
|
-
/** Demo curator — “My questions” filters `author` / `createdBy` to this value. */
|
|
10
|
-
export const QUESTION_BANK_NAV_MY_AUTHOR = "Dr. Chen"
|
|
11
|
-
|
|
12
|
-
export type QuestionBankNavScope = "all" | "my" | "folder"
|
|
13
|
-
|
|
14
|
-
export interface QuestionBankNavState {
|
|
15
|
-
scope: QuestionBankNavScope
|
|
16
|
-
/** Set when `scope === "folder"` */
|
|
17
|
-
folderId: string | null
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const QUESTION_BANK_ENTRY_PATH = "/question-bank"
|
|
21
|
-
|
|
22
|
-
/** Breadcrumb segment for the discovery hub — links from library / search shells back to `/question-bank`. */
|
|
23
|
-
export const QUESTION_BANK_HUB_BREADCRUMB = {
|
|
24
|
-
label: "Question hub",
|
|
25
|
-
href: QUESTION_BANK_ENTRY_PATH,
|
|
26
|
-
} as const
|
|
27
|
-
|
|
28
|
-
/** List hub with secondary nav, views, and table state. */
|
|
29
|
-
export const QUESTION_BANK_LIBRARY_PATH = "/question-bank/library"
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Same hub as the library (table + panel + tree) but intended for search landings (`?q=`).
|
|
33
|
-
* Keeps secondary panel + nav behavior aligned with {@link QUESTION_BANK_LIBRARY_PATH}.
|
|
34
|
-
*/
|
|
35
|
-
export const QUESTION_BANK_LIST_PATH = "/question-bank/list"
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Results from the discovery hub composer — same table stack as {@link QUESTION_BANK_LIST_PATH} but a
|
|
39
|
-
* distinct URL so library “Search” and hub-driven search are not conflated.
|
|
40
|
-
*/
|
|
41
|
-
export const QUESTION_BANK_HUB_FIND_PATH = "/question-bank/find"
|
|
42
|
-
|
|
43
|
-
/** @deprecated Use `QUESTION_BANK_LIBRARY_PATH` for scoped library routes. */
|
|
44
|
-
export const QUESTION_BANK_HUB_PATH = QUESTION_BANK_LIBRARY_PATH
|
|
45
|
-
|
|
46
|
-
export const QUESTION_BANK_LIBRARY_HUB_PATHS: readonly string[] = [
|
|
47
|
-
QUESTION_BANK_LIBRARY_PATH,
|
|
48
|
-
QUESTION_BANK_LIST_PATH,
|
|
49
|
-
QUESTION_BANK_HUB_FIND_PATH,
|
|
50
|
-
]
|
|
51
|
-
|
|
52
|
-
/** Library list search (`/list`) or hub discovery search (`/find`) — both use the dedicated search shell. */
|
|
53
|
-
export function isQuestionBankDedicatedSearchPathname(pathname: string): boolean {
|
|
54
|
-
return pathname === QUESTION_BANK_LIST_PATH || pathname === QUESTION_BANK_HUB_FIND_PATH
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Whether a secondary-nav row (All / My / folder / Search) matches the current URL + parsed nav.
|
|
59
|
-
* Used by `QuestionBankSecondaryNav` and the folder tree branch.
|
|
60
|
-
*/
|
|
61
|
-
export function isQuestionBankNavActive(
|
|
62
|
-
pathname: string,
|
|
63
|
-
nav: QuestionBankNavState,
|
|
64
|
-
scope: QuestionBankNavScope,
|
|
65
|
-
folderId?: string | null,
|
|
66
|
-
): boolean {
|
|
67
|
-
const p = pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname
|
|
68
|
-
if (!QUESTION_BANK_LIBRARY_HUB_PATHS.includes(p)) return false
|
|
69
|
-
if (scope === "all") {
|
|
70
|
-
if (isQuestionBankDedicatedSearchPathname(pathname)) return false
|
|
71
|
-
return nav.scope === "all"
|
|
72
|
-
}
|
|
73
|
-
if (scope === "my") {
|
|
74
|
-
if (isQuestionBankDedicatedSearchPathname(pathname)) return false
|
|
75
|
-
return nav.scope === "my"
|
|
76
|
-
}
|
|
77
|
-
if (scope === "folder" && folderId) {
|
|
78
|
-
return nav.scope === "folder" && nav.folderId === folderId
|
|
79
|
-
}
|
|
80
|
-
return false
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** Default secondary-nav selection — All questions (no `scope` query). */
|
|
84
|
-
export const QUESTION_BANK_DEFAULT_NAV: QuestionBankNavState = {
|
|
85
|
-
scope: "all",
|
|
86
|
-
folderId: null,
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function isQuestionBankDefaultNav(nav: QuestionBankNavState): boolean {
|
|
90
|
-
return nav.scope === "all" && nav.folderId === null
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function parseQuestionBankNav(searchParams: URLSearchParams): QuestionBankNavState {
|
|
94
|
-
const raw = (searchParams.get("scope") ?? "all").toLowerCase()
|
|
95
|
-
if (raw === "my") return { scope: "my", folderId: null }
|
|
96
|
-
if (raw === "folder") {
|
|
97
|
-
const rawId = searchParams.get("folderId") ?? searchParams.get("folder")
|
|
98
|
-
const folderId = typeof rawId === "string" ? rawId.trim() || null : null
|
|
99
|
-
return { scope: "folder", folderId }
|
|
100
|
-
}
|
|
101
|
-
if (raw === "all") return { ...QUESTION_BANK_DEFAULT_NAV }
|
|
102
|
-
return { ...QUESTION_BANK_DEFAULT_NAV }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Rewrite invalid or incomplete scope URLs back to the default All questions hub. Preserves list search params when present. */
|
|
106
|
-
export function questionBankCanonicalNavHref(searchParams: URLSearchParams): string | null {
|
|
107
|
-
const raw = searchParams.get("scope")
|
|
108
|
-
if (!raw) return null
|
|
109
|
-
const lowered = raw.toLowerCase()
|
|
110
|
-
const q = searchParams.get("q")?.trim() || null
|
|
111
|
-
const fav = searchParams.get("fav") === "1"
|
|
112
|
-
const deckClinical = searchParams.get("deck") === "clinical"
|
|
113
|
-
const preserved = {
|
|
114
|
-
q,
|
|
115
|
-
...(fav ? { fav: true as const } : {}),
|
|
116
|
-
...(deckClinical ? { deck: "clinical" as const } : {}),
|
|
117
|
-
}
|
|
118
|
-
if (lowered === "my") return null
|
|
119
|
-
if (lowered === "folder") {
|
|
120
|
-
const folderId = searchParams.get("folderId") ?? searchParams.get("folder")
|
|
121
|
-
return folderId ? null : questionBankNavHref({ scope: "all", ...preserved })
|
|
122
|
-
}
|
|
123
|
-
if (lowered === "all") return null
|
|
124
|
-
return questionBankNavHref({ scope: "all", ...preserved })
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Breadcrumb + title for `SiteHeader` / `PageHeader` (matches secondary nav scopes). */
|
|
128
|
-
export interface QuestionBankHubHeaderModel {
|
|
129
|
-
title: string
|
|
130
|
-
breadcrumbs?: { label: string; href?: string }[]
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Back link for `/question-bank/new` — icon + parent label in `SiteHeader` (`back` prop). */
|
|
134
|
-
export function newQuestionBackNav(
|
|
135
|
-
folders: QuestionBankFolder[],
|
|
136
|
-
folderId?: string,
|
|
137
|
-
): { label: string; href: string } {
|
|
138
|
-
if (folderId) {
|
|
139
|
-
const folder = folders.find(f => f.id === folderId)
|
|
140
|
-
if (folder) {
|
|
141
|
-
return {
|
|
142
|
-
label: folder.name,
|
|
143
|
-
href: questionBankNavHref({ scope: "folder", folderId: folder.id }),
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return {
|
|
148
|
-
label: QUESTION_BANK_HUB_BREADCRUMB.label,
|
|
149
|
-
href: questionBankNavHref({ scope: "all" }),
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Ancestor-only breadcrumbs for `/question-bank/new` (current page is the PageHeader title). */
|
|
154
|
-
export function newQuestionBreadcrumbs(
|
|
155
|
-
folders: QuestionBankFolder[],
|
|
156
|
-
folderId?: string,
|
|
157
|
-
): { label: string; href: string }[] {
|
|
158
|
-
if (folderId) {
|
|
159
|
-
const folder = folders.find(f => f.id === folderId)
|
|
160
|
-
if (folder) {
|
|
161
|
-
return [
|
|
162
|
-
{
|
|
163
|
-
label: folder.name,
|
|
164
|
-
href: questionBankNavHref({ scope: "folder", folderId: folder.id }),
|
|
165
|
-
},
|
|
166
|
-
]
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return [
|
|
170
|
-
{
|
|
171
|
-
label: QUESTION_BANK_HUB_BREADCRUMB.label,
|
|
172
|
-
href: QUESTION_BANK_HUB_BREADCRUMB.href,
|
|
173
|
-
},
|
|
174
|
-
]
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function questionBankHubHeaderModel(
|
|
178
|
-
folders: QuestionBankFolder[],
|
|
179
|
-
nav: QuestionBankNavState,
|
|
180
|
-
): QuestionBankHubHeaderModel {
|
|
181
|
-
if (nav.scope === "my") {
|
|
182
|
-
return {
|
|
183
|
-
breadcrumbs: [{ label: QUESTION_BANK_HUB_BREADCRUMB.label, href: QUESTION_BANK_HUB_BREADCRUMB.href }],
|
|
184
|
-
title: "My questions",
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
if (nav.scope === "folder" && nav.folderId) {
|
|
188
|
-
const name = folders.find(f => f.id === nav.folderId)?.name ?? "Folder"
|
|
189
|
-
return {
|
|
190
|
-
breadcrumbs: [{ label: QUESTION_BANK_HUB_BREADCRUMB.label, href: QUESTION_BANK_HUB_BREADCRUMB.href }],
|
|
191
|
-
title: name,
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return {
|
|
195
|
-
breadcrumbs: [{ label: QUESTION_BANK_HUB_BREADCRUMB.label, href: QUESTION_BANK_HUB_BREADCRUMB.href }],
|
|
196
|
-
title: "All questions",
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function filterQuestionBankItemsByNav(
|
|
201
|
-
items: QuestionBankItem[],
|
|
202
|
-
folders: QuestionBankFolder[],
|
|
203
|
-
nav: QuestionBankNavState,
|
|
204
|
-
): QuestionBankItem[] {
|
|
205
|
-
if (nav.scope === "all") return items
|
|
206
|
-
if (nav.scope === "my") {
|
|
207
|
-
return items.filter(
|
|
208
|
-
i => i.author === QUESTION_BANK_NAV_MY_AUTHOR || i.createdBy === QUESTION_BANK_NAV_MY_AUTHOR,
|
|
209
|
-
)
|
|
210
|
-
}
|
|
211
|
-
if (nav.scope === "folder" && nav.folderId) {
|
|
212
|
-
const allowedFolderIds = collectFolderDescendantIds(folders, nav.folderId)
|
|
213
|
-
return items.filter(i => allowedFolderIds.has(i.folderId))
|
|
214
|
-
}
|
|
215
|
-
return items
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** Mock folder id for the Favorites bucket (see `DEFAULT_QUESTION_BANK_FOLDERS`). */
|
|
219
|
-
export const QUESTION_BANK_FAVORITES_FOLDER_ID = "fld-favorites"
|
|
220
|
-
|
|
221
|
-
/** Root folder id for the mock “Clinical” tree (`fld-skills-lab` is a child). */
|
|
222
|
-
export const QUESTION_BANK_CLINICAL_ROOT_FOLDER_ID = "fld-clinical"
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Client-side “AI / hub” free-text filter — same scan as `useTableState` toolbar search
|
|
226
|
-
* (`Object.values` → string → lowercase `includes`).
|
|
227
|
-
*/
|
|
228
|
-
export function filterQuestionBankItemsByFreeText(items: QuestionBankItem[], q: string): QuestionBankItem[] {
|
|
229
|
-
const t = q.trim()
|
|
230
|
-
if (!t) return items
|
|
231
|
-
const needle = t.toLowerCase()
|
|
232
|
-
return items.filter(row =>
|
|
233
|
-
Object.values(row).some(v => String(v ?? "").toLowerCase().includes(needle)),
|
|
234
|
-
)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export function isQuestionBankItemFavorite(item: QuestionBankItem): boolean {
|
|
238
|
-
return item.folderId === QUESTION_BANK_FAVORITES_FOLDER_ID || item.isStarred === true
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** When a mock row lives in the Favorites folder, toggling off moves it here (no “original folder” yet). */
|
|
242
|
-
const QUESTION_BANK_UNFAVORITE_FALLBACK_FOLDER_ID = "fld-science"
|
|
243
|
-
|
|
244
|
-
/** Demo toggle for `isStarred` / Favorites folder membership (offline mock only). */
|
|
245
|
-
export function toggleQuestionBankItemFavorite(item: QuestionBankItem): QuestionBankItem {
|
|
246
|
-
if (isQuestionBankItemFavorite(item)) {
|
|
247
|
-
if (item.folderId === QUESTION_BANK_FAVORITES_FOLDER_ID) {
|
|
248
|
-
return {
|
|
249
|
-
...item,
|
|
250
|
-
folderId: QUESTION_BANK_UNFAVORITE_FALLBACK_FOLDER_ID,
|
|
251
|
-
isStarred: false,
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return { ...item, isStarred: false }
|
|
255
|
-
}
|
|
256
|
-
return { ...item, isStarred: true }
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export function filterQuestionBankItemsByFavoritesOnly(items: QuestionBankItem[]): QuestionBankItem[] {
|
|
260
|
-
return items.filter(isQuestionBankItemFavorite)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Mock “Clinical deck” (`deck=clinical`): items under the Clinical folder tree **or**
|
|
265
|
-
* demo topics “Clinical skills” / “Neurology” (so list landings can show a coherent slice).
|
|
266
|
-
*/
|
|
267
|
-
export function filterQuestionBankItemsByClinicalDeckMock(
|
|
268
|
-
items: QuestionBankItem[],
|
|
269
|
-
folders: QuestionBankFolder[],
|
|
270
|
-
): QuestionBankItem[] {
|
|
271
|
-
const inClinicalTree = collectFolderDescendantIds(folders, QUESTION_BANK_CLINICAL_ROOT_FOLDER_ID)
|
|
272
|
-
const topicMatch = new Set(["Clinical skills", "Neurology"])
|
|
273
|
-
return items.filter(
|
|
274
|
-
i => inClinicalTree.has(i.folderId) || topicMatch.has(i.topic),
|
|
275
|
-
)
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export type QuestionBankLandingFilterState = {
|
|
279
|
-
hubFreeText: string
|
|
280
|
-
favOnly: boolean
|
|
281
|
-
clinicalDeck: boolean
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Nav scope + optional dedicated search routes (`/question-bank/list`, `/question-bank/find`) landing filters (`q`, `fav`, `deck`).
|
|
286
|
-
* When `hubFreeText` is non-empty but matches no rows, hub text is ignored so the scoped list still shows.
|
|
287
|
-
*/
|
|
288
|
-
export function applyQuestionBankHubDisplayFilters(
|
|
289
|
-
items: QuestionBankItem[],
|
|
290
|
-
folders: QuestionBankFolder[],
|
|
291
|
-
nav: QuestionBankNavState,
|
|
292
|
-
landing: QuestionBankLandingFilterState | null,
|
|
293
|
-
): QuestionBankItem[] {
|
|
294
|
-
let rows = filterQuestionBankItemsByNav(items, folders, nav)
|
|
295
|
-
if (!landing) return rows
|
|
296
|
-
let afterText = filterQuestionBankItemsByFreeText(rows, landing.hubFreeText)
|
|
297
|
-
if (landing.hubFreeText.trim() && afterText.length === 0) {
|
|
298
|
-
afterText = rows
|
|
299
|
-
}
|
|
300
|
-
rows = afterText
|
|
301
|
-
if (landing.favOnly) rows = filterQuestionBankItemsByFavoritesOnly(rows)
|
|
302
|
-
if (landing.clinicalDeck) rows = filterQuestionBankItemsByClinicalDeckMock(rows, folders)
|
|
303
|
-
return rows
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/** True when hub `q` is non-empty but matches no rows under the current nav (before fav/deck). */
|
|
307
|
-
export function questionBankHubTextMatchesNothing(
|
|
308
|
-
items: QuestionBankItem[],
|
|
309
|
-
folders: QuestionBankFolder[],
|
|
310
|
-
nav: QuestionBankNavState,
|
|
311
|
-
landing: QuestionBankLandingFilterState | null,
|
|
312
|
-
): boolean {
|
|
313
|
-
if (!landing?.hubFreeText.trim()) return false
|
|
314
|
-
const navRows = filterQuestionBankItemsByNav(items, folders, nav)
|
|
315
|
-
return filterQuestionBankItemsByFreeText(navRows, landing.hubFreeText).length === 0
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
export function patchQuestionBankUrlSearchParams(
|
|
319
|
-
sp: URLSearchParams,
|
|
320
|
-
patch: {
|
|
321
|
-
q?: string | null
|
|
322
|
-
fav?: boolean | null
|
|
323
|
-
deckClinical?: boolean | null
|
|
324
|
-
},
|
|
325
|
-
): URLSearchParams {
|
|
326
|
-
const next = new URLSearchParams(sp.toString())
|
|
327
|
-
if (patch.q !== undefined) {
|
|
328
|
-
const t = patch.q?.trim() ?? ""
|
|
329
|
-
if (t) next.set("q", t)
|
|
330
|
-
else next.delete("q")
|
|
331
|
-
}
|
|
332
|
-
if (patch.fav !== undefined) {
|
|
333
|
-
if (patch.fav) next.set("fav", "1")
|
|
334
|
-
else next.delete("fav")
|
|
335
|
-
}
|
|
336
|
-
if (patch.deckClinical !== undefined) {
|
|
337
|
-
if (patch.deckClinical) next.set("deck", "clinical")
|
|
338
|
-
else next.delete("deck")
|
|
339
|
-
}
|
|
340
|
-
return next
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/** Build {@link QUESTION_BANK_LIST_PATH} query consistently (nav + hub search + mock toggles). */
|
|
344
|
-
export function questionBankListSearchHref(
|
|
345
|
-
nav: QuestionBankNavState,
|
|
346
|
-
opts: { q?: string | null; fav?: boolean; deckClinical?: boolean },
|
|
347
|
-
): string {
|
|
348
|
-
return questionBankNavHref({
|
|
349
|
-
scope: nav.scope,
|
|
350
|
-
folderId: nav.folderId,
|
|
351
|
-
searchLanding: true,
|
|
352
|
-
q: opts.q,
|
|
353
|
-
fav: opts.fav,
|
|
354
|
-
deck: opts.deckClinical ? "clinical" : undefined,
|
|
355
|
-
})
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/** Favorites bucket — same folder scope on library, list, or hub-find; preserves `q` / mock toggles on dedicated search routes. */
|
|
359
|
-
export function questionBankFavoritesFolderHref(pathname: string, currentSearch: URLSearchParams): string {
|
|
360
|
-
const onList = pathname === QUESTION_BANK_LIST_PATH
|
|
361
|
-
const onHubFind = pathname === QUESTION_BANK_HUB_FIND_PATH
|
|
362
|
-
const q = currentSearch.get("q")
|
|
363
|
-
const fav = currentSearch.get("fav") === "1"
|
|
364
|
-
const deckClinical = currentSearch.get("deck") === "clinical"
|
|
365
|
-
return questionBankNavHref({
|
|
366
|
-
scope: "folder",
|
|
367
|
-
folderId: QUESTION_BANK_FAVORITES_FOLDER_ID,
|
|
368
|
-
...(onList ? { searchLanding: true } : onHubFind ? { hubFind: true } : {}),
|
|
369
|
-
q,
|
|
370
|
-
...(fav ? { fav: true as const } : {}),
|
|
371
|
-
...(deckClinical ? { deck: "clinical" as const } : {}),
|
|
372
|
-
})
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/** “Search” secondary nav — {@link QUESTION_BANK_LIST_PATH} without `?q=` (always the search landing). Preserves `fav` / `deck` when set. */
|
|
376
|
-
export function questionBankSearchLandingNavHref(
|
|
377
|
-
nav: QuestionBankNavState,
|
|
378
|
-
currentSearch: URLSearchParams,
|
|
379
|
-
): string {
|
|
380
|
-
const listNav: QuestionBankNavState =
|
|
381
|
-
nav.scope === "folder" ? QUESTION_BANK_DEFAULT_NAV : nav
|
|
382
|
-
return questionBankListSearchHref(listNav, {
|
|
383
|
-
fav: currentSearch.get("fav") === "1" ? true : undefined,
|
|
384
|
-
deckClinical: currentSearch.get("deck") === "clinical" ? true : undefined,
|
|
385
|
-
})
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/** True when the dedicated search shell should show the “Search” nav row as current (not browsing a folder there). */
|
|
389
|
-
export function isQuestionBankSearchNavActive(pathname: string, nav: QuestionBankNavState): boolean {
|
|
390
|
-
if (pathname !== QUESTION_BANK_LIST_PATH && pathname !== QUESTION_BANK_HUB_FIND_PATH) return false
|
|
391
|
-
if (nav.scope === "folder" && nav.folderId) return false
|
|
392
|
-
return true
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Hub scope link that preserves list-only params (`q`, `fav`, `deck`) when the user is already
|
|
397
|
-
* on {@link QUESTION_BANK_LIST_PATH}; folder / “My” targets use the library path per product rule.
|
|
398
|
-
*/
|
|
399
|
-
export function questionBankHubScopeHref(
|
|
400
|
-
pathname: string,
|
|
401
|
-
currentSearch: URLSearchParams,
|
|
402
|
-
patch: { scope: QuestionBankNavScope; folderId?: string | null },
|
|
403
|
-
): string {
|
|
404
|
-
const onList = pathname === QUESTION_BANK_LIST_PATH
|
|
405
|
-
const onHubFind = pathname === QUESTION_BANK_HUB_FIND_PATH
|
|
406
|
-
const q = currentSearch.get("q")
|
|
407
|
-
const fav = currentSearch.get("fav") === "1"
|
|
408
|
-
const deckClinical = currentSearch.get("deck") === "clinical"
|
|
409
|
-
const landingBits = {
|
|
410
|
-
q,
|
|
411
|
-
...(fav ? { fav: true as const } : {}),
|
|
412
|
-
...(deckClinical ? { deck: "clinical" as const } : {}),
|
|
413
|
-
}
|
|
414
|
-
if (patch.scope === "my" || (patch.scope === "folder" && patch.folderId)) {
|
|
415
|
-
return questionBankNavHref({
|
|
416
|
-
scope: patch.scope,
|
|
417
|
-
folderId: patch.folderId,
|
|
418
|
-
searchLanding: false,
|
|
419
|
-
})
|
|
420
|
-
}
|
|
421
|
-
if (patch.scope === "all") {
|
|
422
|
-
return questionBankNavHref({
|
|
423
|
-
scope: "all",
|
|
424
|
-
...(onList ? { searchLanding: true } : onHubFind ? { hubFind: true } : {}),
|
|
425
|
-
...landingBits,
|
|
426
|
-
})
|
|
427
|
-
}
|
|
428
|
-
return questionBankNavHref({ scope: "all", searchLanding: false })
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/** Build `/question-bank` href with optional query + hash (hash without leading `#`). */
|
|
432
|
-
export function questionBankNavHref(opts: {
|
|
433
|
-
scope: QuestionBankNavScope
|
|
434
|
-
folderId?: string | null
|
|
435
|
-
hash?: string
|
|
436
|
-
/**
|
|
437
|
-
* Hub / panel search string (`?q=`). On {@link QUESTION_BANK_LIST_PATH} and {@link QUESTION_BANK_HUB_FIND_PATH}
|
|
438
|
-
* this filters rows via {@link applyQuestionBankHubDisplayFilters} — **not** the DataTable toolbar
|
|
439
|
-
* (toolbar stays independent on those routes).
|
|
440
|
-
*/
|
|
441
|
-
q?: string | null
|
|
442
|
-
/** Mock list filter: `fav=1` — favorites folder or `isStarred` rows. */
|
|
443
|
-
fav?: boolean
|
|
444
|
-
/** Mock list filter: `deck=clinical` — see {@link filterQuestionBankItemsByClinicalDeckMock}. */
|
|
445
|
-
deck?: "clinical" | null
|
|
446
|
-
/**
|
|
447
|
-
* When true, links use {@link QUESTION_BANK_LIST_PATH} (library “Search” in secondary nav).
|
|
448
|
-
* Scoped folder / mine links should use `searchLanding: false` (library path).
|
|
449
|
-
*/
|
|
450
|
-
searchLanding?: boolean
|
|
451
|
-
/**
|
|
452
|
-
* When true, links use {@link QUESTION_BANK_HUB_FIND_PATH} (discovery hub composer submit).
|
|
453
|
-
* Do not combine with `searchLanding` — hub find wins if both are set.
|
|
454
|
-
*/
|
|
455
|
-
hubFind?: boolean
|
|
456
|
-
}): string {
|
|
457
|
-
const base =
|
|
458
|
-
opts.hubFind === true
|
|
459
|
-
? QUESTION_BANK_HUB_FIND_PATH
|
|
460
|
-
: opts.searchLanding === true
|
|
461
|
-
? QUESTION_BANK_LIST_PATH
|
|
462
|
-
: QUESTION_BANK_LIBRARY_PATH
|
|
463
|
-
const sp = new URLSearchParams()
|
|
464
|
-
if (opts.scope === "my") sp.set("scope", "my")
|
|
465
|
-
if (opts.scope === "folder" && opts.folderId) {
|
|
466
|
-
sp.set("scope", "folder")
|
|
467
|
-
sp.set("folderId", opts.folderId)
|
|
468
|
-
}
|
|
469
|
-
const trimmedQ = opts.q?.trim()
|
|
470
|
-
if (trimmedQ) sp.set("q", trimmedQ)
|
|
471
|
-
if (opts.fav) sp.set("fav", "1")
|
|
472
|
-
if (opts.deck === "clinical") sp.set("deck", "clinical")
|
|
473
|
-
const qs = sp.toString()
|
|
474
|
-
const h = opts.hash?.replace(/^#/, "")
|
|
475
|
-
const hashPart = h ? `#${h}` : ""
|
|
476
|
-
return qs ? `${base}?${qs}${hashPart}` : `${base}${hashPart}`
|
|
477
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { createDedicatedSearchRecentsController } from "@/lib/dedicated-search-recents"
|
|
2
|
-
|
|
3
|
-
const controller = createDedicatedSearchRecentsController("question-bank", {
|
|
4
|
-
storageKey: "exxat-ds.question-bank.recent-searches.v1",
|
|
5
|
-
eventName: "exxat-question-bank-recent-searches",
|
|
6
|
-
})
|
|
7
|
-
|
|
8
|
-
export const QUESTION_BANK_RECENT_SEARCHES_EVENT = controller.eventName
|
|
9
|
-
|
|
10
|
-
export const questionBankDedicatedSearchRecents = controller
|
|
11
|
-
|
|
12
|
-
export function readQuestionBankRecentSearches(): string[] {
|
|
13
|
-
return controller.read()
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function recordQuestionBankRecentSearch(query: string): void {
|
|
17
|
-
controller.record(query)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function clearQuestionBankRecentSearches(): void {
|
|
21
|
-
controller.clear()
|
|
22
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
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
|
-
"folder",
|
|
10
|
-
"panel",
|
|
11
|
-
"tree-panel",
|
|
12
|
-
] as const satisfies readonly DataListViewType[]
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { DataListViewType } from "@/lib/data-list-view"
|
|
2
|
-
|
|
3
|
-
/** Views implemented in `SitesTable` — keep in sync with the renderers passed to `HubTable`. */
|
|
4
|
-
export const SITES_SUPPORTED_VIEWS = [
|
|
5
|
-
"table",
|
|
6
|
-
"list",
|
|
7
|
-
"board",
|
|
8
|
-
"panel",
|
|
9
|
-
"dashboard",
|
|
10
|
-
] as const satisfies readonly DataListViewType[]
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { DataListViewType } from "@/lib/data-list-view"
|
|
2
|
-
|
|
3
|
-
/** Views implemented in `TeamTable` — keep in sync with the renderers passed to `HubTable`. */
|
|
4
|
-
export const TEAM_SUPPORTED_VIEWS = [
|
|
5
|
-
"table",
|
|
6
|
-
"list",
|
|
7
|
-
"board",
|
|
8
|
-
"panel",
|
|
9
|
-
"dashboard",
|
|
10
|
-
] as const satisfies readonly DataListViewType[]
|