@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
@@ -19,7 +19,7 @@ import {
19
19
  import { LIST_HUB_INSPECTOR_CHIP_SHELL } from "@/components/list-hub-status-badge"
20
20
  import { Badge } from "@/components/ui/badge"
21
21
  import { Button } from "@/components/ui/button"
22
- import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
22
+ import { Collapsible, CollapsibleTrigger } from "@/components/ui/collapsible"
23
23
  import { Separator } from "@/components/ui/separator"
24
24
  import { Tip } from "@/components/ui/tip"
25
25
  import {
@@ -28,6 +28,14 @@ import {
28
28
  TooltipTrigger,
29
29
  } from "@/components/ui/tooltip"
30
30
  import { cn } from "@/lib/utils"
31
+ import {
32
+ OutlineTreeCollapsibleContentRail,
33
+ OutlineTreeLeafButton,
34
+ OutlineTreeMenu,
35
+ OutlineTreeMenuItem,
36
+ OutlineTreeSub,
37
+ OutlineTreeSubItem,
38
+ } from "@/components/data-views/outline-tree-menu"
31
39
  import { ListPageTreePanelShell } from "@/components/data-views/list-page-tree-panel-shell"
32
40
  import { ListPageSplitDetailsPlaceholder } from "@/components/data-views/list-page-split-details-placeholder"
33
41
  import { ListPageTreeColumnHeader } from "@/components/data-views/list-page-tree-column-header"
@@ -63,7 +71,6 @@ interface TreeItemProps {
63
71
  folders: QuestionBankFolder[]
64
72
  questions: QuestionBankItem[]
65
73
  selectedItemId: string | null
66
- depth?: number
67
74
  onSelectItem: (itemId: string) => void
68
75
  }
69
76
 
@@ -72,7 +79,6 @@ function TreeItem({
72
79
  folders,
73
80
  questions,
74
81
  selectedItemId,
75
- depth = 0,
76
82
  onSelectItem,
77
83
  }: TreeItemProps) {
78
84
  const childFolders = folders
@@ -83,38 +89,40 @@ function TreeItem({
83
89
 
84
90
  const hasChildren = childFolders.length > 0 || childQuestions.length > 0
85
91
  const isFolderSelected = selectedItemId === folder.id
86
- const indent = depth * 12
87
92
 
88
93
  return (
89
- <Collapsible>
90
- {/* Folder row */}
91
- <div className="group flex items-center hover:bg-muted/50">
92
- <div style={{ width: indent }} className="shrink-0" />
93
-
94
- {/* Expand chevron or spacer */}
94
+ <Collapsible className="group/collapsible">
95
+ {/* Folder row — chevron column + row body (icons align with shadcn tree pattern) */}
96
+ <div
97
+ className={cn(
98
+ "flex min-h-8 items-center rounded-md px-2 hover:bg-muted/50",
99
+ isFolderSelected && "bg-accent text-accent-foreground",
100
+ )}
101
+ >
95
102
  {hasChildren ? (
96
103
  <CollapsibleTrigger asChild>
97
104
  <button
98
105
  type="button"
99
- className="flex h-8 w-5 shrink-0 items-center justify-center text-muted-foreground hover:text-foreground focus-visible:outline-none"
106
+ 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"
107
+ aria-label={folder.name ? `${folder.name} — expand or collapse` : "Expand or collapse folder"}
100
108
  >
101
- <ChevronRightIcon className="h-3.5 w-3.5 transition-transform duration-150 group-data-[state=open]:rotate-90 [[data-state=open]_&]:rotate-90" />
109
+ <ChevronRightIcon
110
+ className="h-3.5 w-3.5 shrink-0 transition-transform duration-150 group-data-[state=open]/collapsible:rotate-90"
111
+ aria-hidden
112
+ />
102
113
  </button>
103
114
  </CollapsibleTrigger>
104
115
  ) : (
105
- <div className="w-5 shrink-0" />
116
+ <div className="size-8 shrink-0" aria-hidden />
106
117
  )}
107
118
 
108
- {/* Folder button */}
109
119
  <button
110
120
  type="button"
111
121
  onClick={() => onSelectItem(folder.id)}
112
122
  className={cn(
113
- "flex flex-1 items-center gap-2 py-1.5 pr-3 text-left text-sm transition-colors duration-75",
123
+ "flex min-w-0 flex-1 items-center gap-2 py-1.5 pr-3 text-left text-sm transition-colors duration-75",
114
124
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
115
- isFolderSelected
116
- ? "bg-accent text-accent-foreground"
117
- : "text-foreground",
125
+ !isFolderSelected && "text-foreground",
118
126
  )}
119
127
  aria-selected={isFolderSelected}
120
128
  role="option"
@@ -134,55 +142,51 @@ function TreeItem({
134
142
  </button>
135
143
  </div>
136
144
 
137
- {/* Children */}
138
145
  {hasChildren && (
139
- <CollapsibleContent>
140
- {childFolders.map(child => (
141
- <TreeItem
142
- key={child.id}
143
- folder={child}
144
- folders={folders}
145
- questions={questions}
146
- selectedItemId={selectedItemId}
147
- depth={depth + 1}
148
- onSelectItem={onSelectItem}
149
- />
150
- ))}
151
- {childQuestions.map(question => {
152
- const isSelected = selectedItemId === question.id
153
- return (
154
- <div key={question.id} className="group flex items-center hover:bg-muted/50">
155
- <div style={{ width: indent + 12 + 20 }} className="shrink-0" />
156
- <button
157
- type="button"
158
- onClick={() => onSelectItem(question.id)}
159
- className={cn(
160
- "flex flex-1 items-center gap-2 py-1.5 pr-3 text-left text-sm transition-colors duration-75",
161
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
162
- isSelected
163
- ? "bg-accent text-accent-foreground"
164
- : "text-foreground",
165
- )}
166
- aria-selected={isSelected}
167
- role="option"
168
- >
169
- <FileIcon
170
- className={cn(
171
- "h-3.5 w-3.5 shrink-0",
172
- isSelected ? "fill-current opacity-80" : "text-muted-foreground",
173
- )}
174
- />
175
- <span className="min-w-0 flex-1">
176
- <span className="block truncate leading-tight">{question.stem}</span>
177
- <span className="block truncate font-mono text-[11px] text-muted-foreground">
178
- {question.questionId}
146
+ <OutlineTreeCollapsibleContentRail>
147
+ <OutlineTreeSub surface="panel" guideLayout="chevronRail">
148
+ {childFolders.map(child => (
149
+ <OutlineTreeMenuItem key={child.id}>
150
+ <TreeItem
151
+ folder={child}
152
+ folders={folders}
153
+ questions={questions}
154
+ selectedItemId={selectedItemId}
155
+ onSelectItem={onSelectItem}
156
+ />
157
+ </OutlineTreeMenuItem>
158
+ ))}
159
+ {childQuestions.map(question => {
160
+ const isSelected = selectedItemId === question.id
161
+ return (
162
+ <OutlineTreeSubItem key={question.id}>
163
+ <OutlineTreeLeafButton
164
+ surface="panel"
165
+ isActive={isSelected}
166
+ onClick={() => onSelectItem(question.id)}
167
+ aria-selected={isSelected}
168
+ role="option"
169
+ className="h-auto min-h-8 items-start py-1.5"
170
+ >
171
+ <FileIcon
172
+ className={cn(
173
+ "mt-0.5 shrink-0",
174
+ isSelected ? "fill-current opacity-80" : "text-muted-foreground",
175
+ )}
176
+ aria-hidden
177
+ />
178
+ <span className="min-w-0 flex-1 text-left">
179
+ <span className="block truncate leading-tight">{question.stem}</span>
180
+ <span className="block truncate font-mono text-[11px] text-muted-foreground">
181
+ {question.questionId}
182
+ </span>
179
183
  </span>
180
- </span>
181
- </button>
182
- </div>
183
- )
184
- })}
185
- </CollapsibleContent>
184
+ </OutlineTreeLeafButton>
185
+ </OutlineTreeSubItem>
186
+ )
187
+ })}
188
+ </OutlineTreeSub>
189
+ </OutlineTreeCollapsibleContentRail>
186
190
  )}
187
191
  </Collapsible>
188
192
  )
@@ -262,7 +266,7 @@ function DetailsPanel({ selectedItemId, folders, questions, onClearSelection }:
262
266
 
263
267
  return (
264
268
  <div className="flex h-full min-h-0 flex-col overflow-hidden bg-card">
265
- <header className="shrink-0 border-b border-border/60 bg-muted/10 px-4 pb-4 pt-3">
269
+ <header className="shrink-0 border-b border-border/60 bg-card px-4 pb-4 pt-3">
266
270
  <div className="flex items-start justify-between gap-3">
267
271
  <p className="font-mono text-xs text-muted-foreground">{question.questionId}</p>
268
272
  {onClearSelection ? (
@@ -517,8 +521,8 @@ function DetailsPanel({ selectedItemId, folders, questions, onClearSelection }:
517
521
  }
518
522
 
519
523
  return (
520
- <div className="flex h-full min-h-0 flex-col items-center justify-center bg-gradient-to-b from-muted/25 to-card px-6 py-10 text-center text-muted-foreground">
521
- <div className="mb-3 flex size-12 items-center justify-center rounded-xl border border-border/60 bg-muted/20">
524
+ <div className="flex h-full min-h-0 flex-col items-center justify-center bg-card px-6 py-10 text-center text-muted-foreground">
525
+ <div className="mb-3 flex size-12 items-center justify-center rounded-xl border border-border/60 bg-card">
522
526
  <FileIcon className="size-6 opacity-50" aria-hidden />
523
527
  </div>
524
528
  <p className="text-sm font-medium text-foreground">Item not found</p>
@@ -615,23 +619,27 @@ export function HubTreePanelView({
615
619
  }
616
620
  />
617
621
 
618
- <div className="min-h-0 flex-1 overflow-y-auto py-1" role="listbox" aria-label="Folder tree">
622
+ <OutlineTreeMenu
623
+ className="min-h-0 flex-1 overflow-y-auto py-1"
624
+ role="listbox"
625
+ aria-label="Folder tree"
626
+ >
619
627
  {rootFolders.length === 0 ? (
620
- <p className="px-3 py-4 text-sm text-muted-foreground">No folders</p>
628
+ <li className="list-none px-3 py-4 text-sm text-muted-foreground">No folders</li>
621
629
  ) : (
622
630
  rootFolders.map(folder => (
623
- <TreeItem
624
- key={folder.id}
625
- folder={folder}
626
- folders={folders}
627
- questions={items}
628
- selectedItemId={selectedItemId}
629
- depth={0}
630
- onSelectItem={setSelectedItemId}
631
- />
631
+ <OutlineTreeMenuItem key={folder.id}>
632
+ <TreeItem
633
+ folder={folder}
634
+ folders={folders}
635
+ questions={items}
636
+ selectedItemId={selectedItemId}
637
+ onSelectItem={setSelectedItemId}
638
+ />
639
+ </OutlineTreeMenuItem>
632
640
  ))
633
641
  )}
634
- </div>
642
+ </OutlineTreeMenu>
635
643
  </>
636
644
  }
637
645
  details={
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Variants:
7
7
  * "card" (default) — shadcn Card wrapper with brand gradient fill
8
- * "flat" — full-width brand gradient band, no card chrome
8
+ * "flat" — full-width soft tint band (brand-tint background) + bottom glow, no card chrome
9
9
  *
10
10
  * AA checklist:
11
11
  * ✓ Trend text never relies on colour alone — icon + label (WCAG 1.4.1)
@@ -484,6 +484,8 @@ interface InnerProps {
484
484
  metricsHalfWidthLayout?: boolean
485
485
  /** Opaque fill behind each KPI cell when using hairline grid gaps (below `lg`). */
486
486
  metricsCellSurfaceClassName?: string
487
+ /** Flat list-page band: softer dividers + tinted cells on a lavender-tinted surface */
488
+ surfaceVariant?: "default" | "flat"
487
489
  }
488
490
 
489
491
  function KeyMetricsInner({
@@ -502,7 +504,9 @@ function KeyMetricsInner({
502
504
  metricsSingleRow = false,
503
505
  metricsHalfWidthLayout = false,
504
506
  metricsCellSurfaceClassName = "bg-background",
507
+ surfaceVariant = "default",
505
508
  }: InnerProps) {
509
+ const isFlatBand = surfaceVariant === "flat"
506
510
  /** Side-by-side KPI + insight rail (md+). Disabled for half-width dashboard cards — insight stacks below. */
507
511
  const insightSideBySide = insight && !insightFullWidth && !metricsHalfWidthLayout
508
512
  const stackedRailInsight = insight && !insightFullWidth && metricsHalfWidthLayout
@@ -566,7 +570,10 @@ function KeyMetricsInner({
566
570
  */}
567
571
  {metricsHalfWidthLayout ? (
568
572
  <div
569
- className="grid grid-cols-2 divide-x divide-border lg:hidden"
573
+ className={cn(
574
+ "grid grid-cols-2 divide-x lg:hidden",
575
+ isFlatBand ? "divide-border/40" : "divide-border",
576
+ )}
570
577
  style={
571
578
  metricsSingleRow
572
579
  ? {
@@ -586,8 +593,9 @@ function KeyMetricsInner({
586
593
  ) : (
587
594
  <div
588
595
  className={cn(
589
- "grid gap-px bg-border lg:hidden",
596
+ "grid gap-px lg:hidden",
590
597
  "grid-cols-1 md:grid-cols-2",
598
+ isFlatBand ? "bg-foreground/[0.04]" : "bg-border",
591
599
  )}
592
600
  >
593
601
  {metrics.map((m) => (
@@ -603,10 +611,16 @@ function KeyMetricsInner({
603
611
  {rows.map((row, rowIdx) => (
604
612
  <React.Fragment key={rowIdx}>
605
613
  {rowIdx > 0 && (
606
- <Separator aria-hidden="true" className="my-1" />
614
+ <Separator
615
+ aria-hidden="true"
616
+ className={cn("my-1", isFlatBand && "bg-foreground/[0.055]")}
617
+ />
607
618
  )}
608
619
  <div
609
- className="grid divide-x divide-border"
620
+ className={cn(
621
+ "grid divide-x",
622
+ isFlatBand ? "divide-border/40" : "divide-border",
623
+ )}
610
624
  style={{
611
625
  gridTemplateColumns: metricsRowColumns(
612
626
  row.length,
@@ -628,11 +642,20 @@ function KeyMetricsInner({
628
642
  {insight && (
629
643
  <>
630
644
  {insightFullWidth ? (
631
- <Separator aria-hidden="true" className="my-4 w-full" />
645
+ <Separator
646
+ aria-hidden="true"
647
+ className={cn("my-4 w-full", isFlatBand && "bg-foreground/[0.06]")}
648
+ />
632
649
  ) : stackedRailInsight ? (
633
- <Separator aria-hidden="true" className="my-4 w-full" />
650
+ <Separator
651
+ aria-hidden="true"
652
+ className={cn("my-4 w-full", isFlatBand && "bg-foreground/[0.06]")}
653
+ />
634
654
  ) : (
635
- <Separator aria-hidden="true" className="my-3 lg:hidden" />
655
+ <Separator
656
+ aria-hidden="true"
657
+ className={cn("my-3 lg:hidden", isFlatBand && "bg-foreground/[0.055]")}
658
+ />
636
659
  )}
637
660
 
638
661
  <div
@@ -641,7 +664,10 @@ function KeyMetricsInner({
641
664
  /* Divider + padding replace vertical Separator so grid stays 2 columns */
642
665
  insightSideBySide &&
643
666
  !insightFullWidth &&
644
- "lg:h-full lg:border-l lg:border-border lg:pl-6"
667
+ cn(
668
+ "lg:h-full lg:border-l lg:pl-6",
669
+ isFlatBand ? "lg:border-border/40" : "lg:border-border",
670
+ )
645
671
  )}
646
672
  >
647
673
  {insight && !insightFullWidth ? (
@@ -801,7 +827,8 @@ export function KeyMetrics({
801
827
  return out
802
828
  })()
803
829
 
804
- const metricsCellSurfaceClassName = variant === "flat" ? "bg-background" : "bg-card"
830
+ const metricsCellSurfaceClassName =
831
+ variant === "flat" ? "bg-[var(--key-metrics-flat-cell-bg)]" : "bg-card"
805
832
 
806
833
  const innerProps: InnerProps = {
807
834
  title,
@@ -817,6 +844,7 @@ export function KeyMetrics({
817
844
  metricsSingleRow,
818
845
  metricsHalfWidthLayout,
819
846
  metricsCellSurfaceClassName,
847
+ surfaceVariant: variant === "flat" ? "flat" : "default",
820
848
  }
821
849
 
822
850
  /*
@@ -849,6 +877,15 @@ export function KeyMetrics({
849
877
  "radial-gradient(ellipse 110% 90% at 50% 100%, oklch(from var(--brand-color) l c h / 0.13) 0%, transparent 65%)",
850
878
  }
851
879
 
880
+ /** List-page KPI band: soft tint → page bg + gentle lift (avoids a hard line into the toolbar). */
881
+ const flatBandStyle: React.CSSProperties = {
882
+ background: [
883
+ "radial-gradient(ellipse 118% 70% at 50% 100%, oklch(from var(--brand-color) l c h / 0.11) 0%, transparent 60%)",
884
+ "linear-gradient(180deg, var(--key-metrics-flat-grad-top) 0%, var(--key-metrics-flat-grad-mid) 48%, var(--background) 100%)",
885
+ ].join(", "),
886
+ boxShadow: "0 18px 42px -26px color-mix(in oklch, var(--brand-color) 10%, transparent)",
887
+ }
888
+
852
889
  /* ── Card variant — ChartCard-style chrome ───────────────────────────── */
853
890
  if (variant === "card") {
854
891
  return (
@@ -917,12 +954,12 @@ export function KeyMetrics({
917
954
  )
918
955
  }
919
956
 
920
- /* ── Flat variant — full-width bottom-glow band ───────────────────────── */
957
+ /* ── Flat variant — soft tint band + bottom glow (no sharp cut to content below) ── */
921
958
  return (
922
959
  <section
923
960
  aria-label={title}
924
- className={cn("w-full py-5", className)}
925
- style={glowStyle}
961
+ className={cn("relative w-full overflow-hidden pt-5 pb-6", className)}
962
+ style={flatBandStyle}
926
963
  >
927
964
  <KeyMetricsInner
928
965
  {...innerProps}
@@ -12,7 +12,7 @@
12
12
  * WCAG 2.1 AA:
13
13
  * ✓ <h1> landmark — one per page (WCAG 1.3.1)
14
14
  * ✓ Sufficient colour contrast ≥ 4.5:1 on title + subtitle (SC 1.4.3)
15
- * ✓ Face stack: `role="group"` + aggregate `aria-label`; each face has a `Tooltip` name
15
+ * ✓ Face row: `role="group"` + aggregate `aria-label`; each face has a `Tooltip` name
16
16
  */
17
17
 
18
18
  import * as React from "react"
@@ -23,6 +23,7 @@ import {
23
23
  } from "@/lib/collaborator-access"
24
24
  import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
25
25
  import { Button } from "@/components/ui/button"
26
+ import { Separator } from "@/components/ui/separator"
26
27
  import {
27
28
  Tooltip,
28
29
  TooltipContent,
@@ -47,16 +48,16 @@ export interface PageHeaderProps {
47
48
  title: string
48
49
  /** Short descriptor or date shown below the title (and below `accessInfo` when set) */
49
50
  subtitle?: string
50
- /** Layout preset — `collaboration` enables access line + face stack ahead of `actions`. */
51
+ /** Layout preset — `collaboration` enables access line + face row ahead of `actions`. */
51
52
  variant?: PageHeaderVariant
52
53
  /**
53
54
  * Role / access copy or badges — rendered between the title and subtitle when
54
55
  * `variant="collaboration"` (e.g. lock icon + “Editors can modify”).
55
56
  */
56
57
  accessInfo?: React.ReactNode
57
- /** People with access — shown as an overlapping face stack when `variant="collaboration"`. */
58
+ /** People with access — shown as a horizontal row of faces when `variant="collaboration"`. */
58
59
  collaborators?: PageHeaderCollaborator[]
59
- /** Max faces before a `+N` chip — default 4 */
60
+ /** Max faces before a `+N` chip — default 3 */
60
61
  collaboratorDisplayLimit?: number
61
62
  /** Opens the invite collaborators sheet when a face, overflow chip, or empty-state CTA is activated. */
62
63
  onCollaboratorsOpen?: () => void
@@ -112,14 +113,13 @@ function PageHeaderCollaborationAccess({
112
113
  aria-label={names ? `People with access: ${names}` : "People with access"}
113
114
  className="flex shrink-0 items-center gap-2 sm:gap-2.5"
114
115
  >
115
- <div className="flex -space-x-2 ps-0.5">
116
- {visible.map((c, index) => (
116
+ <div className="flex shrink-0 items-center gap-1.5">
117
+ {visible.map(c => (
117
118
  <Tooltip key={c.id}>
118
119
  <TooltipTrigger asChild>
119
120
  <button
120
121
  type="button"
121
- className="relative rounded-full ring-2 ring-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
122
- style={{ zIndex: 10 + index }}
122
+ className="relative shrink-0 rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
123
123
  aria-label={`Open collaborators — ${c.name}`}
124
124
  onClick={onOpenCollaborators}
125
125
  disabled={!onOpenCollaborators}
@@ -140,7 +140,7 @@ function PageHeaderCollaborationAccess({
140
140
  {overflow > 0 && (
141
141
  <button
142
142
  type="button"
143
- className="relative z-30 flex size-6 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-medium tabular-nums text-muted-foreground ring-2 ring-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:size-7"
143
+ className="flex size-6 shrink-0 items-center justify-center rounded-full bg-muted text-[11px] font-semibold tabular-nums text-muted-foreground ring-1 ring-border/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
144
144
  aria-label={`Open collaborators — ${overflow} more people with access`}
145
145
  onClick={onOpenCollaborators}
146
146
  disabled={!onOpenCollaborators}
@@ -159,7 +159,7 @@ export function PageHeader({
159
159
  variant = "default",
160
160
  accessInfo,
161
161
  collaborators,
162
- collaboratorDisplayLimit = 4,
162
+ collaboratorDisplayLimit = 3,
163
163
  onCollaboratorsOpen,
164
164
  addCollaboratorLabel = COLLABORATION_HEADER_ADD_LABEL,
165
165
  actions,
@@ -170,6 +170,8 @@ export function PageHeader({
170
170
  const showAccess = Boolean(isCollaboration && accessInfo)
171
171
  const showCollaborationAccess = isCollaboration
172
172
  const showActionsColumn = Boolean(actions) || showCollaborationAccess
173
+ const showCollaboratorActionsSeparator =
174
+ showCollaborationAccess && Boolean(actions)
173
175
 
174
176
  return (
175
177
  <div
@@ -208,6 +210,13 @@ export function PageHeader({
208
210
  addCollaboratorLabel={addCollaboratorLabel}
209
211
  />
210
212
  ) : null}
213
+ {showCollaboratorActionsSeparator ? (
214
+ <Separator
215
+ orientation="vertical"
216
+ decorative
217
+ className="h-8 shrink-0"
218
+ />
219
+ ) : null}
211
220
  {actions}
212
221
  </div>
213
222
  )}
@@ -52,10 +52,7 @@ export function ProductSwitcher() {
52
52
  >
53
53
  {iconRail ? (
54
54
  <span className="flex size-8 shrink-0 items-center justify-center">
55
- <ExxatProductMark
56
- product={current.id}
57
- className="size-7 max-h-none"
58
- />
55
+ <ExxatProductMark product={current.id} className="size-7" />
59
56
  </span>
60
57
  ) : (
61
58
  <>
@@ -124,13 +124,18 @@ function useQuestionBankBoardModel(rows: QuestionBankItem[], groupByColumnKey: s
124
124
  function QuestionBankBoardCard({
125
125
  row,
126
126
  onToggleFavorite,
127
+ onRowActivate,
127
128
  }: {
128
129
  row: QuestionBankItem
129
130
  onToggleFavorite: (row: QuestionBankItem) => void
131
+ onRowActivate?: (row: QuestionBankItem) => void
130
132
  }) {
131
133
  const initials = initialsFromDisplayName(row.author)
132
134
  return (
133
- <ListPageBoardCard className={cn(QUESTION_BANK_FAVORITE_HOVER_GROUP, "w-full")}>
135
+ <ListPageBoardCard
136
+ className={cn(QUESTION_BANK_FAVORITE_HOVER_GROUP, "w-full")}
137
+ onClick={onRowActivate ? () => onRowActivate(row) : undefined}
138
+ >
134
139
  <ListPageBoardCardHeader>
135
140
  <ListPageBoardCardTitleRow
136
141
  title={(
@@ -171,10 +176,12 @@ export function QuestionBankBoardView({
171
176
  rows,
172
177
  groupByColumnKey,
173
178
  onToggleFavorite,
179
+ onRowActivate,
174
180
  }: {
175
181
  rows: QuestionBankItem[]
176
182
  groupByColumnKey: string
177
183
  onToggleFavorite: (row: QuestionBankItem) => void
184
+ onRowActivate?: (row: QuestionBankItem) => void
178
185
  }) {
179
186
  const allowed = QUESTION_BANK_BOARD_GROUP_OPTIONS.some(o => o.key === groupByColumnKey)
180
187
  const key = allowed ? groupByColumnKey : "topic"
@@ -187,7 +194,9 @@ export function QuestionBankBoardView({
187
194
  getRowKey={r => r.id}
188
195
  columnCountBadgeClassName={badgeMap}
189
196
  emptyColumnLabel="No questions"
190
- renderCard={row => <QuestionBankBoardCard row={row} onToggleFavorite={onToggleFavorite} />}
197
+ renderCard={row => (
198
+ <QuestionBankBoardCard row={row} onToggleFavorite={onToggleFavorite} onRowActivate={onRowActivate} />
199
+ )}
191
200
  />
192
201
  )
193
202
  }