@camunda/design-system 0.61.2 → 0.63.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/src/components/ui/alert-dialog.d.ts +1 -1
- package/dist/src/components/ui/alert-dialog.d.ts.map +1 -1
- package/dist/src/components/ui/alert-dialog.js +3 -1
- package/dist/src/components/ui/alert-dialog.js.map +1 -1
- package/dist/src/components/ui/app-sidebar.d.ts +16 -15
- package/dist/src/components/ui/app-sidebar.d.ts.map +1 -1
- package/dist/src/components/ui/app-sidebar.js +395 -71
- package/dist/src/components/ui/app-sidebar.js.map +1 -1
- package/dist/src/components/ui/collapsible.d.ts +1 -1
- package/dist/src/components/ui/collapsible.d.ts.map +1 -1
- package/dist/src/components/ui/collapsible.js +3 -1
- package/dist/src/components/ui/collapsible.js.map +1 -1
- package/dist/src/components/ui/dialog.d.ts +1 -1
- package/dist/src/components/ui/dialog.d.ts.map +1 -1
- package/dist/src/components/ui/dialog.js +3 -1
- package/dist/src/components/ui/dialog.js.map +1 -1
- package/dist/src/components/ui/dropdown.d.ts +3 -3
- package/dist/src/components/ui/dropdown.d.ts.map +1 -1
- package/dist/src/components/ui/dropdown.js +7 -2
- package/dist/src/components/ui/dropdown.js.map +1 -1
- package/dist/src/components/ui/input.d.ts +3 -1
- package/dist/src/components/ui/input.d.ts.map +1 -1
- package/dist/src/components/ui/input.js +2 -2
- package/dist/src/components/ui/input.js.map +1 -1
- package/dist/src/components/ui/label.d.ts +3 -1
- package/dist/src/components/ui/label.d.ts.map +1 -1
- package/dist/src/components/ui/label.js +3 -3
- package/dist/src/components/ui/label.js.map +1 -1
- package/dist/src/components/ui/login-screen.d.ts +24 -0
- package/dist/src/components/ui/login-screen.d.ts.map +1 -0
- package/dist/src/components/ui/login-screen.js +59 -0
- package/dist/src/components/ui/login-screen.js.map +1 -0
- package/dist/src/components/ui/nav-icon.d.ts +8 -2
- package/dist/src/components/ui/nav-icon.d.ts.map +1 -1
- package/dist/src/components/ui/nav-icon.js +3 -1
- package/dist/src/components/ui/nav-icon.js.map +1 -1
- package/dist/src/components/ui/popover.d.ts +1 -1
- package/dist/src/components/ui/popover.d.ts.map +1 -1
- package/dist/src/components/ui/popover.js +8 -1
- package/dist/src/components/ui/popover.js.map +1 -1
- package/dist/src/components/ui/select.d.ts +1 -1
- package/dist/src/components/ui/select.d.ts.map +1 -1
- package/dist/src/components/ui/select.js +7 -3
- package/dist/src/components/ui/select.js.map +1 -1
- package/dist/src/components/ui/sheet.d.ts +1 -1
- package/dist/src/components/ui/sheet.d.ts.map +1 -1
- package/dist/src/components/ui/sheet.js +3 -1
- package/dist/src/components/ui/sheet.js.map +1 -1
- package/dist/src/components/ui/sidebar-provider.d.ts +7 -1
- package/dist/src/components/ui/sidebar-provider.d.ts.map +1 -1
- package/dist/src/components/ui/sidebar-provider.js +10 -6
- package/dist/src/components/ui/sidebar-provider.js.map +1 -1
- package/dist/src/components/ui/tabs.d.ts +1 -1
- package/dist/src/components/ui/tabs.d.ts.map +1 -1
- package/dist/src/components/ui/tabs.js +2 -1
- package/dist/src/components/ui/tabs.js.map +1 -1
- package/dist/src/components/ui/tooltip.d.ts +1 -1
- package/dist/src/components/ui/tooltip.d.ts.map +1 -1
- package/dist/src/components/ui/tooltip.js +5 -1
- package/dist/src/components/ui/tooltip.js.map +1 -1
- package/dist/src/icons/camunda-app-icons.js +2 -2
- package/dist/src/icons/camunda-app-icons.js.map +1 -1
- package/dist/src/icons/index.d.ts +1 -1
- package/dist/src/icons/index.d.ts.map +1 -1
- package/dist/src/icons/index.js +1 -1
- package/dist/src/icons/index.js.map +1 -1
- package/dist/src/icons/registry.d.ts +1 -0
- package/dist/src/icons/registry.d.ts.map +1 -1
- package/dist/src/icons/registry.js +3 -1
- package/dist/src/icons/registry.js.map +1 -1
- package/dist/src/icons/registry.json +12 -4
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/i18n/dictionary.d.ts +8 -1
- package/dist/src/lib/i18n/dictionary.d.ts.map +1 -1
- package/dist/src/lib/i18n/dictionary.js +8 -1
- package/dist/src/lib/i18n/dictionary.js.map +1 -1
- package/dist/styles.css +72 -34
- package/package.json +2 -2
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { cva } from "class-variance-authority";
|
|
4
|
-
import { ChevronDown, ChevronRight, ChevronLeft,
|
|
4
|
+
import { ChevronDown, ChevronRight, ChevronLeft, X } from "../../icons/lucide.js";
|
|
5
5
|
import { cn } from "../../lib/utils.js";
|
|
6
6
|
import { useC4Dictionary } from "../../lib/i18n/context.js";
|
|
7
7
|
import { Text } from "./typography.js";
|
|
8
|
-
import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
|
|
8
|
+
import { Popover, PopoverContent, PopoverHeader, PopoverTitle, PopoverTrigger, } from "./popover.js";
|
|
9
9
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
|
|
10
10
|
import { Sheet, SheetClose, SheetContent, SheetTitle } from "./sheet.js";
|
|
11
11
|
import { useSidebar } from "./sidebar-provider.js";
|
|
@@ -14,6 +14,12 @@ import { useIsTruncated } from "../../hooks/use-is-truncated.js";
|
|
|
14
14
|
import { linkDestination, resolveActive, resolveLinkProps, } from "./nav-data.js";
|
|
15
15
|
import { NavRowIcon } from "./nav-icon.js";
|
|
16
16
|
const NAV_ROW_BASE = "group/navbtn flex w-full items-center overflow-hidden rounded-md transition-colors";
|
|
17
|
+
/**
|
|
18
|
+
* The type a section title reads at. Shared with the collapsed rail's flyout header,
|
|
19
|
+
* which titles a group the same way the expanded rail titles a section — one constant
|
|
20
|
+
* so the two cannot drift apart.
|
|
21
|
+
*/
|
|
22
|
+
const SECTION_LABEL_TYPE = "text-xs leading-4 font-medium text-neutral-foreground-subtle";
|
|
17
23
|
// Hides the scrollbar track; scroll itself still works.
|
|
18
24
|
const HIDDEN_SCROLLBAR = "[scrollbar-width:none] [&::-webkit-scrollbar]:hidden";
|
|
19
25
|
const navButton = cva(cn(NAV_ROW_BASE, "gap-3 min-h-10 text-sm text-left whitespace-nowrap focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"), {
|
|
@@ -22,9 +28,15 @@ const navButton = cva(cn(NAV_ROW_BASE, "gap-3 min-h-10 text-sm text-left whitesp
|
|
|
22
28
|
true: "bg-neutral-background-strong text-neutral-foreground-strong font-medium",
|
|
23
29
|
false: "text-neutral-foreground-subtle hover:bg-neutral-background-medium hover:text-neutral-foreground-strong",
|
|
24
30
|
},
|
|
31
|
+
// Both states left-align on the same 10px inset, so the icon holds its x
|
|
32
|
+
// position across a collapse and only the label arrives or leaves. With
|
|
33
|
+
// the list's own 8px padding that puts a 20px icon at x=18, which is also
|
|
34
|
+
// dead centre of the 56px collapsed rail — the one inset satisfying both.
|
|
35
|
+
// Centring the collapsed row instead lands half a pixel off, because the
|
|
36
|
+
// row's flex `gap` still counts the sr-only label.
|
|
25
37
|
expanded: {
|
|
26
|
-
true: "justify-start px-
|
|
27
|
-
false: "justify-
|
|
38
|
+
true: "justify-start px-2.5 py-2",
|
|
39
|
+
false: "justify-start px-2.5 py-2",
|
|
28
40
|
},
|
|
29
41
|
},
|
|
30
42
|
defaultVariants: { active: false, expanded: true },
|
|
@@ -40,9 +52,13 @@ function useExpanded({ expanded: controlled, defaultExpanded, onExpandedChange,
|
|
|
40
52
|
}, [isControlled, onExpandedChange]);
|
|
41
53
|
return [expanded, setExpanded];
|
|
42
54
|
}
|
|
43
|
-
//
|
|
55
|
+
// Matches navButton's own inset at depth 0, so an indented row steps off the
|
|
56
|
+
// same left edge every other row starts from.
|
|
57
|
+
const ROW_INSET_REM = 0.625;
|
|
44
58
|
function indentStyle(depth) {
|
|
45
|
-
return depth > 0
|
|
59
|
+
return depth > 0
|
|
60
|
+
? { paddingLeft: `${ROW_INSET_REM + depth * 1.75}rem` }
|
|
61
|
+
: undefined;
|
|
46
62
|
}
|
|
47
63
|
function renderNode(node, ctx) {
|
|
48
64
|
switch (node.type) {
|
|
@@ -55,7 +71,7 @@ function renderNode(node, ctx) {
|
|
|
55
71
|
return _jsx(NavSection, { node: node, ...ctx }, node.key);
|
|
56
72
|
}
|
|
57
73
|
}
|
|
58
|
-
function NavItemRow({ node, expanded, depth, LinkEl, isMobile, onNavigate, hasLinkComponent, activeItemKey, }) {
|
|
74
|
+
function NavItemRow({ node, expanded, depth, LinkEl, isMobile, onNavigate, hasLinkComponent, activeItemKey, hideIconFallback, }) {
|
|
59
75
|
const isLink = linkDestination(node.linkProps) !== undefined;
|
|
60
76
|
const Comp = isLink ? LinkEl : "button";
|
|
61
77
|
const isActive = resolveActive(node.isActive, node.key, activeItemKey);
|
|
@@ -71,7 +87,7 @@ function NavItemRow({ node, expanded, depth, LinkEl, isMobile, onNavigate, hasLi
|
|
|
71
87
|
: { type: "button" }), "data-slot": "app-sidebar-item", className: cn(navButton({ active: isActive, expanded })), style: indentStyle(depth), "aria-current": isActive ? "page" : undefined, onClick: () => {
|
|
72
88
|
node.onClick?.();
|
|
73
89
|
onNavigate?.();
|
|
74
|
-
}, onPointerEnter: measure, onFocus: measure, children: [_jsx(NavRowIcon, { icon: node.icon, label: node.label }), _jsx("span", { ref: labelRef, className: cn("flex-1 truncate", !expanded && "sr-only"), children: node.label }), expanded && node.trailingElement] }));
|
|
90
|
+
}, onPointerEnter: measure, onFocus: measure, children: [_jsx(NavRowIcon, { icon: node.icon, label: node.label, hideFallback: hideIconFallback }), _jsx("span", { ref: labelRef, className: cn("flex-1 truncate", !expanded && "sr-only"), children: node.label }), expanded && node.trailingElement] }));
|
|
75
91
|
// Collapsed icon rail: name the icon with a tooltip. Not on mobile (the Sheet
|
|
76
92
|
// shows full labels) and not when expanded (the label is already visible).
|
|
77
93
|
if (!expanded && !isMobile) {
|
|
@@ -82,7 +98,53 @@ function NavItemRow({ node, expanded, depth, LinkEl, isMobile, onNavigate, hasLi
|
|
|
82
98
|
// "Truncated Text" in `docs/specs/shadcn/patterns.md`.
|
|
83
99
|
return isTruncated ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: row }), _jsx(TooltipContent, { children: node.label })] })) : (row);
|
|
84
100
|
}
|
|
85
|
-
|
|
101
|
+
/**
|
|
102
|
+
* The collapsed rail's single flyout slot.
|
|
103
|
+
*
|
|
104
|
+
* Group flyouts share one slot, so opening one closes whichever was open —
|
|
105
|
+
* including a pinned or keyboard-opened flyout, which ignores hover-out and
|
|
106
|
+
* would otherwise stay on screen beside the new one. It lives above the rows
|
|
107
|
+
* because a row cannot close a sibling it has no reference to.
|
|
108
|
+
*/
|
|
109
|
+
const FlyoutSlotContext = React.createContext(null);
|
|
110
|
+
function useFlyoutSlot(nodeKey) {
|
|
111
|
+
const slot = React.useContext(FlyoutSlotContext);
|
|
112
|
+
// A row rendered without the rail around it keeps a slot of its own.
|
|
113
|
+
const [localKey, setLocalKey] = React.useState(null);
|
|
114
|
+
const openKey = slot ? slot.openKey : localKey;
|
|
115
|
+
const setOpenKey = slot ? slot.setOpenKey : setLocalKey;
|
|
116
|
+
return {
|
|
117
|
+
flyoutOpen: openKey === nodeKey,
|
|
118
|
+
setFlyoutOpen: (next) => setOpenKey((prev) => (next ? nodeKey : prev === nodeKey ? null : prev)),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// Hover intent for the collapsed-rail flyout. The open delay keeps a pointer
|
|
122
|
+
// crossing the rail vertically from flashing every group's flyout in turn; the
|
|
123
|
+
// close delay covers the gap between the trigger and the flyout, which the
|
|
124
|
+
// pointer has to traverse to reach the children (WCAG 1.4.13, hoverable).
|
|
125
|
+
const FLYOUT_HOVER_OPEN_DELAY = 120;
|
|
126
|
+
const FLYOUT_HOVER_CLOSE_DELAY = 240;
|
|
127
|
+
/**
|
|
128
|
+
* Whether anything below this node resolves active.
|
|
129
|
+
*
|
|
130
|
+
* A pure `group` has no active state of its own — see `SidebarGroup`, and the C3 contract in
|
|
131
|
+
* `nav-c3-compat.specs.ts`, which both leave the field off deliberately. Expanded that costs
|
|
132
|
+
* nothing, because the active descendant is on screen carrying its own highlight. Collapsed it
|
|
133
|
+
* leaves the rail with no current-location signal at all: the descendant lives in a flyout
|
|
134
|
+
* nobody has opened yet.
|
|
135
|
+
*/
|
|
136
|
+
function hasActiveDescendant(nodes, activeItemKey) {
|
|
137
|
+
return nodes.some((child) => {
|
|
138
|
+
if (child.type === "section" || child.type === "group") {
|
|
139
|
+
return hasActiveDescendant(child.children, activeItemKey);
|
|
140
|
+
}
|
|
141
|
+
if (resolveActive(child.isActive, child.key, activeItemKey))
|
|
142
|
+
return true;
|
|
143
|
+
return (child.type === "group-item" &&
|
|
144
|
+
hasActiveDescendant(child.children, activeItemKey));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function NavGroupRow({ node, expanded, depth, LinkEl, hasLinkComponent, activeItemKey, isMobile, onNavigate, hideIconFallback, }) {
|
|
86
148
|
const isGroupControlled = node.isExpanded !== undefined;
|
|
87
149
|
const [openInternal, setOpenInternal] = React.useState(!!node.isExpanded);
|
|
88
150
|
const open = isGroupControlled ? !!node.isExpanded : openInternal;
|
|
@@ -95,11 +157,58 @@ function NavGroupRow({ node, expanded, depth, LinkEl, hasLinkComponent, activeIt
|
|
|
95
157
|
// Collapsed-rail flyout visibility is its own concern — decoupled from group
|
|
96
158
|
// expansion so collapsing the rail never auto-opens a flyout and opening a
|
|
97
159
|
// flyout never mutates expansion state.
|
|
98
|
-
const
|
|
160
|
+
const { flyoutOpen, setFlyoutOpen } = useFlyoutSlot(node.key);
|
|
161
|
+
const hoverTimerRef = React.useRef(null);
|
|
162
|
+
// Which input opened the flyout. A pointer-driven open must not pull focus
|
|
163
|
+
// into the flyout: the pointer only crossed the rail, and Radix's default
|
|
164
|
+
// open-autofocus would yank focus off whatever the user was actually on.
|
|
165
|
+
const openedByPointerRef = React.useRef(false);
|
|
166
|
+
// The group trigger's tooltip is keyboard-only: on hover the flyout itself
|
|
167
|
+
// names the group, and with `delayDuration = 0` a hover tooltip would appear
|
|
168
|
+
// instantly, then be force-closed 120ms later when the flyout opens — a flash
|
|
169
|
+
// on every pass over the rail. Keyboard focus has no flyout until Enter, so it
|
|
170
|
+
// still needs the tooltip. Leaf rows keep their ordinary hover tooltip.
|
|
171
|
+
const [labelTooltipOpen, setLabelTooltipOpen] = React.useState(false);
|
|
172
|
+
const pointerInsideRef = React.useRef(false);
|
|
173
|
+
const triggerRef = React.useRef(null);
|
|
174
|
+
// Readable from the hover timer, which captured an older render's props.
|
|
175
|
+
const flyoutOpenRef = React.useRef(flyoutOpen);
|
|
176
|
+
React.useEffect(() => {
|
|
177
|
+
flyoutOpenRef.current = flyoutOpen;
|
|
178
|
+
}, [flyoutOpen]);
|
|
179
|
+
const cancelHoverTimer = () => {
|
|
180
|
+
if (hoverTimerRef.current !== null) {
|
|
181
|
+
window.clearTimeout(hoverTimerRef.current);
|
|
182
|
+
hoverTimerRef.current = null;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
React.useEffect(() => cancelHoverTimer, []);
|
|
186
|
+
const scheduleFlyout = (next) => {
|
|
187
|
+
cancelHoverTimer();
|
|
188
|
+
hoverTimerRef.current = window.setTimeout(() => {
|
|
189
|
+
hoverTimerRef.current = null;
|
|
190
|
+
if (next) {
|
|
191
|
+
// Only an open this timer actually performs counts as pointer-opened.
|
|
192
|
+
// Re-entering the trigger of an already-pinned flyout also runs here,
|
|
193
|
+
// and marking that one as pointer-opened would silently un-pin it —
|
|
194
|
+
// the next hover-out would dismiss it.
|
|
195
|
+
if (!flyoutOpenRef.current)
|
|
196
|
+
openedByPointerRef.current = true;
|
|
197
|
+
setFlyoutOpen(true);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// Only a hover-opened flyout closes on hover-out. A click- or
|
|
201
|
+
// keyboard-opened one holds focus inside itself, so a pointer merely
|
|
202
|
+
// crossing the rail must not close it and snap focus back to the
|
|
203
|
+
// trigger.
|
|
204
|
+
if (openedByPointerRef.current)
|
|
205
|
+
setFlyoutOpen(false);
|
|
206
|
+
}, next ? FLYOUT_HOVER_OPEN_DELAY : FLYOUT_HOVER_CLOSE_DELAY);
|
|
207
|
+
};
|
|
99
208
|
const isGroupItem = node.type === "group-item";
|
|
100
209
|
const isActive = isGroupItem
|
|
101
210
|
? resolveActive(node.isActive, node.key, activeItemKey)
|
|
102
|
-
:
|
|
211
|
+
: !expanded && hasActiveDescendant(node.children, activeItemKey);
|
|
103
212
|
const isLink = isGroupItem && linkDestination(node.linkProps) !== undefined;
|
|
104
213
|
const LabelComp = isLink ? LinkEl : "button";
|
|
105
214
|
// Truncation reveal — expanded state only (see the collapsed-rail flyout's
|
|
@@ -108,34 +217,104 @@ function NavGroupRow({ node, expanded, depth, LinkEl, hasLinkComponent, activeIt
|
|
|
108
217
|
React.useLayoutEffect(() => {
|
|
109
218
|
measure();
|
|
110
219
|
});
|
|
111
|
-
const label = (_jsxs(_Fragment, { children: [_jsx(NavRowIcon, { icon: node.icon, label: node.label }), _jsx("span", { ref: labelRef, className: cn("flex-1 truncate", !expanded && "sr-only"), children: node.label }), expanded && node.trailingElement] }));
|
|
220
|
+
const label = (_jsxs(_Fragment, { children: [_jsx(NavRowIcon, { icon: node.icon, label: node.label, hideFallback: hideIconFallback }), _jsx("span", { ref: labelRef, className: cn("flex-1 truncate", !expanded && "sr-only"), children: node.label }), expanded && node.trailingElement] }));
|
|
112
221
|
if (!expanded) {
|
|
113
|
-
return (_jsxs(Popover, { open: flyoutOpen, onOpenChange:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
222
|
+
return (_jsxs(Popover, { open: flyoutOpen, onOpenChange: (next) => {
|
|
223
|
+
cancelHoverTimer();
|
|
224
|
+
setFlyoutOpen(next);
|
|
225
|
+
}, children: [_jsxs(Tooltip, { open: labelTooltipOpen && !flyoutOpen,
|
|
226
|
+
// Controlled, so Radix's own dismissal has to be accepted or Escape
|
|
227
|
+
// leaves the tooltip up until blur (WCAG 1.4.13, dismissible). Only
|
|
228
|
+
// the close request is honoured: an open request would reinstate the
|
|
229
|
+
// hover tooltip this trigger deliberately does not have.
|
|
230
|
+
onOpenChange: (next) => {
|
|
231
|
+
if (!next)
|
|
232
|
+
setLabelTooltipOpen(false);
|
|
233
|
+
}, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { ref: triggerRef, type: "button", "data-slot": "app-sidebar-group-trigger", "aria-label": node.label, "aria-expanded": flyoutOpen, className: cn(navButton({ active: isActive, expanded: false })), onMouseEnter: () => {
|
|
234
|
+
pointerInsideRef.current = true;
|
|
235
|
+
scheduleFlyout(true);
|
|
236
|
+
}, onMouseLeave: () => {
|
|
237
|
+
pointerInsideRef.current = false;
|
|
238
|
+
scheduleFlyout(false);
|
|
239
|
+
}, onFocus: () => {
|
|
240
|
+
if (!pointerInsideRef.current)
|
|
241
|
+
setLabelTooltipOpen(true);
|
|
242
|
+
}, onBlur: () => setLabelTooltipOpen(false), onClick: (event) => {
|
|
243
|
+
cancelHoverTimer();
|
|
244
|
+
// Hover has already opened it; Radix's trigger toggle would
|
|
245
|
+
// close it again, so a click on the icon the pointer is
|
|
246
|
+
// sitting on would dismiss the flyout it just opened. The
|
|
247
|
+
// toggle is composed behind this handler and skips on
|
|
248
|
+
// `defaultPrevented`.
|
|
249
|
+
if (flyoutOpen) {
|
|
250
|
+
event.preventDefault();
|
|
251
|
+
}
|
|
252
|
+
// Either way the open is now an explicit one: it stays until
|
|
253
|
+
// Escape, an outside pointer-down, or a child is selected.
|
|
254
|
+
openedByPointerRef.current = false;
|
|
255
|
+
}, children: [_jsx(NavRowIcon, { icon: node.icon, label: node.label }), _jsx("span", { className: "sr-only", children: node.label })] }) }) }), _jsx(TooltipContent, { side: "right", children: node.label })] }), _jsxs(PopoverContent, { side: "right", align: "start", className: "w-56 gap-0 p-1", "aria-label": node.label,
|
|
256
|
+
// The launcher Popover captures at open time is whatever was focused
|
|
257
|
+
// when the pointer crossed the rail, which is not this trigger. Name
|
|
258
|
+
// the trigger explicitly so an explicit close returns focus to it.
|
|
259
|
+
restoreFocusRef: triggerRef, onMouseEnter: cancelHoverTimer, onMouseLeave: () => scheduleFlyout(false), onOpenAutoFocus: (event) => {
|
|
260
|
+
if (openedByPointerRef.current)
|
|
261
|
+
event.preventDefault();
|
|
262
|
+
}, onFocusOutside: (event) => {
|
|
263
|
+
// A hover-opened flyout holds no focus, so every focus-outside is
|
|
264
|
+
// spurious for it — and one in particular is fatal: displacing a
|
|
265
|
+
// keyboard-opened sibling returns that sibling's focus to its
|
|
266
|
+
// trigger, which would dismiss the flyout the pointer just opened.
|
|
267
|
+
// Keyboard and click opens keep the default (tabbing out closes).
|
|
268
|
+
if (openedByPointerRef.current)
|
|
269
|
+
event.preventDefault();
|
|
270
|
+
}, onCloseAutoFocus: (event) => {
|
|
271
|
+
// A hover-opened flyout never held focus, so restoring would drag
|
|
272
|
+
// it off whatever the user is really on. One that was opened by
|
|
273
|
+
// click or keyboard does hold it, and still returns it to the
|
|
274
|
+
// trigger — including when a sibling flyout displaces this one,
|
|
275
|
+
// which would otherwise drop focus to <body>. (Asking the DOM
|
|
276
|
+
// here instead does not work: by the time this fires Radix has
|
|
277
|
+
// already detached the content, so `activeElement` is `<body>`.)
|
|
278
|
+
if (openedByPointerRef.current)
|
|
279
|
+
event.preventDefault();
|
|
280
|
+
// Cleared here rather than in `onOpenChange`, which Radix calls
|
|
281
|
+
// first on an outside pointer-down: clearing there would make this
|
|
282
|
+
// handler read a hover-opened flyout as a focus-holding one and
|
|
283
|
+
// pull focus to the trigger, off whatever was just clicked.
|
|
284
|
+
openedByPointerRef.current = false;
|
|
285
|
+
}, children: [_jsx(PopoverHeader, { className: "px-2 pt-1.5 pb-1", children: _jsx(PopoverTitle, { className: SECTION_LABEL_TYPE, children: node.label }) }), node.children.map((child) => renderNode(child, {
|
|
286
|
+
expanded: true,
|
|
287
|
+
depth: 0,
|
|
288
|
+
LinkEl,
|
|
289
|
+
hasLinkComponent,
|
|
290
|
+
activeItemKey,
|
|
291
|
+
isMobile,
|
|
292
|
+
// Selecting a child dismisses the flyout. Navigation alone does
|
|
293
|
+
// not: the rail survives an in-app route change, so the flyout
|
|
294
|
+
// would otherwise stay open over the destination.
|
|
295
|
+
onNavigate: () => {
|
|
296
|
+
setFlyoutOpen(false);
|
|
297
|
+
onNavigate?.();
|
|
298
|
+
},
|
|
299
|
+
}))] })] }));
|
|
122
300
|
}
|
|
123
|
-
const
|
|
301
|
+
const chevron = open ? (_jsx(ChevronDown, { "aria-hidden": true, className: "size-4" })) : (_jsx(ChevronRight, { "aria-hidden": true, className: "size-4" }));
|
|
302
|
+
const labelElement = (_jsxs(LabelComp, { ...(isLink ? node.linkProps : { type: "button" }), className: cn("flex flex-1 min-w-0 items-center gap-3 min-h-10 py-2 text-sm text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", isActive
|
|
124
303
|
? "text-neutral-foreground-strong font-medium"
|
|
125
304
|
: "text-neutral-foreground-subtle hover:text-neutral-foreground-strong"), style: depth > 0
|
|
126
|
-
? { paddingLeft: `${
|
|
127
|
-
: { paddingLeft:
|
|
305
|
+
? { paddingLeft: `${ROW_INSET_REM + depth * 1.75}rem` }
|
|
306
|
+
: { paddingLeft: `${ROW_INSET_REM}rem` }, "aria-current": isActive ? "page" : undefined, "aria-expanded": isGroupItem ? undefined : open, onClick: isGroupItem
|
|
128
307
|
? () => {
|
|
129
308
|
node.onClick?.();
|
|
130
309
|
onNavigate?.();
|
|
131
310
|
}
|
|
132
|
-
: toggle, onPointerEnter: measure, onFocus: measure, children: label }));
|
|
311
|
+
: toggle, onPointerEnter: measure, onFocus: measure, children: [label, !isGroupItem && (_jsx("span", { className: "flex items-center mr-3", children: chevron }))] }));
|
|
133
312
|
// Truncation reveal, mounted only when the label actually clips — see
|
|
134
313
|
// "Truncated Text" in `docs/specs/shadcn/patterns.md`.
|
|
135
314
|
const labelWithReveal = isTruncated ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: labelElement }), _jsx(TooltipContent, { children: node.label })] })) : (labelElement);
|
|
136
315
|
return (_jsxs("div", { "data-slot": "app-sidebar-group", className: cn("flex flex-col gap-0.5"), children: [_jsxs("div", { className: cn(NAV_ROW_BASE, isActive
|
|
137
316
|
? "bg-neutral-background-strong"
|
|
138
|
-
: "hover:bg-neutral-background-medium"), children: [labelWithReveal, _jsx("button", { type: "button", "data-slot": "app-sidebar-group-toggle", "aria-label": `Toggle ${node.label}`, "aria-expanded": open, onClick: toggle, className: "flex items-center justify-center mr-3 ml-1 rounded p-0.5 text-neutral-foreground-subtle hover:bg-neutral-background-strong focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", children:
|
|
317
|
+
: "hover:bg-neutral-background-medium"), children: [labelWithReveal, isGroupItem && (_jsx("button", { type: "button", "data-slot": "app-sidebar-group-toggle", "aria-label": `Toggle ${node.label}`, "aria-expanded": open, onClick: toggle, className: "flex items-center justify-center mr-3 ml-1 rounded p-0.5 text-neutral-foreground-subtle hover:bg-neutral-background-strong focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", children: chevron }))] }), open &&
|
|
139
318
|
node.children.map((child) => renderNode(child, {
|
|
140
319
|
expanded,
|
|
141
320
|
depth: depth + 1,
|
|
@@ -144,10 +323,12 @@ function NavGroupRow({ node, expanded, depth, LinkEl, hasLinkComponent, activeIt
|
|
|
144
323
|
activeItemKey,
|
|
145
324
|
isMobile,
|
|
146
325
|
onNavigate,
|
|
326
|
+
hideIconFallback,
|
|
147
327
|
}))] }));
|
|
148
328
|
}
|
|
149
|
-
function NavSection({ node, expanded, depth, LinkEl, hasLinkComponent, activeItemKey, isMobile, onNavigate, }) {
|
|
150
|
-
|
|
329
|
+
function NavSection({ node, expanded, depth, LinkEl, hasLinkComponent, activeItemKey, isMobile, onNavigate, isFirst, }) {
|
|
330
|
+
const isEntities = node.variant === "entities";
|
|
331
|
+
return (_jsxs("div", { "data-slot": "app-sidebar-section", "data-variant": node.variant, className: cn(!node.compact && !isFirst && "pt-2", isEntities && !expanded && "mt-2 border-t border-border pt-2"), children: [node.title && (_jsx("div", { className: cn("px-2.5 pt-4 pb-1", SECTION_LABEL_TYPE, node.compact && "pt-0", !expanded && "sr-only"), children: node.title })), node.children.map((child) => renderNode(child, {
|
|
151
332
|
expanded,
|
|
152
333
|
depth,
|
|
153
334
|
LinkEl,
|
|
@@ -155,6 +336,7 @@ function NavSection({ node, expanded, depth, LinkEl, hasLinkComponent, activeIte
|
|
|
155
336
|
activeItemKey,
|
|
156
337
|
isMobile,
|
|
157
338
|
onNavigate,
|
|
339
|
+
hideIconFallback: isEntities && expanded,
|
|
158
340
|
}))] }));
|
|
159
341
|
}
|
|
160
342
|
function BackNavHeader({ backNav, expanded, LinkEl, }) {
|
|
@@ -177,33 +359,143 @@ function BackNavHeader({ backNav, expanded, LinkEl, }) {
|
|
|
177
359
|
const titleElement = expanded && titleTruncated ? (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: titleSpan }), _jsx(TooltipContent, { children: title })] })) : (titleSpan);
|
|
178
360
|
return (_jsxs("div", { "data-slot": "app-sidebar-back-nav", className: cn("flex h-10 shrink-0 items-center gap-1 border-b border-border", expanded ? "pl-1 pr-3" : "justify-center px-2"), children: [_jsx(BackComp, { ...(isLink ? { href: backHref } : { type: "button", onClick: onBack }), "data-slot": "app-sidebar-back", "aria-label": resolvedBackLabel, className: "flex size-8 shrink-0 items-center justify-center rounded-md text-neutral-foreground-subtle transition-colors hover:bg-neutral-background-strong hover:text-neutral-foreground-strong focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", children: _jsx(ChevronLeft, { "aria-hidden": true, className: "size-4" }) }), titleElement] }));
|
|
179
361
|
}
|
|
180
|
-
|
|
181
|
-
|
|
362
|
+
// Widths cross this component as CSS lengths. Resting values are authored in
|
|
363
|
+
// rem, but a live drag publishes px so the rail can sit between rem steps, so
|
|
364
|
+
// both spellings have to parse — reading "180px" as rem would report 2880.
|
|
365
|
+
function toPx(width) {
|
|
366
|
+
return width.trimEnd().endsWith("px")
|
|
367
|
+
? parseFloat(width)
|
|
368
|
+
: parseFloat(width) * 16;
|
|
182
369
|
}
|
|
183
|
-
|
|
370
|
+
/**
|
|
371
|
+
* Releasing a drag below this width closes the rail. It sits under the 14rem
|
|
372
|
+
* resting minimum so reaching it takes a deliberate push past the minimum
|
|
373
|
+
* rather than an ordinary narrow resize. Only used when the configured
|
|
374
|
+
* collapsed and minimum widths leave room for it — see `snapClosedBelowPx`.
|
|
375
|
+
*/
|
|
376
|
+
const SNAP_CLOSED_BELOW_PX = 180;
|
|
377
|
+
/** Pointer travel that separates a resize drag from a click on the edge. */
|
|
378
|
+
const DRAG_THRESHOLD_PX = 3;
|
|
379
|
+
/** Keyboard resize increment. */
|
|
380
|
+
const RESIZE_STEP_PX = 16;
|
|
381
|
+
/**
|
|
382
|
+
* The rail's right edge as one gesture surface: clicking toggles, dragging
|
|
383
|
+
* resizes, and releasing a drag below `SNAP_CLOSED_BELOW_PX` snaps it closed.
|
|
384
|
+
* Invisible at rest — the col-resize cursor is the whole affordance, and
|
|
385
|
+
* `SidebarCollapseToggle` beside it is what makes the action findable.
|
|
386
|
+
*
|
|
387
|
+
* That split is deliberate: only the separator carries the resize value range,
|
|
388
|
+
* and only the toggle carries expanded/collapsed state, so neither ends up
|
|
389
|
+
* with ARIA that contradicts its role, and the two have distinct accessible
|
|
390
|
+
* names rather than one shared between adjacent tab stops.
|
|
391
|
+
*/
|
|
392
|
+
function SidebarResizeSeparator({ expanded, setExpanded, restingWidth, collapsedWidth, minWidth, maxWidth, resizable, onResize, onPreviewWidth, resizeLabel, resizeValueTextLabel, }) {
|
|
184
393
|
const { t } = useC4Dictionary();
|
|
185
|
-
const minPx =
|
|
186
|
-
const maxPx =
|
|
187
|
-
const
|
|
188
|
-
//
|
|
189
|
-
|
|
394
|
+
const minPx = toPx(minWidth);
|
|
395
|
+
const maxPx = toPx(maxWidth);
|
|
396
|
+
const collapsedPx = toPx(collapsedWidth);
|
|
397
|
+
// Reads the live width mid-drag, since `restingWidth` carries the dragged
|
|
398
|
+
// value while one is in flight — so what is announced matches what is drawn.
|
|
399
|
+
const nowPx = Math.round(expanded ? toPx(restingWidth) : collapsedPx);
|
|
400
|
+
// Resting widths clamp to [min, max]. A drag travels further — down to the
|
|
401
|
+
// rail itself — because the snap-closed gesture needs to cross below min.
|
|
402
|
+
const toRestingRem = (px) => `${Math.round(Math.min(maxPx, Math.max(minPx, px)) / 16)}rem`;
|
|
403
|
+
// The designed threshold, but only while it genuinely sits between the rail
|
|
404
|
+
// and the resting minimum. A drag is floored at the rail, so a consumer
|
|
405
|
+
// raising `collapsedWidth` to 180px or beyond would put the threshold out of
|
|
406
|
+
// reach and silently cost them drag-to-collapse. Falling back to the midpoint
|
|
407
|
+
// leaves a usable closing zone at any configured pair of widths.
|
|
408
|
+
const snapClosedBelowPx = SNAP_CLOSED_BELOW_PX > collapsedPx && SNAP_CLOSED_BELOW_PX < minPx
|
|
409
|
+
? SNAP_CLOSED_BELOW_PX
|
|
410
|
+
: (collapsedPx + minPx) / 2;
|
|
190
411
|
const onPointerDown = (e) => {
|
|
191
412
|
e.preventDefault();
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
413
|
+
// Best-effort: the drag is driven by window listeners, so capture only
|
|
414
|
+
// hardens it against the pointer leaving the strip. It throws when the id
|
|
415
|
+
// belongs to no live pointer — a synthetic pointer from a test, or one
|
|
416
|
+
// already released — and that must not take the gesture down with it.
|
|
417
|
+
try {
|
|
418
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// no capture available; the window listeners still carry the drag
|
|
422
|
+
}
|
|
423
|
+
const startX = e.clientX;
|
|
424
|
+
// Captured before the drag mutates it, so aborting or snapping closed can
|
|
425
|
+
// hand the remembered width back untouched.
|
|
426
|
+
const widthBeforeDrag = restingWidth;
|
|
427
|
+
let moved = false;
|
|
428
|
+
let lastPx = nowPx;
|
|
429
|
+
const stopListening = () => {
|
|
195
430
|
window.removeEventListener("pointermove", onMove);
|
|
196
431
|
window.removeEventListener("pointerup", onUp);
|
|
432
|
+
window.removeEventListener("pointercancel", onCancel);
|
|
433
|
+
onPreviewWidth(null);
|
|
434
|
+
};
|
|
435
|
+
// The rail is fixed at left:0, so the pointer's clientX is the target width.
|
|
436
|
+
const onMove = (ev) => {
|
|
437
|
+
if (!moved) {
|
|
438
|
+
if (Math.abs(ev.clientX - startX) <= DRAG_THRESHOLD_PX)
|
|
439
|
+
return;
|
|
440
|
+
moved = true;
|
|
441
|
+
// Open on the first real movement so the edge tracks the pointer out of
|
|
442
|
+
// the collapsed state, instead of staying put and jumping at release.
|
|
443
|
+
if (!expanded)
|
|
444
|
+
setExpanded(true);
|
|
445
|
+
}
|
|
446
|
+
lastPx = Math.min(maxPx, Math.max(collapsedPx, ev.clientX));
|
|
447
|
+
const live = `${Math.round(lastPx)}px`;
|
|
448
|
+
onPreviewWidth(live);
|
|
449
|
+
// Published unclamped and live: a sibling <main> offset by
|
|
450
|
+
// --app-sidebar-width has to track the rail through the snap zone, and
|
|
451
|
+
// clamping here would leave it hanging at the resting minimum while the
|
|
452
|
+
// rail kept narrowing. The clamp belongs on the committed value only.
|
|
453
|
+
onResize(live);
|
|
454
|
+
};
|
|
455
|
+
const onUp = () => {
|
|
456
|
+
stopListening();
|
|
457
|
+
if (!moved) {
|
|
458
|
+
setExpanded(!expanded);
|
|
459
|
+
}
|
|
460
|
+
else if (lastPx < snapClosedBelowPx) {
|
|
461
|
+
onResize(widthBeforeDrag);
|
|
462
|
+
setExpanded(false);
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
onResize(toRestingRem(lastPx));
|
|
466
|
+
setExpanded(true);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
// A cancelled pointer (interrupted touch, browser gesture takeover) never
|
|
470
|
+
// fires pointerup. Without this the listeners outlive the gesture and the
|
|
471
|
+
// rail keeps resizing with nothing held down.
|
|
472
|
+
const onCancel = () => {
|
|
473
|
+
stopListening();
|
|
474
|
+
onResize(widthBeforeDrag);
|
|
197
475
|
};
|
|
198
476
|
window.addEventListener("pointermove", onMove);
|
|
199
477
|
window.addEventListener("pointerup", onUp);
|
|
478
|
+
window.addEventListener("pointercancel", onCancel);
|
|
200
479
|
};
|
|
201
|
-
const STEP = 16;
|
|
202
480
|
const onKeyDown = (e) => {
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
481
|
+
if (!expanded) {
|
|
482
|
+
// Collapsed, the only resize left is the one that reopens the rail.
|
|
483
|
+
if (e.key !== "ArrowRight" && e.key !== "Home" && e.key !== "End")
|
|
484
|
+
return;
|
|
485
|
+
setExpanded(true);
|
|
486
|
+
}
|
|
487
|
+
else if (e.key === "ArrowLeft") {
|
|
488
|
+
// Stepping left off the minimum closes the rail: the keyboard equivalent
|
|
489
|
+
// of dragging past the snap threshold, so the gesture is not drag-only
|
|
490
|
+
// (WCAG 2.5.1).
|
|
491
|
+
if (nowPx <= minPx)
|
|
492
|
+
setExpanded(false);
|
|
493
|
+
else
|
|
494
|
+
onResize(toRestingRem(nowPx - RESIZE_STEP_PX));
|
|
495
|
+
}
|
|
496
|
+
else if (e.key === "ArrowRight") {
|
|
497
|
+
onResize(toRestingRem(nowPx + RESIZE_STEP_PX));
|
|
498
|
+
}
|
|
207
499
|
else if (e.key === "Home")
|
|
208
500
|
onResize(minWidth);
|
|
209
501
|
else if (e.key === "End")
|
|
@@ -212,10 +504,31 @@ function SidebarResizeHandle({ width, minWidth, maxWidth, onResize, resizeLabel,
|
|
|
212
504
|
return;
|
|
213
505
|
e.preventDefault();
|
|
214
506
|
};
|
|
215
|
-
|
|
216
|
-
|
|
507
|
+
if (!resizable)
|
|
508
|
+
return null;
|
|
509
|
+
return (_jsx("div", { "data-slot": "app-sidebar-resize-handle", role: "separator", "aria-orientation": "vertical", "aria-label": resizeLabel ?? t("appSidebar.resize"), "aria-valuemin": Math.round(collapsedPx), "aria-valuemax": Math.round(maxPx), "aria-valuenow": nowPx, "aria-valuetext": resizeValueTextLabel ??
|
|
510
|
+
t("appSidebar.resizeValueText", { pixels: nowPx }), tabIndex: 0, onPointerDown: onPointerDown, onKeyDown: onKeyDown,
|
|
511
|
+
// Straddles the edge for a bigger target than the 1px border. No resting
|
|
512
|
+
// fill — the col-resize cursor is the affordance, and the button beside
|
|
513
|
+
// it is what makes the action visible.
|
|
514
|
+
className: "absolute top-0 -right-1.5 z-50 h-full w-3 cursor-col-resize select-none focus-visible:bg-ring focus-visible:outline-none" }));
|
|
217
515
|
}
|
|
218
|
-
|
|
516
|
+
/**
|
|
517
|
+
* The visible half of the edge: a circular toggle straddling the rail's border.
|
|
518
|
+
*
|
|
519
|
+
* Rendered as a sibling of the item list rather than of the whole rail, so it
|
|
520
|
+
* bottoms out at the top of the `footer` when there is one instead of sitting
|
|
521
|
+
* on top of it — the footer takes arbitrary consumer content, including
|
|
522
|
+
* right-aligned controls this would otherwise cover and intercept.
|
|
523
|
+
*/
|
|
524
|
+
function SidebarCollapseToggle({ expanded, setExpanded, expandLabel, collapseLabel, }) {
|
|
525
|
+
const label = expanded ? collapseLabel : expandLabel;
|
|
526
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "data-slot": "app-sidebar-collapse-toggle", "aria-label": label, "aria-expanded": expanded, onClick: () => setExpanded(!expanded),
|
|
527
|
+
// Above the separator strip so the pointer lands on the button rather
|
|
528
|
+
// than the resize cursor where the two overlap.
|
|
529
|
+
className: "absolute -right-4 bottom-3.5 z-[60] flex size-8 items-center justify-center rounded-full border border-border bg-background text-neutral-foreground-subtle shadow-xs transition-colors hover:bg-neutral-background-medium hover:text-neutral-foreground-strong focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(ChevronLeft, { "aria-hidden": true, className: cn("size-3.5 transition-transform duration-150", !expanded && "rotate-180") }) }) }), _jsx(TooltipContent, { side: "right", children: label })] }));
|
|
530
|
+
}
|
|
531
|
+
export function AppSidebar({ ariaLabel, items, backNav, expandedWidth = "16rem", collapsedWidth = "3.5rem", expandLabel, collapseLabel, resizable = true, minWidth = "14rem", maxWidth = "24rem", resizeLabel, resizeValueTextLabel, footer, linkComponent, closeNavigationLabel, activeItemKey, className, ...expandProps }) {
|
|
219
532
|
const ctx = useSidebar();
|
|
220
533
|
const standaloneIsMobile = useIsMobile();
|
|
221
534
|
const hasWarnedNoProvider = React.useRef(false);
|
|
@@ -251,23 +564,32 @@ export function AppSidebar({ ariaLabel, items, backNav, expandedWidth = "16rem",
|
|
|
251
564
|
const [localWidth, setLocalWidth] = React.useState(expandedWidth);
|
|
252
565
|
const resizedWidth = ctx ? ctx.width : resizable ? localWidth : expandedWidth;
|
|
253
566
|
const setResizedWidth = ctx ? ctx.setWidth : setLocalWidth;
|
|
567
|
+
// Live width while the edge is being dragged. Non-null takes over from the
|
|
568
|
+
// resting state so the rail can follow the pointer below its resting minimum,
|
|
569
|
+
// through the zone where releasing snaps it closed.
|
|
570
|
+
const [dragWidth, setDragWidth] = React.useState(null);
|
|
254
571
|
const isMobile = ctx?.isMobile ?? false;
|
|
255
572
|
const LinkEl = linkComponent ?? "a";
|
|
256
573
|
const hasLinkComponent = linkComponent !== undefined;
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
// the
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
React.
|
|
264
|
-
|
|
265
|
-
|
|
574
|
+
const [openFlyoutKey, setOpenFlyoutKey] = React.useState(null);
|
|
575
|
+
const flyoutSlot = React.useMemo(() => ({ openKey: openFlyoutKey, setOpenKey: setOpenFlyoutKey }), [openFlyoutKey]);
|
|
576
|
+
// Expanding the rail unmounts the flyouts without clearing the slot, so a
|
|
577
|
+
// later collapse would remount the last one still open, with no hover behind
|
|
578
|
+
// it. Resetting in an effect (rather than on the toggle) is what also covers
|
|
579
|
+
// an expansion driven by the provider or by a controlled `expanded` prop.
|
|
580
|
+
React.useEffect(() => {
|
|
581
|
+
// Resetting one key on an expansion change: there is no external system to
|
|
582
|
+
// sync with, and no `key` to reset by that would not remount every row.
|
|
583
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
584
|
+
if (expanded)
|
|
585
|
+
setOpenFlyoutKey(null);
|
|
586
|
+
}, [expanded]);
|
|
266
587
|
// Mobile: render the nav inside a left Sheet overlay (always full labels).
|
|
267
588
|
if (isMobile) {
|
|
268
|
-
return (_jsx(Sheet, { open: ctx?.mobileOpen ?? false, onOpenChange: (o) => ctx?.setMobileOpen(o), children: _jsxs(SheetContent, { side: "left", showCloseButton: false, className: "w-72 p-0", children: [_jsx(SheetTitle, { className: "sr-only", children: ariaLabel }), _jsxs("nav", { "data-slot": "app-sidebar", "aria-label": ariaLabel, className: "flex h-full flex-col bg-background", children: [_jsx("div", { className: "flex h-12 shrink-0 items-center justify-end px-2", children: _jsx(SheetClose, { asChild: true, children: _jsx("button", { type: "button", "aria-label": closeNavigationLabel ?? t("appSidebar.closeNavigation"), className: "flex size-8 items-center justify-center rounded-md text-neutral-foreground-subtle transition-colors hover:bg-neutral-background-strong hover:text-neutral-foreground-strong focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", children: _jsx(X, { "aria-hidden": true, className: "size-4" }) }) }) }), backNav && (_jsx(BackNavHeader, { backNav: backNav, expanded: true, LinkEl: LinkEl })), _jsx("div", { className: cn("flex flex-1 flex-col gap-0.5 overflow-y-auto px-2 py-2", HIDDEN_SCROLLBAR), children: items.map((node) => renderNode(node, {
|
|
589
|
+
return (_jsx(Sheet, { open: ctx?.mobileOpen ?? false, onOpenChange: (o) => ctx?.setMobileOpen(o), children: _jsxs(SheetContent, { side: "left", showCloseButton: false, className: "w-72 p-0", children: [_jsx(SheetTitle, { className: "sr-only", children: ariaLabel }), _jsxs("nav", { "data-slot": "app-sidebar", "aria-label": ariaLabel, className: "flex h-full flex-col bg-background", children: [_jsx("div", { className: "flex h-12 shrink-0 items-center justify-end px-2", children: _jsx(SheetClose, { asChild: true, children: _jsx("button", { type: "button", "aria-label": closeNavigationLabel ?? t("appSidebar.closeNavigation"), className: "flex size-8 items-center justify-center rounded-md text-neutral-foreground-subtle transition-colors hover:bg-neutral-background-strong hover:text-neutral-foreground-strong focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", children: _jsx(X, { "aria-hidden": true, className: "size-4" }) }) }) }), backNav && (_jsx(BackNavHeader, { backNav: backNav, expanded: true, LinkEl: LinkEl })), _jsx("div", { className: cn("flex flex-1 flex-col gap-0.5 overflow-y-auto px-2 py-2", HIDDEN_SCROLLBAR), children: items.map((node, index) => renderNode(node, {
|
|
269
590
|
expanded: true,
|
|
270
591
|
depth: 0,
|
|
592
|
+
isFirst: index === 0,
|
|
271
593
|
LinkEl,
|
|
272
594
|
hasLinkComponent,
|
|
273
595
|
activeItemKey,
|
|
@@ -277,23 +599,25 @@ export function AppSidebar({ ariaLabel, items, backNav, expandedWidth = "16rem",
|
|
|
277
599
|
})) }), footer && (_jsx("div", { className: "border-t border-border p-2", children: footer }))] })] }) }));
|
|
278
600
|
}
|
|
279
601
|
// Desktop: the fixed rail.
|
|
280
|
-
const width = expanded ? resizedWidth : collapsedWidth;
|
|
602
|
+
const width = dragWidth ?? (expanded ? resizedWidth : collapsedWidth);
|
|
281
603
|
// Shared with the tooltip below so the accessible name (aria-label) and the
|
|
282
604
|
// visible tooltip text never drift apart under a dictionary override.
|
|
283
605
|
const resolvedExpandLabel = expandLabel ?? t("appSidebar.expand");
|
|
284
606
|
const resolvedCollapseLabel = collapseLabel ?? t("appSidebar.collapse");
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
607
|
+
return (
|
|
608
|
+
// The flyout slot is provided only here: the mobile Sheet renders every row
|
|
609
|
+
// expanded, so no flyout is reachable on that path.
|
|
610
|
+
_jsx(FlyoutSlotContext.Provider, { value: flyoutSlot, children: _jsxs("nav", { "data-slot": "app-sidebar", "aria-label": ariaLabel, style: { width, ["--app-sidebar-width"]: width }, className: cn("fixed left-0 top-12 bottom-0 z-40 flex flex-col bg-background border-r border-border transition-[width] duration-150 ease-out",
|
|
611
|
+
// An animated width can't keep up with the pointer it is supposed to
|
|
612
|
+
// be following.
|
|
613
|
+
dragWidth && "transition-none", className), children: [backNav && (_jsx(BackNavHeader, { backNav: backNav, expanded: expanded, LinkEl: LinkEl })), _jsxs("div", { className: "relative flex min-h-0 flex-1 flex-col", children: [_jsx("div", { className: cn("flex-1 min-h-0 flex flex-col gap-0.5 px-2 py-2 overflow-y-auto overflow-x-hidden", HIDDEN_SCROLLBAR), children: items.map((node, index) => renderNode(node, {
|
|
614
|
+
expanded,
|
|
615
|
+
depth: 0,
|
|
616
|
+
isFirst: index === 0,
|
|
617
|
+
LinkEl,
|
|
618
|
+
hasLinkComponent,
|
|
619
|
+
activeItemKey,
|
|
620
|
+
isMobile: false,
|
|
621
|
+
})) }), _jsx(SidebarCollapseToggle, { expanded: expanded, setExpanded: setExpanded, expandLabel: resolvedExpandLabel, collapseLabel: resolvedCollapseLabel })] }), footer && _jsx("div", { className: "border-t border-border p-2", children: footer }), _jsx(SidebarResizeSeparator, { expanded: expanded, setExpanded: setExpanded, restingWidth: resizedWidth, collapsedWidth: collapsedWidth, minWidth: minWidth, maxWidth: maxWidth, resizable: resizable, onResize: setResizedWidth, onPreviewWidth: setDragWidth, resizeLabel: resizeLabel, resizeValueTextLabel: resizeValueTextLabel })] }) }));
|
|
298
622
|
}
|
|
299
623
|
//# sourceMappingURL=app-sidebar.js.map
|