@iloveagents/foundry-web-ui 0.25.0 → 0.27.0
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/dist/components/assistant-chat.d.ts +12 -1
- package/dist/components/assistant-chat.js +18 -13
- package/dist/components/chat-bubble.js +9 -6
- package/dist/components/chat-context.d.ts +2 -0
- package/dist/components/chat-context.js +70 -0
- package/dist/components/chat-slots.d.ts +10 -0
- package/dist/components/chat-slots.js +7 -0
- package/dist/components/composer-add-menu.js +3 -2
- package/dist/components/composer-input.d.ts +4 -0
- package/dist/components/composer-input.js +23 -0
- package/dist/components/context-badges.d.ts +4 -1
- package/dist/components/context-badges.js +7 -4
- package/dist/components/context-bar.d.ts +1 -0
- package/dist/components/context-bar.js +11 -3
- package/dist/components/data-table/data-table-frame.d.ts +1 -22
- package/dist/components/data-table/data-table-frame.js +21 -48
- package/dist/components/data-table/data-table-overflow.d.ts +5 -0
- package/dist/components/data-table/data-table-overflow.js +42 -0
- package/dist/components/data-table/data-table.js +88 -87
- package/dist/components/data-table/focus-pane-resize.d.ts +8 -0
- package/dist/components/data-table/focus-pane-resize.js +88 -0
- package/dist/components/focus-chat-pane.d.ts +8 -0
- package/dist/components/focus-chat-pane.js +9 -0
- package/dist/components/focus-dialog.d.ts +8 -0
- package/dist/components/focus-dialog.js +103 -0
- package/dist/components/focus-source-pane.d.ts +7 -0
- package/dist/components/focus-source-pane.js +15 -0
- package/dist/components/loading-indicator.js +1 -1
- package/dist/components/message-timestamp.js +3 -2
- package/dist/components/pin-state-icon.d.ts +5 -0
- package/dist/components/pin-state-icon.js +7 -0
- package/dist/components/pin-toggle.d.ts +7 -0
- package/dist/components/pin-toggle.js +10 -0
- package/dist/components/sidebar.d.ts +18 -1
- package/dist/components/sidebar.js +193 -100
- package/dist/components/tool-panel-layout.d.ts +3 -1
- package/dist/components/tool-panel-layout.js +5 -5
- package/dist/components/tool-panel.d.ts +8 -1
- package/dist/components/tool-panel.js +12 -8
- package/dist/components/user-menu.js +19 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/app-store.d.ts +0 -8
- package/dist/lib/app-store.js +14 -4
- package/dist/lib/chat-context-summary.d.ts +11 -0
- package/dist/lib/chat-context-summary.js +37 -0
- package/dist/lib/chat-lifecycle-store.d.ts +2 -0
- package/dist/lib/chat-lifecycle-store.js +6 -1
- package/dist/lib/client-tools.js +7 -0
- package/dist/lib/create-ui-id.d.ts +2 -0
- package/dist/lib/create-ui-id.js +9 -0
- package/dist/lib/focus-context.d.ts +2 -0
- package/dist/lib/focus-context.js +78 -0
- package/dist/lib/nav-config.d.ts +26 -0
- package/dist/lib/tool-panel-store.d.ts +7 -6
- package/dist/lib/tool-panel-store.js +29 -3
- package/dist/lib/use-new-conversation.js +1 -1
- package/dist/workbench/chat-header.d.ts +14 -0
- package/dist/workbench/chat-header.js +12 -0
- package/dist/workbench/chat-resize-handle.d.ts +4 -0
- package/dist/workbench/chat-resize-handle.js +40 -0
- package/dist/workbench/content-pane.d.ts +14 -0
- package/dist/workbench/content-pane.js +14 -0
- package/dist/workbench/conversation-history.d.ts +11 -0
- package/dist/workbench/conversation-history.js +83 -0
- package/dist/workbench/navigation.d.ts +6 -0
- package/dist/workbench/navigation.js +6 -0
- package/dist/workbench/selection-bridge.d.ts +5 -0
- package/dist/workbench/selection-bridge.js +84 -0
- package/dist/workbench/use-chat-width.d.ts +14 -0
- package/dist/workbench/use-chat-width.js +56 -0
- package/dist/workbench/use-workbench-state.d.ts +50 -0
- package/dist/workbench/use-workbench-state.js +145 -0
- package/dist/workbench/welcome-intro.d.ts +6 -0
- package/dist/workbench/welcome-intro.js +10 -0
- package/dist/workbench/workbench-styles.d.ts +1 -0
- package/dist/workbench/workbench-styles.js +290 -0
- package/dist/workbench/workbench.d.ts +5 -0
- package/dist/workbench/workbench.js +32 -0
- package/dist/workbench.d.ts +2 -0
- package/dist/workbench.js +2 -0
- package/package.json +8 -4
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import { Fragment, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
3
3
|
import { NavLink, useLocation, useNavigate } from "react-router";
|
|
4
|
-
import { Ellipsis, PanelLeft, PanelLeftOpen, SquarePen, ChevronDown, ChevronRight, GripVertical, Plus, Upload, } from "lucide-react";
|
|
4
|
+
import { Ellipsis, Layers, PanelLeft, PanelLeftOpen, SquarePen, ChevronDown, ChevronRight, GripVertical, Plus, Upload, ChevronLeft, } from "lucide-react";
|
|
5
5
|
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
6
|
-
import { useSidebarStore, RAIL_WIDTH, SIDEBAR_MIN_WIDTH, SIDEBAR_MAX_WIDTH, } from "../lib/sidebar-store.js";
|
|
6
|
+
import { useSidebarStore, RAIL_WIDTH, SIDEBAR_WIDTH, SIDEBAR_MIN_WIDTH, SIDEBAR_MAX_WIDTH, } from "../lib/sidebar-store.js";
|
|
7
7
|
import { useToolPanelStore } from "../lib/tool-panel-store.js";
|
|
8
8
|
import { useNewConversation } from "../lib/use-new-conversation.js";
|
|
9
9
|
import { useNavStore } from "../lib/nav-store.js";
|
|
@@ -83,12 +83,19 @@ const SIDEBAR_ITEM_GAP_CLASS = "gap-2";
|
|
|
83
83
|
// ChatGPT / Linear / Notion "header sits on its children" pattern.
|
|
84
84
|
// Symmetric ``py-2`` made the header float midway between sections
|
|
85
85
|
// instead of belonging to the group below it.
|
|
86
|
-
|
|
87
|
-
// ``
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
86
|
+
// Vertical rhythm: rows are tight (depth-0 ``py-1.5``, nested ``py-1``) and
|
|
87
|
+
// sections keep their ``pt-4`` gap, so what separates is the group, not
|
|
88
|
+
// every row; the header label sits a step brighter than the rows' muted
|
|
89
|
+
// text so the section reads above its items (user's test: rows were
|
|
90
|
+
// airy and headers lighter than their children).
|
|
91
|
+
const SIDEBAR_GROUP_HEADER_PADDING = "pt-4 pb-0";
|
|
92
|
+
// The header's own box: the hover surface spans the row exactly like an
|
|
93
|
+
// item row's does — same radius, same transparent border (part of the box
|
|
94
|
+
// model that decides where the padding box starts: without it the chevron
|
|
95
|
+
// lands 1px left of every row's disclosure cell) and the same horizontal
|
|
96
|
+
// inset, so header and rows highlight on one outline (user's test: the
|
|
97
|
+
// narrower inset box read as misaligned and cramped).
|
|
98
|
+
const SIDEBAR_GROUP_HEADER_BOX = "rounded-xl border border-transparent pl-3 pr-3";
|
|
92
99
|
// Typography for the group-header LABEL text (CHATS / WORKSPACES /
|
|
93
100
|
// ADMIN / …). Stays at ``text-xs`` (12px) — same size as before —
|
|
94
101
|
// but bumps weight from ``font-medium`` to ``font-semibold``. The
|
|
@@ -99,10 +106,13 @@ const SIDEBAR_GROUP_HEADER_PADDING =
|
|
|
99
106
|
// Bumping size AND weight together stacked too much visual mass
|
|
100
107
|
// against the already-wide uppercase tracking — felt shouty.
|
|
101
108
|
const SIDEBAR_GROUP_HEADER_LABEL_CLASS = "text-xs font-semibold uppercase tracking-wider";
|
|
102
|
-
/** Body padding for a row
|
|
103
|
-
*
|
|
104
|
-
* sidebar's other flat chrome
|
|
105
|
-
*
|
|
109
|
+
/** Body padding for a flat row with nothing to indent under: a row in a
|
|
110
|
+
* group without a visible header (New Thread, Activity) sits with the
|
|
111
|
+
* sidebar's other flat chrome, and a focused group's heading sits under
|
|
112
|
+
* its header's chevron. Flat rows UNDER a visible header indent like
|
|
113
|
+
* depth-0 tree rows instead, so their icon sits under the header label
|
|
114
|
+
* and the group reads as a group (user's test: Build's rows sat level
|
|
115
|
+
* with their header). */
|
|
106
116
|
const NAV_FLAT_BODY_CLASS = "pl-3";
|
|
107
117
|
function getNavDepthOffsets(depth) {
|
|
108
118
|
return NAV_DEPTH_OFFSETS[depth] ?? NAV_DEPTH_OFFSETS[NAV_DEPTH_OFFSETS.length - 1];
|
|
@@ -395,11 +405,11 @@ function ContainerDropZone({ dnd }) {
|
|
|
395
405
|
return;
|
|
396
406
|
}
|
|
397
407
|
await dnd.onDrop(draggedData);
|
|
398
|
-
}, className: cn("mx-2 rounded-md
|
|
408
|
+
}, className: cn("relative mx-2 rounded-md overflow-hidden", isDragOver
|
|
399
409
|
? cn("h-7 flex items-center justify-center gap-1", isFileDragOver
|
|
400
410
|
? "bg-primary/10 border border-dashed border-primary"
|
|
401
411
|
: "bg-primary/10 border border-dashed border-primary/30")
|
|
402
|
-
: "h-1"), children: isDragOver && (_jsxs(_Fragment, { children: [isFileDragOver && _jsx(Upload, { className: "size-3 text-primary" }), _jsx("span", { className: "text-[10px] text-primary/70", children: getContainerDropZoneLabel(isFileDragOver) })] })) }));
|
|
412
|
+
: "-mt-1 h-1"), children: isDragOver && (_jsxs(_Fragment, { children: [isFileDragOver && _jsx(Upload, { className: "size-3 text-primary" }), _jsx("span", { className: "text-[10px] text-primary/70", children: getContainerDropZoneLabel(isFileDragOver) })] })) }));
|
|
403
413
|
}
|
|
404
414
|
/**
|
|
405
415
|
* Leading disclosure cell on every nav row — the Confluence page-tree
|
|
@@ -427,7 +437,7 @@ function ContainerDropZone({ dnd }) {
|
|
|
427
437
|
function NavTwisty({ hasChildren, childrenUnloaded = false, isExpanded, onToggle, label, isActive, depth, inTree = true, }) {
|
|
428
438
|
const cell = cn(NAV_TWISTY_CLASS, getNavDepthOffsets(depth).column);
|
|
429
439
|
if (!hasChildren) {
|
|
430
|
-
return (_jsx("span", { "data-nav-twisty-cell": true, "aria-hidden": "true", className: cn(cell, "pointer-events-none"), children: inTree && !childrenUnloaded && (_jsx("span", { "data-nav-leaf-dot": true, className: cn("size-1 rounded-full", isActive ? "bg-primary/70" : "bg-sidebar-foreground/30") })) }));
|
|
440
|
+
return (_jsx("span", { "data-nav-twisty-cell": true, "aria-hidden": "true", className: cn(cell, "pointer-events-none"), children: inTree && !childrenUnloaded && (_jsx("span", { "data-nav-leaf-dot": true, className: cn("block size-1 shrink-0 rounded-full", isActive ? "bg-primary/70" : "bg-sidebar-foreground/30") })) }));
|
|
431
441
|
}
|
|
432
442
|
return (_jsx("span", { "data-nav-twisty-cell": true, className: cell, children: _jsx("button", { type: "button", "data-nav-twisty": true, "aria-expanded": isExpanded, "aria-label": isExpanded ? `Collapse ${label}` : `Expand ${label}`, onClick: (e) => {
|
|
433
443
|
e.preventDefault();
|
|
@@ -468,7 +478,7 @@ function NavLeafItem({ item, isActive, onClick, depth = 0, }) {
|
|
|
468
478
|
const activeButtonRef = useActiveNavRowScroll(isActive && !hasActions);
|
|
469
479
|
// Items with actions use the group layout (button + action controls)
|
|
470
480
|
if (hasActions) {
|
|
471
|
-
return (_jsxs("div", { ref: activeRowRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": depth, ...(isDisabled ? {} : dropTargetProps), ...(isDisabled ? {} : draggableProps), className: cn("group relative flex items-center w-full rounded-xl border border-transparent transition-colors", isDisabled
|
|
481
|
+
return (_jsxs("div", { ref: activeRowRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": depth, style: { "--nav-depth": depth }, ...(isDisabled ? {} : dropTargetProps), ...(isDisabled ? {} : draggableProps), className: cn("group relative flex items-center w-full rounded-xl border border-transparent transition-colors", isDisabled
|
|
472
482
|
? "opacity-40"
|
|
473
483
|
: isActive
|
|
474
484
|
? ACTIVE_NAV_ROW_CLASS
|
|
@@ -477,7 +487,7 @@ function NavLeafItem({ item, isActive, onClick, depth = 0, }) {
|
|
|
477
487
|
!isDisabled &&
|
|
478
488
|
"ring-2 ring-inset ring-sidebar-ring/50 bg-sidebar-selected", isFileDragOver &&
|
|
479
489
|
!isDisabled &&
|
|
480
|
-
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: false, childrenUnloaded: item.childrenUnloaded, label: item.label, isActive: isActive && !isDisabled, depth: depth }), _jsxs("button", { type: "button", disabled: isDisabled, onClick: isDisabled ? undefined : onClick, className: cn("flex items-center gap-2 flex-1 min-w-0 py-1
|
|
490
|
+
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: false, childrenUnloaded: item.childrenUnloaded, label: item.label, isActive: isActive && !isDisabled, depth: depth }), _jsxs("button", { type: "button", disabled: isDisabled, onClick: isDisabled ? undefined : onClick, className: cn("flex items-center gap-2 flex-1 min-w-0 py-1 text-sm text-left", bodyPl, "pr-3 transition-[padding] duration-150", NAV_ROW_ACTIONS_CLEARANCE, isDisabled
|
|
481
491
|
? "text-sidebar-foreground/45 cursor-default"
|
|
482
492
|
: isActive
|
|
483
493
|
? ACTIVE_NAV_TEXT_CLASS
|
|
@@ -485,7 +495,7 @@ function NavLeafItem({ item, isActive, onClick, depth = 0, }) {
|
|
|
485
495
|
? ACTIVE_NAV_BADGE_CLASS
|
|
486
496
|
: "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot })] }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "size-3.5 mr-2 shrink-0 text-primary pointer-events-none" })), _jsx(NavRowControls, { item: item })] }));
|
|
487
497
|
}
|
|
488
|
-
return (_jsxs("button", { ref: activeButtonRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": depth, type: "button", disabled: isDisabled, onClick: isDisabled ? undefined : onClick, ...(isDisabled ? {} : draggableProps), ...(isDisabled ? {} : dropTargetProps), className: cn("relative flex items-center gap-2 w-full", "rounded-lg border border-transparent pr-3 py-1
|
|
498
|
+
return (_jsxs("button", { ref: activeButtonRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": depth, style: { "--nav-depth": depth }, type: "button", disabled: isDisabled, onClick: isDisabled ? undefined : onClick, ...(isDisabled ? {} : draggableProps), ...(isDisabled ? {} : dropTargetProps), className: cn("relative flex items-center gap-2 w-full", "rounded-lg border border-transparent pr-3 py-1 text-sm text-left", bodyPl, isDisabled
|
|
489
499
|
? "opacity-40 cursor-default"
|
|
490
500
|
: "text-sidebar-foreground/80 transition-colors hover:bg-sidebar-accent/70", !isDisabled && isActive && ACTIVE_NAV_ROW_CLASS, isDragOver &&
|
|
491
501
|
!isFileDragOver &&
|
|
@@ -505,7 +515,7 @@ function SubNavItems({ items, parentPath, depth = 1, }) {
|
|
|
505
515
|
// children sharing the parent path (e.g., Documents sub-filters) use label matching.
|
|
506
516
|
const allShareParent = items.every((c) => c.to === parentPath);
|
|
507
517
|
return (_jsx("div", { children: items.map((child) => {
|
|
508
|
-
const hasNestedChildren = child.children && child.children.length > 0;
|
|
518
|
+
const hasNestedChildren = !child.childrenHidden && child.children && child.children.length > 0;
|
|
509
519
|
// Folders with children render as collapsible nested items
|
|
510
520
|
if (hasNestedChildren) {
|
|
511
521
|
return _jsx(NestedFolderItem, { item: child, depth: depth }, child.to + child.label);
|
|
@@ -515,7 +525,7 @@ function SubNavItems({ items, parentPath, depth = 1, }) {
|
|
|
515
525
|
// parent path (e.g., folders) are never highlighted on the parent page.
|
|
516
526
|
const isActive = allShareParent
|
|
517
527
|
? isOnParent && activeLabel === child.label
|
|
518
|
-
: child.to !== parentPath && currentPath
|
|
528
|
+
: child.to !== parentPath && isSelectedNavItem(currentPath, child);
|
|
519
529
|
return (_jsx(NavLeafItem, { item: child, isActive: isActive, depth: depth, onClick: () => {
|
|
520
530
|
setActiveLabel(child.label);
|
|
521
531
|
setNavContext({
|
|
@@ -537,6 +547,11 @@ function isInSubtree(currentPage, item) {
|
|
|
537
547
|
return true;
|
|
538
548
|
return item.children?.some((c) => isInSubtree(currentPage, c)) ?? false;
|
|
539
549
|
}
|
|
550
|
+
/** A list owns the sidebar selection of descendants it deliberately does not draw.
|
|
551
|
+
* The route lookup still resolves the actual descendant for chat and page actions. */
|
|
552
|
+
function isSelectedNavItem(currentPage, item) {
|
|
553
|
+
return currentPage === item.to || Boolean(item.childrenHidden && isInSubtree(currentPage, item));
|
|
554
|
+
}
|
|
540
555
|
/** Nested folder with collapsible children (used inside SubNavItems). */
|
|
541
556
|
function NestedFolderItem({ item, depth }) {
|
|
542
557
|
const navigate = useNavigate();
|
|
@@ -544,16 +559,15 @@ function NestedFolderItem({ item, depth }) {
|
|
|
544
559
|
const setNavContext = useAppStore((s) => s.setNavContext);
|
|
545
560
|
const navContext = useAppStore((s) => s.navContext);
|
|
546
561
|
const isInsideSubtree = isInSubtree(currentPath, item);
|
|
547
|
-
const isActive = currentPath
|
|
562
|
+
const isActive = isSelectedNavItem(currentPath, item);
|
|
548
563
|
const isAncestorActive = isInsideSubtree && !isActive;
|
|
549
|
-
const [
|
|
550
|
-
const isExpanded = manualOverride ?? isInsideSubtree;
|
|
564
|
+
const [isExpanded, setExpanded] = useTreeExpanded(item.to, isInsideSubtree);
|
|
551
565
|
const { draggableProps, dropTargetProps, isDragOver, isFileDragOver } = useNavItemDnd(item.dnd);
|
|
552
566
|
const bodyPl = getNavDepthOffsets(depth).body;
|
|
553
567
|
const isDisabled = item.disabled === true;
|
|
554
568
|
const hasActions = Boolean(!isDisabled && item.actions && item.actions.length > 0);
|
|
555
569
|
const activeRowRef = useActiveNavRowScroll(isActive);
|
|
556
|
-
return (_jsxs("div", { ...(isDisabled ? {} : dropTargetProps), children: [_jsxs("div", { ref: activeRowRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": depth, ...(isDisabled ? {} : draggableProps), className: cn("group relative flex items-center w-full", "rounded-xl border border-transparent transition-colors", isDisabled
|
|
570
|
+
return (_jsxs("div", { ...(isDisabled ? {} : dropTargetProps), children: [_jsxs("div", { ref: activeRowRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": depth, style: { "--nav-depth": depth }, ...(isDisabled ? {} : draggableProps), className: cn("group relative flex items-center w-full", "rounded-xl border border-transparent transition-colors", isDisabled
|
|
557
571
|
? "opacity-40"
|
|
558
572
|
: isActive
|
|
559
573
|
? ACTIVE_NAV_ROW_CLASS
|
|
@@ -562,10 +576,10 @@ function NestedFolderItem({ item, depth }) {
|
|
|
562
576
|
!isDisabled &&
|
|
563
577
|
"ring-2 ring-inset ring-sidebar-ring/50 bg-sidebar-selected", isFileDragOver &&
|
|
564
578
|
!isDisabled &&
|
|
565
|
-
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: true, isExpanded: isExpanded, onToggle: () =>
|
|
579
|
+
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: true, isExpanded: isExpanded, onToggle: () => setExpanded(!isExpanded), label: item.label, isActive: (isActive || isAncestorActive) && !isDisabled, depth: depth }), _jsxs("button", { type: "button", disabled: isDisabled, onClick: () => {
|
|
566
580
|
if (isDisabled)
|
|
567
581
|
return;
|
|
568
|
-
|
|
582
|
+
setExpanded(true);
|
|
569
583
|
setNavContext({
|
|
570
584
|
...navContext,
|
|
571
585
|
label: item.label,
|
|
@@ -576,7 +590,7 @@ function NestedFolderItem({ item, depth }) {
|
|
|
576
590
|
.join("\n") || null,
|
|
577
591
|
});
|
|
578
592
|
navigate(item.to);
|
|
579
|
-
}, className: cn("flex items-center gap-2 flex-1 min-w-0", bodyPl, "pr-3 py-1
|
|
593
|
+
}, className: cn("flex items-center gap-2 flex-1 min-w-0", bodyPl, "pr-3 py-1 text-sm text-left transition-[padding] duration-150", hasActions && NAV_ROW_ACTIONS_CLEARANCE, isDisabled
|
|
580
594
|
? "text-sidebar-foreground/45 cursor-default"
|
|
581
595
|
: isActive
|
|
582
596
|
? ACTIVE_NAV_TEXT_CLASS
|
|
@@ -589,11 +603,11 @@ function NestedFolderItem({ item, depth }) {
|
|
|
589
603
|
: "text-sidebar-icon") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive
|
|
590
604
|
? ACTIVE_NAV_BADGE_CLASS
|
|
591
605
|
: "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot })] }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "size-3.5 mr-2 shrink-0 text-primary pointer-events-none" })), _jsx(NavRowControls, { item: item })] }), isExpanded && item.children && (_jsxs("div", { children: [item.children.map((child) => {
|
|
592
|
-
const hasNestedChildren = child.children && child.children.length > 0;
|
|
606
|
+
const hasNestedChildren = !child.childrenHidden && child.children && child.children.length > 0;
|
|
593
607
|
if (hasNestedChildren) {
|
|
594
608
|
return (_jsx(NestedFolderItem, { item: child, depth: depth + 1 }, child.to + child.label));
|
|
595
609
|
}
|
|
596
|
-
const isChildActive = currentPath
|
|
610
|
+
const isChildActive = isSelectedNavItem(currentPath, child);
|
|
597
611
|
return (_jsx(NavLeafItem, { item: child, isActive: isChildActive, depth: depth + 1, onClick: () => {
|
|
598
612
|
setNavContext({
|
|
599
613
|
...navContext,
|
|
@@ -608,19 +622,28 @@ function NestedFolderItem({ item, depth }) {
|
|
|
608
622
|
} }, child.to + child.label));
|
|
609
623
|
}), item.dnd?.canDrop() && _jsx(ContainerDropZone, { dnd: item.dnd })] }))] }));
|
|
610
624
|
}
|
|
611
|
-
function CollapsibleNavItem({ item, inTree = true }) {
|
|
625
|
+
function CollapsibleNavItem({ item, inTree = true, heading = false, underHeader = false, }) {
|
|
612
626
|
const navigate = useNavigate();
|
|
613
627
|
const currentPath = useLocation().pathname;
|
|
614
628
|
const setNavContext = useAppStore((s) => s.setNavContext);
|
|
615
629
|
const navContext = useAppStore((s) => s.navContext);
|
|
616
630
|
const isInsideSubtree = isInSubtree(currentPath, item);
|
|
617
|
-
const isActive = currentPath
|
|
631
|
+
const isActive = isSelectedNavItem(currentPath, item);
|
|
618
632
|
const isAncestorActive = isInsideSubtree && !isActive;
|
|
619
|
-
const [
|
|
620
|
-
|
|
621
|
-
|
|
633
|
+
const [treeExpanded, setExpanded] = useTreeExpanded(item.to, isInsideSubtree);
|
|
634
|
+
// A heading is never collapsed and carries no chevron: its children ARE
|
|
635
|
+
// the tree it stands for.
|
|
636
|
+
const isExpanded = heading ? true : treeExpanded;
|
|
637
|
+
const hasChildren = Boolean(!item.childrenHidden && item.children && item.children.length > 0);
|
|
622
638
|
const isDisabled = item.disabled === true;
|
|
623
639
|
const hasActions = Boolean(!isDisabled && item.actions && item.actions.length > 0);
|
|
640
|
+
const rowInTree = inTree && !heading;
|
|
641
|
+
const bodyClass = rowInTree
|
|
642
|
+
? getNavDepthOffsets(0).body
|
|
643
|
+
: heading || !underHeader
|
|
644
|
+
? NAV_FLAT_BODY_CLASS
|
|
645
|
+
: getNavDepthOffsets(0).body;
|
|
646
|
+
const headingClass = heading && !isDisabled && "font-semibold text-sidebar-foreground";
|
|
624
647
|
const { draggableProps, dropTargetProps, isDragOver, isFileDragOver } = useNavItemDnd(item.dnd);
|
|
625
648
|
const rendersRow = Boolean(hasChildren || hasActions);
|
|
626
649
|
const activeRowRef = useActiveNavRowScroll(isActive && rendersRow);
|
|
@@ -635,7 +658,7 @@ function CollapsibleNavItem({ item, inTree = true }) {
|
|
|
635
658
|
!isDisabled &&
|
|
636
659
|
"ring-2 ring-inset ring-sidebar-ring/50 bg-sidebar-selected", isFileDragOver &&
|
|
637
660
|
!isDisabled &&
|
|
638
|
-
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: hasChildren, childrenUnloaded: item.childrenUnloaded, inTree:
|
|
661
|
+
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && !isDisabled && "opacity-50"), children: [_jsx(NavTwisty, { hasChildren: hasChildren && !heading, childrenUnloaded: item.childrenUnloaded, inTree: rowInTree, isExpanded: isExpanded, onToggle: hasChildren && !heading ? () => setExpanded(!isExpanded) : undefined, label: item.label, isActive: (selected || ancestor) && !isDisabled, depth: 0 }), _jsxs("button", { type: "button", onClick: () => {
|
|
639
662
|
if (isDisabled)
|
|
640
663
|
return;
|
|
641
664
|
setNavContext({
|
|
@@ -648,38 +671,38 @@ function CollapsibleNavItem({ item, inTree = true }) {
|
|
|
648
671
|
.join("\n") || null,
|
|
649
672
|
});
|
|
650
673
|
if (hasChildren)
|
|
651
|
-
|
|
674
|
+
setExpanded(true);
|
|
652
675
|
navigate(item.to);
|
|
653
|
-
}, className: cn("flex items-center gap-2 flex-1 min-w-0 py-
|
|
676
|
+
}, className: cn("flex items-center gap-2 flex-1 min-w-0 py-1.5 text-sm text-left", bodyClass, "pr-3 transition-[padding] duration-150", hasActions && NAV_ROW_ACTIONS_CLEARANCE, isDisabled
|
|
654
677
|
? "text-sidebar-foreground/45 cursor-default"
|
|
655
678
|
: selected
|
|
656
679
|
? ACTIVE_NAV_TEXT_CLASS
|
|
657
680
|
: ancestor
|
|
658
681
|
? "text-sidebar-foreground font-medium"
|
|
659
|
-
: "text-sidebar-foreground/85"), disabled: isDisabled, children: [_jsx(item.icon, { className: cn("size-4 shrink-0", selected && !isDisabled
|
|
682
|
+
: "text-sidebar-foreground/85", headingClass), disabled: isDisabled, children: [_jsx(item.icon, { className: cn("size-4 shrink-0", selected && !isDisabled
|
|
660
683
|
? ACTIVE_NAV_ICON_CLASS
|
|
661
684
|
: ancestor
|
|
662
685
|
? "text-sidebar-foreground/80"
|
|
663
686
|
: "text-sidebar-icon") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", selected
|
|
664
687
|
? ACTIVE_NAV_BADGE_CLASS
|
|
665
688
|
: "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot })] }), isFileDragOver && !isDisabled && (_jsx(Upload, { "aria-hidden": "true", className: "size-3.5 mr-2 shrink-0 text-primary pointer-events-none" })), _jsx(NavRowControls, { item: item })] }));
|
|
666
|
-
return (_jsxs("div", { ...(isDisabled ? {} : dropTargetProps), ...(isDisabled ? {} : draggableProps), children: [inner(isActive, isAncestorActive), hasChildren && isExpanded && (_jsxs(_Fragment, { children: [_jsx(SubNavItems, { items: item.children, parentPath: item.to }), item.dnd?.canDrop() && _jsx(ContainerDropZone, { dnd: item.dnd })] }))] }));
|
|
689
|
+
return (_jsxs("div", { ...(isDisabled ? {} : dropTargetProps), ...(isDisabled ? {} : draggableProps), children: [inner(isActive, isAncestorActive), hasChildren && isExpanded && (_jsxs(_Fragment, { children: [_jsx(SubNavItems, { items: item.children, parentPath: item.to, depth: heading ? 0 : 1 }), item.dnd?.canDrop() && _jsx(ContainerDropZone, { dnd: item.dnd })] }))] }));
|
|
667
690
|
}
|
|
668
691
|
if (isDisabled) {
|
|
669
|
-
return (_jsxs("div", { "data-nav-depth": 0, className: cn("relative flex items-center gap-2 w-full opacity-40",
|
|
692
|
+
return (_jsxs("div", { "data-nav-depth": 0, className: cn("relative flex items-center gap-2 w-full opacity-40", bodyClass,
|
|
670
693
|
// Transparent border for box-model parity — see the NavLink variant.
|
|
671
|
-
"rounded-lg border border-transparent pr-3 py-
|
|
694
|
+
"rounded-lg border border-transparent pr-3 py-1.5 text-left text-sm text-sidebar-foreground/45"), children: [_jsx(NavTwisty, { hasChildren: false, childrenUnloaded: item.childrenUnloaded, inTree: rowInTree, label: item.label, depth: 0 }), _jsx(item.icon, { className: "size-4 shrink-0 text-sidebar-foreground/45" }), _jsx("span", { className: "truncate", children: item.label })] }));
|
|
672
695
|
}
|
|
673
|
-
return (_jsx(NavLink, { ref: activeLinkRef, "data-active-nav-row":
|
|
696
|
+
return (_jsx(NavLink, { ref: activeLinkRef, "data-active-nav-row": isActive ? "true" : undefined, "data-nav-depth": 0, to: item.to, ...draggableProps, ...dropTargetProps, className: () => cn("relative flex items-center gap-2 w-full", bodyClass,
|
|
674
697
|
// The transparent border is NOT decoration: it is what makes this
|
|
675
698
|
// row's box model match every other variant. An absolutely
|
|
676
699
|
// positioned twisty resolves against the PADDING box, so a row
|
|
677
700
|
// without the border puts its disclosure cell 1px left of its
|
|
678
701
|
// siblings' and the column goes ragged.
|
|
679
|
-
"rounded-xl border border-transparent pr-3 py-
|
|
702
|
+
"rounded-xl border border-transparent pr-3 py-1.5 text-sm text-left", "text-sidebar-foreground/85 transition-colors hover:bg-sidebar-accent/70", headingClass, isActive && cn(ACTIVE_NAV_ROW_CLASS, ACTIVE_NAV_TEXT_CLASS), isDragOver &&
|
|
680
703
|
!isFileDragOver &&
|
|
681
704
|
"bg-sidebar-selected ring-2 ring-inset ring-sidebar-ring/50", isFileDragOver &&
|
|
682
|
-
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && "opacity-50"), children: (
|
|
705
|
+
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && "opacity-50"), children: () => (_jsxs(_Fragment, { children: [_jsx(NavTwisty, { hasChildren: false, childrenUnloaded: item.childrenUnloaded, inTree: rowInTree, label: item.label, isActive: isActive, depth: 0 }), _jsx(item.icon, { className: cn("size-4 shrink-0", isActive ? ACTIVE_NAV_ICON_CLASS : "text-sidebar-icon") }), _jsx("span", { className: "truncate", children: item.label }), item.badge && (_jsx("span", { className: cn("shrink-0 rounded px-1 py-0.5 text-[10px] font-medium", isActive
|
|
683
706
|
? ACTIVE_NAV_BADGE_CLASS
|
|
684
707
|
: "bg-sidebar-accent text-sidebar-accent-foreground"), children: item.badge })), _jsx(NavStatusIcon, { statusIcon: item.statusIcon }), _jsx(NavStatusDot, { statusDot: item.statusDot }), isFileDragOver && (_jsx(Upload, { "aria-hidden": "true", className: "ml-auto size-3.5 shrink-0 text-primary pointer-events-none" }))] })) }));
|
|
685
708
|
}
|
|
@@ -710,7 +733,7 @@ function SidebarResizeHandle() {
|
|
|
710
733
|
window.removeEventListener("pointercancel", onPointerUp);
|
|
711
734
|
};
|
|
712
735
|
}, [setWidth]);
|
|
713
|
-
return (_jsx("div", { className: "absolute inset-y-0 right-0 z-20 hidden w-3 cursor-col-resize md:block", onPointerDown: (event) => {
|
|
736
|
+
return (_jsx("div", { "data-panel-resize": true, title: "Drag to resize navigation. Double-click to reset.", onDoubleClick: () => setWidth(SIDEBAR_WIDTH), className: "absolute inset-y-0 right-0 z-20 hidden w-3 cursor-col-resize md:block", onPointerDown: (event) => {
|
|
714
737
|
event.preventDefault();
|
|
715
738
|
dragState.current = {
|
|
716
739
|
startX: event.clientX,
|
|
@@ -722,11 +745,11 @@ function SidebarResizeHandle() {
|
|
|
722
745
|
}, onKeyDown: (event) => {
|
|
723
746
|
if (event.key === "ArrowLeft") {
|
|
724
747
|
event.preventDefault();
|
|
725
|
-
setWidth(width - 16);
|
|
748
|
+
setWidth(width - (event.shiftKey ? 40 : 16));
|
|
726
749
|
}
|
|
727
750
|
if (event.key === "ArrowRight") {
|
|
728
751
|
event.preventDefault();
|
|
729
|
-
setWidth(width + 16);
|
|
752
|
+
setWidth(width + (event.shiftKey ? 40 : 16));
|
|
730
753
|
}
|
|
731
754
|
if (event.key === "Home") {
|
|
732
755
|
event.preventDefault();
|
|
@@ -808,6 +831,17 @@ function useGroupCollapsed(label, defaultCollapsed) {
|
|
|
808
831
|
// entry. ``map`` is parsed JSON so its prototype is ``Object``'s.
|
|
809
832
|
return Object.prototype.hasOwnProperty.call(map, label) ? map[label] : Boolean(defaultCollapsed);
|
|
810
833
|
}
|
|
834
|
+
/** Keep opened branches stable while routes and asynchronously loaded children change. */
|
|
835
|
+
function useTreeExpanded(to, inside) {
|
|
836
|
+
const key = `tree:${to}`;
|
|
837
|
+
const collapsed = useGroupCollapsed(key, !inside);
|
|
838
|
+
useEffect(() => {
|
|
839
|
+
if (inside && !Object.prototype.hasOwnProperty.call(readCollapseMap(), key)) {
|
|
840
|
+
writeCollapseMap({ ...readCollapseMap(), [key]: false });
|
|
841
|
+
}
|
|
842
|
+
}, [inside, key]);
|
|
843
|
+
return [!collapsed, (expanded) => writeCollapseMap({ ...readCollapseMap(), [key]: !expanded })];
|
|
844
|
+
}
|
|
811
845
|
function toggleGroupCollapsed(label, current) {
|
|
812
846
|
writeCollapseMap({ ...readCollapseMap(), [label]: !current });
|
|
813
847
|
}
|
|
@@ -843,11 +877,30 @@ function NavGroupSection({ group }) {
|
|
|
843
877
|
// that does not exist and reads as a bullet. The disclosure cell still
|
|
844
878
|
// renders either way, so flat rows keep the same label column as tree
|
|
845
879
|
// rows and the sidebar stays on one grid.
|
|
846
|
-
const groupHasBranches = group.items.some((item) => Boolean(item.children && item.children.length > 0));
|
|
880
|
+
const groupHasBranches = group.items.some((item) => Boolean(!item.childrenHidden && item.children && item.children.length > 0));
|
|
847
881
|
// hideLabel + collapsible are mutually exclusive — a hidden label
|
|
848
882
|
// can't host a chevron. The collapsible flag is the opt-in; legacy
|
|
849
883
|
// groups stay uncollapsable so behaviour is unchanged.
|
|
850
|
-
|
|
884
|
+
// Focus — see ``NavGroup.focusOn``: the group draws one item as a heading
|
|
885
|
+
// under a header that leads back. Needs a visible label to lead back
|
|
886
|
+
// from and a ``to`` to lead back to; otherwise it draws as usual.
|
|
887
|
+
const focusedItem = group.focusOn !== undefined && !group.hideLabel && group.to
|
|
888
|
+
? group.items.find((item) => item.to === group.focusOn)
|
|
889
|
+
: undefined;
|
|
890
|
+
const focused = focusedItem !== undefined;
|
|
891
|
+
// The new shape fades in over the old one's place: keyed on the focus,
|
|
892
|
+
// and only after a CHANGE in either direction — not on first paint
|
|
893
|
+
// (user's test: the hard swap felt abrupt). The previous key is read
|
|
894
|
+
// during render and written after it, so a focus that returns to its
|
|
895
|
+
// initial value still counts as a change.
|
|
896
|
+
const focusKey = focused ? focusedItem.to : "";
|
|
897
|
+
const previousFocusKey = useRef(focusKey);
|
|
898
|
+
const animateRows = previousFocusKey.current !== focusKey;
|
|
899
|
+
useEffect(() => {
|
|
900
|
+
previousFocusKey.current = focusKey;
|
|
901
|
+
});
|
|
902
|
+
// Focus takes precedence over collapsing: a focused header is the way back.
|
|
903
|
+
const isCollapsible = !group.hideLabel && Boolean(group.collapsible) && !focused;
|
|
851
904
|
const persistedCollapsed = useGroupCollapsed(group.label, group.defaultCollapsed);
|
|
852
905
|
const collapsed = isCollapsible && persistedCollapsed;
|
|
853
906
|
// Bubble: show the first child statusDot on the collapsed header so
|
|
@@ -877,7 +930,7 @@ function NavGroupSection({ group }) {
|
|
|
877
930
|
// to hit a tiny target. The clickable invisible padding
|
|
878
931
|
// OVERLAPS the gap rather than pushing the label right
|
|
879
932
|
// (negative-margin + extra padding cancel out).
|
|
880
|
-
_jsxs("div", { className: cn("group/header flex items-center justify-between
|
|
933
|
+
_jsxs("div", { "data-nav-group-header": true, className: cn("group/header flex items-center justify-between transition-colors hover:bg-sidebar-accent/70", SIDEBAR_GROUP_HEADER_BOX, SIDEBAR_ITEM_GAP_CLASS), children: [_jsx("button", { type: "button", "data-nav-group-toggle": true, onClick: () => toggleGroupCollapsed(group.label, persistedCollapsed), "aria-expanded": !collapsed, "aria-label": collapsed ? `Expand ${group.label}` : `Collapse ${group.label}`,
|
|
881
934
|
// Visual cell width = ``SIDEBAR_ITEM_ICON_CLASS`` (14px)
|
|
882
935
|
// so the chevron sits in the same column as item icons
|
|
883
936
|
// and the group label aligns with item labels. The
|
|
@@ -888,20 +941,25 @@ function NavGroupSection({ group }) {
|
|
|
888
941
|
// visual feedback even on near-misses.
|
|
889
942
|
className: cn("inline-flex items-center justify-center rounded text-sidebar-foreground/50 hover:text-sidebar-foreground", SIDEBAR_ITEM_ICON_CLASS), children: collapsed ? (_jsx(ChevronRight, { className: "size-3.5 transition-transform" })) : (_jsx(ChevronDown, { className: "size-3.5 transition-transform" })) }), group.to ? (_jsx(NavLink, { to: group.to, end: true, className: "flex min-w-0 flex-1 items-center gap-1.5 py-1.5", children: ({ isActive }) => (_jsxs(_Fragment, { children: [_jsx("span", { className: cn("truncate", SIDEBAR_GROUP_HEADER_LABEL_CLASS, isActive
|
|
890
943
|
? "text-sidebar-selected-foreground"
|
|
891
|
-
: "text-sidebar-foreground/
|
|
944
|
+
: "text-sidebar-foreground/75 group-hover/header:text-sidebar-foreground"), children: group.label }), _jsx(NavStatusDot, { statusDot: bubbledStatusDot })] })) })) : (_jsxs("button", { type: "button", onClick: () => toggleGroupCollapsed(group.label, persistedCollapsed), className: "flex min-w-0 flex-1 items-center gap-1.5 py-1.5 text-left", children: [_jsx("span", { className: cn("truncate text-sidebar-foreground/75 group-hover/header:text-sidebar-foreground", SIDEBAR_GROUP_HEADER_LABEL_CLASS), children: group.label }), _jsx(NavStatusDot, { statusDot: bubbledStatusDot })] })), group.createActions && group.createActions.length > 0 && (_jsx(CreateActionsMenu, { group: group }))] })) : focused ? (
|
|
945
|
+
// The focused header is the way back: the collapse chevron's
|
|
946
|
+
// cell holds a back chevron, the label links to the group's
|
|
947
|
+
// page, and the metrics are the collapsible header's — so the
|
|
948
|
+
// header stays put, in the same type, when the group focuses.
|
|
949
|
+
_jsxs("div", { "data-nav-group-header": true, className: cn("flex items-center justify-between py-1.5 transition-colors hover:bg-sidebar-accent/70", SIDEBAR_GROUP_HEADER_BOX, SIDEBAR_ITEM_GAP_CLASS), children: [_jsxs(NavLink, { to: group.to, end: true, "data-nav-group-back": true, title: group.description, className: cn("flex min-w-0 flex-1 items-center", SIDEBAR_ITEM_GAP_CLASS), children: [_jsx("span", { "aria-hidden": "true", className: cn(SIDEBAR_ITEM_ICON_CLASS, "flex items-center justify-center"), children: _jsx(ChevronLeft, { className: "size-3.5 text-sidebar-foreground/75" }) }), _jsx("span", { className: cn("truncate", SIDEBAR_GROUP_HEADER_LABEL_CLASS, "text-sidebar-foreground/75 hover:text-sidebar-foreground"), children: group.label })] }), group.createActions && group.createActions.length > 0 && (_jsx(CreateActionsMenu, { group: group }))] })) : group.to ? (
|
|
892
950
|
// Non-collapsible header: no chevron, but we still emit an
|
|
893
951
|
// invisible spacer of ``SIDEBAR_ITEM_ICON_CLASS`` width + the
|
|
894
952
|
// shared gap so the group label lands in the same column as
|
|
895
953
|
// collapsible headers and item labels.
|
|
896
|
-
_jsxs("div", { className: cn("flex items-center justify-between
|
|
954
|
+
_jsxs("div", { "data-nav-group-header": true, className: cn("flex items-center justify-between py-1.5 transition-colors hover:bg-sidebar-accent/70", SIDEBAR_GROUP_HEADER_BOX, SIDEBAR_ITEM_GAP_CLASS), children: [_jsx("span", { "aria-hidden": "true", className: SIDEBAR_ITEM_ICON_CLASS }), _jsx(NavLink, { to: group.to, end: true, className: "min-w-0 flex-1", children: ({ isActive }) => (_jsx("span", { className: cn("block", SIDEBAR_GROUP_HEADER_LABEL_CLASS, isActive
|
|
897
955
|
? "text-sidebar-selected-foreground"
|
|
898
|
-
: "text-sidebar-foreground/
|
|
899
|
-
|
|
956
|
+
: "text-sidebar-foreground/75 hover:text-sidebar-foreground"), children: group.label })) }), group.createActions && group.createActions.length > 0 && (_jsx(CreateActionsMenu, { group: group }))] })) : (_jsxs("div", { "data-nav-group-header": true, className: cn("flex items-center", SIDEBAR_GROUP_HEADER_BOX, SIDEBAR_ITEM_GAP_CLASS), children: [_jsx("span", { "aria-hidden": "true", className: SIDEBAR_ITEM_ICON_CLASS }), _jsx("span", { className: cn("text-sidebar-foreground/75", SIDEBAR_GROUP_HEADER_LABEL_CLASS), children: group.label })] })) })), !collapsed && (_jsx("div", { className: cn(animateRows &&
|
|
957
|
+
"motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-top-1 motion-safe:duration-200"), children: (focused ? [focusedItem] : group.items).map((item) => (_jsx(CollapsibleNavItem, { item: item,
|
|
900
958
|
// A leaf dot distinguishes "nothing under this" from a sibling
|
|
901
959
|
// that branches. Decided per GROUP: Workspaces has branches, so
|
|
902
960
|
// its leaves are marked; Chats has none, so a dot on every row
|
|
903
961
|
// would mark a distinction that does not exist.
|
|
904
|
-
inTree: groupHasBranches }, item.to + item.label)))] }));
|
|
962
|
+
inTree: group.tree ?? groupHasBranches, heading: focused, underHeader: !group.hideLabel }, item.to + item.label))) }, focusKey))] }));
|
|
905
963
|
}
|
|
906
964
|
/**
|
|
907
965
|
* The "+" dropdown shown next to a group label when the group declares
|
|
@@ -912,14 +970,15 @@ function NavGroupSection({ group }) {
|
|
|
912
970
|
* doesn't also toggle the surrounding collapsible-header button.
|
|
913
971
|
*/
|
|
914
972
|
function CreateActionsMenu({ group }) {
|
|
915
|
-
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { type: "button", onClick: (e) => e.stopPropagation(), className: "inline-flex size-6 items-center justify-center text-sidebar-foreground/
|
|
973
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { type: "button", onClick: (e) => e.stopPropagation(), className: "inline-flex size-6 items-center justify-center text-sidebar-foreground/75 transition-colors hover:text-sidebar-foreground", "aria-label": `Create in ${group.label}`, children: _jsx(Plus, { className: "size-4" }) }) }), _jsx(DropdownMenuContent, { align: "end", className: "w-48", children: (group.createActions ?? []).map((action) => (_jsx(DropdownMenuItem, { onSelect: (e) => {
|
|
916
974
|
e.preventDefault();
|
|
917
975
|
action.handler();
|
|
918
976
|
}, children: action.label }, action.id))) })] }));
|
|
919
977
|
}
|
|
920
|
-
function SidebarContent({ collapsed }) {
|
|
978
|
+
function SidebarContent({ collapsed, groups, onNewConversation, hideFooterActions, hideNewConversation, collapsedGroupsOnly, newConversationAfterGroup, newConversationLabel = "New Thread", }) {
|
|
921
979
|
const toggle = useSidebarStore((s) => s.toggle);
|
|
922
|
-
const
|
|
980
|
+
const defaultNewConversation = useNewConversation();
|
|
981
|
+
const handleNewConversation = onNewConversation ?? defaultNewConversation;
|
|
923
982
|
const rawNavConfig = useNavStore((s) => s.config);
|
|
924
983
|
const navLoading = useNavStore((s) => s.navLoading);
|
|
925
984
|
// Sort by `priority` descending so feature hooks can declare slot
|
|
@@ -927,15 +986,15 @@ function SidebarContent({ collapsed }) {
|
|
|
927
986
|
// order in which addGroup() happened to be called. Stable sort: ties
|
|
928
987
|
// preserve insertion order, so groups that don't set a priority keep
|
|
929
988
|
// the legacy append-where-you-arrive behaviour.
|
|
930
|
-
const navConfig = useMemo(() => [...rawNavConfig]
|
|
989
|
+
const navConfig = useMemo(() => [...(groups ?? rawNavConfig)]
|
|
931
990
|
.map((g, i) => ({ g, i, p: g.priority ?? 0 }))
|
|
932
991
|
.sort((a, b) => b.p - a.p || a.i - b.i)
|
|
933
|
-
.map(({ g }) => g), [rawNavConfig]);
|
|
992
|
+
.map(({ g }) => g), [rawNavConfig, groups]);
|
|
934
993
|
// Show skeleton rows during the cold nav fetch — but only until the
|
|
935
994
|
// integrator's primary dynamic group ("Workspaces") has been published.
|
|
936
995
|
// Once it exists, real rows render even if a later phase (status dots) is
|
|
937
996
|
// still resolving, so the skeleton never flashes over a populated tree.
|
|
938
|
-
const hasWorkspacesGroup = navConfig.some((group) => group.label === "Workspaces");
|
|
997
|
+
const hasWorkspacesGroup = navConfig.some((group) => group.label === "Workspaces" || group.meta?.type === "spaces-workspaces");
|
|
939
998
|
const showNavSkeleton = navLoading && !hasWorkspacesGroup;
|
|
940
999
|
const currentPage = useLocation().pathname;
|
|
941
1000
|
const navContext = useAppStore((s) => s.navContext);
|
|
@@ -946,41 +1005,68 @@ function SidebarContent({ collapsed }) {
|
|
|
946
1005
|
e.preventDefault();
|
|
947
1006
|
}, []);
|
|
948
1007
|
if (collapsed) {
|
|
949
|
-
return (_jsxs("div", { className: "flex flex-col items-center gap-1 py-3 flex-1", children: [_jsx(TooltipIconButton, { tooltip: "Expand sidebar", side: "right", onClick: toggle, children: _jsx(PanelLeftOpen, { className: "size-5" }) }), _jsx(TooltipIconButton, { tooltip:
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1008
|
+
return (_jsxs("div", { className: "flex flex-col items-center gap-1 py-3 flex-1", children: [_jsx(TooltipIconButton, { tooltip: "Expand sidebar", side: "right", onClick: toggle, children: _jsx(PanelLeftOpen, { className: "size-5" }) }), !hideNewConversation && !collapsedGroupsOnly && (_jsx(TooltipIconButton, { tooltip: newConversationLabel, side: "right", onClick: handleNewConversation, children: _jsx(SquarePen, { className: "size-4 text-sidebar-icon" }) })), _jsx("div", { className: "mt-4 flex w-full flex-col items-stretch gap-1", children: collapsedGroupsOnly
|
|
1009
|
+
? navConfig
|
|
1010
|
+
.filter((group) => !group.meta?.hideInCollapsedRail &&
|
|
1011
|
+
(group.to || group.items.length > 0) &&
|
|
1012
|
+
group.collapsedRail?.({ currentPage, navContextMeta: navContext.meta ?? {} })
|
|
1013
|
+
.length !== 0)
|
|
1014
|
+
.map((group) => {
|
|
1015
|
+
const rail = group.collapsedRail?.({
|
|
957
1016
|
currentPage,
|
|
958
1017
|
navContextMeta: navContext.meta ?? {},
|
|
959
|
-
})
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1018
|
+
});
|
|
1019
|
+
const active = currentPage === group.to ||
|
|
1020
|
+
group.items.some((item) => isInSubtree(currentPage, item)) ||
|
|
1021
|
+
Boolean(rail?.some((item) => item.isActive));
|
|
1022
|
+
const Icon = group.icon ??
|
|
1023
|
+
group.collapsedRail?.({ currentPage, navContextMeta: navContext.meta ?? {} })[0]
|
|
1024
|
+
?.icon ??
|
|
1025
|
+
group.items[0]?.icon ??
|
|
1026
|
+
Layers;
|
|
1027
|
+
return group.to ? (_jsx(CollapsedNavShortcut, { to: group.to, label: group.label, icon: Icon, isActive: active }, group.label)) : (_jsx(TooltipIconButton, { tooltip: group.label, side: "right", "aria-label": `Open ${group.label}`, "aria-current": active ? "page" : undefined, className: active ? ACTIVE_NAV_ROW_CLASS : undefined, onClick: () => {
|
|
1028
|
+
toggleGroupCollapsed(group.label, true);
|
|
1029
|
+
toggle();
|
|
1030
|
+
}, children: _jsx(Icon, { className: "size-4" }) }, group.label));
|
|
1031
|
+
})
|
|
1032
|
+
: navConfig
|
|
1033
|
+
.map((group, groupIndex) => {
|
|
1034
|
+
// Modules with a non-flat collapsed rail (e.g. Spaces'
|
|
1035
|
+
// "header + active workspace") supply `collapsedRail`; the
|
|
1036
|
+
// shell stays module-agnostic.
|
|
1037
|
+
const items = group.collapsedRail
|
|
1038
|
+
? group
|
|
1039
|
+
.collapsedRail({
|
|
1040
|
+
currentPage,
|
|
1041
|
+
navContextMeta: navContext.meta ?? {},
|
|
1042
|
+
})
|
|
1043
|
+
.map((it) => ({
|
|
1044
|
+
to: it.to,
|
|
1045
|
+
label: it.label,
|
|
1046
|
+
icon: it.icon,
|
|
1047
|
+
isActive: it.isActive,
|
|
1048
|
+
}))
|
|
1049
|
+
: group.items.map((it) => ({
|
|
1050
|
+
to: it.to,
|
|
1051
|
+
label: it.label,
|
|
1052
|
+
icon: it.icon,
|
|
1053
|
+
isActive: currentPage === it.to,
|
|
1054
|
+
}));
|
|
1055
|
+
return { group, groupIndex, items };
|
|
1056
|
+
})
|
|
1057
|
+
.filter(({ items }) => items.length > 0)
|
|
1058
|
+
.map(({ group, groupIndex, items }, visibleIndex) => (
|
|
1059
|
+
// Each non-first VISIBLE group gets a thin top border —
|
|
1060
|
+
// visually separates groups (e.g. user Workspaces vs
|
|
1061
|
+
// Admin) in the collapsed rail without changing the
|
|
1062
|
+
// CollapsedRailItem shape. The wrapper itself is a centred
|
|
1063
|
+
// column so icons stay aligned with the rail.
|
|
1064
|
+
_jsx("div", { className: cn("flex flex-col items-center gap-1", visibleIndex > 0 && "mt-2 pt-2 border-t border-sidebar-border/40"), children: items.map(({ to, label, icon, isActive }) => (_jsx(CollapsedNavShortcut, { to: to, label: label, icon: icon, isActive: isActive }, to))) }, `${group.label || ""}-${groupIndex}`))) })] }));
|
|
982
1065
|
}
|
|
983
|
-
|
|
1066
|
+
const newConversationAction = (_jsx("div", { className: "px-1 mb-0.5", children: _jsxs("button", { type: "button", onClick: handleNewConversation, className: cn("flex items-center gap-2 w-full", "rounded-xl px-3 py-2 text-sm text-left", "text-sidebar-foreground/85 transition-colors hover:bg-sidebar-accent/70"), children: [_jsx(SquarePen, { className: "size-4 shrink-0 text-sidebar-icon" }), newConversationLabel] }) }));
|
|
1067
|
+
return (_jsxs("div", { className: "flex flex-col flex-1 min-h-0", children: [_jsxs("div", { "data-navigation-header": true, className: "flex items-center justify-between px-3 py-3", children: [_jsx(NavLink, { to: "/", className: "flex items-center gap-2 hover:opacity-80 transition-opacity", children: _jsx(AppBrand, { iconClassName: "text-primary", labelClassName: "truncate text-sm font-semibold text-sidebar-foreground" }) }), _jsx(TooltipIconButton, { tooltip: "Collapse sidebar", side: "right", onClick: toggle, children: _jsx(PanelLeft, { className: "size-5" }) })] }), !hideNewConversation && !newConversationAfterGroup && newConversationAction, _jsxs("nav", { className: "flex-1 overflow-y-auto px-1 pb-16", onDragOverCapture: preventNativeFileDrop, onDropCapture: preventNativeFileDrop, children: [navConfig.map((group) => (_jsxs(Fragment, { children: [_jsx(NavGroupSection, { group: group }), !hideNewConversation &&
|
|
1068
|
+
group.label === newConversationAfterGroup &&
|
|
1069
|
+
newConversationAction] }, group.label))), showNavSkeleton && (_jsx("div", { className: "space-y-1 px-1 pt-2", "aria-hidden": "true", children: Array.from({ length: 6 }).map((_, index) => (_jsx("div", { className: "h-7 mx-1 rounded-lg animate-pulse bg-sidebar-accent/40" }, index))) }))] }), !hideFooterActions && _jsx(SidebarFooterActions, {})] }));
|
|
984
1070
|
}
|
|
985
1071
|
function SidebarFooterActions() {
|
|
986
1072
|
const footerActions = useNavStore((s) => s.footerActions);
|
|
@@ -991,9 +1077,16 @@ function SidebarFooterActions() {
|
|
|
991
1077
|
: "border-sidebar-foreground/30") }), action.active ? (action.activeLabel ?? action.label) : action.label] }, action.id))) }));
|
|
992
1078
|
}
|
|
993
1079
|
/** Mobile drawer overlay */
|
|
994
|
-
function MobileDrawer() {
|
|
1080
|
+
function MobileDrawer({ footer, ...view }) {
|
|
995
1081
|
const isMobileOpen = useSidebarStore((s) => s.isMobileOpen);
|
|
996
1082
|
const closeMobile = useSidebarStore((s) => s.closeMobile);
|
|
1083
|
+
const locationKey = useLocation().key;
|
|
1084
|
+
const previousLocationKey = useRef(locationKey);
|
|
1085
|
+
useEffect(() => {
|
|
1086
|
+
if (previousLocationKey.current !== locationKey)
|
|
1087
|
+
closeMobile();
|
|
1088
|
+
previousLocationKey.current = locationKey;
|
|
1089
|
+
}, [locationKey, closeMobile]);
|
|
997
1090
|
const isToolPanelOpen = useToolPanelStore((s) => s.isOpen);
|
|
998
1091
|
const width = useSidebarStore((s) => s.width);
|
|
999
1092
|
const drawerWidth = Math.min(width, typeof window !== "undefined" ? window.innerWidth - 24 : width);
|
|
@@ -1009,14 +1102,14 @@ function MobileDrawer() {
|
|
|
1009
1102
|
window.addEventListener("keydown", handleEscape);
|
|
1010
1103
|
return () => window.removeEventListener("keydown", handleEscape);
|
|
1011
1104
|
}, [isMobileOpen, isToolPanelOpen, closeMobile]);
|
|
1012
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { className: cn("fixed inset-0 z-50 bg-black/50 transition-opacity duration-200", isMobileOpen ? "opacity-100" : "opacity-0 pointer-events-none"), onClick: closeMobile, "aria-hidden": "true" }),
|
|
1105
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: cn("fixed inset-0 z-50 bg-black/50 transition-opacity duration-200", isMobileOpen ? "opacity-100" : "opacity-0 pointer-events-none"), onClick: closeMobile, "aria-hidden": "true" }), _jsxs("aside", { "data-sidebar-surface": "mobile", className: cn("fixed top-0 left-0 z-50 h-dvh", "flex flex-col", "border-r border-sidebar-border/60 bg-sidebar text-sidebar-foreground shadow-xl", "transition-transform duration-200 ease-in-out", isMobileOpen ? "translate-x-0" : "-translate-x-full"), style: { width: drawerWidth }, role: "complementary", "aria-label": "Navigation sidebar", children: [_jsx(SidebarContent, { collapsed: false, ...view }), footer] })] }));
|
|
1013
1106
|
}
|
|
1014
1107
|
/** Desktop inline sidebar (expanded or collapsed rail) */
|
|
1015
|
-
function DesktopSidebar() {
|
|
1108
|
+
function DesktopSidebar({ footer, collapsedFooter, ...view }) {
|
|
1016
1109
|
const isOpen = useSidebarStore((s) => s.isOpen);
|
|
1017
1110
|
const width = useSidebarStore((s) => s.width);
|
|
1018
|
-
return (_jsxs("aside", { "data-sidebar-surface": "desktop", className: cn("group/sidebar relative hidden md:flex flex-col shrink-0", "h-full", "border-r border-sidebar-border/60 bg-sidebar text-sidebar-foreground", "transition-[width] duration-200 ease-in-out", "overflow-hidden"), style: { width: isOpen ? width : RAIL_WIDTH }, role: "complementary", "aria-label": "Navigation sidebar", children: [_jsx(SidebarContent, { collapsed: !isOpen }), isOpen && _jsx(SidebarResizeHandle, {})] }));
|
|
1111
|
+
return (_jsxs("aside", { "data-sidebar-surface": "desktop", "data-sidebar-collapsed": !isOpen || undefined, className: cn("group/sidebar relative hidden md:flex flex-col shrink-0", "h-full", "border-r border-sidebar-border/60 bg-sidebar text-sidebar-foreground", "transition-[width] duration-200 ease-in-out", "overflow-hidden"), style: { width: isOpen ? width : RAIL_WIDTH }, role: "complementary", "aria-label": "Navigation sidebar", children: [_jsx(SidebarContent, { collapsed: !isOpen, ...view }), isOpen ? footer : collapsedFooter, isOpen && _jsx(SidebarResizeHandle, {})] }));
|
|
1019
1112
|
}
|
|
1020
|
-
export function Sidebar() {
|
|
1021
|
-
return (_jsxs(_Fragment, { children: [_jsx(MobileDrawer, {}), _jsx(DesktopSidebar, {})] }));
|
|
1113
|
+
export function Sidebar({ footer, collapsedFooter, ...view } = {}) {
|
|
1114
|
+
return (_jsxs(_Fragment, { children: [_jsx(MobileDrawer, { footer: footer, ...view }), _jsx(DesktopSidebar, { footer: footer, collapsedFooter: collapsedFooter, ...view })] }));
|
|
1022
1115
|
}
|