@seedgrid/fe-components 0.2.9 → 2026.3.1
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/buttons/SgFloatActionButton.d.ts.map +1 -1
- package/dist/buttons/SgFloatActionButton.js +168 -38
- package/dist/commons/SgAvatar.d.ts +66 -0
- package/dist/commons/SgAvatar.d.ts.map +1 -0
- package/dist/commons/SgAvatar.js +136 -0
- package/dist/commons/SgSkeleton.d.ts +16 -0
- package/dist/commons/SgSkeleton.d.ts.map +1 -0
- package/dist/commons/SgSkeleton.js +58 -0
- package/dist/commons/SgToaster.d.ts +9 -0
- package/dist/commons/SgToaster.d.ts.map +1 -1
- package/dist/commons/SgToaster.js +86 -17
- package/dist/digits/discard-digit/SgDiscardDigit.d.ts +39 -0
- package/dist/digits/discard-digit/SgDiscardDigit.d.ts.map +1 -0
- package/dist/digits/discard-digit/SgDiscardDigit.js +303 -0
- package/dist/digits/discard-digit/index.d.ts +3 -0
- package/dist/digits/discard-digit/index.d.ts.map +1 -0
- package/dist/digits/discard-digit/index.js +1 -0
- package/dist/digits/fade-digit/SgFadeDigit.d.ts +27 -0
- package/dist/digits/fade-digit/SgFadeDigit.d.ts.map +1 -0
- package/dist/digits/fade-digit/SgFadeDigit.js +85 -0
- package/dist/digits/fade-digit/index.d.ts +3 -0
- package/dist/digits/fade-digit/index.d.ts.map +1 -0
- package/dist/digits/fade-digit/index.js +1 -0
- package/dist/digits/flip-digit/SgFlipDigit.d.ts +27 -0
- package/dist/digits/flip-digit/SgFlipDigit.d.ts.map +1 -0
- package/dist/digits/flip-digit/SgFlipDigit.js +70 -0
- package/dist/digits/flip-digit/index.d.ts.map +1 -0
- package/dist/digits/matrix-digit/SgMatrixDigit.d.ts +32 -0
- package/dist/digits/matrix-digit/SgMatrixDigit.d.ts.map +1 -0
- package/dist/digits/matrix-digit/SgMatrixDigit.js +86 -0
- package/dist/digits/matrix-digit/index.d.ts +3 -0
- package/dist/digits/matrix-digit/index.d.ts.map +1 -0
- package/dist/digits/matrix-digit/index.js +1 -0
- package/dist/digits/neon-digit/SgNeonDigit.d.ts +37 -0
- package/dist/digits/neon-digit/SgNeonDigit.d.ts.map +1 -0
- package/dist/digits/neon-digit/SgNeonDigit.js +59 -0
- package/dist/digits/neon-digit/index.d.ts +3 -0
- package/dist/digits/neon-digit/index.d.ts.map +1 -0
- package/dist/digits/neon-digit/index.js +1 -0
- package/dist/digits/roller3d-digit/SgRoller3DDigit.d.ts +37 -0
- package/dist/digits/roller3d-digit/SgRoller3DDigit.d.ts.map +1 -0
- package/dist/digits/roller3d-digit/SgRoller3DDigit.js +47 -0
- package/dist/digits/roller3d-digit/index.d.ts +3 -0
- package/dist/digits/roller3d-digit/index.d.ts.map +1 -0
- package/dist/digits/roller3d-digit/index.js +1 -0
- package/dist/environment/SgEnvironmentProvider.d.ts +1 -0
- package/dist/environment/SgEnvironmentProvider.d.ts.map +1 -1
- package/dist/environment/SgEnvironmentProvider.js +51 -12
- package/dist/gadgets/clock/SgClock.d.ts +3 -1
- package/dist/gadgets/clock/SgClock.d.ts.map +1 -1
- package/dist/gadgets/clock/SgClock.js +111 -180
- package/dist/gadgets/clock/SgTimeProvider.d.ts +1 -0
- package/dist/gadgets/clock/SgTimeProvider.d.ts.map +1 -1
- package/dist/gadgets/clock/SgTimeProvider.js +11 -4
- package/dist/gadgets/gauge/SgLinearGauge.d.ts +59 -0
- package/dist/gadgets/gauge/SgLinearGauge.d.ts.map +1 -0
- package/dist/gadgets/gauge/SgLinearGauge.js +258 -0
- package/dist/gadgets/gauge/SgRadialGauge.d.ts +73 -0
- package/dist/gadgets/gauge/SgRadialGauge.d.ts.map +1 -0
- package/dist/gadgets/gauge/SgRadialGauge.js +311 -0
- package/dist/gadgets/gauge/index.d.ts +5 -0
- package/dist/gadgets/gauge/index.d.ts.map +1 -0
- package/dist/gadgets/gauge/index.js +2 -0
- package/dist/gadgets/qr-code/SgQRCode.d.ts +25 -0
- package/dist/gadgets/qr-code/SgQRCode.d.ts.map +1 -0
- package/dist/gadgets/qr-code/SgQRCode.js +75 -0
- package/dist/gadgets/qr-code/index.d.ts +3 -0
- package/dist/gadgets/qr-code/index.d.ts.map +1 -0
- package/dist/gadgets/qr-code/index.js +1 -0
- package/dist/gadgets/string-animator/SgStringAnimator.d.ts +91 -0
- package/dist/gadgets/string-animator/SgStringAnimator.d.ts.map +1 -0
- package/dist/gadgets/string-animator/SgStringAnimator.js +145 -0
- package/dist/gadgets/string-animator/index.d.ts +3 -0
- package/dist/gadgets/string-animator/index.d.ts.map +1 -0
- package/dist/gadgets/string-animator/index.js +1 -0
- package/dist/i18n/en-US.json +9 -1
- package/dist/i18n/es.json +55 -47
- package/dist/i18n/index.d.ts +32 -0
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/pt-BR.json +9 -1
- package/dist/i18n/pt-PT.json +9 -1
- package/dist/index.d.ts +53 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -1
- package/dist/inputs/SgAutocomplete.js +21 -5
- package/dist/inputs/SgCombobox.d.ts +26 -0
- package/dist/inputs/SgCombobox.d.ts.map +1 -0
- package/dist/inputs/SgCombobox.js +354 -0
- package/dist/inputs/SgInputOTP.d.ts.map +1 -1
- package/dist/inputs/SgInputOTP.js +9 -2
- package/dist/inputs/SgRadioGroup.d.ts +37 -0
- package/dist/inputs/SgRadioGroup.d.ts.map +1 -0
- package/dist/inputs/SgRadioGroup.js +139 -0
- package/dist/inputs/SgRating.d.ts +55 -0
- package/dist/inputs/SgRating.d.ts.map +1 -0
- package/dist/inputs/SgRating.js +135 -0
- package/dist/inputs/SgSlider.d.ts +20 -0
- package/dist/inputs/SgSlider.d.ts.map +1 -0
- package/dist/inputs/SgSlider.js +40 -0
- package/dist/inputs/SgStepperInput.d.ts +22 -0
- package/dist/inputs/SgStepperInput.d.ts.map +1 -0
- package/dist/inputs/SgStepperInput.js +51 -0
- package/dist/inputs/SgTextEditor.d.ts +1 -0
- package/dist/inputs/SgTextEditor.d.ts.map +1 -1
- package/dist/inputs/SgTextEditor.js +19 -3
- package/dist/inputs/SgToggleSwitch.d.ts +36 -0
- package/dist/inputs/SgToggleSwitch.d.ts.map +1 -0
- package/dist/inputs/SgToggleSwitch.js +174 -0
- package/dist/layout/SgAccordion.d.ts +39 -0
- package/dist/layout/SgAccordion.d.ts.map +1 -0
- package/dist/layout/SgAccordion.js +116 -0
- package/dist/layout/SgBreadcrumb.d.ts +33 -0
- package/dist/layout/SgBreadcrumb.d.ts.map +1 -0
- package/dist/layout/SgBreadcrumb.js +121 -0
- package/dist/layout/SgCarousel.d.ts +43 -0
- package/dist/layout/SgCarousel.d.ts.map +1 -0
- package/dist/layout/SgCarousel.js +166 -0
- package/dist/layout/SgDockLayout.d.ts +14 -0
- package/dist/layout/SgDockLayout.d.ts.map +1 -1
- package/dist/layout/SgDockLayout.js +145 -13
- package/dist/layout/SgDockScreen.d.ts +15 -0
- package/dist/layout/SgDockScreen.d.ts.map +1 -0
- package/dist/layout/SgDockScreen.js +13 -0
- package/dist/layout/SgDockZone.d.ts.map +1 -1
- package/dist/layout/SgDockZone.js +36 -2
- package/dist/layout/SgExpandablePanel.d.ts +50 -0
- package/dist/layout/SgExpandablePanel.d.ts.map +1 -0
- package/dist/layout/SgExpandablePanel.js +302 -0
- package/dist/layout/SgMainPanel.d.ts.map +1 -1
- package/dist/layout/SgMainPanel.js +36 -14
- package/dist/layout/SgMenu.d.ts +91 -0
- package/dist/layout/SgMenu.d.ts.map +1 -0
- package/dist/layout/SgMenu.js +939 -0
- package/dist/layout/SgPageControl.d.ts +49 -0
- package/dist/layout/SgPageControl.d.ts.map +1 -0
- package/dist/layout/SgPageControl.js +152 -0
- package/dist/layout/SgPanel.d.ts.map +1 -1
- package/dist/layout/SgPanel.js +10 -1
- package/dist/layout/SgScreen.d.ts +2 -0
- package/dist/layout/SgScreen.d.ts.map +1 -1
- package/dist/layout/SgScreen.js +4 -2
- package/dist/layout/SgToolBar.d.ts +9 -3
- package/dist/layout/SgToolBar.d.ts.map +1 -1
- package/dist/layout/SgToolBar.js +461 -55
- package/dist/menus/SgDockMenu.d.ts +62 -0
- package/dist/menus/SgDockMenu.d.ts.map +1 -0
- package/dist/menus/SgDockMenu.js +480 -0
- package/dist/others/SgPlayground.js +73 -73
- package/package.json +72 -57
- package/dist/gadgets/flip-digit/SgFlipDigit.d.ts +0 -23
- package/dist/gadgets/flip-digit/SgFlipDigit.d.ts.map +0 -1
- package/dist/gadgets/flip-digit/SgFlipDigit.js +0 -118
- package/dist/gadgets/flip-digit/index.d.ts.map +0 -1
- /package/dist/{gadgets → digits}/flip-digit/index.d.ts +0 -0
- /package/dist/{gadgets → digits}/flip-digit/index.js +0 -0
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react";
|
|
6
|
+
import { SgAvatar } from "../commons/SgAvatar";
|
|
7
|
+
import { SgExpandablePanel } from "./SgExpandablePanel";
|
|
8
|
+
import { SgAutocomplete } from "../inputs/SgAutocomplete";
|
|
9
|
+
import { SgCard } from "./SgCard";
|
|
10
|
+
import { useSgDockLayout } from "./SgDockLayout";
|
|
11
|
+
import { useHasSgEnvironmentProvider, useSgPersistence } from "../environment/SgEnvironmentProvider";
|
|
12
|
+
const ROOT_PARENT_ID = "__sg_menu_root__";
|
|
13
|
+
function cn(...parts) {
|
|
14
|
+
return parts.filter(Boolean).join(" ");
|
|
15
|
+
}
|
|
16
|
+
function toCssSize(value, fallback) {
|
|
17
|
+
if (value === undefined || value === null)
|
|
18
|
+
return `${fallback}px`;
|
|
19
|
+
if (typeof value === "number")
|
|
20
|
+
return `${value}px`;
|
|
21
|
+
const trimmed = value.trim();
|
|
22
|
+
return trimmed.length > 0 ? trimmed : `${fallback}px`;
|
|
23
|
+
}
|
|
24
|
+
function normalizeMenuStyle(value) {
|
|
25
|
+
if (!value)
|
|
26
|
+
return "panel";
|
|
27
|
+
if (value === "PanelMenu")
|
|
28
|
+
return "panel";
|
|
29
|
+
if (value === "Tiered")
|
|
30
|
+
return "tiered";
|
|
31
|
+
if (value === "MegaMenuHorizontal")
|
|
32
|
+
return "mega-horizontal";
|
|
33
|
+
if (value === "MegaMenuVertical")
|
|
34
|
+
return "mega-vertical";
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
function resolveDockZoneFromOrientationDirection(orientationDirection) {
|
|
38
|
+
switch (orientationDirection) {
|
|
39
|
+
case "horizontal-right":
|
|
40
|
+
return "bottom";
|
|
41
|
+
case "horizontal-left":
|
|
42
|
+
return "top";
|
|
43
|
+
case "vertical-up":
|
|
44
|
+
case "vertical-top":
|
|
45
|
+
return "left";
|
|
46
|
+
case "vertical-down":
|
|
47
|
+
return "right";
|
|
48
|
+
default:
|
|
49
|
+
return "left";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function useControllableState(value, defaultValue, onChange) {
|
|
53
|
+
const [internal, setInternal] = React.useState(defaultValue);
|
|
54
|
+
const isControlled = value !== undefined;
|
|
55
|
+
const current = isControlled ? value : internal;
|
|
56
|
+
const currentRef = React.useRef(current);
|
|
57
|
+
React.useEffect(() => {
|
|
58
|
+
currentRef.current = current;
|
|
59
|
+
}, [current]);
|
|
60
|
+
const setValue = React.useCallback((next) => {
|
|
61
|
+
const base = currentRef.current;
|
|
62
|
+
const resolved = typeof next === "function" ? next(base) : next;
|
|
63
|
+
if (!isControlled)
|
|
64
|
+
setInternal(resolved);
|
|
65
|
+
onChange?.(resolved);
|
|
66
|
+
}, [isControlled, onChange]);
|
|
67
|
+
return [current, setValue];
|
|
68
|
+
}
|
|
69
|
+
function buildMenuMaps(menu) {
|
|
70
|
+
const parentById = new Map();
|
|
71
|
+
const nodeById = new Map();
|
|
72
|
+
const childrenByParent = new Map();
|
|
73
|
+
const firstByUrl = new Map();
|
|
74
|
+
const walk = (nodes, parentId) => {
|
|
75
|
+
childrenByParent.set(parentId, nodes.map((node) => node.id));
|
|
76
|
+
for (const node of nodes) {
|
|
77
|
+
parentById.set(node.id, parentId);
|
|
78
|
+
nodeById.set(node.id, node);
|
|
79
|
+
if (node.url && !firstByUrl.has(node.url))
|
|
80
|
+
firstByUrl.set(node.url, node.id);
|
|
81
|
+
if (node.children?.length)
|
|
82
|
+
walk(node.children, node.id);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
walk(menu, ROOT_PARENT_ID);
|
|
86
|
+
return { parentById, nodeById, childrenByParent, firstByUrl };
|
|
87
|
+
}
|
|
88
|
+
function filterMenuNodes(nodes, query) {
|
|
89
|
+
const q = query.trim().toLowerCase();
|
|
90
|
+
if (!q)
|
|
91
|
+
return nodes;
|
|
92
|
+
const walk = (list) => {
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const node of list) {
|
|
95
|
+
const selfMatch = node.label.toLowerCase().includes(q);
|
|
96
|
+
const children = node.children?.length ? walk(node.children) : [];
|
|
97
|
+
if (selfMatch || children.length > 0) {
|
|
98
|
+
out.push({
|
|
99
|
+
...node,
|
|
100
|
+
children: children.length > 0 ? children : undefined
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
};
|
|
106
|
+
return walk(nodes);
|
|
107
|
+
}
|
|
108
|
+
function collectMenuSearchEntries(nodes, trail = [], out = []) {
|
|
109
|
+
for (const node of nodes) {
|
|
110
|
+
const nextTrail = [...trail, node.label];
|
|
111
|
+
out.push({
|
|
112
|
+
id: node.id,
|
|
113
|
+
label: node.label,
|
|
114
|
+
path: nextTrail.join(" > "),
|
|
115
|
+
group: trail[0] ?? node.label
|
|
116
|
+
});
|
|
117
|
+
if (node.children?.length)
|
|
118
|
+
collectMenuSearchEntries(node.children, nextTrail, out);
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
function flattenVisibleNodes(nodes, expandedSet, collapsed, forceExpand, depth = 0, parentId = ROOT_PARENT_ID, out = []) {
|
|
123
|
+
for (const node of nodes) {
|
|
124
|
+
out.push({ node, depth, parentId });
|
|
125
|
+
if (collapsed)
|
|
126
|
+
continue;
|
|
127
|
+
const hasChildren = !!node.children?.length;
|
|
128
|
+
const isOpen = forceExpand || expandedSet.has(node.id);
|
|
129
|
+
if (hasChildren && isOpen) {
|
|
130
|
+
flattenVisibleNodes(node.children, expandedSet, collapsed, forceExpand, depth + 1, node.id, out);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
function collectParentChain(parentById, id) {
|
|
136
|
+
const out = [];
|
|
137
|
+
let current = parentById.get(id);
|
|
138
|
+
while (current && current !== ROOT_PARENT_ID) {
|
|
139
|
+
out.push(current);
|
|
140
|
+
current = parentById.get(current);
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
function firstChars(value, amount = 2) {
|
|
145
|
+
const normalized = value.trim().replace(/\s+/g, " ");
|
|
146
|
+
if (!normalized)
|
|
147
|
+
return "?";
|
|
148
|
+
const words = normalized.split(" ");
|
|
149
|
+
if (words.length >= 2) {
|
|
150
|
+
return `${words[0]?.[0] ?? ""}${words[1]?.[0] ?? ""}`.toUpperCase();
|
|
151
|
+
}
|
|
152
|
+
return normalized.slice(0, amount).toUpperCase();
|
|
153
|
+
}
|
|
154
|
+
function sameStringArray(a, b) {
|
|
155
|
+
if (a === b)
|
|
156
|
+
return true;
|
|
157
|
+
if (a.length !== b.length)
|
|
158
|
+
return false;
|
|
159
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
160
|
+
if (a[i] !== b[i])
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
function computeActiveSets(nodes, activeId, activeUrl) {
|
|
166
|
+
const exact = new Set();
|
|
167
|
+
const branch = new Set();
|
|
168
|
+
const walk = (list) => {
|
|
169
|
+
let found = false;
|
|
170
|
+
for (const node of list) {
|
|
171
|
+
const childFound = node.children?.length ? walk(node.children) : false;
|
|
172
|
+
const selfActive = (activeId ? node.id === activeId : false) ||
|
|
173
|
+
(activeUrl ? node.url === activeUrl : false);
|
|
174
|
+
if (selfActive)
|
|
175
|
+
exact.add(node.id);
|
|
176
|
+
if (childFound)
|
|
177
|
+
branch.add(node.id);
|
|
178
|
+
if (selfActive || childFound)
|
|
179
|
+
found = true;
|
|
180
|
+
}
|
|
181
|
+
return found;
|
|
182
|
+
};
|
|
183
|
+
walk(nodes);
|
|
184
|
+
return { exact, branch };
|
|
185
|
+
}
|
|
186
|
+
function elevationClass(elevation) {
|
|
187
|
+
if (elevation === "sm")
|
|
188
|
+
return "shadow-sm";
|
|
189
|
+
if (elevation === "md")
|
|
190
|
+
return "shadow-md";
|
|
191
|
+
return "";
|
|
192
|
+
}
|
|
193
|
+
function resolveIcon(node) {
|
|
194
|
+
return node.icon ?? null;
|
|
195
|
+
}
|
|
196
|
+
export function SgMenu(props) {
|
|
197
|
+
const { id, menu, selection, brand, user, userMenu, variant = "sidebar", menuStyle = "panel", position = "left", density = "comfortable", indent = 16, collapsedWidth = 72, expandedWidth = 280, overlaySize, overlayBackdrop, dockable = false, dockZone, draggable = false, orientationDirection, mode = "multiple", expandedIds: expandedIdsProp, defaultExpandedIds = [], onExpandedIdsChange, collapsed, defaultCollapsed = false, onCollapsedChange, showCollapseButton = false, open, defaultOpen = false, onOpenChange, closeOnNavigate, pinned, defaultPinned = false, onPinnedChange, showPinButton = false, onNavigate, onAction, onItemClick, ariaLabel = "Menu", keyboardNavigation = true, openSubmenuOnHover = false, search, elevation = "none", border = true, className, style, userSectionClassName, userSectionStyle, footer } = props;
|
|
198
|
+
const densityCfg = density === "compact"
|
|
199
|
+
? {
|
|
200
|
+
row: "min-h-8 px-2 py-1 text-xs",
|
|
201
|
+
icon: "size-4",
|
|
202
|
+
badge: "text-[10px] px-1.5 py-0.5",
|
|
203
|
+
section: "px-2 py-2",
|
|
204
|
+
gap: "gap-2"
|
|
205
|
+
}
|
|
206
|
+
: {
|
|
207
|
+
row: "min-h-10 px-3 py-1.5 text-sm",
|
|
208
|
+
icon: "size-5",
|
|
209
|
+
badge: "text-xs px-2 py-0.5",
|
|
210
|
+
section: "px-3 py-3",
|
|
211
|
+
gap: "gap-2.5"
|
|
212
|
+
};
|
|
213
|
+
const maps = React.useMemo(() => buildMenuMaps(menu), [menu]);
|
|
214
|
+
const resolvedMenuStyle = normalizeMenuStyle(menuStyle);
|
|
215
|
+
const dock = useSgDockLayout();
|
|
216
|
+
const autoId = React.useId();
|
|
217
|
+
const dockableId = React.useMemo(() => id ?? `sg-menu-${autoId.replace(/[:]/g, "")}`, [autoId, id]);
|
|
218
|
+
const dockMode = Boolean(dockable && dock && variant !== "drawer" && variant !== "hybrid");
|
|
219
|
+
const defaultDockZone = dockZone ?? resolveDockZoneFromOrientationDirection(orientationDirection);
|
|
220
|
+
const assignedDockZone = dockMode && dock ? dock.getToolbarZone(dockableId) : null;
|
|
221
|
+
const effectiveDockZone = dockMode ? assignedDockZone ?? defaultDockZone : null;
|
|
222
|
+
const portalTarget = dockMode && dock && effectiveDockZone ? dock.getZoneElement(effectiveDockZone) : null;
|
|
223
|
+
const [expandedIds, setExpandedIds] = useControllableState(expandedIdsProp, defaultExpandedIds, onExpandedIdsChange);
|
|
224
|
+
const expandedSet = React.useMemo(() => new Set(expandedIds), [expandedIds]);
|
|
225
|
+
const hasEnvironmentProvider = useHasSgEnvironmentProvider();
|
|
226
|
+
const { load: loadPersistedState, save: savePersistedState } = useSgPersistence();
|
|
227
|
+
const collapsePersistKey = id ? `sg-menu:${id}:collapsed` : null;
|
|
228
|
+
const isCollapsedPropControlled = collapsed !== undefined;
|
|
229
|
+
const [collapsedInternal, setCollapsedInternal] = React.useState(defaultCollapsed);
|
|
230
|
+
React.useEffect(() => {
|
|
231
|
+
if (!collapsePersistKey || isCollapsedPropControlled)
|
|
232
|
+
return;
|
|
233
|
+
let alive = true;
|
|
234
|
+
(async () => {
|
|
235
|
+
try {
|
|
236
|
+
let loaded;
|
|
237
|
+
if (hasEnvironmentProvider) {
|
|
238
|
+
loaded = await loadPersistedState(collapsePersistKey);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
const raw = localStorage.getItem(collapsePersistKey);
|
|
242
|
+
loaded = raw !== null ? (() => { try {
|
|
243
|
+
return JSON.parse(raw);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return null;
|
|
247
|
+
} })() : null;
|
|
248
|
+
}
|
|
249
|
+
if (!alive || loaded === null || loaded === undefined)
|
|
250
|
+
return;
|
|
251
|
+
if (typeof loaded === "boolean")
|
|
252
|
+
setCollapsedInternal(loaded);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// ignore
|
|
256
|
+
}
|
|
257
|
+
})();
|
|
258
|
+
return () => { alive = false; };
|
|
259
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
260
|
+
}, [collapsePersistKey]);
|
|
261
|
+
const collapsedState = isCollapsedPropControlled ? collapsed : collapsedInternal;
|
|
262
|
+
const setCollapsedState = React.useCallback((next) => {
|
|
263
|
+
if (!isCollapsedPropControlled) {
|
|
264
|
+
setCollapsedInternal(next);
|
|
265
|
+
if (collapsePersistKey) {
|
|
266
|
+
if (hasEnvironmentProvider) {
|
|
267
|
+
void savePersistedState(collapsePersistKey, next);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
try {
|
|
271
|
+
localStorage.setItem(collapsePersistKey, JSON.stringify(next));
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// ignore
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
onCollapsedChange?.(next);
|
|
280
|
+
}, [isCollapsedPropControlled, collapsePersistKey, hasEnvironmentProvider, savePersistedState, onCollapsedChange]);
|
|
281
|
+
const [drawerOpen, setDrawerOpen] = useControllableState(open, defaultOpen, onOpenChange);
|
|
282
|
+
const [pinnedState, setPinnedState] = useControllableState(pinned, defaultPinned, onPinnedChange);
|
|
283
|
+
const isCollapsed = variant === "drawer" ? false : variant === "hybrid" ? !pinnedState : collapsedState;
|
|
284
|
+
const collapsedWidthCss = toCssSize(collapsedWidth, 72);
|
|
285
|
+
const expandedWidthCss = toCssSize(expandedWidth, 280);
|
|
286
|
+
const resolvedOverlaySize = overlaySize ?? { default: variant === "drawer" ? 320 : expandedWidth, min: 240, max: 520 };
|
|
287
|
+
const overlayBackdropResolved = overlayBackdrop ?? (variant === "drawer");
|
|
288
|
+
const closeOnNavigateResolved = closeOnNavigate ?? true;
|
|
289
|
+
const searchEnabled = search?.enabled ?? false;
|
|
290
|
+
const [searchValue, setSearchValue] = React.useState("");
|
|
291
|
+
const menuRootRef = React.useRef(null);
|
|
292
|
+
const sidebarShellRef = React.useRef(null);
|
|
293
|
+
const [dockDragActive, setDockDragActive] = React.useState(false);
|
|
294
|
+
const [horizontalDockAlign, setHorizontalDockAlign] = React.useState(null);
|
|
295
|
+
const dockDragStartRef = React.useRef(null);
|
|
296
|
+
const dockDragMovedRef = React.useRef(false);
|
|
297
|
+
const dockHoverZoneRef = React.useRef(null);
|
|
298
|
+
const searchEntries = React.useMemo(() => collectMenuSearchEntries(menu), [menu]);
|
|
299
|
+
const autocompleteSource = React.useCallback((query) => {
|
|
300
|
+
const q = query.trim().toLowerCase();
|
|
301
|
+
const matches = !q
|
|
302
|
+
? searchEntries
|
|
303
|
+
: searchEntries.filter((entry) => entry.label.toLowerCase().includes(q) ||
|
|
304
|
+
entry.path.toLowerCase().includes(q));
|
|
305
|
+
return matches.slice(0, 120).map((entry) => ({
|
|
306
|
+
id: entry.id,
|
|
307
|
+
label: entry.label,
|
|
308
|
+
value: entry.path,
|
|
309
|
+
group: entry.group,
|
|
310
|
+
data: { nodeId: entry.id, path: entry.path }
|
|
311
|
+
}));
|
|
312
|
+
}, [searchEntries]);
|
|
313
|
+
const filteredMenu = React.useMemo(() => (searchEnabled ? filterMenuNodes(menu, searchValue) : menu), [menu, searchEnabled, searchValue]);
|
|
314
|
+
const hasSearch = searchEnabled && searchValue.trim().length > 0;
|
|
315
|
+
const effectiveMenuStyle = isCollapsed || hasSearch ? "panel" : resolvedMenuStyle;
|
|
316
|
+
const resolvedPosition = dockMode && effectiveDockZone
|
|
317
|
+
? effectiveDockZone === "right"
|
|
318
|
+
? "right"
|
|
319
|
+
: effectiveDockZone === "left"
|
|
320
|
+
? "left"
|
|
321
|
+
: orientationDirection === "horizontal-right"
|
|
322
|
+
? "right"
|
|
323
|
+
: "left"
|
|
324
|
+
: position;
|
|
325
|
+
const isHorizontalDockZone = effectiveDockZone === "top" || effectiveDockZone === "bottom";
|
|
326
|
+
const isVerticalDockZone = effectiveDockZone === "left" || effectiveDockZone === "right";
|
|
327
|
+
const tieredOpenToLeft = resolvedPosition === "right" ||
|
|
328
|
+
(isHorizontalDockZone && horizontalDockAlign === "right");
|
|
329
|
+
const [localActiveId, setLocalActiveId] = React.useState(selection?.activeId);
|
|
330
|
+
const [tieredPath, setTieredPath] = React.useState([]);
|
|
331
|
+
const [megaActiveId, setMegaActiveId] = React.useState(menu[0]?.id);
|
|
332
|
+
React.useEffect(() => {
|
|
333
|
+
if (selection?.activeId)
|
|
334
|
+
setLocalActiveId(selection.activeId);
|
|
335
|
+
}, [selection?.activeId]);
|
|
336
|
+
React.useEffect(() => {
|
|
337
|
+
if (menu.length === 0) {
|
|
338
|
+
setMegaActiveId(undefined);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (!megaActiveId || !menu.some((node) => node.id === megaActiveId)) {
|
|
342
|
+
setMegaActiveId(menu[0]?.id);
|
|
343
|
+
}
|
|
344
|
+
}, [megaActiveId, menu]);
|
|
345
|
+
React.useEffect(() => {
|
|
346
|
+
if (variant !== "hybrid")
|
|
347
|
+
return;
|
|
348
|
+
if (!pinnedState || !drawerOpen)
|
|
349
|
+
return;
|
|
350
|
+
setDrawerOpen(false);
|
|
351
|
+
}, [drawerOpen, pinnedState, setDrawerOpen, variant]);
|
|
352
|
+
React.useEffect(() => {
|
|
353
|
+
if (!dockMode || !dock)
|
|
354
|
+
return;
|
|
355
|
+
const orientation = defaultDockZone === "top" || defaultDockZone === "bottom" ? "horizontal" : "vertical";
|
|
356
|
+
dock.ensureToolbar(dockableId, {
|
|
357
|
+
zone: defaultDockZone,
|
|
358
|
+
collapsed: defaultCollapsed,
|
|
359
|
+
orientation
|
|
360
|
+
});
|
|
361
|
+
}, [defaultCollapsed, defaultDockZone, dock, dockMode, dockableId]);
|
|
362
|
+
React.useEffect(() => {
|
|
363
|
+
if (!dockDragActive)
|
|
364
|
+
return;
|
|
365
|
+
const previousBodyCursor = document.body.style.cursor;
|
|
366
|
+
const previousHtmlCursor = document.documentElement.style.cursor;
|
|
367
|
+
document.body.style.cursor = "grabbing";
|
|
368
|
+
document.documentElement.style.cursor = "grabbing";
|
|
369
|
+
return () => {
|
|
370
|
+
document.body.style.cursor = previousBodyCursor;
|
|
371
|
+
document.documentElement.style.cursor = previousHtmlCursor;
|
|
372
|
+
};
|
|
373
|
+
}, [dockDragActive]);
|
|
374
|
+
const applyDockDragVisual = React.useCallback((dx, dy) => {
|
|
375
|
+
const shell = sidebarShellRef.current;
|
|
376
|
+
if (!shell)
|
|
377
|
+
return;
|
|
378
|
+
shell.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
|
|
379
|
+
shell.style.willChange = "transform";
|
|
380
|
+
shell.style.zIndex = "1300";
|
|
381
|
+
}, []);
|
|
382
|
+
const clearDockDragVisual = React.useCallback(() => {
|
|
383
|
+
const shell = sidebarShellRef.current;
|
|
384
|
+
if (!shell)
|
|
385
|
+
return;
|
|
386
|
+
shell.style.transform = "";
|
|
387
|
+
shell.style.willChange = "";
|
|
388
|
+
shell.style.zIndex = "";
|
|
389
|
+
}, []);
|
|
390
|
+
React.useEffect(() => () => {
|
|
391
|
+
clearDockDragVisual();
|
|
392
|
+
}, [clearDockDragVisual]);
|
|
393
|
+
const effectiveActiveId = selection?.activeId ??
|
|
394
|
+
(selection?.activeUrl ? maps.firstByUrl.get(selection.activeUrl) : undefined) ??
|
|
395
|
+
localActiveId;
|
|
396
|
+
const effectiveActiveUrl = selection?.activeUrl;
|
|
397
|
+
const activeSets = React.useMemo(() => computeActiveSets(menu, effectiveActiveId, effectiveActiveUrl), [menu, effectiveActiveId, effectiveActiveUrl]);
|
|
398
|
+
React.useEffect(() => {
|
|
399
|
+
if (!effectiveActiveId)
|
|
400
|
+
return;
|
|
401
|
+
const chain = collectParentChain(maps.parentById, effectiveActiveId);
|
|
402
|
+
if (chain.length === 0)
|
|
403
|
+
return;
|
|
404
|
+
setExpandedIds((prev) => {
|
|
405
|
+
const next = Array.from(new Set([...prev, ...chain]));
|
|
406
|
+
return sameStringArray(prev, next) ? prev : next;
|
|
407
|
+
});
|
|
408
|
+
}, [effectiveActiveId, maps.parentById, setExpandedIds]);
|
|
409
|
+
React.useEffect(() => {
|
|
410
|
+
if (!effectiveActiveId)
|
|
411
|
+
return;
|
|
412
|
+
const parentChain = collectParentChain(maps.parentById, effectiveActiveId).reverse();
|
|
413
|
+
const activeNode = maps.nodeById.get(effectiveActiveId);
|
|
414
|
+
const nextTieredPath = [...parentChain];
|
|
415
|
+
if (activeNode?.children?.length)
|
|
416
|
+
nextTieredPath.push(activeNode.id);
|
|
417
|
+
setTieredPath(nextTieredPath);
|
|
418
|
+
setMegaActiveId(parentChain[0] ?? activeNode?.id ?? menu[0]?.id);
|
|
419
|
+
}, [effectiveActiveId, maps.nodeById, maps.parentById, menu]);
|
|
420
|
+
React.useEffect(() => {
|
|
421
|
+
if (effectiveMenuStyle !== "tiered")
|
|
422
|
+
return;
|
|
423
|
+
const handlePointerDown = (event) => {
|
|
424
|
+
const target = event.target;
|
|
425
|
+
if (!(target instanceof Node))
|
|
426
|
+
return;
|
|
427
|
+
if (menuRootRef.current?.contains(target))
|
|
428
|
+
return;
|
|
429
|
+
setTieredPath((prev) => (prev.length > 0 ? [] : prev));
|
|
430
|
+
};
|
|
431
|
+
document.addEventListener("pointerdown", handlePointerDown);
|
|
432
|
+
return () => document.removeEventListener("pointerdown", handlePointerDown);
|
|
433
|
+
}, [effectiveMenuStyle]);
|
|
434
|
+
const toggleExpanded = React.useCallback((nodeId) => {
|
|
435
|
+
setExpandedIds((prev) => {
|
|
436
|
+
const next = new Set(prev);
|
|
437
|
+
const isOpen = next.has(nodeId);
|
|
438
|
+
if (isOpen) {
|
|
439
|
+
next.delete(nodeId);
|
|
440
|
+
return Array.from(next);
|
|
441
|
+
}
|
|
442
|
+
if (mode === "accordion") {
|
|
443
|
+
const parentId = maps.parentById.get(nodeId) ?? ROOT_PARENT_ID;
|
|
444
|
+
const siblings = maps.childrenByParent.get(parentId) ?? [];
|
|
445
|
+
for (const siblingId of siblings) {
|
|
446
|
+
if (siblingId !== nodeId)
|
|
447
|
+
next.delete(siblingId);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
next.add(nodeId);
|
|
451
|
+
return Array.from(next);
|
|
452
|
+
});
|
|
453
|
+
}, [maps.childrenByParent, maps.parentById, mode, setExpandedIds]);
|
|
454
|
+
const activateNode = React.useCallback((node) => {
|
|
455
|
+
if (node.disabled)
|
|
456
|
+
return;
|
|
457
|
+
onItemClick?.(node);
|
|
458
|
+
setLocalActiveId(node.id);
|
|
459
|
+
const hasUrl = typeof node.url === "string" && node.url.length > 0;
|
|
460
|
+
if (hasUrl) {
|
|
461
|
+
if (onNavigate)
|
|
462
|
+
onNavigate(node);
|
|
463
|
+
else if (typeof window !== "undefined")
|
|
464
|
+
window.location.assign(node.url);
|
|
465
|
+
}
|
|
466
|
+
if (node.onClick) {
|
|
467
|
+
node.onClick();
|
|
468
|
+
onAction?.(node);
|
|
469
|
+
}
|
|
470
|
+
if ((variant === "drawer" || variant === "hybrid") && hasUrl && closeOnNavigateResolved && !pinnedState) {
|
|
471
|
+
setDrawerOpen(false);
|
|
472
|
+
}
|
|
473
|
+
}, [
|
|
474
|
+
closeOnNavigateResolved,
|
|
475
|
+
onAction,
|
|
476
|
+
onItemClick,
|
|
477
|
+
onNavigate,
|
|
478
|
+
pinnedState,
|
|
479
|
+
setDrawerOpen,
|
|
480
|
+
variant
|
|
481
|
+
]);
|
|
482
|
+
const handleDockDragPointerDown = React.useCallback((event) => {
|
|
483
|
+
if (event.button !== 0)
|
|
484
|
+
return;
|
|
485
|
+
if (!dockMode || !dock || !draggable || !effectiveDockZone)
|
|
486
|
+
return;
|
|
487
|
+
event.preventDefault();
|
|
488
|
+
event.stopPropagation();
|
|
489
|
+
const shellRectBeforeDrag = sidebarShellRef.current?.getBoundingClientRect() ?? null;
|
|
490
|
+
dock.setDropPreviewActive(true);
|
|
491
|
+
setDockDragActive(true);
|
|
492
|
+
dockDragStartRef.current = { x: event.clientX, y: event.clientY };
|
|
493
|
+
dockDragMovedRef.current = false;
|
|
494
|
+
dockHoverZoneRef.current = dock.getZoneAtPoint(event.clientX, event.clientY) ?? effectiveDockZone;
|
|
495
|
+
applyDockDragVisual(0, 0);
|
|
496
|
+
if (shellRectBeforeDrag) {
|
|
497
|
+
window.requestAnimationFrame(() => {
|
|
498
|
+
const shell = sidebarShellRef.current;
|
|
499
|
+
const start = dockDragStartRef.current;
|
|
500
|
+
if (!shell || !start)
|
|
501
|
+
return;
|
|
502
|
+
const shellRectAfterDrag = shell.getBoundingClientRect();
|
|
503
|
+
const adjustDx = shellRectBeforeDrag.left - shellRectAfterDrag.left;
|
|
504
|
+
const adjustDy = shellRectBeforeDrag.top - shellRectAfterDrag.top;
|
|
505
|
+
if (Math.abs(adjustDx) < 0.5 && Math.abs(adjustDy) < 0.5)
|
|
506
|
+
return;
|
|
507
|
+
start.x -= adjustDx;
|
|
508
|
+
start.y -= adjustDy;
|
|
509
|
+
applyDockDragVisual(adjustDx, adjustDy);
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
const handleMove = (moveEvent) => {
|
|
513
|
+
const start = dockDragStartRef.current;
|
|
514
|
+
if (!start)
|
|
515
|
+
return;
|
|
516
|
+
const dx = moveEvent.clientX - start.x;
|
|
517
|
+
const dy = moveEvent.clientY - start.y;
|
|
518
|
+
if (Math.abs(dx) > 3 || Math.abs(dy) > 3)
|
|
519
|
+
dockDragMovedRef.current = true;
|
|
520
|
+
applyDockDragVisual(dx, dy);
|
|
521
|
+
dockHoverZoneRef.current = dock.getZoneAtPoint(moveEvent.clientX, moveEvent.clientY);
|
|
522
|
+
};
|
|
523
|
+
const handleEnd = (upEvent) => {
|
|
524
|
+
window.removeEventListener("pointermove", handleMove);
|
|
525
|
+
window.removeEventListener("pointerup", handleEnd);
|
|
526
|
+
window.removeEventListener("pointercancel", handleEnd);
|
|
527
|
+
setDockDragActive(false);
|
|
528
|
+
dock.setDropPreviewActive(false);
|
|
529
|
+
clearDockDragVisual();
|
|
530
|
+
if (!dockDragStartRef.current)
|
|
531
|
+
return;
|
|
532
|
+
dockDragStartRef.current = null;
|
|
533
|
+
if (!dockDragMovedRef.current) {
|
|
534
|
+
dockHoverZoneRef.current = null;
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const zone = dockHoverZoneRef.current ?? dock.getZoneAtPoint(upEvent.clientX, upEvent.clientY);
|
|
538
|
+
dockHoverZoneRef.current = null;
|
|
539
|
+
if (zone) {
|
|
540
|
+
dock.moveToolbar(dockableId, zone);
|
|
541
|
+
if (zone === "top" || zone === "bottom") {
|
|
542
|
+
const zoneEl = dock.getZoneElement(zone);
|
|
543
|
+
if (zoneEl) {
|
|
544
|
+
const zoneRect = zoneEl.getBoundingClientRect();
|
|
545
|
+
setHorizontalDockAlign(upEvent.clientX < zoneRect.left + zoneRect.width / 2 ? "left" : "right");
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
setHorizontalDockAlign(null);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
window.addEventListener("pointermove", handleMove);
|
|
554
|
+
window.addEventListener("pointerup", handleEnd);
|
|
555
|
+
window.addEventListener("pointercancel", handleEnd);
|
|
556
|
+
}, [applyDockDragVisual, clearDockDragVisual, dock, dockMode, dockableId, draggable, effectiveDockZone, setHorizontalDockAlign]);
|
|
557
|
+
const visibleNodes = React.useMemo(() => effectiveMenuStyle === "panel"
|
|
558
|
+
? flattenVisibleNodes(filteredMenu, expandedSet, isCollapsed, hasSearch)
|
|
559
|
+
: [], [effectiveMenuStyle, expandedSet, filteredMenu, hasSearch, isCollapsed]);
|
|
560
|
+
const searchInputId = React.useId();
|
|
561
|
+
const itemRefs = React.useRef({});
|
|
562
|
+
const [focusedId, setFocusedId] = React.useState(undefined);
|
|
563
|
+
React.useEffect(() => {
|
|
564
|
+
if (visibleNodes.length === 0) {
|
|
565
|
+
setFocusedId(undefined);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const exists = visibleNodes.some((item) => item.node.id === focusedId);
|
|
569
|
+
if (!exists)
|
|
570
|
+
setFocusedId(visibleNodes[0]?.node.id);
|
|
571
|
+
}, [focusedId, visibleNodes]);
|
|
572
|
+
const focusById = React.useCallback((id) => {
|
|
573
|
+
if (!id)
|
|
574
|
+
return;
|
|
575
|
+
itemRefs.current[id]?.focus?.();
|
|
576
|
+
setFocusedId(id);
|
|
577
|
+
}, []);
|
|
578
|
+
const onListKeyDown = React.useCallback((event) => {
|
|
579
|
+
if (!keyboardNavigation)
|
|
580
|
+
return;
|
|
581
|
+
if (visibleNodes.length === 0)
|
|
582
|
+
return;
|
|
583
|
+
const currentIndex = visibleNodes.findIndex((item) => item.node.id === focusedId);
|
|
584
|
+
const activeIndex = currentIndex >= 0 ? currentIndex : 0;
|
|
585
|
+
const current = visibleNodes[activeIndex];
|
|
586
|
+
if (!current)
|
|
587
|
+
return;
|
|
588
|
+
const node = current.node;
|
|
589
|
+
const hasChildren = !!node.children?.length;
|
|
590
|
+
const isOpen = hasSearch || expandedSet.has(node.id);
|
|
591
|
+
if (event.key === "ArrowDown") {
|
|
592
|
+
event.preventDefault();
|
|
593
|
+
const next = visibleNodes[Math.min(activeIndex + 1, visibleNodes.length - 1)];
|
|
594
|
+
focusById(next?.node.id);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (event.key === "ArrowUp") {
|
|
598
|
+
event.preventDefault();
|
|
599
|
+
const next = visibleNodes[Math.max(activeIndex - 1, 0)];
|
|
600
|
+
focusById(next?.node.id);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (event.key === "ArrowRight") {
|
|
604
|
+
if (hasChildren && !isCollapsed && !isOpen && !hasSearch) {
|
|
605
|
+
event.preventDefault();
|
|
606
|
+
toggleExpanded(node.id);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (hasChildren && !isCollapsed) {
|
|
610
|
+
event.preventDefault();
|
|
611
|
+
const next = visibleNodes[activeIndex + 1];
|
|
612
|
+
if (next && next.depth > current.depth)
|
|
613
|
+
focusById(next.node.id);
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (event.key === "ArrowLeft") {
|
|
618
|
+
if (hasChildren && !isCollapsed && isOpen && !hasSearch) {
|
|
619
|
+
event.preventDefault();
|
|
620
|
+
toggleExpanded(node.id);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
event.preventDefault();
|
|
624
|
+
const parentId = maps.parentById.get(node.id);
|
|
625
|
+
if (parentId && parentId !== ROOT_PARENT_ID)
|
|
626
|
+
focusById(parentId);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
630
|
+
event.preventDefault();
|
|
631
|
+
if (hasChildren && !node.url && !node.onClick && !isCollapsed) {
|
|
632
|
+
toggleExpanded(node.id);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
activateNode(node);
|
|
636
|
+
}
|
|
637
|
+
}, [
|
|
638
|
+
activateNode,
|
|
639
|
+
expandedSet,
|
|
640
|
+
focusById,
|
|
641
|
+
focusedId,
|
|
642
|
+
hasSearch,
|
|
643
|
+
isCollapsed,
|
|
644
|
+
keyboardNavigation,
|
|
645
|
+
maps.parentById,
|
|
646
|
+
toggleExpanded,
|
|
647
|
+
visibleNodes
|
|
648
|
+
]);
|
|
649
|
+
const renderNode = React.useCallback((node, depth) => {
|
|
650
|
+
const hasChildren = !!node.children?.length;
|
|
651
|
+
const openNow = hasSearch || expandedSet.has(node.id);
|
|
652
|
+
const isExactActive = activeSets.exact.has(node.id);
|
|
653
|
+
const isBranchActive = activeSets.branch.has(node.id);
|
|
654
|
+
const disabled = !!node.disabled;
|
|
655
|
+
const iconNode = resolveIcon(node);
|
|
656
|
+
const hideChildren = isCollapsed;
|
|
657
|
+
return (_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: cn("group flex items-center rounded-md", densityCfg.row, densityCfg.gap, isExactActive
|
|
658
|
+
? "bg-primary/15 text-primary"
|
|
659
|
+
: isBranchActive
|
|
660
|
+
? "bg-muted/60 text-foreground"
|
|
661
|
+
: "text-foreground hover:bg-muted/60", disabled ? "cursor-not-allowed opacity-55" : ""), style: isCollapsed ? undefined : { paddingLeft: 8 + depth * indent }, title: isCollapsed ? node.label : undefined, children: [_jsxs("button", { ref: (el) => {
|
|
662
|
+
itemRefs.current[node.id] = el;
|
|
663
|
+
}, "data-sg-menu-node": node.id, type: "button", "aria-disabled": disabled || undefined, "aria-current": isExactActive ? "page" : undefined, onFocus: () => setFocusedId(node.id), onClick: () => {
|
|
664
|
+
if (disabled)
|
|
665
|
+
return;
|
|
666
|
+
if (hasChildren && !node.url && !node.onClick && !isCollapsed) {
|
|
667
|
+
toggleExpanded(node.id);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
activateNode(node);
|
|
671
|
+
}, className: cn("min-w-0 flex-1 rounded-md", "flex items-center", densityCfg.gap, "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35"), children: [_jsx("span", { className: cn("inline-flex shrink-0 items-center justify-center rounded", densityCfg.icon, iconNode ? "" : "bg-muted text-[10px] font-semibold"), children: iconNode ?? firstChars(node.label, 1) }), !isCollapsed ? _jsx("span", { className: "min-w-0 flex-1 truncate", children: node.label }) : null, !isCollapsed && node.badge !== undefined ? (_jsx("span", { className: cn("inline-flex shrink-0 items-center justify-center rounded-full border border-border bg-muted text-muted-foreground", densityCfg.badge), children: node.badge })) : null] }), !isCollapsed && hasChildren ? (_jsx("button", { type: "button", "aria-label": openNow ? "Collapse group" : "Expand group", onClick: () => !disabled && toggleExpanded(node.id), className: cn("inline-flex size-7 shrink-0 items-center justify-center rounded", "hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35"), children: openNow ? _jsx(ChevronDown, { className: "size-4" }) : _jsx(ChevronRight, { className: "size-4" }) })) : null] }), !hideChildren && hasChildren && openNow ? (_jsx("div", { className: "mt-0.5 space-y-0.5", children: node.children?.map((child) => renderNode(child, depth + 1)) })) : null] }, node.id));
|
|
672
|
+
}, [
|
|
673
|
+
activateNode,
|
|
674
|
+
activeSets.branch,
|
|
675
|
+
activeSets.exact,
|
|
676
|
+
densityCfg.badge,
|
|
677
|
+
densityCfg.gap,
|
|
678
|
+
densityCfg.icon,
|
|
679
|
+
densityCfg.row,
|
|
680
|
+
expandedSet,
|
|
681
|
+
hasSearch,
|
|
682
|
+
indent,
|
|
683
|
+
isCollapsed,
|
|
684
|
+
toggleExpanded
|
|
685
|
+
]);
|
|
686
|
+
const renderFlatAction = React.useCallback((node, className) => {
|
|
687
|
+
const isExactActive = activeSets.exact.has(node.id);
|
|
688
|
+
const isBranchActive = activeSets.branch.has(node.id);
|
|
689
|
+
const hasChildren = !!node.children?.length;
|
|
690
|
+
const iconNode = resolveIcon(node);
|
|
691
|
+
return (_jsxs("button", { type: "button", disabled: node.disabled, "aria-current": isExactActive ? "page" : undefined, onClick: () => {
|
|
692
|
+
if (node.disabled)
|
|
693
|
+
return;
|
|
694
|
+
if (hasChildren && !node.url && !node.onClick)
|
|
695
|
+
return;
|
|
696
|
+
activateNode(node);
|
|
697
|
+
}, className: cn("flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left text-sm transition-colors", isExactActive
|
|
698
|
+
? "bg-primary/15 text-primary"
|
|
699
|
+
: isBranchActive
|
|
700
|
+
? "bg-muted/60 text-foreground"
|
|
701
|
+
: "text-foreground hover:bg-muted/60", node.disabled ? "cursor-not-allowed opacity-55" : "", className), children: [_jsx("span", { className: cn("inline-flex shrink-0 items-center justify-center rounded", densityCfg.icon, iconNode ? "" : "bg-muted text-[10px] font-semibold"), children: iconNode ?? firstChars(node.label, 1) }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: node.label }), hasChildren ? _jsx(ChevronRight, { className: "size-4 shrink-0 text-muted-foreground" }) : null] }, node.id));
|
|
702
|
+
}, [activateNode, activeSets.branch, activeSets.exact, densityCfg.icon]);
|
|
703
|
+
const renderTieredLevels = React.useCallback((nodes, depth) => {
|
|
704
|
+
if (!nodes.length)
|
|
705
|
+
return null;
|
|
706
|
+
const activeIdAtDepth = tieredPath[depth];
|
|
707
|
+
const activeNode = nodes.find((node) => node.id === activeIdAtDepth);
|
|
708
|
+
return (_jsxs("div", { className: cn(depth === 0
|
|
709
|
+
? "relative w-full"
|
|
710
|
+
: cn("absolute top-0 min-w-[220px]", tieredOpenToLeft ? "right-full mr-1" : "left-full ml-1"), depth > 0 ? "z-20" : ""), children: [_jsx("div", { className: "rounded-md border border-border bg-background p-1 shadow-sm", children: nodes.map((node) => {
|
|
711
|
+
const hasChildren = !!node.children?.length;
|
|
712
|
+
const isOpen = activeIdAtDepth === node.id;
|
|
713
|
+
const isExactActive = activeSets.exact.has(node.id);
|
|
714
|
+
const iconNode = resolveIcon(node);
|
|
715
|
+
return (_jsxs("button", { type: "button", disabled: node.disabled, "aria-current": isExactActive ? "page" : undefined, onMouseEnter: () => {
|
|
716
|
+
if (!openSubmenuOnHover)
|
|
717
|
+
return;
|
|
718
|
+
if (!hasChildren) {
|
|
719
|
+
setTieredPath((prev) => prev.slice(0, depth));
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
setTieredPath((prev) => [...prev.slice(0, depth), node.id]);
|
|
723
|
+
}, onClick: () => {
|
|
724
|
+
if (node.disabled)
|
|
725
|
+
return;
|
|
726
|
+
if (hasChildren) {
|
|
727
|
+
const isOpenAtDepth = tieredPath[depth] === node.id;
|
|
728
|
+
setTieredPath((prev) => {
|
|
729
|
+
const base = prev.slice(0, depth);
|
|
730
|
+
return isOpenAtDepth ? base : [...base, node.id];
|
|
731
|
+
});
|
|
732
|
+
if (isOpenAtDepth)
|
|
733
|
+
return;
|
|
734
|
+
if (!node.url && !node.onClick)
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
activateNode(node);
|
|
738
|
+
}, className: cn("flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left text-sm transition-colors", isExactActive ? "bg-primary/15 text-primary" : isOpen ? "bg-muted/70" : "hover:bg-muted/60", node.disabled ? "cursor-not-allowed opacity-55" : ""), children: [_jsx("span", { className: cn("inline-flex shrink-0 items-center justify-center rounded", densityCfg.icon, iconNode ? "" : "bg-muted text-[10px] font-semibold"), children: iconNode ?? firstChars(node.label, 1) }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: node.label }), hasChildren ? (tieredOpenToLeft ? (_jsx(ChevronLeft, { className: "size-4 shrink-0 text-muted-foreground" })) : (_jsx(ChevronRight, { className: "size-4 shrink-0 text-muted-foreground" }))) : null] }, node.id));
|
|
739
|
+
}) }), activeNode?.children?.length
|
|
740
|
+
? renderTieredLevels(activeNode.children, depth + 1)
|
|
741
|
+
: null] }));
|
|
742
|
+
}, [activateNode, activeSets.exact, densityCfg.icon, openSubmenuOnHover, tieredOpenToLeft, tieredPath]);
|
|
743
|
+
const renderMegaColumns = React.useCallback((nodes) => {
|
|
744
|
+
if (!nodes.length) {
|
|
745
|
+
return _jsx("div", { className: "text-sm text-muted-foreground", children: "No items found." });
|
|
746
|
+
}
|
|
747
|
+
return (_jsx("div", { className: "grid gap-6 sm:grid-cols-2 lg:grid-cols-3", children: nodes.map((group) => (_jsx("div", { className: "space-y-3", children: group.children?.length ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "text-base font-semibold", children: group.label }), _jsx("div", { className: "space-y-1", children: group.children.map((item) => item.children?.length ? (_jsxs("div", { className: "space-y-1 pt-1", children: [_jsx("p", { className: "text-sm font-semibold", children: item.label }), _jsx("div", { className: "space-y-1 pl-1", children: item.children.map((leaf) => renderFlatAction(leaf, "px-1.5 py-1.5 text-sm")) })] }, item.id)) : (renderFlatAction(item, "px-1.5 py-1.5 text-sm"))) })] })) : (renderFlatAction(group, "px-1.5 py-1.5 text-sm")) }, group.id))) }));
|
|
748
|
+
}, [renderFlatAction]);
|
|
749
|
+
const renderMenuTree = (items) => {
|
|
750
|
+
if (items.length === 0) {
|
|
751
|
+
return _jsx("div", { className: "px-3 py-2 text-xs text-muted-foreground", children: "No items found." });
|
|
752
|
+
}
|
|
753
|
+
if (effectiveMenuStyle === "panel") {
|
|
754
|
+
return _jsx("div", { className: "space-y-0.5", children: items.map((node) => renderNode(node, 0)) });
|
|
755
|
+
}
|
|
756
|
+
if (effectiveMenuStyle === "tiered") {
|
|
757
|
+
return _jsx("div", { className: "relative min-h-[120px]", children: renderTieredLevels(items, 0) });
|
|
758
|
+
}
|
|
759
|
+
const activeMegaNode = items.find((node) => node.id === megaActiveId) ??
|
|
760
|
+
items[0];
|
|
761
|
+
if (effectiveMenuStyle === "mega-horizontal") {
|
|
762
|
+
return (_jsxs("div", { className: "rounded-md border border-border bg-background", children: [_jsx("div", { className: "flex flex-wrap items-center gap-1 border-b border-border p-1", children: items.map((node) => {
|
|
763
|
+
const hasChildren = !!node.children?.length;
|
|
764
|
+
const active = activeMegaNode?.id === node.id;
|
|
765
|
+
const iconNode = resolveIcon(node);
|
|
766
|
+
return (_jsxs("button", { type: "button", disabled: node.disabled, onMouseEnter: () => hasChildren && setMegaActiveId(node.id), onClick: () => {
|
|
767
|
+
if (node.disabled)
|
|
768
|
+
return;
|
|
769
|
+
if (hasChildren) {
|
|
770
|
+
setMegaActiveId(node.id);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
activateNode(node);
|
|
774
|
+
}, className: cn("inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors", active ? "bg-muted/80 text-foreground" : "hover:bg-muted/60", node.disabled ? "cursor-not-allowed opacity-55" : ""), children: [iconNode ? (_jsx("span", { className: cn("inline-flex shrink-0 items-center justify-center", densityCfg.icon), children: iconNode })) : null, _jsx("span", { className: "truncate", children: node.label }), hasChildren ? _jsx(ChevronDown, { className: "size-4 shrink-0 text-muted-foreground" }) : null] }, node.id));
|
|
775
|
+
}) }), _jsx("div", { className: "p-4", children: renderMegaColumns(activeMegaNode?.children ?? []) })] }));
|
|
776
|
+
}
|
|
777
|
+
return (_jsxs("div", { className: "flex rounded-md border border-border bg-background", children: [_jsx("div", { className: "w-52 shrink-0 border-r border-border p-1", children: items.map((node) => {
|
|
778
|
+
const hasChildren = !!node.children?.length;
|
|
779
|
+
const active = activeMegaNode?.id === node.id;
|
|
780
|
+
const iconNode = resolveIcon(node);
|
|
781
|
+
return (_jsxs("button", { type: "button", disabled: node.disabled, onMouseEnter: () => hasChildren && setMegaActiveId(node.id), onClick: () => {
|
|
782
|
+
if (node.disabled)
|
|
783
|
+
return;
|
|
784
|
+
if (hasChildren) {
|
|
785
|
+
setMegaActiveId(node.id);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
activateNode(node);
|
|
789
|
+
}, className: cn("flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left text-sm transition-colors", active ? "bg-muted/80 text-foreground" : "hover:bg-muted/60", node.disabled ? "cursor-not-allowed opacity-55" : ""), children: [iconNode ? (_jsx("span", { className: cn("inline-flex shrink-0 items-center justify-center", densityCfg.icon), children: iconNode })) : null, _jsx("span", { className: "min-w-0 flex-1 truncate", children: node.label }), hasChildren ? _jsx(ChevronRight, { className: "size-4 shrink-0 text-muted-foreground" }) : null] }, node.id));
|
|
790
|
+
}) }), _jsx("div", { className: "min-h-[220px] flex-1 p-4", children: renderMegaColumns(activeMegaNode?.children ?? []) })] }));
|
|
791
|
+
};
|
|
792
|
+
const showDockDragHandle = dockMode && draggable && (!isCollapsed || isHorizontalDockZone);
|
|
793
|
+
const collapseIconSide = isHorizontalDockZone ? effectiveDockZone : resolvedPosition;
|
|
794
|
+
const collapseButton = showCollapseButton && variant !== "drawer" ? (_jsx("button", { type: "button", onClick: () => {
|
|
795
|
+
if (variant === "hybrid") {
|
|
796
|
+
if (pinnedState) {
|
|
797
|
+
setPinnedState(false);
|
|
798
|
+
setDrawerOpen(false);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
setDrawerOpen(!drawerOpen);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
setCollapsedState(!collapsedState);
|
|
805
|
+
}, "aria-label": variant === "hybrid"
|
|
806
|
+
? pinnedState
|
|
807
|
+
? "Unpin and collapse menu"
|
|
808
|
+
: drawerOpen
|
|
809
|
+
? "Close menu"
|
|
810
|
+
: "Open menu"
|
|
811
|
+
: collapsedState
|
|
812
|
+
? "Expand menu"
|
|
813
|
+
: "Collapse menu", className: cn("inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-border", "bg-background hover:bg-muted/60", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35"), children: _jsx(CollapseIcon, { collapsed: variant === "hybrid" ? !(pinnedState || drawerOpen) : collapsedState, side: collapseIconSide }) })) : null;
|
|
814
|
+
const showBrandContent = Boolean(brand && (!isCollapsed || isHorizontalDockZone));
|
|
815
|
+
const showBrandSpacer = !showBrandContent && !isCollapsed;
|
|
816
|
+
// In horizontal zones: collapse goes left when aligned left/center, right when aligned right.
|
|
817
|
+
// In vertical zones: keep the original resolvedPosition logic.
|
|
818
|
+
const collapseOnLeft = isHorizontalDockZone
|
|
819
|
+
? horizontalDockAlign !== "right"
|
|
820
|
+
: resolvedPosition === "right";
|
|
821
|
+
const shellHeaderRow = (brand || showCollapseButton || showPinButton || showDockDragHandle) ? (_jsxs("div", { className: cn("flex items-center gap-2 border-b border-border", densityCfg.section), children: [collapseOnLeft ? collapseButton : null, showBrandContent && brand ? (_jsxs("button", { type: "button", onClick: brand.onClick, className: cn("min-w-0 flex-1 rounded-md", "flex items-center gap-2", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35"), children: [brand.image ? (_jsx("span", { className: "inline-flex shrink-0 items-center justify-center", children: brand.image })) : brand.imageSrc ? (_jsx("img", { src: brand.imageSrc, alt: brand.title ?? "brand", className: "h-7 w-auto max-w-[120px]" })) : null, _jsx("span", { className: "truncate text-sm font-semibold", children: brand.title ?? "Menu" })] })) : showBrandSpacer ? (_jsx("div", { className: "flex-1" })) : null, showDockDragHandle ? (_jsx("button", { type: "button", onPointerDown: handleDockDragPointerDown, "aria-label": "Drag menu", className: cn("inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-border", "bg-background hover:bg-muted/60", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35", dockDragActive ? "cursor-grabbing" : "cursor-grab"), children: _jsxs("svg", { viewBox: "0 0 24 24", className: "size-4", "aria-hidden": "true", children: [_jsx("circle", { cx: "8", cy: "8", r: "1.25", fill: "currentColor" }), _jsx("circle", { cx: "8", cy: "12", r: "1.25", fill: "currentColor" }), _jsx("circle", { cx: "8", cy: "16", r: "1.25", fill: "currentColor" }), _jsx("circle", { cx: "16", cy: "8", r: "1.25", fill: "currentColor" }), _jsx("circle", { cx: "16", cy: "12", r: "1.25", fill: "currentColor" }), _jsx("circle", { cx: "16", cy: "16", r: "1.25", fill: "currentColor" })] }) })) : null, showPinButton ? (_jsx("button", { type: "button", onClick: () => {
|
|
822
|
+
const next = !pinnedState;
|
|
823
|
+
setPinnedState(next);
|
|
824
|
+
if (variant === "hybrid" && next)
|
|
825
|
+
setDrawerOpen(false);
|
|
826
|
+
}, "aria-label": pinnedState ? "Unpin menu" : "Pin menu", className: cn("inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-border", "bg-background hover:bg-muted/60", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35"), children: _jsx(PinIcon, { pinned: pinnedState }) })) : null, !collapseOnLeft ? collapseButton : null] })) : null;
|
|
827
|
+
const shellContentArea = (_jsxs(_Fragment, { children: [searchEnabled && !isCollapsed ? (_jsx("div", { className: cn("border-b border-border", densityCfg.section), children: _jsx(SgAutocomplete, { id: `${searchInputId}-menu-search`, label: search?.placeholder ?? "Search", placeholder: search?.placeholder ?? "Search", value: searchValue, onChange: setSearchValue, source: autocompleteSource, minLengthForSearch: 0, openOnFocus: true, showDropDownButton: true, clearOnSelect: true, grouped: true, onSelect: (item) => {
|
|
828
|
+
const nodeId = item.data?.nodeId;
|
|
829
|
+
if (!nodeId)
|
|
830
|
+
return;
|
|
831
|
+
const node = maps.nodeById.get(nodeId);
|
|
832
|
+
if (!node || node.disabled)
|
|
833
|
+
return;
|
|
834
|
+
const parentChain = collectParentChain(maps.parentById, node.id);
|
|
835
|
+
const rootToParent = [...parentChain].reverse();
|
|
836
|
+
if (parentChain.length > 0) {
|
|
837
|
+
setExpandedIds((prev) => {
|
|
838
|
+
const next = Array.from(new Set([...prev, ...parentChain]));
|
|
839
|
+
return sameStringArray(prev, next) ? prev : next;
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
setTieredPath([
|
|
843
|
+
...rootToParent,
|
|
844
|
+
...(node.children?.length ? [node.id] : [])
|
|
845
|
+
]);
|
|
846
|
+
setMegaActiveId(rootToParent[0] ?? node.id);
|
|
847
|
+
if (node.children?.length && !node.url && !node.onClick) {
|
|
848
|
+
setExpandedIds((prev) => {
|
|
849
|
+
const nextSet = new Set(prev);
|
|
850
|
+
nextSet.add(node.id);
|
|
851
|
+
const next = Array.from(nextSet);
|
|
852
|
+
return sameStringArray(prev, next) ? prev : next;
|
|
853
|
+
});
|
|
854
|
+
setLocalActiveId(node.id);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
activateNode(node);
|
|
858
|
+
}, renderItem: (item) => {
|
|
859
|
+
const data = item.data;
|
|
860
|
+
const path = data?.path;
|
|
861
|
+
const node = data?.nodeId ? maps.nodeById.get(data.nodeId) : undefined;
|
|
862
|
+
const iconNode = node ? resolveIcon(node) : null;
|
|
863
|
+
return (_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [_jsx("span", { className: cn("mt-0.5 inline-flex shrink-0 items-center justify-center rounded", densityCfg.icon, iconNode ? "" : "bg-muted text-[10px] font-semibold"), children: iconNode ?? firstChars(item.label, 1) }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate", children: item.label }), path ? _jsx("div", { className: "truncate text-xs text-muted-foreground", children: path }) : null] })] }));
|
|
864
|
+
} }) })) : null, _jsx("div", { className: cn("min-h-0 flex-1", effectiveMenuStyle === "panel" ? "overflow-auto" : "overflow-visible", densityCfg.section), role: "navigation", "aria-label": ariaLabel, onKeyDown: effectiveMenuStyle === "panel" ? onListKeyDown : undefined, children: renderMenuTree(filteredMenu) }), (user || (userMenu && userMenu.length > 0) || footer) ? (_jsx("div", { className: cn("border-t border-border", densityCfg.section, userSectionClassName), style: userSectionStyle, children: user ? (isCollapsed ? (_jsx("button", { type: "button", onClick: user.onClick, className: cn("mb-2 w-full rounded-md", "flex items-center gap-2", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35", "hover:bg-muted/60", densityCfg.row), title: user.name, children: _jsx(SgAvatar, { src: user.avatar ? undefined : user.avatarSrc, label: user.name, size: density === "compact" ? "sm" : "md", severity: "secondary", children: user.avatar ?? undefined }) })) : (_jsxs(SgCard, { className: "mb-2", variant: "outlined", size: density === "compact" ? "sm" : "md", collapsible: true, defaultOpen: false, title: user.name, description: user.subtitle, leading: (_jsx("button", { type: "button", onClick: (event) => {
|
|
865
|
+
event.stopPropagation();
|
|
866
|
+
user.onClick?.();
|
|
867
|
+
}, className: "inline-flex rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35", "aria-label": user.name, children: _jsx(SgAvatar, { src: user.avatar ? undefined : user.avatarSrc, label: user.name, size: density === "compact" ? "sm" : "md", severity: "secondary", children: user.avatar ?? undefined }) })), children: [userMenu && userMenu.length > 0 ? (_jsx("div", { className: "space-y-0.5", children: userMenu.map((node) => renderNode(node, 0)) })) : null, footer ? _jsx("div", { className: userMenu && userMenu.length > 0 ? "mt-2" : undefined, children: footer }) : null] }))) : (_jsxs(_Fragment, { children: [userMenu && userMenu.length > 0 && !isCollapsed ? (_jsx("div", { className: "space-y-0.5", children: userMenu.map((node) => renderNode(node, 0)) })) : null, footer ? _jsx("div", { className: "mt-2", children: footer }) : null] })) })) : null] }));
|
|
868
|
+
const shellBody = (_jsxs("div", { ref: menuRootRef, className: cn("flex h-full min-h-0 flex-col bg-background text-foreground"), children: [shellHeaderRow, shellContentArea] }));
|
|
869
|
+
const isMegaMenuStyle = effectiveMenuStyle === "mega-horizontal" || effectiveMenuStyle === "mega-vertical";
|
|
870
|
+
const dockAlignStyle = dockMode && effectiveDockZone === "right" && !isCollapsed
|
|
871
|
+
? { alignSelf: "flex-end" }
|
|
872
|
+
: dockMode && effectiveDockZone === "left"
|
|
873
|
+
? { alignSelf: "flex-start" }
|
|
874
|
+
: undefined;
|
|
875
|
+
const sidebarWidthCss = dockMode && isHorizontalDockZone && variant !== "sidebar"
|
|
876
|
+
? "100%"
|
|
877
|
+
: variant === "inline" && isMegaMenuStyle
|
|
878
|
+
? "100%"
|
|
879
|
+
: variant === "hybrid"
|
|
880
|
+
? pinnedState
|
|
881
|
+
? expandedWidthCss
|
|
882
|
+
: collapsedWidthCss
|
|
883
|
+
: isCollapsed
|
|
884
|
+
? collapsedWidthCss
|
|
885
|
+
: expandedWidthCss;
|
|
886
|
+
const sidebarWidthStyle = sidebarWidthCss;
|
|
887
|
+
const sidebarShell = (_jsx("aside", { ref: (node) => {
|
|
888
|
+
sidebarShellRef.current = node;
|
|
889
|
+
}, className: cn("flex h-full min-h-0 flex-col bg-background text-foreground", effectiveMenuStyle === "tiered" ? "overflow-visible" : "overflow-hidden", border ? "border border-border" : "", elevationClass(elevation), resolvedPosition === "right" ? "ml-auto" : "", className), style: { width: sidebarWidthStyle, ...dockAlignStyle, ...style }, children: shellBody }));
|
|
890
|
+
const shellForRender = dockMode && effectiveDockZone === "right" && isCollapsed ? (_jsx("div", { style: { width: "100%", height: "100%", display: "flex", justifyContent: "flex-end" }, children: sidebarShell })) : dockMode && isHorizontalDockZone ? (_jsxs("aside", { ref: (node) => {
|
|
891
|
+
menuRootRef.current = node;
|
|
892
|
+
sidebarShellRef.current = node;
|
|
893
|
+
}, className: cn("relative flex flex-col bg-background text-foreground", !dockDragActive && horizontalDockAlign === null ? "w-full" : "", !dockDragActive ? "self-stretch" : "", !dockDragActive && horizontalDockAlign === "right" ? "ml-auto" : "", border ? "border border-border" : "", elevationClass(elevation), className), style: {
|
|
894
|
+
width: dockDragActive || horizontalDockAlign !== null
|
|
895
|
+
? expandedWidthCss
|
|
896
|
+
: undefined,
|
|
897
|
+
...style
|
|
898
|
+
}, children: [shellHeaderRow, !isCollapsed ? (_jsx("div", { className: cn("absolute z-50 flex flex-col bg-background text-foreground", effectiveMenuStyle === "tiered" ? "overflow-visible" : "overflow-auto", effectiveDockZone === "bottom" ? "bottom-full" : "top-full", horizontalDockAlign === "right" ? "right-0" : "left-0", "min-w-[240px]", effectiveMenuStyle !== "tiered" ? "max-h-[60vh]" : "", "border border-border", elevationClass(elevation === "none" ? "sm" : elevation)), children: shellContentArea })) : null] })) : (sidebarShell);
|
|
899
|
+
if (dockMode && portalTarget) {
|
|
900
|
+
return createPortal(shellForRender, portalTarget);
|
|
901
|
+
}
|
|
902
|
+
if (variant === "drawer") {
|
|
903
|
+
return (_jsx(SgExpandablePanel, { mode: "overlay", open: drawerOpen, onOpenChange: setDrawerOpen, expandTo: resolvedPosition === "left" ? "right" : "left", placement: "start", size: resolvedOverlaySize, animation: { type: "slide", durationMs: 180 }, border: border, elevation: elevation === "md" ? "lg" : elevation, rounded: "none", closeOnOutsideClick: !pinnedState, closeOnEsc: !pinnedState, trapFocus: true, showBackdrop: overlayBackdropResolved, ariaLabel: ariaLabel, role: "dialog", className: className, style: style, children: shellBody }));
|
|
904
|
+
}
|
|
905
|
+
if (variant === "hybrid") {
|
|
906
|
+
return (_jsxs(_Fragment, { children: [shellForRender, !pinnedState ? (_jsx(SgExpandablePanel, { mode: "overlay", open: drawerOpen, onOpenChange: setDrawerOpen, expandTo: resolvedPosition === "left" ? "right" : "left", placement: "start", size: resolvedOverlaySize, animation: { type: "slide", durationMs: 180 }, border: border, elevation: elevation === "md" ? "lg" : elevation, rounded: "none", closeOnOutsideClick: true, closeOnEsc: true, trapFocus: true, showBackdrop: overlayBackdropResolved, ariaLabel: ariaLabel, role: "dialog", children: _jsx(SgMenu, { menu: menu, selection: { activeId: effectiveActiveId, activeUrl: effectiveActiveUrl }, brand: brand, user: user, userMenu: userMenu, variant: "inline", menuStyle: menuStyle, position: position, density: density, indent: indent, collapsed: false, collapsedWidth: collapsedWidth, expandedWidth: expandedWidth, mode: mode, expandedIds: expandedIds, onExpandedIdsChange: setExpandedIds, showCollapseButton: false, showPinButton: showPinButton, pinned: pinnedState, onPinnedChange: (next) => {
|
|
907
|
+
setPinnedState(next);
|
|
908
|
+
if (next)
|
|
909
|
+
setDrawerOpen(false);
|
|
910
|
+
}, closeOnNavigate: closeOnNavigateResolved, onNavigate: (node) => {
|
|
911
|
+
setLocalActiveId(node.id);
|
|
912
|
+
if (onNavigate)
|
|
913
|
+
onNavigate(node);
|
|
914
|
+
else if (typeof window !== "undefined" && node.url)
|
|
915
|
+
window.location.assign(node.url);
|
|
916
|
+
if (closeOnNavigateResolved && !pinnedState)
|
|
917
|
+
setDrawerOpen(false);
|
|
918
|
+
}, onAction: onAction, onItemClick: (node) => {
|
|
919
|
+
setLocalActiveId(node.id);
|
|
920
|
+
onItemClick?.(node);
|
|
921
|
+
}, ariaLabel: ariaLabel, keyboardNavigation: keyboardNavigation, openSubmenuOnHover: openSubmenuOnHover, search: search, elevation: "none", border: false, footer: footer }) })) : null] }));
|
|
922
|
+
}
|
|
923
|
+
return shellForRender;
|
|
924
|
+
}
|
|
925
|
+
SgMenu.displayName = "SgMenu";
|
|
926
|
+
function CollapseIcon(props) {
|
|
927
|
+
const { collapsed, side } = props;
|
|
928
|
+
const rotation = side === "top" ? (collapsed ? 90 : -90) :
|
|
929
|
+
side === "bottom" ? (collapsed ? -90 : 90) :
|
|
930
|
+
side === "left" ? (collapsed ? 0 : 180) :
|
|
931
|
+
(collapsed ? 180 : 0);
|
|
932
|
+
return (_jsx("svg", { viewBox: "0 0 24 24", className: "size-4", style: { transform: `rotate(${rotation}deg)` }, "aria-hidden": "true", children: _jsx("path", { d: "M9 6l6 6-6 6", fill: "currentColor" }) }));
|
|
933
|
+
}
|
|
934
|
+
function PinIcon(props) {
|
|
935
|
+
if (props.pinned) {
|
|
936
|
+
return (_jsx("svg", { viewBox: "0 0 24 24", className: "size-4", "aria-hidden": "true", children: _jsx("path", { d: "M9 3h6v3l2 3v2H7V9l2-3V3zm3 8v10", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
937
|
+
}
|
|
938
|
+
return (_jsx("svg", { viewBox: "0 0 24 24", className: "size-4", "aria-hidden": "true", children: _jsx("path", { d: "M9 3h6v3l2 3v2H7V9l2-3V3m-3 14l12-12", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
939
|
+
}
|