@exxatdesignux/ui 0.2.15 → 0.2.17
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 +23 -0
- package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -1
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +151 -3
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/accessibility.md +142 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/coach-marks.md +169 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/data-table-pattern.md +382 -0
- package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +56 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +17 -1
- package/consumer-extras/patterns/collaboration-access-pattern.md +2 -0
- package/package.json +3 -3
- package/src/components/ui/banner.tsx +2 -0
- package/src/components/ui/chart.tsx +57 -2
- package/src/components/ui/sidebar.tsx +1 -0
- package/src/globals.css +21 -2
- package/src/theme.css +4 -2
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +1 -1
- package/template/.cursor/rules/exxat-mono-ids.mdc +30 -0
- package/template/AGENTS.md +23 -18
- package/template/app/(app)/data-list/page.tsx +2 -2
- package/template/app/(app)/question-bank/layout.tsx +27 -7
- package/template/app/(app)/question-bank/new/page.tsx +58 -0
- package/template/app/globals.css +136 -2
- package/template/app/layout.tsx +41 -5
- package/template/components/app-sidebar.tsx +141 -59
- package/template/components/ask-leo-sidebar.tsx +1 -4
- package/template/components/brand-color-picker.tsx +344 -0
- package/template/components/compliance-list-view.tsx +33 -51
- package/template/components/compliance-table.tsx +24 -0
- package/template/components/data-table/index.tsx +68 -24
- package/template/components/data-table/pagination.tsx +0 -1
- package/template/components/data-table/types.ts +4 -1
- package/template/components/data-table/use-table-state.ts +243 -94
- package/template/components/data-views/data-row-list.tsx +183 -0
- package/template/components/data-views/finder-panel-view.tsx +2 -2
- package/template/components/data-views/index.ts +26 -3
- 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/os-folder-glyph.tsx +8 -0
- 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/export-drawer.tsx +1 -1
- package/template/components/exxat-product-logo.tsx +173 -379
- package/template/components/folder-details-shell.tsx +1 -1
- package/template/components/hub-tree-panel-view.tsx +88 -80
- package/template/components/invite-collaborators-drawer.tsx +5 -3
- package/template/components/key-metrics.tsx +116 -51
- package/template/components/new-placement-form.tsx +4 -2
- package/template/components/new-question-composer.tsx +2208 -0
- package/template/components/page-breadcrumb-trail.tsx +131 -0
- package/template/components/page-header.tsx +21 -11
- package/template/components/{data-views/placement-board-card.tsx → placement-board-card.tsx} +1 -1
- package/template/components/placement-detail.tsx +1 -1
- package/template/components/placements-board-view.tsx +1 -1
- package/template/components/{data-list-client.tsx → placements-client.tsx} +9 -7
- package/template/components/placements-list-view.tsx +18 -132
- package/template/components/{data-list-table-cells.test.tsx → placements-table-cells.test.tsx} +2 -2
- package/template/components/{data-list-table-cells.tsx → placements-table-cells.tsx} +1 -1
- package/template/components/placements-table-columns.tsx +2 -2
- package/template/components/{data-list-table.tsx → placements-table.tsx} +67 -58
- package/template/components/product-switcher.tsx +26 -11
- package/template/components/product-wordmark.tsx +285 -0
- package/template/components/question-bank-client.tsx +130 -70
- package/template/components/question-bank-hub-client.tsx +108 -115
- package/template/components/question-bank-list-view.tsx +30 -54
- package/template/components/question-bank-new-folder-sheet.tsx +1 -1
- package/template/components/question-bank-page-header.tsx +18 -2
- package/template/components/question-bank-secondary-nav.tsx +12 -228
- package/template/components/question-bank-table.tsx +30 -5
- package/template/components/rotations-empty-state.tsx +3 -0
- package/template/components/secondary-panel.tsx +24 -4
- package/template/components/settings-appearance-card.tsx +584 -141
- package/template/components/site-header.tsx +56 -32
- package/template/components/sites-list-view.tsx +31 -36
- package/template/components/sites-table.tsx +24 -0
- package/template/components/table-properties/drawer.tsx +1 -1
- package/template/components/team-client.tsx +1 -1
- package/template/components/team-list-view.tsx +34 -50
- package/template/components/team-table.tsx +29 -3
- 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 +11 -6
- package/template/components/ui/dot-pattern.tsx +50 -26
- package/template/components/ui/leo-icon.tsx +23 -3
- package/template/contexts/product-context.tsx +51 -7
- package/template/contexts/system-banner-context.tsx +112 -4
- package/template/docs/collaboration-access-pattern.md +2 -0
- package/template/docs/question-bank-hub-header-pattern.md +25 -0
- package/template/eslint.config.mjs +18 -0
- package/template/hooks/use-secondary-panel-hub-nav.ts +17 -1
- package/template/hooks/use-sidebar-reflow-zoom.ts +21 -11
- package/template/lib/data-list-persistence.ts +57 -257
- package/template/lib/dev-log.test.ts +6 -5
- package/template/lib/exxat-palette.json +1462 -0
- package/template/lib/exxat-palette.ts +136 -0
- package/template/lib/list-page-table-properties.ts +1 -1
- package/template/lib/list-status-badges.ts +1 -1
- package/template/lib/mailto.ts +29 -0
- package/template/lib/mock/navigation.tsx +30 -1
- package/template/lib/placement-board-card-layout.ts +1 -1
- package/template/lib/product-brand.ts +268 -0
- package/template/lib/question-bank-authoring.ts +308 -0
- package/template/lib/question-bank-nav.ts +70 -0
- package/template/lib/raf-throttle.ts +45 -0
- package/template/lib/table-state-lifecycle.ts +474 -0
- package/template/next.config.mjs +156 -0
- package/template/package.json +6 -6
- package/template/stores/app-store.ts +46 -1
- 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,
|
|
@@ -70,6 +71,7 @@ import { NavUser } from "@/components/nav-user"
|
|
|
70
71
|
import { useSecondaryPanel } from "@/components/secondary-panel"
|
|
71
72
|
import { ExxatProductLogo, ExxatProductMark } from "@/components/exxat-product-logo"
|
|
72
73
|
import { motionHeaderEnter } from "@/lib/motion-ui"
|
|
74
|
+
import { customProductBrandConfig, productBrandLabel } from "@/lib/product-brand"
|
|
73
75
|
import {
|
|
74
76
|
NAV_DOCUMENTS,
|
|
75
77
|
NAV_DOCUMENTS_LABEL,
|
|
@@ -85,8 +87,6 @@ import {
|
|
|
85
87
|
type NavSchool,
|
|
86
88
|
type NavProgram,
|
|
87
89
|
} from "@/lib/mock/navigation"
|
|
88
|
-
import { QUESTION_BANK_ENTRY_PATH } from "@/lib/question-bank-nav"
|
|
89
|
-
|
|
90
90
|
/** Path segment of a nav URL (strip `#fragment` for matching). */
|
|
91
91
|
function navUrlPath(url: string): string {
|
|
92
92
|
if (!url || url === "#") return ""
|
|
@@ -108,7 +108,8 @@ function normalizedLocationHash(locationHash: string): string {
|
|
|
108
108
|
/**
|
|
109
109
|
* Whether `pathname` (+ optional `location.hash`) matches a sidebar `href`.
|
|
110
110
|
* When several links share the same path (e.g. `/settings`), disambiguate with `#fragment`
|
|
111
|
-
*
|
|
111
|
+
* in each `href` — those rows use the `frag !== null` branch below.
|
|
112
|
+
* For `href` without `#…`, an in-page hash (e.g. QB view tabs) does not clear the match.
|
|
112
113
|
*/
|
|
113
114
|
function isNavActive(pathname: string, url: string, locationHash = ""): boolean {
|
|
114
115
|
const pathOnly = navUrlPath(url)
|
|
@@ -129,7 +130,8 @@ function isNavActive(pathname: string, url: string, locationHash = ""): boolean
|
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
if (pathOnly === "/") return pathname === "/" && h === ""
|
|
132
|
-
|
|
133
|
+
/** Exact path match — ignore `location.hash` when the nav `href` has no `#…` fragment (QB view tabs use hash). */
|
|
134
|
+
if (pathname === pathOnly) return true
|
|
133
135
|
// Design system library — active on hub and detail routes.
|
|
134
136
|
if (pathOnly === "/library") {
|
|
135
137
|
return pathname.startsWith("/library/")
|
|
@@ -165,6 +167,16 @@ function isCollapsibleChildActive(
|
|
|
165
167
|
|
|
166
168
|
if (!isNavActive(pathname, child.url, locationHash)) return false
|
|
167
169
|
|
|
170
|
+
/** Hub entry (`/question-bank`) must not stay “active” on `/question-bank/library` etc. */
|
|
171
|
+
if (parent.primaryHubChildKey && child.key === parent.primaryHubChildKey) {
|
|
172
|
+
const hubPath = navUrlPath(parent.url)
|
|
173
|
+
if (hubPath) {
|
|
174
|
+
const normalized =
|
|
175
|
+
pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname
|
|
176
|
+
if (normalized !== hubPath) return false
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
168
180
|
const urls = children.map(c => c.url)
|
|
169
181
|
const allSameUrl = urls.length > 1 && urls.every(u => u === urls[0])
|
|
170
182
|
if (allSameUrl) {
|
|
@@ -176,6 +188,33 @@ function isCollapsibleChildActive(
|
|
|
176
188
|
return true
|
|
177
189
|
}
|
|
178
190
|
|
|
191
|
+
/**
|
|
192
|
+
* “Selected” styling on a collapsible **parent** row in the **expanded** sidebar.
|
|
193
|
+
*
|
|
194
|
+
* Rule: when any descendant child is the current destination, the parent stays
|
|
195
|
+
* visually NEUTRAL — the active child carries `data-active` on its own. The
|
|
196
|
+
* parent is only highlighted when no child matches but the parent URL still
|
|
197
|
+
* matches (edge case: route that isn't represented in the sub-list).
|
|
198
|
+
*
|
|
199
|
+
* Note: this is for the expanded view only. The collapsed icon rail uses
|
|
200
|
+
* `iconRailActive = isAnyChildActive` because the parent icon is the only
|
|
201
|
+
* visible affordance there (see `CollapsibleNavItem`).
|
|
202
|
+
*/
|
|
203
|
+
function isCollapsibleParentMenuButtonActive(
|
|
204
|
+
pathname: string,
|
|
205
|
+
item: NavLinkItem,
|
|
206
|
+
locationHash: string,
|
|
207
|
+
): boolean {
|
|
208
|
+
const children = item.children
|
|
209
|
+
if (!children?.length) return isNavActive(pathname, item.url, locationHash)
|
|
210
|
+
|
|
211
|
+
const anyChildActive = children.some(c =>
|
|
212
|
+
isCollapsibleChildActive(pathname, item, c, locationHash),
|
|
213
|
+
)
|
|
214
|
+
if (anyChildActive) return false
|
|
215
|
+
return isNavActive(pathname, item.url, locationHash)
|
|
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,26 @@ 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
|
-
|
|
299
|
+
// In the icon rail the parent icon is the ONLY visible thing for this item
|
|
300
|
+
// (no sub-list, no labels) — so it must reflect "I'm somewhere inside this
|
|
301
|
+
// section" by lighting up on any descendant route (e.g. `/question-bank/library`),
|
|
302
|
+
// not only on the parent URL itself. In the expanded view we keep the
|
|
303
|
+
// parent neutral and let the active child row carry `data-active` (see
|
|
304
|
+
// `isCollapsibleParentMenuButtonActive`).
|
|
305
|
+
const iconRailActive = isAnyChildActive
|
|
249
306
|
const triggerIcon =
|
|
250
|
-
|
|
307
|
+
(iconRailCollapsed ? iconRailActive : parentMenuButtonActive) && item.iconActive
|
|
308
|
+
? item.iconActive
|
|
309
|
+
: item.icon
|
|
251
310
|
|
|
252
311
|
React.useEffect(() => {
|
|
253
312
|
setOpen(isAnyChildActive)
|
|
@@ -267,23 +326,22 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
267
326
|
open={flyoutOpen}
|
|
268
327
|
onOpenChange={next => {
|
|
269
328
|
setFlyoutOpen(next)
|
|
270
|
-
if (next && item.secondaryPanel) {
|
|
271
|
-
openPanel(item.secondaryPanel)
|
|
272
|
-
}
|
|
273
329
|
}}
|
|
274
330
|
>
|
|
275
331
|
<Tooltip>
|
|
276
332
|
<TooltipTrigger asChild>
|
|
277
333
|
<PopoverTrigger asChild>
|
|
278
334
|
<SidebarMenuButton
|
|
279
|
-
isActive={
|
|
335
|
+
isActive={iconRailActive}
|
|
336
|
+
aria-current={iconRailActive ? "page" : undefined}
|
|
280
337
|
aria-haspopup="dialog"
|
|
281
338
|
aria-label={`${item.title} — open subpages`}
|
|
282
339
|
>
|
|
283
340
|
<span
|
|
341
|
+
key={iconRailActive ? "active" : "idle"}
|
|
284
342
|
className={cn(
|
|
285
343
|
"size-4 shrink-0 flex items-center justify-center",
|
|
286
|
-
|
|
344
|
+
iconRailActive &&
|
|
287
345
|
"[animation:sidebar-icon-pop_380ms_cubic-bezier(0.34,1.56,0.64,1)_both]",
|
|
288
346
|
)}
|
|
289
347
|
aria-hidden="true"
|
|
@@ -341,22 +399,22 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
341
399
|
open={open}
|
|
342
400
|
onOpenChange={next => {
|
|
343
401
|
setOpen(next)
|
|
344
|
-
if (next && item.secondaryPanel) {
|
|
345
|
-
openPanel(item.secondaryPanel)
|
|
346
|
-
}
|
|
347
402
|
}}
|
|
348
403
|
asChild
|
|
349
404
|
>
|
|
350
|
-
|
|
405
|
+
{/* `group/collapsible` lets descendant utilities react to the
|
|
406
|
+
Radix `data-state` (e.g. chevron rotate, content slide). Radix's
|
|
407
|
+
asChild merges the data-state onto this `<SidebarMenuItem>`. */}
|
|
408
|
+
<SidebarMenuItem className="group/collapsible">
|
|
351
409
|
<Tooltip>
|
|
352
410
|
<TooltipTrigger asChild>
|
|
353
411
|
<CollapsibleTrigger asChild>
|
|
354
|
-
<SidebarMenuButton isActive={
|
|
412
|
+
<SidebarMenuButton isActive={parentMenuButtonActive}>
|
|
355
413
|
<span
|
|
356
|
-
key={
|
|
414
|
+
key={parentMenuButtonActive ? "active" : "idle"}
|
|
357
415
|
className={cn(
|
|
358
416
|
"size-4 shrink-0 flex items-center justify-center",
|
|
359
|
-
|
|
417
|
+
parentMenuButtonActive &&
|
|
360
418
|
"[animation:sidebar-icon-pop_380ms_cubic-bezier(0.34,1.56,0.64,1)_both]",
|
|
361
419
|
)}
|
|
362
420
|
aria-hidden="true"
|
|
@@ -365,7 +423,7 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
365
423
|
</span>
|
|
366
424
|
<span>{item.title}</span>
|
|
367
425
|
<i
|
|
368
|
-
className="fa-light fa-chevron-right ml-auto text-xs text-current transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
|
426
|
+
className="fa-light fa-chevron-right ml-auto text-xs text-current transition-transform duration-200 ease-out group-data-[state=open]/collapsible:rotate-90 motion-reduce:transition-none"
|
|
369
427
|
aria-hidden="true"
|
|
370
428
|
/>
|
|
371
429
|
</SidebarMenuButton>
|
|
@@ -375,7 +433,11 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
375
433
|
{item.title}
|
|
376
434
|
</TooltipContent>
|
|
377
435
|
</Tooltip>
|
|
378
|
-
|
|
436
|
+
{/* Slide the children open/closed using Radix's
|
|
437
|
+
`--radix-collapsible-content-height` CSS variable. `overflow-hidden`
|
|
438
|
+
is required so the height clip is visible during the animation.
|
|
439
|
+
Keyframes defined in `app/globals.css` (`collapsible-down/up`). */}
|
|
440
|
+
<CollapsibleContent className="overflow-hidden group-data-[collapsible=icon]:hidden data-[state=open]:[animation:collapsible-down_200ms_ease-out] data-[state=closed]:[animation:collapsible-up_200ms_ease-out] motion-reduce:animate-none">
|
|
379
441
|
<SidebarMenuSub>
|
|
380
442
|
{item.children.map(child => {
|
|
381
443
|
const childActive = isCollapsibleChildActive(pathname, item, child, locationHash)
|
|
@@ -400,7 +462,7 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
400
462
|
}
|
|
401
463
|
|
|
402
464
|
function NavLinkItems({ items, pathname }: { items: NavLinkItem[]; pathname: string }) {
|
|
403
|
-
const { openPanel
|
|
465
|
+
const { openPanel } = useSecondaryPanel()
|
|
404
466
|
const locationHash = useLocationHash()
|
|
405
467
|
return (
|
|
406
468
|
<>
|
|
@@ -426,6 +488,12 @@ function NavLinkItems({ items, pathname }: { items: NavLinkItem[]; pathname: str
|
|
|
426
488
|
: undefined
|
|
427
489
|
}
|
|
428
490
|
onClick={e => {
|
|
491
|
+
// Reopen the panel when the user clicks a panel-driving row
|
|
492
|
+
// while ALREADY on its route — Next.js `<Link>` does not
|
|
493
|
+
// navigate to the same URL, so without this the panel could
|
|
494
|
+
// stay closed (e.g. after the user collapsed it manually).
|
|
495
|
+
// On first click (different route), default navigation runs
|
|
496
|
+
// and the route's `useAutoPanel` opens the panel itself.
|
|
429
497
|
if (
|
|
430
498
|
item.secondaryPanel &&
|
|
431
499
|
itemPath &&
|
|
@@ -433,11 +501,7 @@ function NavLinkItems({ items, pathname }: { items: NavLinkItem[]; pathname: str
|
|
|
433
501
|
!item.url.includes("#")
|
|
434
502
|
) {
|
|
435
503
|
e.preventDefault()
|
|
436
|
-
|
|
437
|
-
closePanel({ mainSidebar: "leave" })
|
|
438
|
-
} else {
|
|
439
|
-
openPanel(item.secondaryPanel)
|
|
440
|
-
}
|
|
504
|
+
openPanel(item.secondaryPanel)
|
|
441
505
|
}
|
|
442
506
|
}}
|
|
443
507
|
>
|
|
@@ -738,14 +802,26 @@ function TeamSwitcher() {
|
|
|
738
802
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
739
803
|
|
|
740
804
|
const PRODUCTS: { id: Product; label: string }[] = [
|
|
741
|
-
{ id: "exxat-one",
|
|
742
|
-
{ id: "exxat-prism",
|
|
805
|
+
{ id: "exxat-one", label: "Exxat One" },
|
|
806
|
+
{ id: "exxat-prism", label: "Exxat Prism" },
|
|
807
|
+
{ id: "exxat-assessment", label: "Exxat Assessment" },
|
|
808
|
+
{ id: "exxat-custom", label: "Custom product" },
|
|
743
809
|
]
|
|
744
810
|
|
|
745
811
|
function ProductLogoButton() {
|
|
746
|
-
const { product, setProduct } = useProduct()
|
|
812
|
+
const { product, setProduct, customProductBrand, hiddenProductIds } = useProduct()
|
|
747
813
|
const { state, isMobile } = useSidebar()
|
|
748
|
-
const
|
|
814
|
+
const products = React.useMemo(
|
|
815
|
+
() => PRODUCTS.flatMap(p => {
|
|
816
|
+
if (hiddenProductIds.includes(p.id)) return []
|
|
817
|
+
if (p.id !== "exxat-custom") return [p]
|
|
818
|
+
return customProductBrand
|
|
819
|
+
? [{ ...p, label: productBrandLabel(customProductBrandConfig(customProductBrand)) }]
|
|
820
|
+
: []
|
|
821
|
+
}),
|
|
822
|
+
[customProductBrand, hiddenProductIds],
|
|
823
|
+
)
|
|
824
|
+
const current = products.find(p => p.id === product) ?? products[0]
|
|
749
825
|
const iconRail = state === "collapsed" && !isMobile
|
|
750
826
|
const expandedOrMobile = state === "expanded" || isMobile
|
|
751
827
|
|
|
@@ -768,11 +844,10 @@ function ProductLogoButton() {
|
|
|
768
844
|
suppressHydrationWarning
|
|
769
845
|
>
|
|
770
846
|
{iconRail ? (
|
|
847
|
+
// Match the school selector footprint in the icon rail; the
|
|
848
|
+
// inner mark cutout uses the rail surface instead of a white fill.
|
|
771
849
|
<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
|
-
/>
|
|
850
|
+
<ExxatProductMark product={current.id} className="size-8" cutoutColor="var(--sidebar)" />
|
|
776
851
|
</span>
|
|
777
852
|
) : (
|
|
778
853
|
<span className="flex min-h-0 min-w-0 flex-1 items-stretch gap-2">
|
|
@@ -783,7 +858,7 @@ function ProductLogoButton() {
|
|
|
783
858
|
<ExxatProductLogo
|
|
784
859
|
product={current.id}
|
|
785
860
|
variant="mutedSuffix"
|
|
786
|
-
className="
|
|
861
|
+
className="w-auto max-w-[min(100%,280px)] object-left object-contain"
|
|
787
862
|
/>
|
|
788
863
|
</span>
|
|
789
864
|
<span
|
|
@@ -810,7 +885,7 @@ function ProductLogoButton() {
|
|
|
810
885
|
Switch product
|
|
811
886
|
</DropdownMenuLabel>
|
|
812
887
|
<DropdownMenuSeparator />
|
|
813
|
-
{
|
|
888
|
+
{products.map(p => (
|
|
814
889
|
<DropdownMenuItem
|
|
815
890
|
key={p.id}
|
|
816
891
|
onClick={() => setProduct(p.id)}
|
|
@@ -820,7 +895,7 @@ function ProductLogoButton() {
|
|
|
820
895
|
<ExxatProductLogo
|
|
821
896
|
product={p.id}
|
|
822
897
|
variant="mutedSuffix"
|
|
823
|
-
className="
|
|
898
|
+
className="w-auto shrink-0 max-w-[min(100%,260px)]"
|
|
824
899
|
/>
|
|
825
900
|
{p.id === product && (
|
|
826
901
|
<i className="fa-solid fa-check ml-auto text-brand text-xs" aria-hidden="true" />
|
|
@@ -901,6 +976,13 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
|
901
976
|
<ProductLogoButton />
|
|
902
977
|
</SidebarMenuItem>
|
|
903
978
|
</SidebarMenu>
|
|
979
|
+
<div className="flex w-full justify-center px-2">
|
|
980
|
+
<Separator
|
|
981
|
+
orientation="horizontal"
|
|
982
|
+
decorative
|
|
983
|
+
className="my-1.5 h-px w-full max-w-none shrink-0 bg-sidebar-border group-data-[collapsible=icon]:w-8"
|
|
984
|
+
/>
|
|
985
|
+
</div>
|
|
904
986
|
<TeamSwitcher />
|
|
905
987
|
</SidebarHeaderStack>
|
|
906
988
|
</SidebarHeader>
|
|
@@ -188,8 +188,6 @@ export function AskLeoSidebar() {
|
|
|
188
188
|
const routeContext = React.useMemo(() => getAskLeoRouteContext(pathname), [pathname])
|
|
189
189
|
const isThinking = threadMessages.some((m) => m.pending)
|
|
190
190
|
|
|
191
|
-
const pageTitle = pageContext?.title ?? routeContext.title
|
|
192
|
-
const pageDescription = pageContext?.description ?? routeContext.description
|
|
193
191
|
const suggestions =
|
|
194
192
|
pageContext?.suggestions && pageContext.suggestions.length > 0
|
|
195
193
|
? pageContext.suggestions
|
|
@@ -286,8 +284,7 @@ export function AskLeoSidebar() {
|
|
|
286
284
|
style={
|
|
287
285
|
open
|
|
288
286
|
? {
|
|
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%)",
|
|
287
|
+
background: "var(--leo-surface-gradient)",
|
|
291
288
|
}
|
|
292
289
|
: undefined
|
|
293
290
|
}
|