@seedgrid/fe-components 0.2.10 → 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.
Files changed (140) hide show
  1. package/dist/buttons/SgFloatActionButton.d.ts.map +1 -1
  2. package/dist/buttons/SgFloatActionButton.js +168 -38
  3. package/dist/commons/SgAvatar.d.ts +66 -0
  4. package/dist/commons/SgAvatar.d.ts.map +1 -0
  5. package/dist/commons/SgAvatar.js +136 -0
  6. package/dist/commons/SgSkeleton.d.ts +16 -0
  7. package/dist/commons/SgSkeleton.d.ts.map +1 -0
  8. package/dist/commons/SgSkeleton.js +58 -0
  9. package/dist/digits/discard-digit/SgDiscardDigit.d.ts +39 -0
  10. package/dist/digits/discard-digit/SgDiscardDigit.d.ts.map +1 -0
  11. package/dist/digits/discard-digit/SgDiscardDigit.js +303 -0
  12. package/dist/digits/discard-digit/index.d.ts +3 -0
  13. package/dist/digits/discard-digit/index.d.ts.map +1 -0
  14. package/dist/digits/discard-digit/index.js +1 -0
  15. package/dist/digits/fade-digit/SgFadeDigit.d.ts +27 -0
  16. package/dist/digits/fade-digit/SgFadeDigit.d.ts.map +1 -0
  17. package/dist/digits/fade-digit/SgFadeDigit.js +85 -0
  18. package/dist/digits/fade-digit/index.d.ts +3 -0
  19. package/dist/digits/fade-digit/index.d.ts.map +1 -0
  20. package/dist/digits/fade-digit/index.js +1 -0
  21. package/dist/digits/flip-digit/SgFlipDigit.d.ts +27 -0
  22. package/dist/digits/flip-digit/SgFlipDigit.d.ts.map +1 -0
  23. package/dist/digits/flip-digit/SgFlipDigit.js +70 -0
  24. package/dist/digits/flip-digit/index.d.ts.map +1 -0
  25. package/dist/digits/matrix-digit/SgMatrixDigit.d.ts +32 -0
  26. package/dist/digits/matrix-digit/SgMatrixDigit.d.ts.map +1 -0
  27. package/dist/digits/matrix-digit/SgMatrixDigit.js +86 -0
  28. package/dist/digits/matrix-digit/index.d.ts +3 -0
  29. package/dist/digits/matrix-digit/index.d.ts.map +1 -0
  30. package/dist/digits/matrix-digit/index.js +1 -0
  31. package/dist/digits/neon-digit/SgNeonDigit.d.ts +37 -0
  32. package/dist/digits/neon-digit/SgNeonDigit.d.ts.map +1 -0
  33. package/dist/digits/neon-digit/SgNeonDigit.js +59 -0
  34. package/dist/digits/neon-digit/index.d.ts +3 -0
  35. package/dist/digits/neon-digit/index.d.ts.map +1 -0
  36. package/dist/digits/neon-digit/index.js +1 -0
  37. package/dist/digits/roller3d-digit/SgRoller3DDigit.d.ts +37 -0
  38. package/dist/digits/roller3d-digit/SgRoller3DDigit.d.ts.map +1 -0
  39. package/dist/digits/roller3d-digit/SgRoller3DDigit.js +47 -0
  40. package/dist/digits/roller3d-digit/index.d.ts +3 -0
  41. package/dist/digits/roller3d-digit/index.d.ts.map +1 -0
  42. package/dist/digits/roller3d-digit/index.js +1 -0
  43. package/dist/environment/SgEnvironmentProvider.d.ts +1 -0
  44. package/dist/environment/SgEnvironmentProvider.d.ts.map +1 -1
  45. package/dist/environment/SgEnvironmentProvider.js +51 -12
  46. package/dist/gadgets/clock/SgClock.d.ts +3 -1
  47. package/dist/gadgets/clock/SgClock.d.ts.map +1 -1
  48. package/dist/gadgets/clock/SgClock.js +111 -180
  49. package/dist/gadgets/clock/SgTimeProvider.d.ts +1 -0
  50. package/dist/gadgets/clock/SgTimeProvider.d.ts.map +1 -1
  51. package/dist/gadgets/clock/SgTimeProvider.js +11 -4
  52. package/dist/gadgets/gauge/SgLinearGauge.d.ts +59 -0
  53. package/dist/gadgets/gauge/SgLinearGauge.d.ts.map +1 -0
  54. package/dist/gadgets/gauge/SgLinearGauge.js +258 -0
  55. package/dist/gadgets/gauge/SgRadialGauge.d.ts +73 -0
  56. package/dist/gadgets/gauge/SgRadialGauge.d.ts.map +1 -0
  57. package/dist/gadgets/gauge/SgRadialGauge.js +311 -0
  58. package/dist/gadgets/gauge/index.d.ts +5 -0
  59. package/dist/gadgets/gauge/index.d.ts.map +1 -0
  60. package/dist/gadgets/gauge/index.js +2 -0
  61. package/dist/gadgets/string-animator/SgStringAnimator.d.ts +91 -0
  62. package/dist/gadgets/string-animator/SgStringAnimator.d.ts.map +1 -0
  63. package/dist/gadgets/string-animator/SgStringAnimator.js +145 -0
  64. package/dist/gadgets/string-animator/index.d.ts +3 -0
  65. package/dist/gadgets/string-animator/index.d.ts.map +1 -0
  66. package/dist/gadgets/string-animator/index.js +1 -0
  67. package/dist/i18n/en-US.json +9 -1
  68. package/dist/i18n/es.json +55 -47
  69. package/dist/i18n/index.d.ts +32 -0
  70. package/dist/i18n/index.d.ts.map +1 -1
  71. package/dist/i18n/pt-BR.json +9 -1
  72. package/dist/i18n/pt-PT.json +9 -1
  73. package/dist/index.d.ts +46 -4
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +22 -1
  76. package/dist/inputs/SgAutocomplete.js +21 -5
  77. package/dist/inputs/SgCombobox.d.ts.map +1 -1
  78. package/dist/inputs/SgCombobox.js +8 -3
  79. package/dist/inputs/SgRadioGroup.d.ts +37 -0
  80. package/dist/inputs/SgRadioGroup.d.ts.map +1 -0
  81. package/dist/inputs/SgRadioGroup.js +139 -0
  82. package/dist/inputs/SgRating.d.ts +55 -0
  83. package/dist/inputs/SgRating.d.ts.map +1 -0
  84. package/dist/inputs/SgRating.js +135 -0
  85. package/dist/inputs/SgSlider.d.ts +20 -0
  86. package/dist/inputs/SgSlider.d.ts.map +1 -0
  87. package/dist/inputs/SgSlider.js +40 -0
  88. package/dist/inputs/SgStepperInput.d.ts +22 -0
  89. package/dist/inputs/SgStepperInput.d.ts.map +1 -0
  90. package/dist/inputs/SgStepperInput.js +51 -0
  91. package/dist/inputs/SgTextEditor.d.ts +1 -0
  92. package/dist/inputs/SgTextEditor.d.ts.map +1 -1
  93. package/dist/inputs/SgTextEditor.js +19 -3
  94. package/dist/layout/SgAccordion.d.ts +39 -0
  95. package/dist/layout/SgAccordion.d.ts.map +1 -0
  96. package/dist/layout/SgAccordion.js +116 -0
  97. package/dist/layout/SgBreadcrumb.d.ts +33 -0
  98. package/dist/layout/SgBreadcrumb.d.ts.map +1 -0
  99. package/dist/layout/SgBreadcrumb.js +121 -0
  100. package/dist/layout/SgCarousel.d.ts +43 -0
  101. package/dist/layout/SgCarousel.d.ts.map +1 -0
  102. package/dist/layout/SgCarousel.js +166 -0
  103. package/dist/layout/SgDockLayout.d.ts +14 -0
  104. package/dist/layout/SgDockLayout.d.ts.map +1 -1
  105. package/dist/layout/SgDockLayout.js +145 -13
  106. package/dist/layout/SgDockScreen.d.ts +15 -0
  107. package/dist/layout/SgDockScreen.d.ts.map +1 -0
  108. package/dist/layout/SgDockScreen.js +13 -0
  109. package/dist/layout/SgDockZone.d.ts.map +1 -1
  110. package/dist/layout/SgDockZone.js +36 -2
  111. package/dist/layout/SgExpandablePanel.d.ts +50 -0
  112. package/dist/layout/SgExpandablePanel.d.ts.map +1 -0
  113. package/dist/layout/SgExpandablePanel.js +302 -0
  114. package/dist/layout/SgMainPanel.d.ts.map +1 -1
  115. package/dist/layout/SgMainPanel.js +36 -14
  116. package/dist/layout/SgMenu.d.ts +91 -0
  117. package/dist/layout/SgMenu.d.ts.map +1 -0
  118. package/dist/layout/SgMenu.js +939 -0
  119. package/dist/layout/SgPageControl.d.ts +49 -0
  120. package/dist/layout/SgPageControl.d.ts.map +1 -0
  121. package/dist/layout/SgPageControl.js +152 -0
  122. package/dist/layout/SgPanel.d.ts.map +1 -1
  123. package/dist/layout/SgPanel.js +10 -1
  124. package/dist/layout/SgScreen.d.ts +2 -0
  125. package/dist/layout/SgScreen.d.ts.map +1 -1
  126. package/dist/layout/SgScreen.js +4 -2
  127. package/dist/layout/SgToolBar.d.ts +9 -3
  128. package/dist/layout/SgToolBar.d.ts.map +1 -1
  129. package/dist/layout/SgToolBar.js +461 -55
  130. package/dist/menus/SgDockMenu.d.ts +62 -0
  131. package/dist/menus/SgDockMenu.d.ts.map +1 -0
  132. package/dist/menus/SgDockMenu.js +480 -0
  133. package/dist/others/SgPlayground.js +72 -72
  134. package/package.json +72 -63
  135. package/dist/gadgets/flip-digit/SgFlipDigit.d.ts +0 -23
  136. package/dist/gadgets/flip-digit/SgFlipDigit.d.ts.map +0 -1
  137. package/dist/gadgets/flip-digit/SgFlipDigit.js +0 -118
  138. package/dist/gadgets/flip-digit/index.d.ts.map +0 -1
  139. /package/dist/{gadgets → digits}/flip-digit/index.d.ts +0 -0
  140. /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
+ }