@exxatdesignux/ui 0.2.14 → 0.2.16

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 (56) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -1
  3. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +3 -0
  4. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +3 -1
  5. package/consumer-extras/patterns/collaboration-access-pattern.md +2 -0
  6. package/package.json +1 -1
  7. package/src/components/ui/dropdown-menu.tsx +2 -0
  8. package/src/components/ui/popover.tsx +2 -2
  9. package/src/components/ui/select.tsx +1 -1
  10. package/src/components/ui/tooltip.tsx +7 -1
  11. package/src/globals.css +27 -2
  12. package/src/theme.css +4 -2
  13. package/template/AGENTS.md +6 -4
  14. package/template/app/(app)/question-bank/layout.tsx +11 -4
  15. package/template/app/globals.css +34 -2
  16. package/template/components/app-sidebar.tsx +89 -41
  17. package/template/components/ask-leo-sidebar.tsx +1 -2
  18. package/template/components/compliance-board-view.tsx +11 -3
  19. package/template/components/compliance-list-view.tsx +16 -3
  20. package/template/components/compliance-table.tsx +5 -1
  21. package/template/components/data-table/index.tsx +25 -11
  22. package/template/components/data-views/finder-panel-view.tsx +2 -2
  23. package/template/components/data-views/index.ts +19 -0
  24. package/template/components/data-views/list-page-split-details-placeholder.tsx +3 -3
  25. package/template/components/data-views/list-page-split-hub-tokens.ts +1 -1
  26. package/template/components/data-views/list-page-tree-column-header.tsx +1 -1
  27. package/template/components/data-views/outline-tree-menu.tsx +157 -0
  28. package/template/components/data-views/question-bank-folder-tree-branch.tsx +210 -0
  29. package/template/components/exxat-product-logo.tsx +11 -72
  30. package/template/components/folder-details-shell.tsx +1 -1
  31. package/template/components/hub-tree-panel-view.tsx +88 -80
  32. package/template/components/key-metrics.tsx +50 -13
  33. package/template/components/page-header.tsx +19 -10
  34. package/template/components/product-switcher.tsx +1 -4
  35. package/template/components/question-bank-board-view.tsx +11 -2
  36. package/template/components/question-bank-client.tsx +111 -69
  37. package/template/components/question-bank-list-view.tsx +12 -1
  38. package/template/components/question-bank-page-header.tsx +18 -2
  39. package/template/components/question-bank-secondary-nav.tsx +12 -225
  40. package/template/components/question-bank-table.tsx +6 -1
  41. package/template/components/secondary-panel.tsx +1 -1
  42. package/template/components/site-header.tsx +21 -2
  43. package/template/components/team-board-view.tsx +11 -3
  44. package/template/components/team-list-view.tsx +16 -3
  45. package/template/components/team-table.tsx +6 -2
  46. package/template/components/templates/dedicated-search-landing-template.tsx +86 -20
  47. package/template/components/templates/list-page.tsx +1 -3
  48. package/template/components/templates/nested-secondary-panel-shell.tsx +3 -4
  49. package/template/docs/collaboration-access-pattern.md +2 -0
  50. package/template/docs/question-bank-hub-header-pattern.md +25 -0
  51. package/template/hooks/use-secondary-panel-hub-nav.ts +17 -1
  52. package/template/lib/mock/navigation.tsx +30 -1
  53. package/template/lib/question-bank-nav.ts +26 -0
  54. package/template/package.json +3 -3
  55. package/template/components/command-menu-01.tsx +0 -133
  56. package/template/components/command-menu-02.tsx +0 -386
@@ -3,6 +3,7 @@
3
3
  /**
4
4
  * Question bank hub — ListPageTemplate + KeyMetrics + QuestionBankTable (Team / Compliance pattern).
5
5
  * URL hash syncs the active view tab; `?scope=` + `folderId=` sync with the secondary nav (`lib/question-bank-nav.ts`).
6
+ * (Primary sidebar “Library” must not treat that hash as “off-route” — `app-sidebar` `isNavActive` ignores hash for `href` without `#…`.)
6
7
  */
7
8
 
8
9
  import * as React from "react"
@@ -14,6 +15,7 @@ import {
14
15
  type DataListViewType,
15
16
  } from "@/components/data-views"
16
17
  import { QuestionBankPageHeader } from "@/components/question-bank-page-header"
18
+ import { QuestionBankNewFolderSheet } from "@/components/question-bank-new-folder-sheet"
17
19
  import { CollaborationAccessFlow } from "@/components/collaboration-access-flow"
18
20
  import { QuestionBankTable, type QuestionBankTableHandle } from "@/components/question-bank-table"
19
21
  import { SecondaryPanelHubTemplate } from "@/components/templates/secondary-panel-hub-template"
@@ -22,7 +24,7 @@ import { KeyMetrics } from "@/components/key-metrics"
22
24
  import { useSecondaryPanelHubNav } from "@/hooks/use-secondary-panel-hub-nav"
23
25
  import { QUESTION_BANK_ITEMS } from "@/lib/mock/question-bank"
24
26
  import { QUESTION_BANK_HEADER_COLLABORATORS } from "@/lib/mock/question-bank-header-collaborators"
25
- import { DEFAULT_QUESTION_BANK_FOLDERS } from "@/lib/mock/question-bank-folders"
27
+ import { DEFAULT_QUESTION_BANK_FOLDERS, type QuestionBankFolder } from "@/lib/mock/question-bank-folders"
26
28
  import { questionBankKpiInsight, questionBankKpiMetrics } from "@/lib/mock/question-bank-kpi"
27
29
  import {
28
30
  applyQuestionBankHubDisplayFilters,
@@ -97,6 +99,8 @@ export function QuestionBankClient() {
97
99
  parseNav: parseQuestionBankNav,
98
100
  canonicalHref: questionBankCanonicalNavHref,
99
101
  shouldReopenPanel: isQuestionBankDefaultNav,
102
+ /** Hub/find + list are full-width — layout closes the panel; do not fight it with `openPanel`. */
103
+ reopenPanelOnPathnames: [QUESTION_BANK_LIBRARY_PATH],
100
104
  })
101
105
  const isDedicatedSearch = isQuestionBankDedicatedSearchPathname(pathname)
102
106
  const isHubFindSurface = pathname === QUESTION_BANK_HUB_FIND_PATH
@@ -173,6 +177,17 @@ export function QuestionBankClient() {
173
177
  const [items, setItems] = React.useState(() => QUESTION_BANK_ITEMS.map(q => ({ ...q })))
174
178
  const [folders, setFolders] = React.useState(() => DEFAULT_QUESTION_BANK_FOLDERS.map(f => ({ ...f })))
175
179
 
180
+ const [hubFolderCustomizeSheetOpen, setHubFolderCustomizeSheetOpen] = React.useState(false)
181
+ const [hubFolderCustomizeTarget, setHubFolderCustomizeTarget] = React.useState<QuestionBankFolder | null>(null)
182
+
183
+ const openHubScopedFolderCustomize = React.useCallback(() => {
184
+ if (navState.scope !== "folder" || !navState.folderId) return
185
+ const f = folders.find(x => x.id === navState.folderId)
186
+ if (!f) return
187
+ setHubFolderCustomizeTarget(f)
188
+ setHubFolderCustomizeSheetOpen(true)
189
+ }, [folders, navState.folderId, navState.scope])
190
+
176
191
  const filteredItems = React.useMemo(
177
192
  () => applyQuestionBankHubDisplayFilters(items, folders, navState, landingFilters),
178
193
  [items, folders, landingFilters, navState],
@@ -331,76 +346,103 @@ export function QuestionBankClient() {
331
346
  resourceLabel={hubHeader.title}
332
347
  >
333
348
  {({ collaborators, openInvite }) => (
334
- <SecondaryPanelHubTemplate
335
- bridges={(
336
- <>
337
- <QuestionBankFolderBridge
338
- folders={folders}
339
- onFoldersChange={setFolders}
340
- items={items}
341
- onItemsChange={setItems}
342
- />
343
- <QuestionBankAccessBridge openManageAccess={openInvite} />
344
- </>
345
- )}
346
- siteHeader={{
347
- title: hubHeader.title,
348
- breadcrumbs: hubHeader.breadcrumbs,
349
- }}
350
- >
351
- <ListPageTemplate
352
- defaultTabs={DEFAULT_TABS}
353
- tabs={tabs}
354
- onTabsChange={setTabs}
355
- activeTabId={activeTabId}
356
- onActiveTabChange={onActiveTabChange}
357
- getTabCount={() => count}
358
- tablePropertiesRef={tableRef}
359
- header={(
360
- <QuestionBankPageHeader
361
- variant="collaboration"
362
- title={hubHeader.title}
363
- questionCount={count}
364
- collaborators={collaborators}
365
- onNewQuestion={() => {}}
366
- onExport={() => setExportOpen(true)}
367
- onAddCollaborator={openInvite}
368
- onCollaboratorsOpen={openInvite}
369
- showMetrics={showMetrics}
370
- onToggleMetrics={() => setShowMetrics(v => !v)}
371
- />
372
- )}
373
- metrics={(
374
- <KeyMetrics
375
- variant="flat"
376
- metrics={metrics}
377
- insight={insight}
378
- showHeader={false}
379
- metricsSingleRow
380
- />
381
- )}
382
- showMetrics={showMetrics}
383
- exportOpen={exportOpen}
384
- onExportOpenChange={setExportOpen}
385
- exportTotalRows={count}
386
- renderContent={(tab, updateTab) => (
387
- <QuestionBankTable
388
- key={tab.id}
389
- ref={tableRef}
390
- items={items}
391
- navState={navState}
392
- urlListSearch={urlToolbarSearchSync}
393
- landingFilters={null}
394
- searchLanding={false}
395
- folders={folders}
396
- onFoldersChange={setFolders}
397
- onItemsChange={setItems}
398
- view={tab.viewType}
399
- onViewChange={(v: DataListViewType) => updateTab({ viewType: v, icon: dataListViewIcon(v) })}
400
- />
349
+ <>
350
+ <SecondaryPanelHubTemplate
351
+ bridges={(
352
+ <>
353
+ <QuestionBankFolderBridge
354
+ folders={folders}
355
+ onFoldersChange={setFolders}
356
+ items={items}
357
+ onItemsChange={setItems}
358
+ />
359
+ <QuestionBankAccessBridge openManageAccess={openInvite} />
360
+ </>
401
361
  )}
362
+ siteHeader={{
363
+ title: hubHeader.title,
364
+ breadcrumbs: hubHeader.breadcrumbs,
365
+ }}
366
+ >
367
+ <ListPageTemplate
368
+ defaultTabs={DEFAULT_TABS}
369
+ tabs={tabs}
370
+ onTabsChange={setTabs}
371
+ activeTabId={activeTabId}
372
+ onActiveTabChange={onActiveTabChange}
373
+ getTabCount={() => count}
374
+ tablePropertiesRef={tableRef}
375
+ header={(
376
+ <QuestionBankPageHeader
377
+ variant="collaboration"
378
+ title={hubHeader.title}
379
+ questionCount={count}
380
+ collaborators={collaborators}
381
+ onNewQuestion={() => {}}
382
+ onExport={() => setExportOpen(true)}
383
+ onAddCollaborator={openInvite}
384
+ onCollaboratorsOpen={openInvite}
385
+ showMetrics={showMetrics}
386
+ onToggleMetrics={() => setShowMetrics(v => !v)}
387
+ onCustomizeFolder={
388
+ navState.scope === "folder" && navState.folderId ? openHubScopedFolderCustomize : undefined
389
+ }
390
+ />
391
+ )}
392
+ metrics={(
393
+ <KeyMetrics
394
+ variant="flat"
395
+ metrics={metrics}
396
+ insight={insight}
397
+ showHeader={false}
398
+ metricsSingleRow
399
+ />
400
+ )}
401
+ showMetrics={showMetrics}
402
+ exportOpen={exportOpen}
403
+ onExportOpenChange={setExportOpen}
404
+ exportTotalRows={count}
405
+ renderContent={(tab, updateTab) => (
406
+ <QuestionBankTable
407
+ key={tab.id}
408
+ ref={tableRef}
409
+ items={items}
410
+ navState={navState}
411
+ urlListSearch={urlToolbarSearchSync}
412
+ landingFilters={null}
413
+ searchLanding={false}
414
+ folders={folders}
415
+ onFoldersChange={setFolders}
416
+ onItemsChange={setItems}
417
+ view={tab.viewType}
418
+ onViewChange={(v: DataListViewType) => updateTab({ viewType: v, icon: dataListViewIcon(v) })}
419
+ />
420
+ )}
421
+ />
422
+ </SecondaryPanelHubTemplate>
423
+ <QuestionBankNewFolderSheet
424
+ open={hubFolderCustomizeSheetOpen}
425
+ onOpenChange={open => {
426
+ setHubFolderCustomizeSheetOpen(open)
427
+ if (!open) setHubFolderCustomizeTarget(null)
428
+ }}
429
+ parentFolderId={hubFolderCustomizeTarget?.parentId ?? null}
430
+ customizingFolder={hubFolderCustomizeTarget}
431
+ descriptionText="Update how this folder appears in the bank. Name, color, and icon apply everywhere the folder is shown."
432
+ onCreated={newFolder => {
433
+ const target = hubFolderCustomizeTarget
434
+ if (!target) return
435
+ setFolders(prev =>
436
+ prev.map(f =>
437
+ f.id === target.id
438
+ ? { ...f, name: newFolder.name, icon: newFolder.icon, colorKey: newFolder.colorKey }
439
+ : f,
440
+ ),
441
+ )
442
+ setHubFolderCustomizeTarget(null)
443
+ }}
402
444
  />
403
- </SecondaryPanelHubTemplate>
445
+ </>
404
446
  )}
405
447
  </CollaborationAccessFlow>
406
448
  )
@@ -11,9 +11,11 @@ import {
11
11
  function QuestionBankListRow({
12
12
  row,
13
13
  onToggleFavorite,
14
+ onRowActivate,
14
15
  }: {
15
16
  row: QuestionBankItem
16
17
  onToggleFavorite: (row: QuestionBankItem) => void
18
+ onRowActivate?: (row: QuestionBankItem) => void
17
19
  }) {
18
20
  return (
19
21
  <li>
@@ -21,6 +23,7 @@ function QuestionBankListRow({
21
23
  className={QUESTION_BANK_FAVORITE_HOVER_GROUP}
22
24
  layout="row"
23
25
  rowContainerClassName="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:gap-4"
26
+ onClick={onRowActivate ? () => onRowActivate(row) : undefined}
24
27
  rowEnd={(
25
28
  <div className="flex shrink-0 items-center gap-1">
26
29
  <QuestionBankFavoriteButton row={row} onToggleFavorite={onToggleFavorite} />
@@ -44,9 +47,12 @@ function QuestionBankListRow({
44
47
  export function QuestionBankListView({
45
48
  rows,
46
49
  onToggleFavorite,
50
+ onRowActivate,
47
51
  }: {
48
52
  rows: QuestionBankItem[]
49
53
  onToggleFavorite: (row: QuestionBankItem) => void
54
+ /** When set (e.g. table selection), clicking a row toggles the same selection as the grid. */
55
+ onRowActivate?: (row: QuestionBankItem) => void
50
56
  }) {
51
57
  if (rows.length === 0) {
52
58
  return (
@@ -59,7 +65,12 @@ export function QuestionBankListView({
59
65
  return (
60
66
  <ul className="flex list-none flex-col gap-2 px-4 pb-8 pt-2 lg:px-6">
61
67
  {rows.map(row => (
62
- <QuestionBankListRow key={row.id} row={row} onToggleFavorite={onToggleFavorite} />
68
+ <QuestionBankListRow
69
+ key={row.id}
70
+ row={row}
71
+ onToggleFavorite={onToggleFavorite}
72
+ onRowActivate={onRowActivate}
73
+ />
63
74
  ))}
64
75
  </ul>
65
76
  )
@@ -27,7 +27,7 @@ export interface QuestionBankPageHeaderProps {
27
27
  showMetrics?: boolean
28
28
  onToggleMetrics?: () => void
29
29
  showTitleBlock?: boolean
30
- /** `collaboration` adds access line + collaborator stack before CTAs. */
30
+ /** `collaboration` adds access line + collaborator face row before CTAs. */
31
31
  variant?: PageHeaderVariant
32
32
  /** Optional role / access row when `variant="collaboration"` (badge + copy). */
33
33
  accessInfo?: React.ReactNode
@@ -44,6 +44,11 @@ export interface QuestionBankPageHeaderProps {
44
44
  subtitleOverride?: string
45
45
  /** Omits the action column (e.g. search landing before first query). */
46
46
  hideActions?: boolean
47
+ /**
48
+ * When provided, the **More** menu includes **Customize folder** (opens the hub folder sheet).
49
+ * Wire this when the library is scoped to a folder (`?scope=folder&folderId=…`).
50
+ */
51
+ onCustomizeFolder?: () => void
47
52
  }
48
53
 
49
54
  export function QuestionBankPageHeader({
@@ -57,7 +62,7 @@ export function QuestionBankPageHeader({
57
62
  variant = "default",
58
63
  accessInfo,
59
64
  collaborators = QUESTION_BANK_HEADER_COLLABORATORS,
60
- collaboratorDisplayLimit = 4,
65
+ collaboratorDisplayLimit = 3,
61
66
  onAddCollaborator = () => {},
62
67
  onCollaboratorsOpen,
63
68
  collaborationAddLabel = COLLABORATION_HEADER_ADD_LABEL,
@@ -65,6 +70,7 @@ export function QuestionBankPageHeader({
65
70
  hideNewQuestion = false,
66
71
  subtitleOverride,
67
72
  hideActions = false,
73
+ onCustomizeFolder,
68
74
  }: QuestionBankPageHeaderProps) {
69
75
  const [moreOpen, setMoreOpen] = React.useState(false)
70
76
  const countLine =
@@ -119,6 +125,16 @@ export function QuestionBankPageHeader({
119
125
  {addCollaboratorLabel}
120
126
  </DropdownMenuItem>
121
127
  ) : null}
128
+ {onCustomizeFolder ? (
129
+ <DropdownMenuItem
130
+ onSelect={() => {
131
+ window.setTimeout(() => onCustomizeFolder(), 0)
132
+ }}
133
+ >
134
+ <i className="fa-light fa-wand-magic-sparkles" aria-hidden="true" />
135
+ Customize folder
136
+ </DropdownMenuItem>
137
+ ) : null}
122
138
  <DropdownMenuItem
123
139
  onSelect={() => {
124
140
  window.setTimeout(() => onExport(), 0)
@@ -8,7 +8,6 @@
8
8
  import * as React from "react"
9
9
  import Link from "next/link"
10
10
  import { usePathname, useRouter, useSearchParams } from "next/navigation"
11
- import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
12
11
  import { Button } from "@/components/ui/button"
13
12
  import {
14
13
  Dialog,
@@ -30,43 +29,19 @@ import { Kbd, KbdGroup } from "@/components/ui/kbd"
30
29
  import { Tip } from "@/components/ui/tip"
31
30
  import { cn } from "@/lib/utils"
32
31
  import type { QuestionBankFolder } from "@/lib/mock/question-bank-folders"
33
- import { QUESTION_BANK_FOLDER_ICON_COLORS } from "@/lib/mock/question-bank-folders"
34
32
  import { DEFAULT_QUESTION_BANK_FOLDERS, newFolderId, collectFolderDescendantIds } from "@/lib/mock/question-bank-folders"
35
33
  import { useSecondaryPanel } from "@/components/secondary-panel"
36
34
  import { QuestionBankNewFolderSheet } from "@/components/question-bank-new-folder-sheet"
37
35
  import {
38
- isQuestionBankDedicatedSearchPathname,
36
+ isQuestionBankNavActive,
39
37
  parseQuestionBankNav,
40
38
  QUESTION_BANK_FAVORITES_FOLDER_ID,
41
39
  QUESTION_BANK_LIBRARY_HUB_PATHS,
42
- isQuestionBankSearchNavActive,
43
40
  questionBankFavoritesFolderHref,
44
41
  questionBankHubScopeHref,
45
- questionBankSearchLandingNavHref,
46
42
  type QuestionBankNavScope,
47
43
  } from "@/lib/question-bank-nav"
48
-
49
- function isNavActive(
50
- pathname: string,
51
- nav: ReturnType<typeof parseQuestionBankNav>,
52
- scope: QuestionBankNavScope,
53
- folderId?: string | null,
54
- ) {
55
- if (!QUESTION_BANK_LIBRARY_HUB_PATHS.includes(pathname)) return false
56
- // Dedicated search shells (list / hub-find) use the “Search” row for All/My — not these rows.
57
- if (scope === "all") {
58
- if (isQuestionBankDedicatedSearchPathname(pathname)) return false
59
- return nav.scope === "all"
60
- }
61
- if (scope === "my") {
62
- if (isQuestionBankDedicatedSearchPathname(pathname)) return false
63
- return nav.scope === "my"
64
- }
65
- if (scope === "folder" && folderId) {
66
- return nav.scope === "folder" && nav.folderId === folderId
67
- }
68
- return false
69
- }
44
+ import { QuestionBankFolderTreeBranch } from "@/components/data-views/question-bank-folder-tree-branch"
70
45
 
71
46
  function NavRow({
72
47
  href,
@@ -147,179 +122,6 @@ function IconNavRow({
147
122
  )
148
123
  }
149
124
 
150
- function PanelFolderBranch({
151
- folder,
152
- folders,
153
- depth,
154
- pathname,
155
- hubSearchParams,
156
- nav,
157
- canManageFolders,
158
- canManageAccess,
159
- onAddSubfolder,
160
- onCustomizeFolder,
161
- onManageAccess,
162
- onDeleteFolder,
163
- }: {
164
- folder: QuestionBankFolder
165
- folders: QuestionBankFolder[]
166
- depth: number
167
- pathname: string
168
- hubSearchParams: URLSearchParams
169
- nav: ReturnType<typeof parseQuestionBankNav>
170
- canManageFolders: boolean
171
- canManageAccess: boolean
172
- onAddSubfolder: (parentId: string) => void
173
- onCustomizeFolder: (folder: QuestionBankFolder) => void
174
- onManageAccess: () => void
175
- onDeleteFolder: (folder: QuestionBankFolder) => void
176
- }) {
177
- const childFolders = React.useMemo(
178
- () =>
179
- folders
180
- .filter(f => f.parentId === folder.id)
181
- .sort((a, b) => a.name.localeCompare(b.name)),
182
- [folders, folder.id],
183
- )
184
-
185
- const hasSubfolders = childFolders.length > 0
186
- const indent = depth * 10
187
-
188
- const folderHref = questionBankHubScopeHref(pathname, hubSearchParams, {
189
- scope: "folder",
190
- folderId: folder.id,
191
- })
192
- const folderActive = isNavActive(pathname, nav, "folder", folder.id)
193
-
194
- return (
195
- <Collapsible defaultOpen={depth < 1} className="group">
196
- <div
197
- className={cn(
198
- "group/row flex min-w-0 items-center rounded-md px-2 transition-colors",
199
- folderActive
200
- ? "bg-sidebar-accent text-sidebar-accent-foreground"
201
- : "hover:bg-sidebar-accent/50",
202
- )}
203
- >
204
- <div style={{ width: indent }} className="shrink-0" aria-hidden />
205
- {hasSubfolders ? (
206
- <CollapsibleTrigger asChild>
207
- <button
208
- type="button"
209
- className="flex size-8 shrink-0 items-center justify-center text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
210
- aria-label={`${folder.name} — expand or collapse`}
211
- >
212
- <i
213
- className="fa-light fa-chevron-right text-xs transition-transform duration-150 group-data-[state=open]:rotate-90"
214
- aria-hidden
215
- />
216
- </button>
217
- </CollapsibleTrigger>
218
- ) : (
219
- <div className="size-8 shrink-0" aria-hidden />
220
- )}
221
- <Tip label={folder.name} side="right">
222
- <Link
223
- href={folderHref}
224
- scroll={false}
225
- aria-current={folderActive ? "page" : undefined}
226
- className={cn(
227
- "flex min-w-0 flex-1 items-center gap-2 py-1.5 text-left text-sm transition-colors",
228
- "rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
229
- folderActive ? "font-medium" : "text-sidebar-foreground",
230
- )}
231
- >
232
- <i
233
- className={cn("fa-light shrink-0 text-sm", folder.icon, QUESTION_BANK_FOLDER_ICON_COLORS[folder.colorKey])}
234
- aria-hidden
235
- />
236
- <span className="min-w-0 flex-1 truncate leading-tight">{folder.name}</span>
237
- {hasSubfolders ? (
238
- <span className="shrink-0 text-xs tabular-nums text-muted-foreground">{childFolders.length}</span>
239
- ) : null}
240
- </Link>
241
- </Tip>
242
- {canManageFolders ? (
243
- <DropdownMenu>
244
- <Tip label={`Folder actions for ${folder.name}`} side="right">
245
- <DropdownMenuTrigger asChild>
246
- <Button
247
- type="button"
248
- size="icon-xs"
249
- variant="ghost"
250
- aria-label={`Folder actions for ${folder.name}`}
251
- className="shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover/row:opacity-100 group-focus-within/row:opacity-100"
252
- onClick={event => event.stopPropagation()}
253
- >
254
- <i className="fa-light fa-ellipsis text-xs" aria-hidden="true" />
255
- </Button>
256
- </DropdownMenuTrigger>
257
- </Tip>
258
- <DropdownMenuContent align="end">
259
- <DropdownMenuItem
260
- onSelect={() => {
261
- window.setTimeout(() => onAddSubfolder(folder.id), 0)
262
- }}
263
- >
264
- <i className="fa-light fa-plus text-xs" aria-hidden="true" />
265
- Add folder
266
- </DropdownMenuItem>
267
- <DropdownMenuItem
268
- onSelect={() => {
269
- window.setTimeout(() => onCustomizeFolder(folder), 0)
270
- }}
271
- >
272
- <i className="fa-light fa-wand-magic-sparkles text-xs" aria-hidden="true" />
273
- Customize
274
- </DropdownMenuItem>
275
- <DropdownMenuItem
276
- disabled={!canManageAccess}
277
- onSelect={() => {
278
- window.setTimeout(() => onManageAccess(), 0)
279
- }}
280
- >
281
- <i className="fa-light fa-user-gear text-xs" aria-hidden="true" />
282
- Manage access
283
- </DropdownMenuItem>
284
- <DropdownMenuSeparator />
285
- <DropdownMenuItem
286
- variant="destructive"
287
- onSelect={() => {
288
- window.setTimeout(() => onDeleteFolder(folder), 0)
289
- }}
290
- >
291
- <i className="fa-light fa-trash text-xs" aria-hidden="true" />
292
- Delete
293
- </DropdownMenuItem>
294
- </DropdownMenuContent>
295
- </DropdownMenu>
296
- ) : null}
297
- </div>
298
- {hasSubfolders ? (
299
- <CollapsibleContent>
300
- {childFolders.map(child => (
301
- <PanelFolderBranch
302
- key={child.id}
303
- folder={child}
304
- folders={folders}
305
- depth={depth + 1}
306
- pathname={pathname}
307
- hubSearchParams={hubSearchParams}
308
- nav={nav}
309
- canManageFolders={canManageFolders}
310
- canManageAccess={canManageAccess}
311
- onAddSubfolder={onAddSubfolder}
312
- onCustomizeFolder={onCustomizeFolder}
313
- onManageAccess={onManageAccess}
314
- onDeleteFolder={onDeleteFolder}
315
- />
316
- ))}
317
- </CollapsibleContent>
318
- ) : null}
319
- </Collapsible>
320
- )
321
- }
322
-
323
125
  export function QuestionBankSecondaryNav() {
324
126
  const pathname = usePathname()
325
127
  const router = useRouter()
@@ -340,7 +142,7 @@ export function QuestionBankSecondaryNav() {
340
142
  const canManageFolders = questionBankFolderBridge != null
341
143
  const canManageAccess = questionBankAccessBridge != null
342
144
 
343
- /** Favorites is a primary nav row (with All / My / Search), not under “Folders”. */
145
+ /** Favorites is a primary nav row (with All / My), not under “Folders”. */
344
146
  const folderTreeRoots = React.useMemo(
345
147
  () =>
346
148
  folders
@@ -518,32 +320,25 @@ export function QuestionBankSecondaryNav() {
518
320
  <ul className="flex flex-1 flex-col items-center gap-1 overflow-y-auto px-1 py-2" role="list">
519
321
  <IconNavRow
520
322
  href={questionBankHubScopeHref(pathname, searchParams, { scope: "all" })}
521
- active={isNavActive(pathname, nav, "all")}
323
+ active={isQuestionBankNavActive(pathname, nav, "all")}
522
324
  iconClass="fa-table-list"
523
325
  label="All questions"
524
326
  onClick={() => openPanel("question-bank")}
525
327
  />
526
328
  <IconNavRow
527
329
  href={questionBankHubScopeHref(pathname, searchParams, { scope: "my" })}
528
- active={isNavActive(pathname, nav, "my")}
330
+ active={isQuestionBankNavActive(pathname, nav, "my")}
529
331
  iconClass="fa-user"
530
332
  label="My questions"
531
333
  onClick={() => openPanel("question-bank")}
532
334
  />
533
335
  <IconNavRow
534
336
  href={questionBankFavoritesFolderHref(pathname, searchParams)}
535
- active={isNavActive(pathname, nav, "folder", QUESTION_BANK_FAVORITES_FOLDER_ID)}
337
+ active={isQuestionBankNavActive(pathname, nav, "folder", QUESTION_BANK_FAVORITES_FOLDER_ID)}
536
338
  iconClass="fa-star"
537
339
  label="Favorites"
538
340
  onClick={() => openPanel("question-bank")}
539
341
  />
540
- <IconNavRow
541
- href={questionBankSearchLandingNavHref(nav, searchParams)}
542
- active={isQuestionBankSearchNavActive(pathname, nav)}
543
- iconClass="fa-magnifying-glass"
544
- label="Search"
545
- onClick={() => openPanel("question-bank")}
546
- />
547
342
  <li className="flex w-full justify-center pt-1" role="none">
548
343
  <DropdownMenu>
549
344
  <DropdownMenuTrigger asChild>
@@ -566,7 +361,7 @@ export function QuestionBankSecondaryNav() {
566
361
  scope: "folder",
567
362
  folderId: folder.id,
568
363
  })
569
- const active = isNavActive(pathname, nav, "folder", folder.id)
364
+ const active = isQuestionBankNavActive(pathname, nav, "folder", folder.id)
570
365
  return (
571
366
  <DropdownMenuItem key={folder.id} asChild>
572
367
  <Link
@@ -613,31 +408,24 @@ export function QuestionBankSecondaryNav() {
613
408
  <ul className="space-y-0.5" role="list">
614
409
  <NavRow
615
410
  href={questionBankHubScopeHref(pathname, searchParams, { scope: "all" })}
616
- active={isNavActive(pathname, nav, "all")}
411
+ active={isQuestionBankNavActive(pathname, nav, "all")}
617
412
  iconClass="fa-table-list"
618
413
  label="All questions"
619
414
  onClick={() => openPanel("question-bank")}
620
415
  />
621
416
  <NavRow
622
417
  href={questionBankHubScopeHref(pathname, searchParams, { scope: "my" })}
623
- active={isNavActive(pathname, nav, "my")}
418
+ active={isQuestionBankNavActive(pathname, nav, "my")}
624
419
  iconClass="fa-user"
625
420
  label="My questions"
626
421
  />
627
422
  <NavRow
628
423
  href={questionBankFavoritesFolderHref(pathname, searchParams)}
629
- active={isNavActive(pathname, nav, "folder", QUESTION_BANK_FAVORITES_FOLDER_ID)}
424
+ active={isQuestionBankNavActive(pathname, nav, "folder", QUESTION_BANK_FAVORITES_FOLDER_ID)}
630
425
  iconClass="fa-star"
631
426
  label="Favorites"
632
427
  onClick={() => openPanel("question-bank")}
633
428
  />
634
- <NavRow
635
- href={questionBankSearchLandingNavHref(nav, searchParams)}
636
- active={isQuestionBankSearchNavActive(pathname, nav)}
637
- iconClass="fa-magnifying-glass"
638
- label="Search"
639
- onClick={() => openPanel("question-bank")}
640
- />
641
429
  <li role="presentation" className="select-none">
642
430
  <div className="flex items-center justify-between gap-2 px-2 pt-3 pb-1">
643
431
  <span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground/70">
@@ -659,11 +447,10 @@ export function QuestionBankSecondaryNav() {
659
447
  </div>
660
448
  </li>
661
449
  {folderTreeRoots.map(folder => (
662
- <li key={folder.id} className="min-w-0">
663
- <PanelFolderBranch
450
+ <li key={folder.id} className="min-w-0 w-full list-none">
451
+ <QuestionBankFolderTreeBranch
664
452
  folder={folder}
665
453
  folders={folders}
666
- depth={0}
667
454
  pathname={pathname}
668
455
  hubSearchParams={searchParams}
669
456
  nav={nav}
@@ -898,7 +898,11 @@ export const QuestionBankTable = React.forwardRef<
898
898
  return (
899
899
  <div className="flex min-h-0 flex-1 flex-col">
900
900
  {sharedToolbar}
901
- <QuestionBankListView rows={tableState.rows as QuestionBankItem[]} onToggleFavorite={toggleFavorite} />
901
+ <QuestionBankListView
902
+ rows={tableState.rows as QuestionBankItem[]}
903
+ onToggleFavorite={toggleFavorite}
904
+ onRowActivate={row => tableState.toggleRow(row.id)}
905
+ />
902
906
  </div>
903
907
  )
904
908
  }
@@ -911,6 +915,7 @@ export const QuestionBankTable = React.forwardRef<
911
915
  rows={tableState.rows as QuestionBankItem[]}
912
916
  groupByColumnKey={questionBankBoardGroupKey}
913
917
  onToggleFavorite={toggleFavorite}
918
+ onRowActivate={row => tableState.toggleRow(row.id)}
914
919
  />
915
920
  </div>
916
921
  )