@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
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
53
53
|
import { Badge } from "@/components/ui/badge"
|
|
54
54
|
import { StatusBadge } from "@/components/ui/status-badge"
|
|
55
|
+
import { Separator } from "@/components/ui/separator"
|
|
55
56
|
import {
|
|
56
57
|
Tooltip,
|
|
57
58
|
TooltipContent,
|
|
@@ -108,7 +109,8 @@ function normalizedLocationHash(locationHash: string): string {
|
|
|
108
109
|
/**
|
|
109
110
|
* Whether `pathname` (+ optional `location.hash`) matches a sidebar `href`.
|
|
110
111
|
* When several links share the same path (e.g. `/settings`), disambiguate with `#fragment`
|
|
111
|
-
*
|
|
112
|
+
* in each `href` — those rows use the `frag !== null` branch below.
|
|
113
|
+
* For `href` without `#…`, an in-page hash (e.g. QB view tabs) does not clear the match.
|
|
112
114
|
*/
|
|
113
115
|
function isNavActive(pathname: string, url: string, locationHash = ""): boolean {
|
|
114
116
|
const pathOnly = navUrlPath(url)
|
|
@@ -129,7 +131,8 @@ function isNavActive(pathname: string, url: string, locationHash = ""): boolean
|
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
if (pathOnly === "/") return pathname === "/" && h === ""
|
|
132
|
-
|
|
134
|
+
/** Exact path match — ignore `location.hash` when the nav `href` has no `#…` fragment (QB view tabs use hash). */
|
|
135
|
+
if (pathname === pathOnly) return true
|
|
133
136
|
// Design system library — active on hub and detail routes.
|
|
134
137
|
if (pathOnly === "/library") {
|
|
135
138
|
return pathname.startsWith("/library/")
|
|
@@ -165,6 +168,16 @@ function isCollapsibleChildActive(
|
|
|
165
168
|
|
|
166
169
|
if (!isNavActive(pathname, child.url, locationHash)) return false
|
|
167
170
|
|
|
171
|
+
/** Hub entry (`/question-bank`) must not stay “active” on `/question-bank/library` etc. */
|
|
172
|
+
if (parent.primaryHubChildKey && child.key === parent.primaryHubChildKey) {
|
|
173
|
+
const hubPath = navUrlPath(parent.url)
|
|
174
|
+
if (hubPath) {
|
|
175
|
+
const normalized =
|
|
176
|
+
pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname
|
|
177
|
+
if (normalized !== hubPath) return false
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
168
181
|
const urls = children.map(c => c.url)
|
|
169
182
|
const allSameUrl = urls.length > 1 && urls.every(u => u === urls[0])
|
|
170
183
|
if (allSameUrl) {
|
|
@@ -176,6 +189,32 @@ function isCollapsibleChildActive(
|
|
|
176
189
|
return true
|
|
177
190
|
}
|
|
178
191
|
|
|
192
|
+
/**
|
|
193
|
+
* “Selected” styling on a collapsible **parent** row — not the same as “a descendant route is open”.
|
|
194
|
+
* When a child row is the current destination (e.g. Library on `/question-bank/library`), the parent
|
|
195
|
+
* should stay visually neutral while the child carries `data-active`. Only highlight the parent when
|
|
196
|
+
* the active child is the hub row whose `href` matches the parent (e.g. Question hub on `/question-bank`),
|
|
197
|
+
* or when no child matches but the parent URL still matches (edge routes).
|
|
198
|
+
*/
|
|
199
|
+
function isCollapsibleParentMenuButtonActive(
|
|
200
|
+
pathname: string,
|
|
201
|
+
item: NavLinkItem,
|
|
202
|
+
locationHash: string,
|
|
203
|
+
): boolean {
|
|
204
|
+
const children = item.children
|
|
205
|
+
if (!children?.length) return isNavActive(pathname, item.url, locationHash)
|
|
206
|
+
|
|
207
|
+
const activeChildren = children.filter(c =>
|
|
208
|
+
isCollapsibleChildActive(pathname, item, c, locationHash),
|
|
209
|
+
)
|
|
210
|
+
if (activeChildren.length === 0) {
|
|
211
|
+
return isNavActive(pathname, item.url, locationHash)
|
|
212
|
+
}
|
|
213
|
+
if (activeChildren.length !== 1) return false
|
|
214
|
+
const [child] = activeChildren
|
|
215
|
+
return navUrlPath(child.url) === navUrlPath(item.url)
|
|
216
|
+
}
|
|
217
|
+
|
|
179
218
|
/** Accessible suffix for sidebar badges (badge is rendered outside the link node). */
|
|
180
219
|
function badgeAccessibleSuffix(badge: number | string): string {
|
|
181
220
|
if (typeof badge === "number") return `${badge} items`
|
|
@@ -183,30 +222,40 @@ function badgeAccessibleSuffix(badge: number | string): string {
|
|
|
183
222
|
}
|
|
184
223
|
|
|
185
224
|
/** Child row for expandable nav items — shared by inline sub-menu and collapsed-rail popover. */
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
225
|
+
const SidebarNavChildLink = React.forwardRef<
|
|
226
|
+
HTMLAnchorElement,
|
|
227
|
+
{
|
|
228
|
+
parent: NavLinkItem
|
|
229
|
+
child: NavLinkItem
|
|
230
|
+
pathname: string
|
|
231
|
+
locationHash: string
|
|
232
|
+
onNavigate?: () => void
|
|
233
|
+
/** Popover uses surface tokens; inline sub-menu uses `SidebarMenuSubButton`. */
|
|
234
|
+
linkClassName?: string
|
|
235
|
+
} & Omit<React.ComponentPropsWithoutRef<typeof Link>, "href">
|
|
236
|
+
>(function SidebarNavChildLink(
|
|
237
|
+
{
|
|
238
|
+
parent,
|
|
239
|
+
child,
|
|
240
|
+
pathname,
|
|
241
|
+
locationHash,
|
|
242
|
+
onNavigate,
|
|
243
|
+
linkClassName,
|
|
244
|
+
className: incomingClassName,
|
|
245
|
+
onClick,
|
|
246
|
+
...linkRest
|
|
247
|
+
},
|
|
248
|
+
ref,
|
|
249
|
+
) {
|
|
202
250
|
const { openPanel } = useSecondaryPanel()
|
|
203
251
|
const childActive = isCollapsibleChildActive(pathname, parent, child, locationHash)
|
|
204
252
|
const childPath = navUrlPath(child.url)
|
|
205
253
|
|
|
206
254
|
return (
|
|
207
255
|
<Link
|
|
256
|
+
ref={ref}
|
|
208
257
|
href={child.url}
|
|
209
|
-
className={cn("flex min-w-0 items-center gap-2", linkClassName)}
|
|
258
|
+
className={cn("flex min-w-0 items-center gap-2", linkClassName, incomingClassName)}
|
|
210
259
|
aria-current={childActive ? "page" : undefined}
|
|
211
260
|
onClick={e => {
|
|
212
261
|
onNavigate?.()
|
|
@@ -218,15 +267,18 @@ function SidebarNavChildLink({
|
|
|
218
267
|
e.preventDefault()
|
|
219
268
|
openPanel(parent.secondaryPanel)
|
|
220
269
|
}
|
|
270
|
+
onClick?.(e)
|
|
221
271
|
}}
|
|
272
|
+
{...linkRest}
|
|
222
273
|
>
|
|
223
274
|
<span className="size-4 shrink-0 inline-flex items-center justify-center" aria-hidden="true">
|
|
224
|
-
{child.icon}
|
|
275
|
+
{childActive && child.iconActive ? child.iconActive : child.icon}
|
|
225
276
|
</span>
|
|
226
277
|
<span className="min-w-0 flex-1 truncate">{child.title}</span>
|
|
227
278
|
</Link>
|
|
228
279
|
)
|
|
229
|
-
}
|
|
280
|
+
})
|
|
281
|
+
SidebarNavChildLink.displayName = "SidebarNavChildLink"
|
|
230
282
|
|
|
231
283
|
/**
|
|
232
284
|
* CollapsibleNavItem — isolated component so each collapsible has its own
|
|
@@ -235,19 +287,17 @@ function SidebarNavChildLink({
|
|
|
235
287
|
* server (SSR) vs the client (router not yet available).
|
|
236
288
|
*/
|
|
237
289
|
function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: string }) {
|
|
238
|
-
const locationHash
|
|
239
|
-
const isActive = isNavActive(pathname, item.url, locationHash)
|
|
290
|
+
const locationHash = useLocationHash()
|
|
240
291
|
const isAnyChildActive =
|
|
241
292
|
item.children?.some(c => isCollapsibleChildActive(pathname, item, c, locationHash)) ?? false
|
|
293
|
+
const parentMenuButtonActive = isCollapsibleParentMenuButtonActive(pathname, item, locationHash)
|
|
242
294
|
const { state, isMobile } = useSidebar()
|
|
243
|
-
const { openPanel } = useSecondaryPanel()
|
|
244
295
|
const [open, setOpen] = React.useState(false)
|
|
245
296
|
const [flyoutOpen, setFlyoutOpen] = React.useState(false)
|
|
246
297
|
const flyoutTitleId = React.useId()
|
|
247
298
|
const iconRailCollapsed = state === "collapsed" && !isMobile
|
|
248
|
-
const showActiveStyle = isActive || isAnyChildActive
|
|
249
299
|
const triggerIcon =
|
|
250
|
-
|
|
300
|
+
parentMenuButtonActive && item.iconActive ? item.iconActive : item.icon
|
|
251
301
|
|
|
252
302
|
React.useEffect(() => {
|
|
253
303
|
setOpen(isAnyChildActive)
|
|
@@ -267,23 +317,20 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
267
317
|
open={flyoutOpen}
|
|
268
318
|
onOpenChange={next => {
|
|
269
319
|
setFlyoutOpen(next)
|
|
270
|
-
if (next && item.secondaryPanel) {
|
|
271
|
-
openPanel(item.secondaryPanel)
|
|
272
|
-
}
|
|
273
320
|
}}
|
|
274
321
|
>
|
|
275
322
|
<Tooltip>
|
|
276
323
|
<TooltipTrigger asChild>
|
|
277
324
|
<PopoverTrigger asChild>
|
|
278
325
|
<SidebarMenuButton
|
|
279
|
-
isActive={
|
|
326
|
+
isActive={parentMenuButtonActive}
|
|
280
327
|
aria-haspopup="dialog"
|
|
281
328
|
aria-label={`${item.title} — open subpages`}
|
|
282
329
|
>
|
|
283
330
|
<span
|
|
284
331
|
className={cn(
|
|
285
332
|
"size-4 shrink-0 flex items-center justify-center",
|
|
286
|
-
|
|
333
|
+
parentMenuButtonActive &&
|
|
287
334
|
"[animation:sidebar-icon-pop_380ms_cubic-bezier(0.34,1.56,0.64,1)_both]",
|
|
288
335
|
)}
|
|
289
336
|
aria-hidden="true"
|
|
@@ -341,9 +388,6 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
341
388
|
open={open}
|
|
342
389
|
onOpenChange={next => {
|
|
343
390
|
setOpen(next)
|
|
344
|
-
if (next && item.secondaryPanel) {
|
|
345
|
-
openPanel(item.secondaryPanel)
|
|
346
|
-
}
|
|
347
391
|
}}
|
|
348
392
|
asChild
|
|
349
393
|
>
|
|
@@ -351,12 +395,12 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
351
395
|
<Tooltip>
|
|
352
396
|
<TooltipTrigger asChild>
|
|
353
397
|
<CollapsibleTrigger asChild>
|
|
354
|
-
<SidebarMenuButton isActive={
|
|
398
|
+
<SidebarMenuButton isActive={parentMenuButtonActive}>
|
|
355
399
|
<span
|
|
356
|
-
key={
|
|
400
|
+
key={parentMenuButtonActive ? "active" : "idle"}
|
|
357
401
|
className={cn(
|
|
358
402
|
"size-4 shrink-0 flex items-center justify-center",
|
|
359
|
-
|
|
403
|
+
parentMenuButtonActive &&
|
|
360
404
|
"[animation:sidebar-icon-pop_380ms_cubic-bezier(0.34,1.56,0.64,1)_both]",
|
|
361
405
|
)}
|
|
362
406
|
aria-hidden="true"
|
|
@@ -769,10 +813,7 @@ function ProductLogoButton() {
|
|
|
769
813
|
>
|
|
770
814
|
{iconRail ? (
|
|
771
815
|
<span className="flex size-8 shrink-0 items-center justify-center">
|
|
772
|
-
<ExxatProductMark
|
|
773
|
-
product={current.id}
|
|
774
|
-
className="size-7 max-h-none"
|
|
775
|
-
/>
|
|
816
|
+
<ExxatProductMark product={current.id} className="size-7" />
|
|
776
817
|
</span>
|
|
777
818
|
) : (
|
|
778
819
|
<span className="flex min-h-0 min-w-0 flex-1 items-stretch gap-2">
|
|
@@ -901,6 +942,13 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
|
901
942
|
<ProductLogoButton />
|
|
902
943
|
</SidebarMenuItem>
|
|
903
944
|
</SidebarMenu>
|
|
945
|
+
<div className="flex w-full justify-center px-2">
|
|
946
|
+
<Separator
|
|
947
|
+
orientation="horizontal"
|
|
948
|
+
decorative
|
|
949
|
+
className="my-1.5 h-px w-full max-w-none shrink-0 bg-sidebar-border group-data-[collapsible=icon]:w-8"
|
|
950
|
+
/>
|
|
951
|
+
</div>
|
|
904
952
|
<TeamSwitcher />
|
|
905
953
|
</SidebarHeaderStack>
|
|
906
954
|
</SidebarHeader>
|
|
@@ -286,8 +286,7 @@ export function AskLeoSidebar() {
|
|
|
286
286
|
style={
|
|
287
287
|
open
|
|
288
288
|
? {
|
|
289
|
-
background:
|
|
290
|
-
"linear-gradient(180deg, color-mix(in oklch, var(--brand-color) 4%, var(--background)) 0%, color-mix(in oklch, var(--brand-color) 8%, var(--background)) 100%)",
|
|
289
|
+
background: "var(--leo-surface-gradient)",
|
|
291
290
|
}
|
|
292
291
|
: undefined
|
|
293
292
|
}
|
|
@@ -79,10 +79,16 @@ function useComplianceBoardModel(rows: ComplianceItem[], groupByColumnKey: strin
|
|
|
79
79
|
}, [rows, groupByColumnKey])
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
function ComplianceBoardCard({
|
|
82
|
+
function ComplianceBoardCard({
|
|
83
|
+
row,
|
|
84
|
+
onRowActivate,
|
|
85
|
+
}: {
|
|
86
|
+
row: ComplianceItem
|
|
87
|
+
onRowActivate?: (row: ComplianceItem) => void
|
|
88
|
+
}) {
|
|
83
89
|
const ownerInitials = initialsFromDisplayName(row.owner)
|
|
84
90
|
return (
|
|
85
|
-
<ListPageBoardCard className="w-full">
|
|
91
|
+
<ListPageBoardCard className="w-full" onClick={onRowActivate ? () => onRowActivate(row) : undefined}>
|
|
86
92
|
<ListPageBoardCardHeader>
|
|
87
93
|
<ListPageBoardCardTitleRow
|
|
88
94
|
title={row.title}
|
|
@@ -114,9 +120,11 @@ export const COMPLIANCE_BOARD_GROUP_OPTIONS = [
|
|
|
114
120
|
export function ComplianceBoardView({
|
|
115
121
|
rows,
|
|
116
122
|
groupByColumnKey,
|
|
123
|
+
onRowActivate,
|
|
117
124
|
}: {
|
|
118
125
|
rows: ComplianceItem[]
|
|
119
126
|
groupByColumnKey: string
|
|
127
|
+
onRowActivate?: (row: ComplianceItem) => void
|
|
120
128
|
}) {
|
|
121
129
|
const key = groupByColumnKey === "category" ? "category" : "status"
|
|
122
130
|
const { columns, badgeMap } = useComplianceBoardModel(rows, key)
|
|
@@ -128,7 +136,7 @@ export function ComplianceBoardView({
|
|
|
128
136
|
getRowKey={r => r.id}
|
|
129
137
|
columnCountBadgeClassName={badgeMap}
|
|
130
138
|
emptyColumnLabel="No items"
|
|
131
|
-
renderCard={row => <ComplianceBoardCard row={row} />}
|
|
139
|
+
renderCard={row => <ComplianceBoardCard row={row} onRowActivate={onRowActivate} />}
|
|
132
140
|
/>
|
|
133
141
|
)
|
|
134
142
|
}
|
|
@@ -10,12 +10,19 @@ import {
|
|
|
10
10
|
} from "@/lib/list-status-badges"
|
|
11
11
|
import type { ComplianceItem } from "@/lib/mock/compliance"
|
|
12
12
|
|
|
13
|
-
function ComplianceListRow({
|
|
13
|
+
function ComplianceListRow({
|
|
14
|
+
row,
|
|
15
|
+
onRowActivate,
|
|
16
|
+
}: {
|
|
17
|
+
row: ComplianceItem
|
|
18
|
+
onRowActivate?: (row: ComplianceItem) => void
|
|
19
|
+
}) {
|
|
14
20
|
return (
|
|
15
21
|
<li>
|
|
16
22
|
<ListPageBoardCard
|
|
17
23
|
layout="row"
|
|
18
24
|
rowContainerClassName="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:gap-4"
|
|
25
|
+
onClick={onRowActivate ? () => onRowActivate(row) : undefined}
|
|
19
26
|
rowEnd={
|
|
20
27
|
<div className="flex shrink-0 items-center gap-2">
|
|
21
28
|
<ListHubStatusBadge
|
|
@@ -40,7 +47,13 @@ function ComplianceListRow({ row }: { row: ComplianceItem }) {
|
|
|
40
47
|
)
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
export function ComplianceListView({
|
|
50
|
+
export function ComplianceListView({
|
|
51
|
+
rows,
|
|
52
|
+
onRowActivate,
|
|
53
|
+
}: {
|
|
54
|
+
rows: ComplianceItem[]
|
|
55
|
+
onRowActivate?: (row: ComplianceItem) => void
|
|
56
|
+
}) {
|
|
44
57
|
if (rows.length === 0) {
|
|
45
58
|
return (
|
|
46
59
|
<div className="px-4 py-16 text-center lg:px-6">
|
|
@@ -52,7 +65,7 @@ export function ComplianceListView({ rows }: { rows: ComplianceItem[] }) {
|
|
|
52
65
|
return (
|
|
53
66
|
<ul className="flex list-none flex-col gap-2 px-4 pb-8 pt-2 lg:px-6">
|
|
54
67
|
{rows.map(row => (
|
|
55
|
-
<ComplianceListRow key={row.id} row={row} />
|
|
68
|
+
<ComplianceListRow key={row.id} row={row} onRowActivate={onRowActivate} />
|
|
56
69
|
))}
|
|
57
70
|
</ul>
|
|
58
71
|
)
|
|
@@ -509,7 +509,10 @@ export const ComplianceTable = React.forwardRef<
|
|
|
509
509
|
return (
|
|
510
510
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
511
511
|
{sharedToolbar}
|
|
512
|
-
<ComplianceListView
|
|
512
|
+
<ComplianceListView
|
|
513
|
+
rows={tableState.rows as ComplianceItem[]}
|
|
514
|
+
onRowActivate={row => tableState.toggleRow(row.id)}
|
|
515
|
+
/>
|
|
513
516
|
</div>
|
|
514
517
|
)
|
|
515
518
|
}
|
|
@@ -521,6 +524,7 @@ export const ComplianceTable = React.forwardRef<
|
|
|
521
524
|
<ComplianceBoardView
|
|
522
525
|
rows={tableState.rows as ComplianceItem[]}
|
|
523
526
|
groupByColumnKey={complianceBoardGroupKey}
|
|
527
|
+
onRowActivate={row => tableState.toggleRow(row.id)}
|
|
524
528
|
/>
|
|
525
529
|
</div>
|
|
526
530
|
)
|
|
@@ -214,7 +214,7 @@ function FilterPillBase<TData>({
|
|
|
214
214
|
<PopoverAnchor asChild>
|
|
215
215
|
<div
|
|
216
216
|
className={cn(
|
|
217
|
-
"inline-flex items-center rounded border text-xs transition-colors",
|
|
217
|
+
"inline-flex cursor-pointer items-center rounded border text-xs transition-colors",
|
|
218
218
|
isActive ? "border-brand/45 bg-brand/10" : "border-input bg-background"
|
|
219
219
|
)}
|
|
220
220
|
>
|
|
@@ -222,7 +222,7 @@ function FilterPillBase<TData>({
|
|
|
222
222
|
<button
|
|
223
223
|
type="button"
|
|
224
224
|
className={cn(
|
|
225
|
-
"inline-flex items-center gap-1 h-6 pl-2 pr-1.5 rounded-l transition-colors",
|
|
225
|
+
"inline-flex cursor-pointer items-center gap-1 h-6 pl-2 pr-1.5 rounded-l transition-colors",
|
|
226
226
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
|
|
227
227
|
isActive ? "hover:bg-brand/15" : "hover:bg-interactive-hover",
|
|
228
228
|
)}
|
|
@@ -240,7 +240,7 @@ function FilterPillBase<TData>({
|
|
|
240
240
|
aria-label={`Remove ${col.label} filter`}
|
|
241
241
|
onClick={() => onRemove(filter.id)}
|
|
242
242
|
className={cn(
|
|
243
|
-
"inline-flex items-center justify-center h-6 w-5 rounded-r transition-colors",
|
|
243
|
+
"inline-flex cursor-pointer items-center justify-center h-6 w-5 rounded-r transition-colors",
|
|
244
244
|
"text-muted-foreground hover:text-destructive",
|
|
245
245
|
isActive ? "hover:bg-brand/15" : "hover:bg-interactive-hover",
|
|
246
246
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
|
|
@@ -496,7 +496,7 @@ export function DataTableToolbar<TData extends Record<string, unknown>>({
|
|
|
496
496
|
<DropdownMenu>
|
|
497
497
|
<DropdownMenuTrigger asChild>
|
|
498
498
|
<button type="button"
|
|
499
|
-
className="inline-flex items-center gap-1 h-6 px-2 rounded text-xs text-muted-foreground hover:text-interactive-hover-foreground border border-dashed border-input/70 hover:border-input hover:bg-interactive-hover-subtle transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
499
|
+
className="inline-flex cursor-pointer items-center gap-1 h-6 px-2 rounded text-xs text-muted-foreground hover:text-interactive-hover-foreground border border-dashed border-input/70 hover:border-input hover:bg-interactive-hover-subtle transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
500
500
|
>
|
|
501
501
|
<i className="fa-light fa-plus text-xs" aria-hidden="true" />
|
|
502
502
|
Add filter
|
|
@@ -518,7 +518,7 @@ export function DataTableToolbar<TData extends Record<string, unknown>>({
|
|
|
518
518
|
<button
|
|
519
519
|
type="button"
|
|
520
520
|
onClick={() => setActiveFilters([])}
|
|
521
|
-
className="text-xs text-muted-foreground hover:text-interactive-hover-foreground transition-colors px-1"
|
|
521
|
+
className="cursor-pointer text-xs text-muted-foreground hover:text-interactive-hover-foreground transition-colors px-1"
|
|
522
522
|
>
|
|
523
523
|
Clear all
|
|
524
524
|
</button>
|
|
@@ -556,7 +556,7 @@ export function DataTableToolbar<TData extends Record<string, unknown>>({
|
|
|
556
556
|
type="button"
|
|
557
557
|
aria-label="Clear search"
|
|
558
558
|
onClick={() => setSearch("")}
|
|
559
|
-
className="absolute right-1.5 top-1/2 -translate-y-1/2 inline-flex size-6 items-center justify-center rounded text-muted-foreground transition-colors hover:text-interactive-hover-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
559
|
+
className="absolute right-1.5 top-1/2 -translate-y-1/2 inline-flex cursor-pointer size-6 items-center justify-center rounded text-muted-foreground transition-colors hover:text-interactive-hover-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
560
560
|
>
|
|
561
561
|
<i className="fa-light fa-xmark text-xs" aria-hidden="true" />
|
|
562
562
|
</button>
|
|
@@ -568,7 +568,7 @@ export function DataTableToolbar<TData extends Record<string, unknown>>({
|
|
|
568
568
|
<TooltipTrigger asChild>
|
|
569
569
|
<button type="button" aria-label="Search"
|
|
570
570
|
onClick={() => { setSearchOpen(true); setTimeout(() => searchRef.current?.focus(), 10) }}
|
|
571
|
-
className="inline-flex shrink-0 items-center justify-center size-8 rounded-md text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
571
|
+
className="inline-flex shrink-0 cursor-pointer items-center justify-center size-8 rounded-md text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
572
572
|
>
|
|
573
573
|
<i className="fa-light fa-magnifying-glass text-[13px]" aria-hidden="true" />
|
|
574
574
|
</button>
|
|
@@ -596,7 +596,7 @@ export function DataTableToolbar<TData extends Record<string, unknown>>({
|
|
|
596
596
|
aria-label={filterBarVisible ? "Hide filters" : "Show filters"}
|
|
597
597
|
onClick={() => setFilterBarVisible(v => !v)}
|
|
598
598
|
className={cn(
|
|
599
|
-
"inline-flex shrink-0 items-center gap-1 size-8 justify-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
599
|
+
"inline-flex shrink-0 cursor-pointer items-center gap-1 size-8 justify-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
600
600
|
filterBarVisible
|
|
601
601
|
? "bg-accent text-accent-foreground hover:bg-accent/90"
|
|
602
602
|
: "text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover",
|
|
@@ -610,7 +610,7 @@ export function DataTableToolbar<TData extends Record<string, unknown>>({
|
|
|
610
610
|
<DropdownMenuTrigger asChild>
|
|
611
611
|
<button type="button" aria-label="Add filter"
|
|
612
612
|
onClick={() => setFilterBarVisible(true)}
|
|
613
|
-
className="inline-flex shrink-0 items-center justify-center size-8 rounded-md text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
613
|
+
className="inline-flex shrink-0 cursor-pointer items-center justify-center size-8 rounded-md text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
614
614
|
>
|
|
615
615
|
<i className="fa-light fa-filter text-[13px]" aria-hidden="true" />
|
|
616
616
|
</button>
|
|
@@ -1325,19 +1325,33 @@ function DataTableInner<TData extends Record<string, unknown>>({
|
|
|
1325
1325
|
const rowId = getRowId(row, rowIndex, getRowIdProp)
|
|
1326
1326
|
const isSelected = selected.has(rowId)
|
|
1327
1327
|
const isHovered = hoveredRow === rowId
|
|
1328
|
+
const rowClickable = Boolean(onRowClick) || selectable
|
|
1329
|
+
function handleRowClick(e: React.MouseEvent<HTMLTableRowElement>) {
|
|
1330
|
+
if (!rowClickable) return
|
|
1331
|
+
const el = e.target as HTMLElement | null
|
|
1332
|
+
if (!el) return
|
|
1333
|
+
if (el.closest("button, a, input, textarea, select, label, [role='checkbox']")) return
|
|
1334
|
+
if (onRowClick) {
|
|
1335
|
+
onRowClick(row)
|
|
1336
|
+
return
|
|
1337
|
+
}
|
|
1338
|
+
if (selectable) {
|
|
1339
|
+
toggleRow(rowId)
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1328
1342
|
return (
|
|
1329
1343
|
<tr
|
|
1330
1344
|
key={String(rowId)}
|
|
1331
1345
|
data-state={isSelected ? "selected" : undefined}
|
|
1332
1346
|
onMouseEnter={() => setHoveredRow(rowId)}
|
|
1333
1347
|
onMouseLeave={() => setHoveredRow(null)}
|
|
1334
|
-
onClick={
|
|
1348
|
+
onClick={rowClickable ? handleRowClick : undefined}
|
|
1335
1349
|
data-new={Boolean((row as Record<string, unknown>).isNew) || undefined}
|
|
1336
1350
|
className={cn(
|
|
1337
1351
|
"group/row transition-colors",
|
|
1338
1352
|
"hover:bg-dt-row-hover",
|
|
1339
1353
|
isSelected && "bg-dt-row-selected text-dt-row-selected-fg",
|
|
1340
|
-
|
|
1354
|
+
rowClickable && "cursor-pointer",
|
|
1341
1355
|
Boolean((row as Record<string, unknown>).isNew) && "bg-dt-new-row-bg border-l-2 border-l-dt-new-row-border"
|
|
1342
1356
|
)}
|
|
1343
1357
|
>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* FinderPanelView — Miller-style 3-column split for list-page hubs.
|
|
5
5
|
*
|
|
6
|
-
* Visual shell matches Question bank panel (`ListPageTreeColumnHeader`, `
|
|
6
|
+
* Visual shell matches Question bank panel (`ListPageTreeColumnHeader`, `LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS`,
|
|
7
7
|
* shared resizable handles) — see `list-page-split-hub-tokens.ts`.
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -142,7 +142,7 @@ export function FinderGroupStrip({
|
|
|
142
142
|
<div
|
|
143
143
|
role="toolbar"
|
|
144
144
|
aria-label={ariaLabel}
|
|
145
|
-
className="flex min-h-10 flex-wrap items-center gap-1.5 border-b border-border bg-
|
|
145
|
+
className="flex min-h-10 flex-wrap items-center gap-1.5 border-b border-border bg-card px-2 py-2"
|
|
146
146
|
>
|
|
147
147
|
{groups.map(group => {
|
|
148
148
|
const isSelected = group.id === selectedGroupId
|
|
@@ -47,6 +47,25 @@ export {
|
|
|
47
47
|
type ListPageTreeColumnHeaderProps,
|
|
48
48
|
} from "@/components/data-views/list-page-tree-column-header"
|
|
49
49
|
|
|
50
|
+
/** VS Code–style outline tree chrome — mirrors shadcn `SidebarMenuSub` (see module doc). */
|
|
51
|
+
export {
|
|
52
|
+
OutlineTreeCollapsibleContentRail,
|
|
53
|
+
OutlineTreeLeafButton,
|
|
54
|
+
OutlineTreeMenu,
|
|
55
|
+
OutlineTreeMenuItem,
|
|
56
|
+
OutlineTreeSub,
|
|
57
|
+
OutlineTreeSubItem,
|
|
58
|
+
OUTLINE_TREE_COLLAPSIBLE_CONTENT_RAIL_CLASS,
|
|
59
|
+
OUTLINE_TREE_CHEVRON_GUIDE_SPACER_CLASS,
|
|
60
|
+
OUTLINE_TREE_SUB_ROW_SHIFT_CLASS,
|
|
61
|
+
type OutlineTreeGuideLayout,
|
|
62
|
+
type OutlineTreeLeafButtonProps,
|
|
63
|
+
type OutlineTreeSurface,
|
|
64
|
+
} from "@/components/data-views/outline-tree-menu"
|
|
65
|
+
|
|
66
|
+
export { QuestionBankFolderTreeBranch } from "@/components/data-views/question-bank-folder-tree-branch"
|
|
67
|
+
export type { QuestionBankFolderTreeBranchProps } from "@/components/data-views/question-bank-folder-tree-branch"
|
|
68
|
+
|
|
50
69
|
export {
|
|
51
70
|
LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS,
|
|
52
71
|
LIST_PAGE_SPLIT_MILLER_DETAIL_PANEL_CLASS,
|
|
@@ -10,7 +10,7 @@ export interface ListPageSplitDetailsPlaceholderProps {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Empty right pane for split hubs —
|
|
13
|
+
* Empty right pane for split hubs — flat `bg-card` to match Miller / tree columns.
|
|
14
14
|
*/
|
|
15
15
|
export function ListPageSplitDetailsPlaceholder({
|
|
16
16
|
title = "Nothing selected",
|
|
@@ -20,11 +20,11 @@ export function ListPageSplitDetailsPlaceholder({
|
|
|
20
20
|
return (
|
|
21
21
|
<div
|
|
22
22
|
className={cn(
|
|
23
|
-
"flex h-full min-h-0 flex-col items-center justify-center bg-
|
|
23
|
+
"flex h-full min-h-0 flex-col items-center justify-center bg-card px-6 py-10 text-center",
|
|
24
24
|
className,
|
|
25
25
|
)}
|
|
26
26
|
>
|
|
27
|
-
<div className="mb-4 flex size-14 items-center justify-center rounded-2xl border border-dashed border-border/70 bg-
|
|
27
|
+
<div className="mb-4 flex size-14 items-center justify-center rounded-2xl border border-dashed border-border/70 bg-card">
|
|
28
28
|
<i
|
|
29
29
|
className="fa-light fa-sidebar text-[1.65rem] leading-none text-muted-foreground/70"
|
|
30
30
|
aria-hidden="true"
|
|
@@ -9,7 +9,7 @@ export const LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS =
|
|
|
9
9
|
|
|
10
10
|
/** Primary column stack (scope list, folder list, record list, …). */
|
|
11
11
|
export const LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS =
|
|
12
|
-
"flex min-h-0 min-w-0 flex-col bg-
|
|
12
|
+
"flex min-h-0 min-w-0 flex-col bg-card"
|
|
13
13
|
|
|
14
14
|
/** Right-hand inspector / detail column shell. */
|
|
15
15
|
export const LIST_PAGE_SPLIT_MILLER_DETAIL_PANEL_CLASS =
|
|
@@ -19,7 +19,7 @@ export function ListPageTreeColumnHeader({
|
|
|
19
19
|
className,
|
|
20
20
|
}: ListPageTreeColumnHeaderProps) {
|
|
21
21
|
return (
|
|
22
|
-
<div className={cn("shrink-0 border-b border-border/50 bg-
|
|
22
|
+
<div className={cn("shrink-0 border-b border-border/50 bg-card px-3 py-2", className)}>
|
|
23
23
|
<div className="flex h-9 items-center justify-between gap-2">
|
|
24
24
|
<h3 className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">{title}</h3>
|
|
25
25
|
{trailing ? (
|