@exxatdesignux/ui 0.2.18 → 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 (140) hide show
  1. package/CHANGELOG.md +15 -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 +21 -6
  7. package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
  8. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +4 -2
  9. package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
  10. package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
  11. package/consumer-extras/patterns/data-views-pattern.md +40 -3
  12. package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
  13. package/consumer-extras/patterns/shell-surface-elevation-pattern.md +5 -3
  14. package/package.json +2 -1
  15. package/src/components/ui/button-group.tsx +81 -0
  16. package/src/components/ui/button.tsx +4 -4
  17. package/src/globals.css +7 -1858
  18. package/src/theme.css +10 -1126
  19. package/src/tokens/README.md +15 -0
  20. package/src/tokens/base.css +337 -0
  21. package/src/tokens/high-contrast.css +1195 -0
  22. package/src/tokens/layers.css +224 -0
  23. package/src/tokens/tailwind-bridge.css +118 -0
  24. package/src/tokens/themes.css +201 -0
  25. package/template/AGENTS.md +60 -22
  26. package/template/app/(app)/dashboard/loading.tsx +3 -15
  27. package/template/app/(app)/dashboard/page.tsx +2 -14
  28. package/template/app/(app)/data-list/layout.tsx +43 -0
  29. package/template/app/(app)/data-list/page.tsx +2 -2
  30. package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
  31. package/template/app/(app)/examples/page.tsx +1 -0
  32. package/template/app/(app)/loading.tsx +1 -18
  33. package/template/app/(app)/question-bank/find/page.tsx +2 -1
  34. package/template/app/(app)/question-bank/library/page.tsx +2 -1
  35. package/template/app/(app)/question-bank/list/page.tsx +2 -1
  36. package/template/app/(app)/question-bank/new/page.tsx +15 -23
  37. package/template/app/(app)/question-bank/page.tsx +2 -1
  38. package/template/app/(app)/settings/page.tsx +4 -5
  39. package/template/app/globals.css +7 -1964
  40. package/template/components/app-route-loading.tsx +14 -0
  41. package/template/components/app-sidebar.tsx +70 -55
  42. package/template/components/data-views/index.ts +37 -9
  43. package/template/components/data-views/list-page-calendar-view.tsx +593 -0
  44. package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
  45. package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
  46. package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
  47. package/template/components/examples/focused-workflow-showcase.tsx +183 -0
  48. package/template/components/list-hub-board-view.tsx +68 -0
  49. package/template/components/list-hub-client.tsx +186 -0
  50. package/template/components/list-hub-list-view.tsx +36 -0
  51. package/template/components/list-hub-panel-activator.tsx +8 -0
  52. package/template/components/list-hub-secondary-nav.tsx +121 -0
  53. package/template/components/list-hub-table.tsx +336 -0
  54. package/template/components/new-question-composer.tsx +6 -24
  55. package/template/components/product-switcher.tsx +3 -2
  56. package/template/components/question-bank-client.tsx +4 -1
  57. package/template/components/question-bank-folder-columns-panel.tsx +104 -0
  58. package/template/components/question-bank-table.tsx +143 -485
  59. package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
  60. package/template/components/secondary-panel.tsx +4 -44
  61. package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
  62. package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
  63. package/template/components/secondary-panels/registry.tsx +15 -0
  64. package/template/components/settings-appearance-card.tsx +3 -2
  65. package/template/components/settings-client.tsx +59 -15
  66. package/template/components/settings-form-row.tsx +9 -4
  67. package/template/components/table-properties/drawer-button.tsx +13 -0
  68. package/template/components/table-properties/drawer.tsx +65 -4
  69. package/template/components/templates/focused-workflow-layouts.tsx +448 -0
  70. package/template/components/templates/focused-workflow-page-template.tsx +69 -0
  71. package/template/components/templates/list-page.tsx +29 -5
  72. package/template/components/templates/nested-secondary-panel-shell.tsx +2 -1
  73. package/template/components/templates/page-loading-shell.tsx +262 -0
  74. package/template/components/ui/button-group.tsx +1 -0
  75. package/template/docs/consumer-app-pattern.md +39 -0
  76. package/template/docs/data-views-pattern.md +40 -3
  77. package/template/docs/drawer-vs-dialog-pattern.md +3 -1
  78. package/template/docs/focused-workflow-page-pattern.md +84 -0
  79. package/template/docs/shell-surface-elevation-pattern.md +5 -3
  80. package/template/lib/command-menu-search-data.ts +11 -27
  81. package/template/lib/data-list-display-options.ts +16 -2
  82. package/template/lib/data-list-view-registry.ts +104 -0
  83. package/template/lib/data-list-view-surface.ts +15 -1
  84. package/template/lib/data-list-view.ts +10 -1
  85. package/template/lib/data-view-dashboard-storage.ts +38 -35
  86. package/template/lib/hub-connected-view-renderers.ts +58 -0
  87. package/template/lib/list-hub-nav.ts +121 -0
  88. package/template/lib/list-hub-supported-views.ts +10 -0
  89. package/template/lib/list-page-table-properties.ts +3 -7
  90. package/template/lib/list-status-badges.ts +4 -97
  91. package/template/lib/mock/list-hub-directory.ts +27 -0
  92. package/template/lib/mock/list-hub-kpi.ts +27 -0
  93. package/template/lib/mock/navigation.tsx +1 -0
  94. package/template/lib/page-loading-variant.ts +40 -0
  95. package/template/lib/question-bank-supported-views.ts +13 -0
  96. package/template/lib/table-state-lifecycle.ts +2 -2
  97. package/template/app/(app)/data-list/[id]/page.tsx +0 -44
  98. package/template/app/(app)/data-list/new/page.tsx +0 -34
  99. package/template/components/compliance-board-view.tsx +0 -142
  100. package/template/components/compliance-client.tsx +0 -92
  101. package/template/components/compliance-list-view.tsx +0 -54
  102. package/template/components/compliance-page-header.tsx +0 -89
  103. package/template/components/compliance-table.tsx +0 -612
  104. package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
  105. package/template/components/data-view-dashboard-charts-team.tsx +0 -971
  106. package/template/components/data-view-dashboard-charts.tsx +0 -1503
  107. package/template/components/new-placement-back-btn.tsx +0 -28
  108. package/template/components/new-placement-form.tsx +0 -1068
  109. package/template/components/placement-board-card.tsx +0 -262
  110. package/template/components/placement-detail.tsx +0 -438
  111. package/template/components/placements-board-view.tsx +0 -404
  112. package/template/components/placements-client.tsx +0 -252
  113. package/template/components/placements-list-view.tsx +0 -171
  114. package/template/components/placements-page-header.tsx +0 -166
  115. package/template/components/placements-table-cells.test.tsx +0 -22
  116. package/template/components/placements-table-cells.tsx +0 -173
  117. package/template/components/placements-table-columns.tsx +0 -640
  118. package/template/components/placements-table.tsx +0 -1642
  119. package/template/components/rotations-empty-state.tsx +0 -50
  120. package/template/components/rotations-panel-activator.tsx +0 -8
  121. package/template/components/sites-all-client.tsx +0 -154
  122. package/template/components/sites-board-view.tsx +0 -67
  123. package/template/components/sites-list-view.tsx +0 -42
  124. package/template/components/sites-table.tsx +0 -382
  125. package/template/components/team-board-view.tsx +0 -122
  126. package/template/components/team-client.tsx +0 -100
  127. package/template/components/team-list-view.tsx +0 -59
  128. package/template/components/team-page-header.tsx +0 -92
  129. package/template/components/team-table.tsx +0 -693
  130. package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
  131. package/template/lib/mock/compliance-kpi.ts +0 -61
  132. package/template/lib/mock/compliance.ts +0 -146
  133. package/template/lib/mock/placements-kpi.ts +0 -134
  134. package/template/lib/mock/placements.ts +0 -183
  135. package/template/lib/mock/sites-directory.ts +0 -16
  136. package/template/lib/mock/sites-kpi.ts +0 -25
  137. package/template/lib/mock/team-kpi.ts +0 -60
  138. package/template/lib/mock/team.ts +0 -118
  139. package/template/lib/placement-board-card-layout.ts +0 -79
  140. package/template/lib/placement-lifecycle.ts +0 -5
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  /**
4
- * Question bank — DataTable + TablePropertiesDrawer + list/board/dashboard (KPI + charts on dashboard).
4
+ * Question bank — DataTable + TablePropertiesDrawer + connected views via `ListPageConnectedViewBody`.
5
5
  */
6
6
 
7
7
  import * as React from "react"
@@ -10,6 +10,7 @@ import { mailtoHref } from "@/lib/mailto"
10
10
  import { DataTable, DataTableToolbar } from "@/components/data-table"
11
11
  import type { DataListViewType } from "@/lib/data-list-view"
12
12
  import type { OpenTablePropertiesHandle } from "@/lib/list-page-table-properties"
13
+ import { QUESTION_BANK_SUPPORTED_VIEWS } from "@/lib/question-bank-supported-views"
13
14
  import type { ColumnDef } from "@/components/data-table/types"
14
15
  import { useTableState } from "@/components/data-table/use-table-state"
15
16
  import { TablePropertiesDrawerButton } from "@/components/table-properties"
@@ -23,23 +24,11 @@ import {
23
24
  } from "@/components/ui/dropdown-menu"
24
25
  import { Tip } from "@/components/ui/tip"
25
26
  import { Skeleton } from "@/components/ui/skeleton"
26
- import {
27
- ResizableHandle,
28
- ResizablePanel,
29
- ResizablePanelGroup,
30
- } from "@/components/ui/resizable"
31
- import {
32
- Tooltip,
33
- TooltipContent,
34
- TooltipTrigger,
35
- } from "@/components/ui/tooltip"
27
+ import { ListPageConnectedViewBody } from "@/components/data-views/list-page-connected-view-body"
28
+ import { ListPageCalendarView } from "@/components/data-views/list-page-calendar-view"
36
29
  import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
37
- import {
38
- LIST_PAGE_SPLIT_MILLER_DETAIL_PANEL_CLASS,
39
- LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS,
40
- LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS,
41
- } from "@/components/data-views/list-page-split-hub-tokens"
42
- import { ListPageTreeColumnHeader } from "@/components/data-views/list-page-tree-column-header"
30
+ import { defineHubViewRenderers } from "@/lib/hub-connected-view-renderers"
31
+ import { QuestionBankFolderColumnsPanel } from "@/components/question-bank-folder-columns-panel"
43
32
  import { QuestionBankBoardView, QUESTION_BANK_BOARD_GROUP_OPTIONS } from "@/components/question-bank-board-view"
44
33
  import { QuestionBankListView } from "@/components/question-bank-list-view"
45
34
  import {
@@ -60,7 +49,7 @@ import {
60
49
  type QuestionBankItem,
61
50
  type QuestionBankType,
62
51
  } from "@/lib/mock/question-bank"
63
- import { type QuestionBankFolder, QUESTION_BANK_FOLDER_COLOR_STYLES, QUESTION_BANK_FOLDER_ICON_COLORS } from "@/lib/mock/question-bank-folders"
52
+ import type { QuestionBankFolder } from "@/lib/mock/question-bank-folders"
64
53
  import {
65
54
  toggleQuestionBankItemFavorite,
66
55
  applyQuestionBankHubDisplayFilters,
@@ -328,312 +317,6 @@ function buildQuestionBankColumns(
328
317
  return cols
329
318
  }
330
319
 
331
- interface HubFolderColumnsPanelProps {
332
- folders: QuestionBankFolder[]
333
- rows: QuestionBankItem[]
334
- panelRenderDetail: (row: QuestionBankItem) => React.ReactNode
335
- onAddFolder: (parentId: string | null) => void
336
- onAddQuestion: (parentId: string | null) => void
337
- onCustomizeFolder?: (folder: QuestionBankFolder) => void
338
- }
339
-
340
- type HierarchyItem = QuestionBankFolder | QuestionBankItem
341
-
342
- function isFolder(item: HierarchyItem): item is QuestionBankFolder {
343
- return 'parentId' in item
344
- }
345
-
346
- function isQuestion(item: HierarchyItem): item is QuestionBankItem {
347
- return 'stem' in item
348
- }
349
-
350
- /** **Panel view** — multi-column folder explorer + optional detail column (Finder-style). */
351
- function HubFolderColumnsPanel({
352
- folders,
353
- rows,
354
- panelRenderDetail,
355
- onAddFolder,
356
- onAddQuestion,
357
- onCustomizeFolder,
358
- }: HubFolderColumnsPanelProps) {
359
- // Track the selected path through the hierarchy
360
- // Initialize with first folder selected by default
361
- const [selectedPath, setSelectedPath] = React.useState<HierarchyItem[]>(() => {
362
- const rootFolders = folders
363
- .filter(f => f.parentId === null)
364
- .sort((a, b) => a.name.localeCompare(b.name))
365
- if (rootFolders.length > 0) {
366
- return [rootFolders[0]]
367
- }
368
- return []
369
- })
370
-
371
- // Track if this is the first render for auto-selection
372
- const isFirstRenderRef = React.useRef(true)
373
-
374
- // Get root items (top-level folders)
375
- const rootFolders = React.useMemo(() => {
376
- return folders
377
- .filter(f => f.parentId === null)
378
- .sort((a, b) => a.name.localeCompare(b.name))
379
- }, [folders])
380
-
381
- // Handle selection at any depth
382
- const handleSelect = (item: HierarchyItem, depth: number) => {
383
- setSelectedPath(prev => [...prev.slice(0, depth), item])
384
- }
385
-
386
- // Auto-select first item at each level (only on first render). Intentional
387
- // empty deps: we want this to run exactly once on mount; depending on the
388
- // referenced values (folders / rows / selectedPath) would re-run on every
389
- // edit and keep re-seeding the selection, undoing the user's choice.
390
- React.useEffect(() => {
391
- if (isFirstRenderRef.current && selectedPath.length > 0) {
392
- const lastItem = selectedPath[selectedPath.length - 1]
393
- if (isFolder(lastItem)) {
394
- const folder = lastItem as QuestionBankFolder
395
- const subfolders = folders.filter(f => f.parentId === folder.id).sort((a, b) => a.name.localeCompare(b.name))
396
- const questionsInFolder = rows.filter(r => r.folderId === folder.id)
397
- const items: HierarchyItem[] = [...subfolders, ...questionsInFolder]
398
-
399
- if (items.length > 0 && !selectedPath[selectedPath.length + 1]) {
400
- setSelectedPath(prev => [...prev, items[0]])
401
- isFirstRenderRef.current = false
402
- }
403
- }
404
- }
405
- // eslint-disable-next-line react-hooks/exhaustive-deps
406
- }, [])
407
-
408
- // Build columns dynamically based on selected path
409
- const columns: Array<{ items: HierarchyItem[]; depth: number; parentId?: string | null }> = React.useMemo(() => {
410
- const cols: Array<{ items: HierarchyItem[]; depth: number; parentId?: string | null }> = [
411
- { items: rootFolders, depth: 0, parentId: null },
412
- ]
413
-
414
- // For each selected folder in the path, add a column with its children
415
- for (let i = 0; i < selectedPath.length; i++) {
416
- const item = selectedPath[i]
417
- if (isFolder(item)) {
418
- // Get subfolders
419
- const subfolders = folders
420
- .filter(f => f.parentId === item.id)
421
- .sort((a, b) => a.name.localeCompare(b.name))
422
-
423
- // Get questions in this folder
424
- const questionsInFolder = rows.filter(r => r.folderId === item.id)
425
-
426
- // Combine folders and questions
427
- const items: HierarchyItem[] = [...subfolders, ...questionsInFolder]
428
-
429
- if (items.length > 0) {
430
- cols.push({ items, depth: i + 1, parentId: item.id })
431
- }
432
- }
433
- }
434
-
435
- return cols
436
- }, [selectedPath, rootFolders, folders, rows])
437
-
438
- const selectedLeaf = selectedPath.length > 0 ? selectedPath.at(-1)! : null
439
- const selectedQuestion =
440
- selectedLeaf && isQuestion(selectedLeaf) ? (selectedLeaf as QuestionBankItem) : null
441
- const selectedFolderLeaf =
442
- selectedLeaf && isFolder(selectedLeaf) ? (selectedLeaf as QuestionBankFolder) : null
443
-
444
- return (
445
- <ResizablePanelGroup
446
- direction="horizontal"
447
- className="flex h-full min-h-0 w-full flex-1 overflow-hidden"
448
- >
449
- {/* Render all columns with handles between them */}
450
- {columns.map(({ items, depth, parentId }, columnIdx) => (
451
- <React.Fragment key={`col-${depth}`}>
452
- {columnIdx > 0 && (
453
- <ResizableHandle withHandle className={LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS} />
454
- )}
455
- <ResizablePanel
456
- id={`col-${depth}`}
457
- defaultSize={columnIdx === 0 ? 35 : columnIdx === 1 ? 35 : 30}
458
- minSize={15}
459
- className={LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS}
460
- >
461
- <ListPageTreeColumnHeader
462
- title={
463
- depth === 0
464
- ? "Categories"
465
- : selectedPath[depth - 1] && isFolder(selectedPath[depth - 1])
466
- ? (selectedPath[depth - 1] as QuestionBankFolder).name
467
- : "Items"
468
- }
469
- trailing={
470
- <>
471
- <span className="shrink-0 text-xs font-medium text-muted-foreground tabular-nums">
472
- {items.length}
473
- </span>
474
- {depth < columns.length - 1 && items.length > 0 ? (
475
- <div className="flex shrink-0 items-center gap-0.5">
476
- <Tooltip>
477
- <TooltipTrigger asChild>
478
- <Button
479
- size="icon-sm"
480
- variant="ghost"
481
- onClick={() => onAddFolder(parentId ?? null)}
482
- aria-label="Add folder"
483
- >
484
- <i className="fa-light fa-folder-plus text-xs" aria-hidden="true" />
485
- </Button>
486
- </TooltipTrigger>
487
- <TooltipContent side="top" sideOffset={4}>
488
- Add folder
489
- </TooltipContent>
490
- </Tooltip>
491
- <Tooltip>
492
- <TooltipTrigger asChild>
493
- <Button
494
- size="icon-sm"
495
- variant="ghost"
496
- onClick={() => onAddQuestion(parentId ?? null)}
497
- aria-label="Add question"
498
- >
499
- <i className="fa-light fa-plus text-xs" aria-hidden="true" />
500
- </Button>
501
- </TooltipTrigger>
502
- <TooltipContent side="top" sideOffset={4}>
503
- Add question
504
- </TooltipContent>
505
- </Tooltip>
506
- </div>
507
- ) : null}
508
- </>
509
- }
510
- />
511
-
512
- {/* Scrollable Items List */}
513
- <div className="min-h-0 flex-1 overflow-y-auto py-1">
514
- {items.map(item => {
515
- const isSelected = selectedPath[depth]?.id === item.id
516
- const isFolder_ = isFolder(item)
517
- const folder = isFolder_ ? item : null
518
- const question = isQuestion(item) ? item : null
519
-
520
- // Get count for folders
521
- const subfolderCount = isFolder_
522
- ? folders.filter(f => f.parentId === item.id).length
523
- : 0
524
- const questionCount = isFolder_
525
- ? rows.filter(r => r.folderId === item.id).length
526
- : 0
527
- const itemCount = subfolderCount + questionCount
528
-
529
- return (
530
- <div
531
- key={item.id}
532
- className="group flex items-center hover:bg-muted/50"
533
- >
534
- <button
535
- onClick={() => handleSelect(item, depth)}
536
- className={cn(
537
- "flex flex-1 items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-75",
538
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
539
- isSelected
540
- ? "bg-accent text-accent-foreground"
541
- : "text-foreground",
542
- // Apply folder background color if it's a folder and not selected, but NOT in the first column
543
- isFolder_ && !isSelected && folder?.colorKey && depth > 0
544
- ? QUESTION_BANK_FOLDER_COLOR_STYLES[folder.colorKey]?.tile
545
- : "",
546
- )}
547
- aria-selected={isSelected}
548
- role="option"
549
- >
550
- {/* Icon - show for folders and questions */}
551
- {isFolder_ ? (
552
- <i className={cn(
553
- "fa-folder text-sm shrink-0",
554
- isSelected ? "fa-solid" : "fa-light",
555
- // Apply folder color from customization (for both selected and unselected)
556
- folder?.colorKey && QUESTION_BANK_FOLDER_ICON_COLORS[folder.colorKey]
557
- )} aria-hidden="true" />
558
- ) : (
559
- <i className={cn("fa-file text-sm shrink-0", isSelected ? "fa-solid" : "fa-light")} aria-hidden="true" />
560
- )}
561
-
562
- {/* Name */}
563
- <span className={cn(
564
- "min-w-0 flex-1 truncate leading-tight",
565
- isSelected && "font-medium"
566
- )}>
567
- {isFolder_ ? folder?.name : question?.stem}
568
- </span>
569
-
570
- {/* Count or metadata */}
571
- <span className={cn(
572
- "shrink-0 tabular-nums text-xs ml-auto",
573
- isSelected ? "text-accent-foreground/70" : "text-muted-foreground",
574
- )}>
575
- {isFolder_ ? itemCount : (question?.type === 'multiple_choice' ? 'MCQ' : question?.difficulty?.charAt(0).toUpperCase())}
576
- </span>
577
- </button>
578
-
579
- {/* Folder actions menu - only for folders */}
580
- {isFolder_ && folder && (
581
- <DropdownMenu>
582
- <DropdownMenuTrigger asChild>
583
- <Button
584
- type="button"
585
- size="icon-xs"
586
- variant="ghost"
587
- aria-label={`Actions for folder ${folder.name}`}
588
- className="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
589
- >
590
- <i className="fa-light fa-ellipsis text-xs" aria-hidden="true" />
591
- </Button>
592
- </DropdownMenuTrigger>
593
- <DropdownMenuContent align="end">
594
- <DropdownMenuItem onSelect={() => onCustomizeFolder?.(folder)}>
595
- <i className="fa-light fa-wand-magic-sparkles text-xs" aria-hidden="true" />
596
- Customize
597
- </DropdownMenuItem>
598
- </DropdownMenuContent>
599
- </DropdownMenu>
600
- )}
601
- </div>
602
- )
603
- })}
604
- </div>
605
- </ResizablePanel>
606
- </React.Fragment>
607
- ))}
608
-
609
- {/* Details panel — question (summary) or folder (aggregates) */}
610
- {(selectedQuestion || selectedFolderLeaf) && (
611
- <>
612
- <ResizableHandle withHandle className={LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS} />
613
- <ResizablePanel id="col-detail" defaultSize={30} minSize={20} className={LIST_PAGE_SPLIT_MILLER_DETAIL_PANEL_CLASS}>
614
- {selectedQuestion ? (
615
- <>
616
- <ListPageTreeColumnHeader title="Details" className="px-4" />
617
- <div className="min-h-0 flex-1 overflow-y-auto px-4 py-4">
618
- {panelRenderDetail(selectedQuestion)}
619
- </div>
620
- </>
621
- ) : selectedFolderLeaf ? (
622
- <div className="min-h-0 flex-1 overflow-hidden">
623
- <FolderDetailsShell
624
- folder={selectedFolderLeaf}
625
- folders={folders}
626
- questions={rows}
627
- />
628
- </div>
629
- ) : null}
630
- </ResizablePanel>
631
- </>
632
- )}
633
- </ResizablePanelGroup>
634
- )
635
- }
636
-
637
320
  export type QuestionBankTableHandle = OpenTablePropertiesHandle
638
321
 
639
322
  export const QuestionBankTable = React.forwardRef<
@@ -650,6 +333,8 @@ export const QuestionBankTable = React.forwardRef<
650
333
  landingFilters?: QuestionBankLandingFilterState | null
651
334
  view?: DataListViewType
652
335
  onViewChange?: (v: DataListViewType) => void
336
+ /** Aligns Properties view tiles with `ListPageTemplate` `supportedViewTypes`. */
337
+ supportedViewTypes?: readonly DataListViewType[]
653
338
  folders: QuestionBankFolder[]
654
339
  onFoldersChange: React.Dispatch<React.SetStateAction<QuestionBankFolder[]>>
655
340
  onItemsChange: React.Dispatch<React.SetStateAction<QuestionBankItem[]>>
@@ -663,6 +348,7 @@ export const QuestionBankTable = React.forwardRef<
663
348
  landingFilters,
664
349
  view = "table",
665
350
  onViewChange,
351
+ supportedViewTypes = QUESTION_BANK_SUPPORTED_VIEWS,
666
352
  folders,
667
353
  onFoldersChange,
668
354
  onItemsChange,
@@ -849,6 +535,7 @@ export const QuestionBankTable = React.forwardRef<
849
535
  onUpdateConditionalRule: updateConditionalRule,
850
536
  currentView: view,
851
537
  onViewChange,
538
+ supportedViewTypes,
852
539
  lifecycleTabLabel: "Question bank",
853
540
  boardGroupByColumnOptions: [...QUESTION_BANK_BOARD_GROUP_OPTIONS],
854
541
  renderFilterOptionValue,
@@ -899,172 +586,143 @@ export const QuestionBankTable = React.forwardRef<
899
586
  />
900
587
  )
901
588
 
902
- if (view === "table") {
903
- return (
904
- <div className="pb-6">
905
- <DataTable<QuestionBankItem> {...tableProps} />
906
- </div>
907
- )
908
- }
909
-
910
- if (view === "list") {
911
- return (
912
- <div className="flex min-h-0 flex-1 flex-col">
913
- {sharedToolbar}
914
- <QuestionBankListView
915
- rows={tableState.rows as QuestionBankItem[]}
916
- onToggleFavorite={toggleFavorite}
917
- onRowActivate={row => tableState.toggleRow(row.id)}
918
- />
919
- </div>
920
- )
921
- }
922
-
923
- if (view === "board") {
924
- return (
925
- <div className="flex min-h-0 flex-1 flex-col">
926
- {sharedToolbar}
927
- <QuestionBankBoardView
928
- rows={tableState.rows as QuestionBankItem[]}
929
- groupByColumnKey={questionBankBoardGroupKey}
930
- onToggleFavorite={toggleFavorite}
931
- onRowActivate={row => tableState.toggleRow(row.id)}
932
- />
933
- </div>
934
- )
935
- }
589
+ const toolbarShell = (body: React.ReactNode) => (
590
+ <div className="flex min-h-0 flex-1 flex-col">
591
+ {sharedToolbar}
592
+ {body}
593
+ </div>
594
+ )
936
595
 
937
- if (view === "folder") {
938
- return (
939
- <div className="flex min-h-0 flex-1 flex-col">
940
- {sharedToolbar}
941
- <QuestionBankOsFolderView
942
- folders={folders}
943
- onFoldersChange={onFoldersChange}
944
- questions={tableState.rows as QuestionBankItem[]}
945
- onQuestionsChange={onItemsChange}
946
- />
947
- </div>
948
- )
949
- }
596
+ const handleFolderSheetCreated = React.useCallback(
597
+ (newFolder: {
598
+ name: string
599
+ icon: string
600
+ colorKey: QuestionBankFolder["colorKey"]
601
+ parentId: string | null
602
+ }) => {
603
+ if (customizingFolder) {
604
+ onFoldersChange(prev =>
605
+ prev.map(f =>
606
+ f.id === customizingFolder.id
607
+ ? { ...f, name: newFolder.name, icon: newFolder.icon, colorKey: newFolder.colorKey }
608
+ : f,
609
+ ),
610
+ )
611
+ setCustomizingFolder(null)
612
+ } else {
613
+ onFoldersChange(prev => [
614
+ ...prev,
615
+ {
616
+ id: `fld-${Date.now()}`,
617
+ name: newFolder.name,
618
+ icon: newFolder.icon,
619
+ colorKey: newFolder.colorKey,
620
+ parentId: newFolder.parentId,
621
+ },
622
+ ])
623
+ }
624
+ setNewFolderOpen(false)
625
+ },
626
+ [customizingFolder, onFoldersChange],
627
+ )
950
628
 
951
- if (view === "panel") {
952
- return (
953
- <>
954
- <div className="flex min-h-0 flex-1 flex-col">
955
- {sharedToolbar}
956
- <ListPageSplitHubChrome aria-label="Question bank folder columns">
957
- <HubFolderColumnsPanel
958
- folders={folders}
959
- rows={tableState.rows as QuestionBankItem[]}
960
- panelRenderDetail={panelRenderDetail}
961
- onAddFolder={openNewFolderForColumn}
962
- onAddQuestion={addQuestionInColumn}
963
- onCustomizeFolder={openCustomizeFolderSheet}
964
- />
965
- </ListPageSplitHubChrome>
966
- </div>
967
- <QuestionBankNewFolderSheet
968
- open={newFolderOpen}
969
- onOpenChange={setNewFolderOpen}
970
- parentFolderId={customizingFolder?.parentId ?? newFolderParentId}
971
- customizingFolder={customizingFolder}
972
- onCreated={(newFolder) => {
973
- if (customizingFolder) {
974
- // Update existing folder
975
- onFoldersChange(prev =>
976
- prev.map(f =>
977
- f.id === customizingFolder.id
978
- ? {
979
- ...f,
980
- name: newFolder.name,
981
- icon: newFolder.icon,
982
- colorKey: newFolder.colorKey,
983
- }
984
- : f,
985
- ),
986
- )
987
- setCustomizingFolder(null)
988
- } else {
989
- // Create new folder
990
- onFoldersChange(prev => [
991
- ...prev,
992
- {
993
- id: `fld-${Date.now()}`,
994
- name: newFolder.name,
995
- icon: newFolder.icon,
996
- colorKey: newFolder.colorKey,
997
- parentId: newFolder.parentId,
998
- },
999
- ])
1000
- }
1001
- setNewFolderOpen(false)
1002
- }}
1003
- />
1004
- </>
1005
- )
1006
- }
629
+ const folderSheet = (
630
+ <QuestionBankNewFolderSheet
631
+ open={newFolderOpen}
632
+ onOpenChange={setNewFolderOpen}
633
+ parentFolderId={customizingFolder?.parentId ?? newFolderParentId}
634
+ customizingFolder={customizingFolder}
635
+ onCreated={handleFolderSheetCreated}
636
+ />
637
+ )
1007
638
 
1008
- if (view === "tree-panel") {
1009
- return (
1010
- <>
1011
- <div className="flex min-h-0 flex-1 flex-col">
1012
- {sharedToolbar}
1013
- <div className="flex min-h-0 flex-1 flex-col">
1014
- <HubTreePanelView
1015
- items={tableState.rows as QuestionBankItem[]}
1016
- folders={folders}
1017
- onItemsChange={onItemsChange}
1018
- onFoldersChange={onFoldersChange}
1019
- />
1020
- </div>
1021
- </div>
1022
- <QuestionBankNewFolderSheet
1023
- open={newFolderOpen}
1024
- onOpenChange={setNewFolderOpen}
1025
- parentFolderId={customizingFolder?.parentId ?? newFolderParentId}
1026
- customizingFolder={customizingFolder}
1027
- onCreated={(newFolder) => {
1028
- if (customizingFolder) {
1029
- // Update existing folder
1030
- onFoldersChange(prev =>
1031
- prev.map(f =>
1032
- f.id === customizingFolder.id
1033
- ? {
1034
- ...f,
1035
- name: newFolder.name,
1036
- icon: newFolder.icon,
1037
- colorKey: newFolder.colorKey,
1038
- }
1039
- : f,
1040
- ),
1041
- )
1042
- setCustomizingFolder(null)
1043
- } else {
1044
- // Create new folder
1045
- onFoldersChange(prev => [
1046
- ...prev,
1047
- {
1048
- id: `fld-${Date.now()}`,
1049
- name: newFolder.name,
1050
- icon: newFolder.icon,
1051
- colorKey: newFolder.colorKey,
1052
- parentId: newFolder.parentId,
1053
- },
1054
- ])
1055
- }
1056
- setNewFolderOpen(false)
1057
- }}
1058
- />
1059
- </>
1060
- )
1061
- }
639
+ const rows = tableState.rows as QuestionBankItem[]
1062
640
 
1063
641
  return (
1064
- <div className="flex min-h-0 flex-1 flex-col">
1065
- {sharedToolbar}
1066
- <QuestionBankDashboardChartsSection rows={tableState.rows as QuestionBankItem[]} />
1067
- </div>
642
+ <ListPageConnectedViewBody
643
+ view={view}
644
+ hubLabel="Question bank"
645
+ renderers={defineHubViewRenderers(QUESTION_BANK_SUPPORTED_VIEWS, {
646
+ "data-table": (
647
+ <div className="pb-6">
648
+ <DataTable<QuestionBankItem> {...tableProps} />
649
+ </div>
650
+ ),
651
+ "list-with-toolbar": toolbarShell(
652
+ <QuestionBankListView
653
+ rows={rows}
654
+ onToggleFavorite={toggleFavorite}
655
+ onRowActivate={row => tableState.toggleRow(row.id)}
656
+ />,
657
+ ),
658
+ "board-with-toolbar": toolbarShell(
659
+ <QuestionBankBoardView
660
+ rows={rows}
661
+ groupByColumnKey={questionBankBoardGroupKey}
662
+ onToggleFavorite={toggleFavorite}
663
+ onRowActivate={row => tableState.toggleRow(row.id)}
664
+ />,
665
+ ),
666
+ "folder-with-toolbar": toolbarShell(
667
+ <QuestionBankOsFolderView
668
+ folders={folders}
669
+ onFoldersChange={onFoldersChange}
670
+ questions={rows}
671
+ onQuestionsChange={onItemsChange}
672
+ />,
673
+ ),
674
+ "panel-with-toolbar": (
675
+ <>
676
+ {toolbarShell(
677
+ <ListPageSplitHubChrome aria-label="Question bank folder columns">
678
+ <QuestionBankFolderColumnsPanel
679
+ folders={folders}
680
+ rows={rows}
681
+ panelRenderDetail={panelRenderDetail}
682
+ onAddFolder={openNewFolderForColumn}
683
+ onAddQuestion={addQuestionInColumn}
684
+ onCustomizeFolder={openCustomizeFolderSheet}
685
+ />
686
+ </ListPageSplitHubChrome>,
687
+ )}
688
+ {folderSheet}
689
+ </>
690
+ ),
691
+ "tree-panel-with-toolbar": (
692
+ <>
693
+ {toolbarShell(
694
+ <div className="flex min-h-0 flex-1 flex-col">
695
+ <HubTreePanelView
696
+ items={rows}
697
+ folders={folders}
698
+ onItemsChange={onItemsChange}
699
+ onFoldersChange={onFoldersChange}
700
+ />
701
+ </div>,
702
+ )}
703
+ {folderSheet}
704
+ </>
705
+ ),
706
+ "calendar-with-toolbar": toolbarShell(
707
+ <ListPageCalendarView
708
+ rows={rows}
709
+ getRowId={row => row.id}
710
+ getEventDate={row => row.updatedAt}
711
+ getEventLabel={row => row.stem}
712
+ getEventMeta={row => row.topic}
713
+ emptyMonthLabel="No questions on this day."
714
+ ariaLabel="Question bank calendar"
715
+ showSummaryPanel={displayOptions.showCalendarSummaryPanel}
716
+ calendarMainView={displayOptions.calendarMainView}
717
+ onCalendarMainViewChange={v => patchDisplay({ calendarMainView: v })}
718
+ onEventActivate={row => tableState.toggleRow(row.id)}
719
+ />,
720
+ ),
721
+ "dashboard-with-toolbar": toolbarShell(
722
+ <QuestionBankDashboardChartsSection rows={rows} />,
723
+ ),
724
+ })}
725
+ />
1068
726
  )
1069
727
  })
1070
728