@iloveagents/foundry-web-ui 0.25.0 → 0.26.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/sidebar.js +76 -33
- package/dist/lib/nav-config.d.ts +19 -0
- package/package.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { 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, PanelLeft, PanelLeftOpen, SquarePen, ChevronDown, ChevronRight, GripVertical, Plus, Upload, ChevronLeft, } from "lucide-react";
|
|
5
5
|
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
6
6
|
import { useSidebarStore, RAIL_WIDTH, SIDEBAR_MIN_WIDTH, SIDEBAR_MAX_WIDTH, } from "../lib/sidebar-store.js";
|
|
7
7
|
import { useToolPanelStore } from "../lib/tool-panel-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];
|
|
@@ -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, 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 &&
|
|
@@ -576,7 +586,7 @@ function NestedFolderItem({ item, depth }) {
|
|
|
576
586
|
.join("\n") || null,
|
|
577
587
|
});
|
|
578
588
|
navigate(item.to);
|
|
579
|
-
}, className: cn("flex items-center gap-2 flex-1 min-w-0", bodyPl, "pr-3 py-1
|
|
589
|
+
}, 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
590
|
? "text-sidebar-foreground/45 cursor-default"
|
|
581
591
|
: isActive
|
|
582
592
|
? ACTIVE_NAV_TEXT_CLASS
|
|
@@ -608,7 +618,7 @@ function NestedFolderItem({ item, depth }) {
|
|
|
608
618
|
} }, child.to + child.label));
|
|
609
619
|
}), item.dnd?.canDrop() && _jsx(ContainerDropZone, { dnd: item.dnd })] }))] }));
|
|
610
620
|
}
|
|
611
|
-
function CollapsibleNavItem({ item, inTree = true }) {
|
|
621
|
+
function CollapsibleNavItem({ item, inTree = true, heading = false, underHeader = false, }) {
|
|
612
622
|
const navigate = useNavigate();
|
|
613
623
|
const currentPath = useLocation().pathname;
|
|
614
624
|
const setNavContext = useAppStore((s) => s.setNavContext);
|
|
@@ -617,10 +627,19 @@ function CollapsibleNavItem({ item, inTree = true }) {
|
|
|
617
627
|
const isActive = currentPath === item.to;
|
|
618
628
|
const isAncestorActive = isInsideSubtree && !isActive;
|
|
619
629
|
const [manualOverride, setManualOverride] = useState(null);
|
|
620
|
-
|
|
630
|
+
// A heading is never collapsed and carries no chevron: its children ARE
|
|
631
|
+
// the tree it stands for.
|
|
632
|
+
const isExpanded = heading ? true : (manualOverride ?? isInsideSubtree);
|
|
621
633
|
const hasChildren = Boolean(item.children && item.children.length > 0);
|
|
622
634
|
const isDisabled = item.disabled === true;
|
|
623
635
|
const hasActions = Boolean(!isDisabled && item.actions && item.actions.length > 0);
|
|
636
|
+
const rowInTree = inTree && !heading;
|
|
637
|
+
const bodyClass = rowInTree
|
|
638
|
+
? getNavDepthOffsets(0).body
|
|
639
|
+
: heading || !underHeader
|
|
640
|
+
? NAV_FLAT_BODY_CLASS
|
|
641
|
+
: getNavDepthOffsets(0).body;
|
|
642
|
+
const headingClass = heading && !isDisabled && "font-semibold text-sidebar-foreground";
|
|
624
643
|
const { draggableProps, dropTargetProps, isDragOver, isFileDragOver } = useNavItemDnd(item.dnd);
|
|
625
644
|
const rendersRow = Boolean(hasChildren || hasActions);
|
|
626
645
|
const activeRowRef = useActiveNavRowScroll(isActive && rendersRow);
|
|
@@ -635,7 +654,7 @@ function CollapsibleNavItem({ item, inTree = true }) {
|
|
|
635
654
|
!isDisabled &&
|
|
636
655
|
"ring-2 ring-inset ring-sidebar-ring/50 bg-sidebar-selected", isFileDragOver &&
|
|
637
656
|
!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:
|
|
657
|
+
"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 ? () => setManualOverride(!isExpanded) : undefined, label: item.label, isActive: (selected || ancestor) && !isDisabled, depth: 0 }), _jsxs("button", { type: "button", onClick: () => {
|
|
639
658
|
if (isDisabled)
|
|
640
659
|
return;
|
|
641
660
|
setNavContext({
|
|
@@ -650,36 +669,36 @@ function CollapsibleNavItem({ item, inTree = true }) {
|
|
|
650
669
|
if (hasChildren)
|
|
651
670
|
setManualOverride(true);
|
|
652
671
|
navigate(item.to);
|
|
653
|
-
}, className: cn("flex items-center gap-2 flex-1 min-w-0 py-
|
|
672
|
+
}, 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
673
|
? "text-sidebar-foreground/45 cursor-default"
|
|
655
674
|
: selected
|
|
656
675
|
? ACTIVE_NAV_TEXT_CLASS
|
|
657
676
|
: ancestor
|
|
658
677
|
? "text-sidebar-foreground font-medium"
|
|
659
|
-
: "text-sidebar-foreground/85"), disabled: isDisabled, children: [_jsx(item.icon, { className: cn("size-4 shrink-0", selected && !isDisabled
|
|
678
|
+
: "text-sidebar-foreground/85", headingClass), disabled: isDisabled, children: [_jsx(item.icon, { className: cn("size-4 shrink-0", selected && !isDisabled
|
|
660
679
|
? ACTIVE_NAV_ICON_CLASS
|
|
661
680
|
: ancestor
|
|
662
681
|
? "text-sidebar-foreground/80"
|
|
663
682
|
: "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
683
|
? ACTIVE_NAV_BADGE_CLASS
|
|
665
684
|
: "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 })] }))] }));
|
|
685
|
+
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
686
|
}
|
|
668
687
|
if (isDisabled) {
|
|
669
|
-
return (_jsxs("div", { "data-nav-depth": 0, className: cn("relative flex items-center gap-2 w-full opacity-40",
|
|
688
|
+
return (_jsxs("div", { "data-nav-depth": 0, className: cn("relative flex items-center gap-2 w-full opacity-40", bodyClass,
|
|
670
689
|
// Transparent border for box-model parity — see the NavLink variant.
|
|
671
|
-
"rounded-lg border border-transparent pr-3 py-
|
|
690
|
+
"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
691
|
}
|
|
673
|
-
return (_jsx(NavLink, { ref: activeLinkRef, "data-active-nav-row": currentPath === item.to ? "true" : undefined, "data-nav-depth": 0, to: item.to, ...draggableProps, ...dropTargetProps, className: ({ isActive }) => cn("relative flex items-center gap-2 w-full",
|
|
692
|
+
return (_jsx(NavLink, { ref: activeLinkRef, "data-active-nav-row": currentPath === item.to ? "true" : undefined, "data-nav-depth": 0, to: item.to, ...draggableProps, ...dropTargetProps, className: ({ isActive }) => cn("relative flex items-center gap-2 w-full", bodyClass,
|
|
674
693
|
// The transparent border is NOT decoration: it is what makes this
|
|
675
694
|
// row's box model match every other variant. An absolutely
|
|
676
695
|
// positioned twisty resolves against the PADDING box, so a row
|
|
677
696
|
// without the border puts its disclosure cell 1px left of its
|
|
678
697
|
// siblings' and the column goes ragged.
|
|
679
|
-
"rounded-xl border border-transparent pr-3 py-
|
|
698
|
+
"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
699
|
!isFileDragOver &&
|
|
681
700
|
"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: ({ isActive }) => (_jsxs(_Fragment, { children: [_jsx(NavTwisty, { hasChildren: false, childrenUnloaded: item.childrenUnloaded, inTree:
|
|
701
|
+
"outline-dashed outline-2 -outline-offset-2 outline-primary bg-primary/10", item.dimmed && "opacity-50"), children: ({ isActive }) => (_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
702
|
? ACTIVE_NAV_BADGE_CLASS
|
|
684
703
|
: "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
704
|
}
|
|
@@ -847,7 +866,26 @@ function NavGroupSection({ group }) {
|
|
|
847
866
|
// hideLabel + collapsible are mutually exclusive — a hidden label
|
|
848
867
|
// can't host a chevron. The collapsible flag is the opt-in; legacy
|
|
849
868
|
// groups stay uncollapsable so behaviour is unchanged.
|
|
850
|
-
|
|
869
|
+
// Focus — see ``NavGroup.focusOn``: the group draws one item as a heading
|
|
870
|
+
// under a header that leads back. Needs a visible label to lead back
|
|
871
|
+
// from and a ``to`` to lead back to; otherwise it draws as usual.
|
|
872
|
+
const focusedItem = group.focusOn !== undefined && !group.hideLabel && group.to
|
|
873
|
+
? group.items.find((item) => item.to === group.focusOn)
|
|
874
|
+
: undefined;
|
|
875
|
+
const focused = focusedItem !== undefined;
|
|
876
|
+
// The new shape fades in over the old one's place: keyed on the focus,
|
|
877
|
+
// and only after a CHANGE in either direction — not on first paint
|
|
878
|
+
// (user's test: the hard swap felt abrupt). The previous key is read
|
|
879
|
+
// during render and written after it, so a focus that returns to its
|
|
880
|
+
// initial value still counts as a change.
|
|
881
|
+
const focusKey = focused ? focusedItem.to : "";
|
|
882
|
+
const previousFocusKey = useRef(focusKey);
|
|
883
|
+
const animateRows = previousFocusKey.current !== focusKey;
|
|
884
|
+
useEffect(() => {
|
|
885
|
+
previousFocusKey.current = focusKey;
|
|
886
|
+
});
|
|
887
|
+
// Focus takes precedence over collapsing: a focused header is the way back.
|
|
888
|
+
const isCollapsible = !group.hideLabel && Boolean(group.collapsible) && !focused;
|
|
851
889
|
const persistedCollapsed = useGroupCollapsed(group.label, group.defaultCollapsed);
|
|
852
890
|
const collapsed = isCollapsible && persistedCollapsed;
|
|
853
891
|
// Bubble: show the first child statusDot on the collapsed header so
|
|
@@ -877,7 +915,7 @@ function NavGroupSection({ group }) {
|
|
|
877
915
|
// to hit a tiny target. The clickable invisible padding
|
|
878
916
|
// OVERLAPS the gap rather than pushing the label right
|
|
879
917
|
// (negative-margin + extra padding cancel out).
|
|
880
|
-
_jsxs("div", { className: cn("group/header flex items-center justify-between
|
|
918
|
+
_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
919
|
// Visual cell width = ``SIDEBAR_ITEM_ICON_CLASS`` (14px)
|
|
882
920
|
// so the chevron sits in the same column as item icons
|
|
883
921
|
// and the group label aligns with item labels. The
|
|
@@ -888,20 +926,25 @@ function NavGroupSection({ group }) {
|
|
|
888
926
|
// visual feedback even on near-misses.
|
|
889
927
|
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
928
|
? "text-sidebar-selected-foreground"
|
|
891
|
-
: "text-sidebar-foreground/
|
|
929
|
+
: "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 ? (
|
|
930
|
+
// The focused header is the way back: the collapse chevron's
|
|
931
|
+
// cell holds a back chevron, the label links to the group's
|
|
932
|
+
// page, and the metrics are the collapsible header's — so the
|
|
933
|
+
// header stays put, in the same type, when the group focuses.
|
|
934
|
+
_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
935
|
// Non-collapsible header: no chevron, but we still emit an
|
|
893
936
|
// invisible spacer of ``SIDEBAR_ITEM_ICON_CLASS`` width + the
|
|
894
937
|
// shared gap so the group label lands in the same column as
|
|
895
938
|
// collapsible headers and item labels.
|
|
896
|
-
_jsxs("div", { className: cn("flex items-center justify-between
|
|
939
|
+
_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
940
|
? "text-sidebar-selected-foreground"
|
|
898
|
-
: "text-sidebar-foreground/
|
|
899
|
-
|
|
941
|
+
: "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 &&
|
|
942
|
+
"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
943
|
// A leaf dot distinguishes "nothing under this" from a sibling
|
|
901
944
|
// that branches. Decided per GROUP: Workspaces has branches, so
|
|
902
945
|
// its leaves are marked; Chats has none, so a dot on every row
|
|
903
946
|
// would mark a distinction that does not exist.
|
|
904
|
-
inTree: groupHasBranches }, item.to + item.label)))] }));
|
|
947
|
+
inTree: groupHasBranches, heading: focused, underHeader: !group.hideLabel }, item.to + item.label))) }, focusKey))] }));
|
|
905
948
|
}
|
|
906
949
|
/**
|
|
907
950
|
* The "+" dropdown shown next to a group label when the group declares
|
|
@@ -912,7 +955,7 @@ function NavGroupSection({ group }) {
|
|
|
912
955
|
* doesn't also toggle the surrounding collapsible-header button.
|
|
913
956
|
*/
|
|
914
957
|
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/
|
|
958
|
+
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
959
|
e.preventDefault();
|
|
917
960
|
action.handler();
|
|
918
961
|
}, children: action.label }, action.id))) })] }));
|
package/dist/lib/nav-config.d.ts
CHANGED
|
@@ -138,6 +138,25 @@ export interface NavGroup {
|
|
|
138
138
|
to?: string;
|
|
139
139
|
/** Hide the group header while preserving group semantics. */
|
|
140
140
|
hideLabel?: boolean;
|
|
141
|
+
/**
|
|
142
|
+
* The ``to`` of the item this group is focused on, if any.
|
|
143
|
+
*
|
|
144
|
+
* A focused group draws that item alone: the header keeps its label,
|
|
145
|
+
* typography and height and leads back to ``to`` (a back chevron in the
|
|
146
|
+
* collapse chevron's column, the label as the link); the item is drawn
|
|
147
|
+
* as a flat, emphasised heading with its children beneath it — one
|
|
148
|
+
* indent step left of a branch's children, at their usual row size —
|
|
149
|
+
* and its siblings are not drawn. Nothing above the item moves and no
|
|
150
|
+
* row resizes when the group focuses or unfocuses.
|
|
151
|
+
*
|
|
152
|
+
* Focus takes precedence over ``collapsible``. It needs a visible label
|
|
153
|
+
* and a ``to`` to lead back to; without either, or when no item carries
|
|
154
|
+
* that ``to``, the group draws as usual. Only the group's own items can
|
|
155
|
+
* be focused, not their descendants. The value is the caller's: the
|
|
156
|
+
* back link navigates to ``to`` and nothing more, so the caller derives
|
|
157
|
+
* ``focusOn`` from the route (or its own state) and clears it there.
|
|
158
|
+
*/
|
|
159
|
+
focusOn?: string;
|
|
141
160
|
/** Group description sent to the agent */
|
|
142
161
|
description?: string;
|
|
143
162
|
/** Group-level metadata (e.g., org settings, workspace config, custom instructions) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iloveagents/foundry-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React agent UI core for Foundry UI — chat, composer, AG-UI adapter for assistant-ui, tool cards, panels, sidebar, theme runtime, and UI stores.",
|
|
6
6
|
"keywords": [
|
|
@@ -76,8 +76,8 @@
|
|
|
76
76
|
"recharts": "^3.10.1",
|
|
77
77
|
"remark-gfm": "^4.0.0",
|
|
78
78
|
"tailwind-merge": "^3.5.0",
|
|
79
|
-
"@iloveagents/foundry-agent": "^0.
|
|
80
|
-
"@iloveagents/foundry-web-primitives": "^0.
|
|
79
|
+
"@iloveagents/foundry-agent": "^0.26.0",
|
|
80
|
+
"@iloveagents/foundry-web-primitives": "^0.26.0"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@ag-ui/client": "^0.0.52",
|