@exxatdesignux/ui 0.2.9 → 0.2.11

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 (126) hide show
  1. package/consumer-extras/cursor-skills/exxat-card-vs-list-rows/SKILL.md +20 -0
  2. package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +33 -0
  3. package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +45 -0
  4. package/consumer-extras/cursor-skills/exxat-drawer-vs-dialog/SKILL.md +20 -0
  5. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +31 -5
  6. package/consumer-extras/cursor-skills/exxat-kpi-max-four/SKILL.md +19 -0
  7. package/consumer-extras/cursor-skills/exxat-kpi-trends/SKILL.md +27 -0
  8. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +1 -0
  9. package/consumer-extras/patterns/collaboration-access-pattern.md +114 -0
  10. package/consumer-extras/patterns/data-views-pattern.md +12 -4
  11. package/package.json +4 -1
  12. package/src/components/ui/banner.tsx +20 -7
  13. package/src/components/ui/date-picker-field.tsx +3 -3
  14. package/src/components/ui/dropdown-menu.tsx +17 -6
  15. package/src/components/ui/input-group.tsx +1 -1
  16. package/src/components/ui/input.tsx +1 -1
  17. package/src/components/ui/select.tsx +1 -1
  18. package/src/components/ui/separator.tsx +2 -2
  19. package/src/components/ui/sidebar.tsx +31 -3
  20. package/src/components/ui/textarea.tsx +1 -1
  21. package/src/globals.css +0 -1
  22. package/src/index.ts +1 -0
  23. package/src/lib/date-filter.ts +13 -4
  24. package/src/lib/dropdown-menu-surface.ts +13 -0
  25. package/template/.claude/skills/exxat-ds-skill/SKILL.md +27 -9
  26. package/template/.cursor/rules/exxat-data-tables.mdc +1 -0
  27. package/template/.nvmrc +1 -1
  28. package/template/AGENTS.md +82 -27
  29. package/template/app/(app)/examples/page.tsx +2 -1
  30. package/template/app/(app)/help/page.tsx +6 -0
  31. package/template/app/(app)/layout.tsx +7 -4
  32. package/template/app/(app)/question-bank/find/page.tsx +12 -0
  33. package/template/app/(app)/question-bank/layout.tsx +46 -0
  34. package/template/app/(app)/question-bank/library/page.tsx +11 -0
  35. package/template/app/(app)/question-bank/list/page.tsx +12 -0
  36. package/template/app/(app)/question-bank/page.tsx +4 -3
  37. package/template/app/globals.css +1 -2
  38. package/template/components/app-sidebar.tsx +51 -13
  39. package/template/components/ask-leo-composer.tsx +173 -45
  40. package/template/components/ask-leo-sidebar.tsx +9 -1
  41. package/template/components/chart-area-interactive.tsx +3 -13
  42. package/template/components/charts-overview.tsx +33 -6
  43. package/template/components/collaboration-access-flow.tsx +144 -0
  44. package/template/components/compliance-page-header.tsx +1 -1
  45. package/template/components/compliance-table.tsx +2 -2
  46. package/template/components/dashboard-tabs.tsx +4 -3
  47. package/template/components/data-list-table-cells.tsx +1 -1
  48. package/template/components/data-list-table.tsx +1 -1
  49. package/template/components/data-table/index.tsx +5 -5
  50. package/template/components/data-table/use-table-state.ts +18 -2
  51. package/template/components/data-view-dashboard-charts-compliance.tsx +8 -5
  52. package/template/components/data-view-dashboard-charts-team.tsx +8 -5
  53. package/template/components/data-view-dashboard-charts.tsx +62 -227
  54. package/template/components/dedicated-search-recents.tsx +96 -0
  55. package/template/components/dedicated-search-url-composer.tsx +112 -0
  56. package/template/components/getting-started.tsx +1 -1
  57. package/template/components/hub-tree-panel-view.tsx +10 -26
  58. package/template/components/invite-collaborators-drawer.tsx +453 -0
  59. package/template/components/key-metrics.tsx +54 -8
  60. package/template/components/nav-documents.tsx +1 -1
  61. package/template/components/new-placement-form.tsx +3 -3
  62. package/template/components/page-header.tsx +76 -59
  63. package/template/components/placements-board-view.tsx +3 -3
  64. package/template/components/placements-page-header.tsx +1 -1
  65. package/template/components/placements-table-columns.tsx +3 -2
  66. package/template/components/product-switcher.tsx +0 -1
  67. package/template/components/question-bank-board-view.tsx +35 -47
  68. package/template/components/question-bank-client.tsx +293 -81
  69. package/template/components/question-bank-dashboard-charts.tsx +174 -0
  70. package/template/components/question-bank-favorite-button.tsx +46 -0
  71. package/template/components/question-bank-hub-client.tsx +436 -0
  72. package/template/components/question-bank-list-view.tsx +26 -19
  73. package/template/components/question-bank-new-folder-sheet.tsx +56 -42
  74. package/template/components/question-bank-os-folder-view.tsx +3 -14
  75. package/template/components/question-bank-page-header.tsx +85 -53
  76. package/template/components/question-bank-panel-activator.tsx +3 -4
  77. package/template/components/question-bank-secondary-nav.tsx +523 -65
  78. package/template/components/question-bank-table.tsx +125 -343
  79. package/template/components/secondary-panel.tsx +130 -63
  80. package/template/components/settings-client.tsx +3 -1
  81. package/template/components/sidebar-shell.tsx +2 -0
  82. package/template/components/sites-all-client.tsx +1 -1
  83. package/template/components/sites-table.tsx +1 -1
  84. package/template/components/system-banner-slot.tsx +2 -1
  85. package/template/components/table-properties/drawer.tsx +3 -3
  86. package/template/components/table-properties/sort-card.tsx +1 -1
  87. package/template/components/team-page-header.tsx +1 -1
  88. package/template/components/team-table.tsx +8 -4
  89. package/template/components/templates/dedicated-search-landing-template.tsx +58 -0
  90. package/template/components/templates/dedicated-search-results-template.tsx +19 -0
  91. package/template/components/templates/discovery-hub-template.tsx +273 -0
  92. package/template/components/templates/list-page.tsx +11 -4
  93. package/template/components/templates/nested-secondary-panel-shell.tsx +57 -0
  94. package/template/components/templates/secondary-panel-hub-template.tsx +54 -0
  95. package/template/docs/card-vs-rows-pattern.md +36 -0
  96. package/template/docs/collaboration-access-pattern.md +114 -0
  97. package/template/docs/data-views-pattern.md +12 -4
  98. package/template/docs/drawer-vs-dialog-pattern.md +50 -0
  99. package/template/docs/kpi-strip-max-four-pattern.md +29 -0
  100. package/template/docs/kpi-trend-pattern.md +43 -0
  101. package/template/fontawesome-subset.manifest.json +2 -2
  102. package/template/hooks/use-location-hash.ts +14 -8
  103. package/template/hooks/use-secondary-panel-hub-nav.ts +98 -0
  104. package/template/lib/ask-leo-route-context.ts +24 -0
  105. package/template/lib/collaborator-access.ts +92 -0
  106. package/template/lib/command-menu-config.ts +8 -1
  107. package/template/lib/command-menu-search-data.ts +11 -8
  108. package/template/lib/data-list-display-options.ts +1 -1
  109. package/template/lib/data-view-dashboard-placements-layout.ts +215 -0
  110. package/template/lib/date-filter.ts +1 -0
  111. package/template/lib/dedicated-search-recents.ts +76 -0
  112. package/template/lib/dedicated-search-url.ts +23 -0
  113. package/template/lib/discovery-hub.ts +15 -0
  114. package/template/lib/list-status-badges.ts +1 -21
  115. package/template/lib/mock/navigation.tsx +4 -2
  116. package/template/lib/mock/placements.ts +9 -9
  117. package/template/lib/mock/question-bank-folders.ts +7 -0
  118. package/template/lib/mock/question-bank-header-collaborators.ts +45 -5
  119. package/template/lib/mock/question-bank-inspector.ts +1 -2
  120. package/template/lib/mock/question-bank-kpi.ts +38 -26
  121. package/template/lib/mock/question-bank.ts +43 -16
  122. package/template/lib/question-bank-dedicated-search.ts +19 -0
  123. package/template/lib/question-bank-hub-search.ts +90 -0
  124. package/template/lib/question-bank-nav.ts +322 -6
  125. package/template/lib/question-bank-recent-searches.ts +22 -0
  126. package/template/package.json +1 -2
@@ -5,8 +5,7 @@
5
5
  */
6
6
 
7
7
  import * as React from "react"
8
- import { Bar, BarChart, CartesianGrid, Cell, XAxis, YAxis } from "recharts"
9
- import { ChartCard, ChartDataTable, ChartFigure } from "@/components/charts-overview"
8
+ import dynamic from "next/dynamic"
10
9
  import { DataTable, DataTableToolbar } from "@/components/data-table"
11
10
  import type { DataListViewType } from "@/lib/data-list-view"
12
11
  import type { OpenTablePropertiesHandle } from "@/lib/list-page-table-properties"
@@ -14,15 +13,7 @@ import type { ColumnDef } from "@/components/data-table/types"
14
13
  import { useTableState } from "@/components/data-table/use-table-state"
15
14
  import { TablePropertiesDrawerButton } from "@/components/table-properties"
16
15
  import type { ConditionalRule, FilterFieldDef, FilterOperator } from "@/components/table-properties/types"
17
- import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
18
16
  import { Button } from "@/components/ui/button"
19
- import {
20
- ChartContainer,
21
- ChartTooltip,
22
- chartTooltipKeyboardSyncProps,
23
- ChartTooltipContent,
24
- type ChartConfig,
25
- } from "@/components/ui/chart"
26
17
  import {
27
18
  DropdownMenu,
28
19
  DropdownMenuContent,
@@ -30,7 +21,7 @@ import {
30
21
  DropdownMenuTrigger,
31
22
  } from "@/components/ui/dropdown-menu"
32
23
  import { Tip } from "@/components/ui/tip"
33
- import { KeyMetrics } from "@/components/key-metrics"
24
+ import { Skeleton } from "@/components/ui/skeleton"
34
25
  import {
35
26
  ResizableHandle,
36
27
  ResizablePanel,
@@ -50,32 +41,66 @@ import {
50
41
  import { ListPageTreeColumnHeader } from "@/components/data-views/list-page-tree-column-header"
51
42
  import { QuestionBankBoardView, QUESTION_BANK_BOARD_GROUP_OPTIONS } from "@/components/question-bank-board-view"
52
43
  import { QuestionBankListView } from "@/components/question-bank-list-view"
44
+ import {
45
+ QuestionBankFavoriteButton,
46
+ QUESTION_BANK_FAVORITE_HOVER_GROUP,
47
+ } from "@/components/question-bank-favorite-button"
53
48
  import { QuestionBankOsFolderView } from "@/components/question-bank-os-folder-view"
54
49
  import { QuestionBankNewFolderSheet } from "@/components/question-bank-new-folder-sheet"
55
50
  import { FolderDetailsShell } from "@/components/folder-details-shell"
56
51
  import { HubTreePanelView } from "@/components/hub-tree-panel-view"
57
- import { OsFolderGlyph } from "@/components/data-views/os-folder-glyph"
58
- import { Avatar, AvatarFallback } from "@/components/ui/avatar"
59
- import { initialsFromDisplayName } from "@/lib/initials-from-name"
52
+ import { AvatarInitials } from "@/components/ui/avatar"
60
53
  import { cn } from "@/lib/utils"
61
- import { CHART_KBD_ACTIVE_BAR } from "@/lib/chart-keyboard-selection"
62
- import type { QuestionBankDifficulty, QuestionBankItem, QuestionBankType } from "@/lib/mock/question-bank"
63
- import { newFolderId, type QuestionBankFolder, QUESTION_BANK_FOLDER_COLOR_STYLES, QUESTION_BANK_FOLDER_ICON_COLORS } from "@/lib/mock/question-bank-folders"
64
- import { questionBankKpiInsight, questionBankKpiMetrics } from "@/lib/mock/question-bank-kpi"
54
+ import { formatDateUS } from "@/lib/date-filter"
55
+ import { initialsFromDisplayName } from "@/lib/initials-from-name"
65
56
  import {
66
- filterQuestionBankItemsByNav,
57
+ newQuestionBankQuestionId,
58
+ type QuestionBankDifficulty,
59
+ type QuestionBankItem,
60
+ type QuestionBankType,
61
+ } from "@/lib/mock/question-bank"
62
+ import { type QuestionBankFolder, QUESTION_BANK_FOLDER_COLOR_STYLES, QUESTION_BANK_FOLDER_ICON_COLORS } from "@/lib/mock/question-bank-folders"
63
+ import {
64
+ toggleQuestionBankItemFavorite,
65
+ applyQuestionBankHubDisplayFilters,
66
+ type QuestionBankLandingFilterState,
67
67
  type QuestionBankNavState,
68
68
  } from "@/lib/question-bank-nav"
69
- import {
70
- QUESTION_BANK_STATUS_BADGE_CLASS,
71
- QUESTION_BANK_STATUS_ICON,
72
- QUESTION_BANK_STATUS_LABEL,
73
- } from "@/lib/list-status-badges"
74
69
  import {
75
70
  DEFAULT_DATA_LIST_DISPLAY_OPTIONS,
76
71
  type DataListDisplayOptions,
77
72
  } from "@/lib/data-list-display-options"
78
73
 
74
+ const QuestionBankDashboardChartsSection = dynamic(
75
+ () =>
76
+ import("@/components/question-bank-dashboard-charts").then(mod => ({
77
+ default: mod.QuestionBankDashboardChartsSection,
78
+ })),
79
+ {
80
+ ssr: false,
81
+ loading: () => (
82
+ <div className="flex min-h-0 flex-1 flex-col gap-4 pb-6">
83
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
84
+ <Skeleton className="h-20 w-full rounded-lg" />
85
+ <Skeleton className="h-20 w-full rounded-lg" />
86
+ <Skeleton className="h-20 w-full rounded-lg" />
87
+ <Skeleton className="h-20 w-full rounded-lg" />
88
+ </div>
89
+ <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
90
+ <div className="flex flex-col gap-3 rounded-xl border border-border p-4">
91
+ <Skeleton className="h-5 w-32" />
92
+ <Skeleton className="min-h-[220px] w-full rounded-lg" />
93
+ </div>
94
+ <div className="flex flex-col gap-3 rounded-xl border border-border p-4">
95
+ <Skeleton className="h-5 w-28" />
96
+ <Skeleton className="min-h-[220px] w-full rounded-lg" />
97
+ </div>
98
+ </div>
99
+ </div>
100
+ ),
101
+ },
102
+ )
103
+
79
104
  const TYPE_LABEL: Record<QuestionBankType, string> = {
80
105
  multiple_choice: "Multiple choice",
81
106
  true_false: "True / false",
@@ -88,10 +113,6 @@ const DIFFICULTY_LABEL: Record<QuestionBankDifficulty, string> = {
88
113
  hard: "Hard",
89
114
  }
90
115
 
91
- const BAR_CFG: ChartConfig = {
92
- count: { label: "Questions", color: "var(--color-chart-2)" },
93
- }
94
-
95
116
  function newQuestionBankItemId() {
96
117
  return `q-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`
97
118
  }
@@ -107,12 +128,6 @@ function uniqueTopics(items: QuestionBankItem[]) {
107
128
  return [...new Set(items.map(i => i.topic))].sort().map(t => ({ value: t, label: t }))
108
129
  }
109
130
 
110
- const STATUS_FILTER_OPTS = [
111
- { value: "published", label: QUESTION_BANK_STATUS_LABEL.published },
112
- { value: "draft", label: QUESTION_BANK_STATUS_LABEL.draft },
113
- { value: "in_review", label: QUESTION_BANK_STATUS_LABEL.in_review },
114
- ]
115
-
116
131
  const TYPE_FILTER_OPTS = (Object.keys(TYPE_LABEL) as QuestionBankType[]).map(k => ({
117
132
  value: k,
118
133
  label: TYPE_LABEL[k],
@@ -145,8 +160,12 @@ function columnsToFilterFields(cols: ColumnDef<QuestionBankItem>[]) {
145
160
  return cols.map(columnToFilterFieldDef).filter((x): x is FilterFieldDef => x !== null)
146
161
  }
147
162
 
148
- function buildQuestionBankColumns(items: QuestionBankItem[]): ColumnDef<QuestionBankItem>[] {
163
+ function buildQuestionBankColumns(
164
+ items: QuestionBankItem[],
165
+ opts: { onToggleFavorite: (row: QuestionBankItem) => void },
166
+ ): ColumnDef<QuestionBankItem>[] {
149
167
  const topicOpts = uniqueTopics(items)
168
+ const { onToggleFavorite } = opts
150
169
 
151
170
  const COLUMN_SELECT: ColumnDef<QuestionBankItem> = {
152
171
  key: "select",
@@ -157,8 +176,8 @@ function buildQuestionBankColumns(items: QuestionBankItem[]): ColumnDef<Question
157
176
  lockPin: true,
158
177
  }
159
178
 
160
- const cols: ColumnDef<QuestionBankItem>[] = [
161
- COLUMN_SELECT,
179
+ const cols: ColumnDef<QuestionBankItem>[] = [COLUMN_SELECT]
180
+ cols.push(
162
181
  {
163
182
  key: "stem",
164
183
  label: "Question",
@@ -173,7 +192,13 @@ function buildQuestionBankColumns(items: QuestionBankItem[]): ColumnDef<Question
173
192
  operators: ["contains", "not_contains"],
174
193
  },
175
194
  cell: row => (
176
- <span className="line-clamp-2 text-sm font-medium text-foreground">{row.stem}</span>
195
+ <div className={cn(QUESTION_BANK_FAVORITE_HOVER_GROUP, "flex min-w-0 items-start gap-2")}>
196
+ <div className="flex min-w-0 flex-1 flex-col gap-0.5 pr-1">
197
+ <span className="line-clamp-2 text-sm font-medium text-foreground">{row.stem}</span>
198
+ <span className="font-mono text-xs text-muted-foreground">{row.questionId}</span>
199
+ </div>
200
+ <QuestionBankFavoriteButton row={row} onToggleFavorite={onToggleFavorite} />
201
+ </div>
177
202
  ),
178
203
  },
179
204
  {
@@ -223,27 +248,6 @@ function buildQuestionBankColumns(items: QuestionBankItem[]): ColumnDef<Question
223
248
  <span className="text-sm text-foreground/90">{DIFFICULTY_LABEL[row.difficulty]}</span>
224
249
  ),
225
250
  },
226
- {
227
- key: "status",
228
- label: "Status",
229
- width: 120,
230
- minWidth: 100,
231
- sortable: true,
232
- sortKey: "status",
233
- filter: {
234
- type: "select",
235
- icon: "fa-circle-dot",
236
- operators: ["is", "is_not"],
237
- options: STATUS_FILTER_OPTS,
238
- },
239
- cell: row => (
240
- <ListHubStatusBadge
241
- label={QUESTION_BANK_STATUS_LABEL[row.status]}
242
- tintClassName={QUESTION_BANK_STATUS_BADGE_CLASS[row.status]}
243
- icon={QUESTION_BANK_STATUS_ICON[row.status]}
244
- />
245
- ),
246
- },
247
251
  {
248
252
  key: "updatedAt",
249
253
  label: "Updated",
@@ -253,14 +257,14 @@ function buildQuestionBankColumns(items: QuestionBankItem[]): ColumnDef<Question
253
257
  sortKey: "updatedAt",
254
258
  filter: { type: "date", icon: "fa-calendar-days", operators: ["is", "is_not"] },
255
259
  cell: row => (
256
- <span className="text-sm tabular-nums text-foreground/90 whitespace-nowrap">{row.updatedAt}</span>
260
+ <span className="text-sm tabular-nums text-foreground/90 whitespace-nowrap">{formatDateUS(row.updatedAt)}</span>
257
261
  ),
258
262
  },
259
263
  {
260
264
  key: "author",
261
265
  label: "Author",
262
- width: 140,
263
- minWidth: 120,
266
+ width: 260,
267
+ minWidth: 200,
264
268
  sortable: true,
265
269
  sortKey: "author",
266
270
  filter: {
@@ -268,7 +272,26 @@ function buildQuestionBankColumns(items: QuestionBankItem[]): ColumnDef<Question
268
272
  icon: "fa-user",
269
273
  operators: ["contains", "not_contains"],
270
274
  },
271
- cell: row => <span className="text-sm text-foreground/90">{row.author}</span>,
275
+ cell: row => {
276
+ const initials = initialsFromDisplayName(row.author)
277
+ return (
278
+ <div className="flex min-w-0 items-center gap-2.5">
279
+ <AvatarInitials initials={initials} className="size-8 shrink-0 text-xs" />
280
+ <div className="flex min-w-0 flex-col gap-0.5">
281
+ <span className="truncate text-sm font-medium text-foreground">{row.author}</span>
282
+ {row.authorEmail ? (
283
+ <a
284
+ href={`mailto:${row.authorEmail}`}
285
+ className="truncate text-xs text-muted-foreground transition-colors hover:text-foreground hover:underline"
286
+ onClick={e => e.stopPropagation()}
287
+ >
288
+ {row.authorEmail}
289
+ </a>
290
+ ) : null}
291
+ </div>
292
+ </div>
293
+ )
294
+ },
272
295
  },
273
296
  {
274
297
  key: "actions",
@@ -285,7 +308,7 @@ function buildQuestionBankColumns(items: QuestionBankItem[]): ColumnDef<Question
285
308
  <i className="fa-light fa-ellipsis text-sm" aria-hidden="true" />
286
309
  </Button>
287
310
  </DropdownMenuTrigger>
288
- <DropdownMenuContent align="end" className="w-40">
311
+ <DropdownMenuContent align="end">
289
312
  <DropdownMenuItem disabled>
290
313
  <i className="fa-light fa-eye" aria-hidden="true" />
291
314
  Preview
@@ -299,257 +322,9 @@ function buildQuestionBankColumns(items: QuestionBankItem[]): ColumnDef<Question
299
322
  </div>
300
323
  ),
301
324
  },
302
- ]
303
-
304
- return cols
305
- }
306
-
307
-
308
- function aggregateByStatus(rows: QuestionBankItem[]) {
309
- const c = { published: 0, draft: 0, in_review: 0 }
310
- for (const r of rows) c[r.status]++
311
- return [
312
- { name: QUESTION_BANK_STATUS_LABEL.published, value: c.published, key: "published" },
313
- { name: QUESTION_BANK_STATUS_LABEL.draft, value: c.draft, key: "draft" },
314
- { name: QUESTION_BANK_STATUS_LABEL.in_review, value: c.in_review, key: "in_review" },
315
- ]
316
- }
317
-
318
- function aggregateByTopic(rows: QuestionBankItem[]) {
319
- const map = new Map<string, number>()
320
- for (const r of rows) map.set(r.topic, (map.get(r.topic) ?? 0) + 1)
321
- return [...map.entries()]
322
- .map(([name, value]) => ({ name: name.length > 20 ? `${name.slice(0, 18)}…` : name, value }))
323
- .sort((a, b) => b.value - a.value)
324
- .slice(0, 8)
325
- }
326
-
327
- function QuestionsByStatusChart({ rows }: { rows: QuestionBankItem[] }) {
328
- const data = React.useMemo(() => aggregateByStatus(rows), [rows])
329
- if (rows.length === 0) {
330
- return (
331
- <div className="flex h-[200px] items-center justify-center text-sm text-muted-foreground" role="status">
332
- No questions in this view.
333
- </div>
334
- )
335
- }
336
- const summary = `Status breakdown: ${data.map(d => `${d.name} ${d.value}`).join(", ")}. Total ${rows.length}.`
337
- return (
338
- <ChartFigure label="Questions by status" summary={summary} dataLength={data.length}>
339
- {(activeIndex) => (
340
- <>
341
- <ChartContainer config={BAR_CFG} className="h-[220px] w-full">
342
- <BarChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
343
- <CartesianGrid vertical={false} strokeDasharray="3 3" className="stroke-border" />
344
- <XAxis dataKey="name" tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
345
- <YAxis allowDecimals={false} width={32} tick={{ fontSize: 12 }} tickLine={false} axisLine={false} />
346
- <ChartTooltip key={chartTooltipKeyboardSyncProps(activeIndex).key} {...chartTooltipKeyboardSyncProps(activeIndex).props} content={<ChartTooltipContent />} />
347
- <Bar
348
- dataKey="value"
349
- fill="var(--color-chart-2)"
350
- radius={[4, 4, 0, 0]}
351
- maxBarSize={40}
352
- activeBar={CHART_KBD_ACTIVE_BAR}
353
- activeIndex={activeIndex ?? undefined}
354
- >
355
- {data.map((_, i) => (
356
- <Cell key={i} fill="var(--color-chart-2)" />
357
- ))}
358
- </Bar>
359
- </BarChart>
360
- </ChartContainer>
361
- <ChartDataTable
362
- caption="Questions by status"
363
- headers={["Status", "Count"]}
364
- rows={data.map(d => [d.name, d.value])}
365
- />
366
- </>
367
- )}
368
- </ChartFigure>
369
- )
370
- }
371
-
372
- function QuestionBankFinderListRow({
373
- row,
374
- isSelected,
375
- compact,
376
- }: {
377
- row: QuestionBankItem
378
- isSelected: boolean
379
- compact?: boolean
380
- }) {
381
- const initials = initialsFromDisplayName(row.author)
382
- return (
383
- <div
384
- className={cn(
385
- "flex w-full items-center px-3 py-2",
386
- compact ? "gap-2 py-1.5 pl-2 pr-2.5" : "gap-3",
387
- )}
388
- >
389
- <Avatar className={cn("shrink-0", compact ? "size-6" : "size-8")}>
390
- <AvatarFallback
391
- className={cn(
392
- "font-semibold",
393
- compact ? "text-[10px]" : "text-[11px]",
394
- isSelected ? "bg-background/25 text-accent-foreground" : "bg-brand/15 text-brand",
395
- )}
396
- >
397
- {initials}
398
- </AvatarFallback>
399
- </Avatar>
400
- <div className="min-w-0 flex-1">
401
- <p className={cn("line-clamp-2 font-medium leading-tight", compact ? "text-[11px]" : "text-xs")}>
402
- {row.stem}
403
- </p>
404
- <p
405
- className={cn(
406
- "mt-0.5 truncate text-[11px] leading-tight",
407
- isSelected ? "text-accent-foreground/85" : "text-muted-foreground",
408
- )}
409
- >
410
- {row.topic} · {row.author}
411
- </p>
412
- </div>
413
- {!isSelected && (
414
- <ListHubStatusBadge
415
- surface="board"
416
- label={QUESTION_BANK_STATUS_LABEL[row.status]}
417
- tintClassName={QUESTION_BANK_STATUS_BADGE_CLASS[row.status]}
418
- icon={QUESTION_BANK_STATUS_ICON[row.status]}
419
- />
420
- )}
421
- </div>
422
- )
423
- }
424
-
425
- function QuestionBankFinderDetail({ row }: { row: QuestionBankItem }) {
426
- return (
427
- <div className="flex h-full min-h-0 flex-col">
428
- <div className="flex shrink-0 flex-col gap-2 border-b border-border px-5 py-4">
429
- <h2 className="line-clamp-4 text-base font-semibold leading-tight text-foreground">{row.stem}</h2>
430
- <ListHubStatusBadge
431
- surface="board"
432
- label={QUESTION_BANK_STATUS_LABEL[row.status]}
433
- tintClassName={QUESTION_BANK_STATUS_BADGE_CLASS[row.status]}
434
- icon={QUESTION_BANK_STATUS_ICON[row.status]}
435
- />
436
- </div>
437
- <div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
438
- <dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
439
- <div className="flex flex-col gap-0.5">
440
- <dt className="flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
441
- <i className="fa-light fa-layer-group text-xs" aria-hidden="true" />
442
- Topic
443
- </dt>
444
- <dd className="text-xs text-foreground">{row.topic}</dd>
445
- </div>
446
- <div className="flex flex-col gap-0.5">
447
- <dt className="flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
448
- <i className="fa-light fa-list-check text-xs" aria-hidden="true" />
449
- Type
450
- </dt>
451
- <dd className="text-xs text-foreground">{TYPE_LABEL[row.type]}</dd>
452
- </div>
453
- <div className="flex flex-col gap-0.5">
454
- <dt className="flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
455
- <i className="fa-light fa-signal text-xs" aria-hidden="true" />
456
- Difficulty
457
- </dt>
458
- <dd className="text-xs text-foreground">{DIFFICULTY_LABEL[row.difficulty]}</dd>
459
- </div>
460
- <div className="flex flex-col gap-0.5">
461
- <dt className="flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
462
- <i className="fa-light fa-user text-xs" aria-hidden="true" />
463
- Author
464
- </dt>
465
- <dd className="text-xs text-foreground">{row.author}</dd>
466
- </div>
467
- <div className="flex flex-col gap-0.5 sm:col-span-2">
468
- <dt className="flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
469
- <i className="fa-light fa-calendar-days text-xs" aria-hidden="true" />
470
- Updated
471
- </dt>
472
- <dd className="text-xs tabular-nums text-foreground">{row.updatedAt}</dd>
473
- </div>
474
- </dl>
475
- </div>
476
- </div>
477
- )
478
- }
479
-
480
- function QuestionsByTopicChart({ rows }: { rows: QuestionBankItem[] }) {
481
- const data = React.useMemo(() => aggregateByTopic(rows), [rows])
482
- if (rows.length === 0) {
483
- return (
484
- <div className="flex h-[200px] items-center justify-center text-sm text-muted-foreground" role="status">
485
- No questions in this view.
486
- </div>
487
- )
488
- }
489
- const summary = `${data.length} topics shown. Total ${rows.length} questions.`
490
- return (
491
- <ChartFigure label="Questions by topic" summary={summary} dataLength={data.length}>
492
- {(activeIndex) => (
493
- <>
494
- <ChartContainer config={BAR_CFG} className="h-[220px] w-full">
495
- <BarChart data={data} layout="vertical" margin={{ top: 8, right: 8, left: 4, bottom: 0 }}>
496
- <CartesianGrid horizontal={false} strokeDasharray="3 3" className="stroke-border" />
497
- <XAxis type="number" allowDecimals={false} tick={{ fontSize: 12 }} tickLine={false} axisLine={false} />
498
- <YAxis type="category" dataKey="name" width={100} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
499
- <ChartTooltip key={chartTooltipKeyboardSyncProps(activeIndex).key} {...chartTooltipKeyboardSyncProps(activeIndex).props} content={<ChartTooltipContent />} />
500
- <Bar
501
- dataKey="value"
502
- fill="var(--color-chart-4)"
503
- radius={[0, 4, 4, 0]}
504
- maxBarSize={22}
505
- activeBar={CHART_KBD_ACTIVE_BAR}
506
- activeIndex={activeIndex ?? undefined}
507
- >
508
- {data.map((_, i) => (
509
- <Cell key={i} fill="var(--color-chart-4)" />
510
- ))}
511
- </Bar>
512
- </BarChart>
513
- </ChartContainer>
514
- <ChartDataTable
515
- caption="Questions by topic"
516
- headers={["Topic", "Count"]}
517
- rows={data.map(d => [d.name, d.value])}
518
- />
519
- </>
520
- )}
521
- </ChartFigure>
522
- )
523
- }
524
-
525
- function QuestionBankDashboardSimple({ rows }: { rows: QuestionBankItem[] }) {
526
- const kpi = React.useMemo(
527
- () => ({
528
- metrics: questionBankKpiMetrics(rows),
529
- insight: questionBankKpiInsight(rows),
530
- }),
531
- [rows],
532
325
  )
533
326
 
534
- return (
535
- <div className="flex min-h-0 flex-1 flex-col gap-4 pb-6">
536
- <KeyMetrics
537
- variant="flat"
538
- metrics={kpi.metrics}
539
- insight={kpi.insight}
540
- showHeader={false}
541
- metricsSingleRow
542
- />
543
- <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
544
- <ChartCard variant="normal" title="By status" description="Filtered question set">
545
- <QuestionsByStatusChart rows={rows} />
546
- </ChartCard>
547
- <ChartCard variant="normal" title="By topic" description="Up to eight topics">
548
- <QuestionsByTopicChart rows={rows} />
549
- </ChartCard>
550
- </div>
551
- </div>
552
- )
327
+ return cols
553
328
  }
554
329
 
555
330
  interface HubFolderColumnsPanelProps {
@@ -813,7 +588,7 @@ function HubFolderColumnsPanel({
813
588
  <i className="fa-light fa-ellipsis text-xs" aria-hidden="true" />
814
589
  </Button>
815
590
  </DropdownMenuTrigger>
816
- <DropdownMenuContent align="end" className="w-40">
591
+ <DropdownMenuContent align="end">
817
592
  <DropdownMenuItem onSelect={() => onCustomizeFolder?.(folder)}>
818
593
  <i className="fa-light fa-wand-magic-sparkles text-xs" aria-hidden="true" />
819
594
  Customize
@@ -865,6 +640,12 @@ export const QuestionBankTable = React.forwardRef<
865
640
  items: QuestionBankItem[]
866
641
  /** When set, table / board / tree rows are limited to this nav scope (secondary sidebar). */
867
642
  navState?: QuestionBankNavState
643
+ /** URL toolbar search binding (`?q=`) — omit on search landing so hub `q` does not pre-fill the grid search. */
644
+ urlListSearch?: string
645
+ /** When true, dedicated search shell: hub landing row filters; table toolbar search stays independent of URL `q`. */
646
+ searchLanding?: boolean
647
+ /** Applied with nav filters before `useTableState` when {@link searchLanding} is true. */
648
+ landingFilters?: QuestionBankLandingFilterState | null
868
649
  view?: DataListViewType
869
650
  onViewChange?: (v: DataListViewType) => void
870
651
  folders: QuestionBankFolder[]
@@ -872,20 +653,31 @@ export const QuestionBankTable = React.forwardRef<
872
653
  onItemsChange: React.Dispatch<React.SetStateAction<QuestionBankItem[]>>
873
654
  }
874
655
  >(function QuestionBankTable(
875
- { items, navState, view = "table", onViewChange, folders, onFoldersChange, onItemsChange },
656
+ { items, navState, urlListSearch, searchLanding, landingFilters, view = "table", onViewChange, folders, onFoldersChange, onItemsChange },
876
657
  ref,
877
658
  ) {
878
659
  const tableSourceItems = React.useMemo(() => {
879
660
  const nav = navState ?? { scope: "all" as const, folderId: null }
880
- return filterQuestionBankItemsByNav(items, folders, nav)
881
- }, [items, folders, navState])
661
+ const landing = searchLanding ? (landingFilters ?? null) : null
662
+ return applyQuestionBankHubDisplayFilters(items, folders, nav, landing)
663
+ }, [items, folders, navState, searchLanding, landingFilters])
882
664
 
883
- const columns = React.useMemo(() => buildQuestionBankColumns(tableSourceItems), [tableSourceItems])
665
+ const toggleFavorite = React.useCallback(
666
+ (row: QuestionBankItem) => {
667
+ onItemsChange(prev => prev.map(r => (r.id === row.id ? toggleQuestionBankItemFavorite(r) : r)))
668
+ },
669
+ [onItemsChange],
670
+ )
671
+
672
+ const columns = React.useMemo(
673
+ () => buildQuestionBankColumns(tableSourceItems, { onToggleFavorite: toggleFavorite }),
674
+ [tableSourceItems, toggleFavorite],
675
+ )
884
676
  const filterFields = React.useMemo(() => columnsToFilterFields(columns), [columns])
885
677
  const fieldDefinitionsForDrawer = React.useMemo(
886
678
  () =>
887
679
  columns
888
- .filter(c => c.key !== "select" && c.key !== "actions")
680
+ .filter(c => c.key !== "select" && c.key !== "favorite" && c.key !== "actions")
889
681
  .map(c => ({ key: c.key, label: c.label, sortable: !!(c.sortable && (c.sortKey ?? c.key)) })),
890
682
  [columns],
891
683
  )
@@ -915,7 +707,13 @@ export const QuestionBankTable = React.forwardRef<
915
707
  setConditionalRules(prev => prev.map(r => r.id === id ? { ...r, ...patch } : r))
916
708
  }, [])
917
709
 
918
- const tableState = useTableState(tableSourceItems, columns, { key: "updatedAt", dir: "desc" })
710
+ const tableState = useTableState(
711
+ tableSourceItems,
712
+ columns,
713
+ { key: "updatedAt", dir: "desc" },
714
+ undefined,
715
+ searchLanding ? undefined : urlListSearch,
716
+ )
919
717
 
920
718
  const openNewFolderForColumn = React.useCallback((parentId: string | null) => {
921
719
  setNewFolderParentId(parentId)
@@ -940,12 +738,13 @@ export const QuestionBankTable = React.forwardRef<
940
738
  ...prev,
941
739
  {
942
740
  id: newQuestionBankItemId(),
741
+ questionId: newQuestionBankQuestionId(),
943
742
  stem: "New question",
944
743
  topic: "General",
945
744
  type: "short_answer",
946
745
  difficulty: "medium",
947
- status: "draft",
948
746
  author: "Demo user",
747
+ authorEmail: "demo.user@demo.exxat.io",
949
748
  updatedAt: `${y}-${m}-${d}`,
950
749
  folderId,
951
750
  },
@@ -956,19 +755,6 @@ export const QuestionBankTable = React.forwardRef<
956
755
 
957
756
  const renderFilterOptionValue = React.useCallback(
958
757
  (fieldKey: string, value: string): React.ReactNode => {
959
- if (fieldKey === "status") {
960
- if (value in QUESTION_BANK_STATUS_LABEL) {
961
- const status = value as keyof typeof QUESTION_BANK_STATUS_LABEL
962
- return (
963
- <ListHubStatusBadge
964
- label={QUESTION_BANK_STATUS_LABEL[status]}
965
- tintClassName={QUESTION_BANK_STATUS_BADGE_CLASS[status]}
966
- icon={QUESTION_BANK_STATUS_ICON[status]}
967
- />
968
- )
969
- }
970
- return <span className="text-foreground">{value}</span>
971
- }
972
758
  const col = columns.find(c => c.key === fieldKey)
973
759
  const opt = col?.filter?.options?.find(o => o.value === value)
974
760
  return <span className="text-foreground">{opt?.label ?? value}</span>
@@ -986,13 +772,14 @@ export const QuestionBankTable = React.forwardRef<
986
772
  o => o.key === displayOptions.boardGroupByColumnKey,
987
773
  )
988
774
  ? displayOptions.boardGroupByColumnKey
989
- : "status"
775
+ : "topic"
990
776
 
991
777
  const panelRenderDetail = (row: QuestionBankItem) => (
992
778
  <div className="flex min-w-0 flex-col gap-4">
993
779
  <div>
994
780
  <h3 className="text-sm font-semibold text-foreground mb-2">Question</h3>
995
781
  <p className="text-sm text-foreground">{row.stem}</p>
782
+ <p className="mt-1 font-mono text-xs text-muted-foreground">{row.questionId}</p>
996
783
  </div>
997
784
  <div className="flex gap-3 flex-wrap">
998
785
  <div className="flex flex-col gap-1">
@@ -1003,12 +790,6 @@ export const QuestionBankTable = React.forwardRef<
1003
790
  <span className="text-xs font-medium text-muted-foreground">Difficulty</span>
1004
791
  <span className="text-sm text-foreground">{DIFFICULTY_LABEL[row.difficulty]}</span>
1005
792
  </div>
1006
- <div className="flex flex-col gap-1">
1007
- <span className="text-xs font-medium text-muted-foreground">Status</span>
1008
- <span className="text-sm text-foreground">
1009
- {QUESTION_BANK_STATUS_LABEL[row.status]}
1010
- </span>
1011
- </div>
1012
793
  </div>
1013
794
  {row.topic && (
1014
795
  <div>
@@ -1117,7 +898,7 @@ export const QuestionBankTable = React.forwardRef<
1117
898
  return (
1118
899
  <div className="flex min-h-0 flex-1 flex-col">
1119
900
  {sharedToolbar}
1120
- <QuestionBankListView rows={tableState.rows as QuestionBankItem[]} />
901
+ <QuestionBankListView rows={tableState.rows as QuestionBankItem[]} onToggleFavorite={toggleFavorite} />
1121
902
  </div>
1122
903
  )
1123
904
  }
@@ -1129,6 +910,7 @@ export const QuestionBankTable = React.forwardRef<
1129
910
  <QuestionBankBoardView
1130
911
  rows={tableState.rows as QuestionBankItem[]}
1131
912
  groupByColumnKey={questionBankBoardGroupKey}
913
+ onToggleFavorite={toggleFavorite}
1132
914
  />
1133
915
  </div>
1134
916
  )
@@ -1263,7 +1045,7 @@ export const QuestionBankTable = React.forwardRef<
1263
1045
  return (
1264
1046
  <div className="flex min-h-0 flex-1 flex-col">
1265
1047
  {sharedToolbar}
1266
- <QuestionBankDashboardSimple rows={tableState.rows as QuestionBankItem[]} />
1048
+ <QuestionBankDashboardChartsSection rows={tableState.rows as QuestionBankItem[]} />
1267
1049
  </div>
1268
1050
  )
1269
1051
  })