@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.
- package/CHANGELOG.md +20 -0
- package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -1
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +3 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +3 -1
- package/consumer-extras/patterns/collaboration-access-pattern.md +2 -0
- package/package.json +1 -1
- package/src/components/ui/dropdown-menu.tsx +2 -0
- package/src/components/ui/popover.tsx +2 -2
- package/src/components/ui/select.tsx +1 -1
- package/src/components/ui/tooltip.tsx +7 -1
- package/src/globals.css +27 -2
- package/src/theme.css +4 -2
- package/template/AGENTS.md +6 -4
- package/template/app/(app)/question-bank/layout.tsx +11 -4
- package/template/app/globals.css +34 -2
- package/template/components/app-sidebar.tsx +89 -41
- package/template/components/ask-leo-sidebar.tsx +1 -2
- package/template/components/compliance-board-view.tsx +11 -3
- package/template/components/compliance-list-view.tsx +16 -3
- package/template/components/compliance-table.tsx +5 -1
- package/template/components/data-table/index.tsx +25 -11
- package/template/components/data-views/finder-panel-view.tsx +2 -2
- package/template/components/data-views/index.ts +19 -0
- package/template/components/data-views/list-page-split-details-placeholder.tsx +3 -3
- package/template/components/data-views/list-page-split-hub-tokens.ts +1 -1
- package/template/components/data-views/list-page-tree-column-header.tsx +1 -1
- package/template/components/data-views/outline-tree-menu.tsx +157 -0
- package/template/components/data-views/question-bank-folder-tree-branch.tsx +210 -0
- package/template/components/exxat-product-logo.tsx +11 -72
- package/template/components/folder-details-shell.tsx +1 -1
- package/template/components/hub-tree-panel-view.tsx +88 -80
- package/template/components/key-metrics.tsx +50 -13
- package/template/components/page-header.tsx +19 -10
- package/template/components/product-switcher.tsx +1 -4
- package/template/components/question-bank-board-view.tsx +11 -2
- package/template/components/question-bank-client.tsx +111 -69
- package/template/components/question-bank-list-view.tsx +12 -1
- package/template/components/question-bank-page-header.tsx +18 -2
- package/template/components/question-bank-secondary-nav.tsx +12 -225
- package/template/components/question-bank-table.tsx +6 -1
- package/template/components/secondary-panel.tsx +1 -1
- package/template/components/site-header.tsx +21 -2
- package/template/components/team-board-view.tsx +11 -3
- package/template/components/team-list-view.tsx +16 -3
- package/template/components/team-table.tsx +6 -2
- package/template/components/templates/dedicated-search-landing-template.tsx +86 -20
- package/template/components/templates/list-page.tsx +1 -3
- package/template/components/templates/nested-secondary-panel-shell.tsx +3 -4
- package/template/docs/collaboration-access-pattern.md +2 -0
- package/template/docs/question-bank-hub-header-pattern.md +25 -0
- package/template/hooks/use-secondary-panel-hub-nav.ts +17 -1
- package/template/lib/mock/navigation.tsx +30 -1
- package/template/lib/question-bank-nav.ts +26 -0
- package/template/package.json +3 -3
- package/template/components/command-menu-01.tsx +0 -133
- 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,
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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
|
|
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="
|
|
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
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
key={child.id}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
</
|
|
181
|
-
</
|
|
182
|
-
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
</
|
|
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-
|
|
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-
|
|
521
|
-
<div className="mb-3 flex size-12 items-center justify-center rounded-xl border border-border/60 bg-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
</
|
|
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
|
|
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=
|
|
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
|
|
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
|
|
614
|
+
<Separator
|
|
615
|
+
aria-hidden="true"
|
|
616
|
+
className={cn("my-1", isFlatBand && "bg-foreground/[0.055]")}
|
|
617
|
+
/>
|
|
607
618
|
)}
|
|
608
619
|
<div
|
|
609
|
-
className=
|
|
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
|
|
645
|
+
<Separator
|
|
646
|
+
aria-hidden="true"
|
|
647
|
+
className={cn("my-4 w-full", isFlatBand && "bg-foreground/[0.06]")}
|
|
648
|
+
/>
|
|
632
649
|
) : stackedRailInsight ? (
|
|
633
|
-
<Separator
|
|
650
|
+
<Separator
|
|
651
|
+
aria-hidden="true"
|
|
652
|
+
className={cn("my-4 w-full", isFlatBand && "bg-foreground/[0.06]")}
|
|
653
|
+
/>
|
|
634
654
|
) : (
|
|
635
|
-
<Separator
|
|
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
|
-
|
|
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 =
|
|
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 —
|
|
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
|
|
925
|
-
style={
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 -
|
|
116
|
-
{visible.map(
|
|
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
|
|
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="
|
|
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 =
|
|
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
|
|
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 =>
|
|
197
|
+
renderCard={row => (
|
|
198
|
+
<QuestionBankBoardCard row={row} onToggleFavorite={onToggleFavorite} onRowActivate={onRowActivate} />
|
|
199
|
+
)}
|
|
191
200
|
/>
|
|
192
201
|
)
|
|
193
202
|
}
|