@exxatdesignux/ui 0.2.15 → 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 (42) hide show
  1. package/CHANGELOG.md +12 -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/globals.css +21 -2
  8. package/src/theme.css +4 -2
  9. package/template/AGENTS.md +6 -4
  10. package/template/app/(app)/question-bank/layout.tsx +11 -4
  11. package/template/app/globals.css +29 -2
  12. package/template/components/app-sidebar.tsx +89 -41
  13. package/template/components/ask-leo-sidebar.tsx +1 -2
  14. package/template/components/data-views/finder-panel-view.tsx +2 -2
  15. package/template/components/data-views/index.ts +19 -0
  16. package/template/components/data-views/list-page-split-details-placeholder.tsx +3 -3
  17. package/template/components/data-views/list-page-split-hub-tokens.ts +1 -1
  18. package/template/components/data-views/list-page-tree-column-header.tsx +1 -1
  19. package/template/components/data-views/outline-tree-menu.tsx +157 -0
  20. package/template/components/data-views/question-bank-folder-tree-branch.tsx +210 -0
  21. package/template/components/exxat-product-logo.tsx +11 -72
  22. package/template/components/folder-details-shell.tsx +1 -1
  23. package/template/components/hub-tree-panel-view.tsx +88 -80
  24. package/template/components/key-metrics.tsx +50 -13
  25. package/template/components/page-header.tsx +19 -10
  26. package/template/components/product-switcher.tsx +1 -4
  27. package/template/components/question-bank-client.tsx +111 -69
  28. package/template/components/question-bank-page-header.tsx +18 -2
  29. package/template/components/question-bank-secondary-nav.tsx +12 -225
  30. package/template/components/secondary-panel.tsx +1 -1
  31. package/template/components/site-header.tsx +21 -2
  32. package/template/components/templates/dedicated-search-landing-template.tsx +86 -20
  33. package/template/components/templates/list-page.tsx +1 -3
  34. package/template/components/templates/nested-secondary-panel-shell.tsx +3 -4
  35. package/template/docs/collaboration-access-pattern.md +2 -0
  36. package/template/docs/question-bank-hub-header-pattern.md +25 -0
  37. package/template/hooks/use-secondary-panel-hub-nav.ts +17 -1
  38. package/template/lib/mock/navigation.tsx +30 -1
  39. package/template/lib/question-bank-nav.ts +26 -0
  40. package/template/package.json +3 -3
  41. package/template/components/command-menu-01.tsx +0 -133
  42. 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
  )
@@ -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}
@@ -160,7 +160,7 @@ function QuestionBankPanel() {
160
160
  className="text-xl font-semibold leading-tight text-sidebar-foreground"
161
161
  style={{ fontFamily: "var(--font-heading)" }}
162
162
  >
163
- Question bank
163
+ Library
164
164
  </h2>
165
165
  <Tip label="Collapse to icons" side="bottom">
166
166
  <Button
@@ -5,10 +5,13 @@
5
5
  *
6
6
  * ✓ SidebarTrigger wrapped in Tooltip — icon-only button (WCAG 4.1.2, 1.1.1)
7
7
  * ✓ <header role="banner"> landmark for AT navigation (WCAG 1.3.6)
8
- * ✓ No bottom border (per design spec)
8
+ * ✓ Sticky at top when stuck, the rounded breadcrumb sits on the app bg and a
9
+ * bottom separator appears to anchor it; transparent at rest so the rounded
10
+ * corners blend into the inset card.
9
11
  * ✓ Uses Inter (font-sans) — Ivy Presto is reserved for PageHeader <h1> only
10
12
  */
11
13
 
14
+ import * as React from "react"
12
15
  import Link from "next/link"
13
16
  import { Separator } from "@/components/ui/separator"
14
17
  import { SidebarTrigger } from "@/components/ui/sidebar"
@@ -20,6 +23,7 @@ import {
20
23
  } from "@/components/ui/tooltip"
21
24
  import { AskLeoToggle } from "@/components/ask-leo-sidebar"
22
25
  import { useModKeyLabel } from "@/hooks/use-mod-key-label"
26
+ import { cn } from "@/lib/utils"
23
27
 
24
28
  export interface BreadcrumbItem {
25
29
  label: string
@@ -35,11 +39,25 @@ export interface SiteHeaderProps {
35
39
 
36
40
  export function SiteHeader({ title = "Dashboard", breadcrumbs }: SiteHeaderProps) {
37
41
  const mod = useModKeyLabel()
42
+ const [isStuck, setIsStuck] = React.useState(false)
43
+
44
+ React.useEffect(() => {
45
+ const onScroll = () => setIsStuck(window.scrollY > 0)
46
+ onScroll()
47
+ window.addEventListener("scroll", onScroll, { passive: true })
48
+ return () => window.removeEventListener("scroll", onScroll)
49
+ }, [])
38
50
 
39
51
  return (
52
+ <div
53
+ className={cn(
54
+ "sticky top-0 z-60 transition-colors",
55
+ isStuck ? "bg-sidebar border-b border-border" : "bg-transparent",
56
+ )}
57
+ >
40
58
  <header
41
59
  role="banner"
42
- className="flex h-(--header-height) shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)"
60
+ className="flex h-(--header-height) shrink-0 items-center gap-2 bg-background rounded-t-xl transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)"
43
61
  >
44
62
  <div className="flex w-full items-center gap-1 ps-4 pe-2 lg:gap-2 lg:ps-6 lg:pe-2">
45
63
  <Tooltip>
@@ -89,5 +107,6 @@ export function SiteHeader({ title = "Dashboard", breadcrumbs }: SiteHeaderProps
89
107
  </div>
90
108
  </div>
91
109
  </header>
110
+ </div>
92
111
  )
93
112
  }