@seedgrid/fe-components 2026.3.20 → 2026.3.26

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 (172) hide show
  1. package/dist/buttons/SgFloatActionButton.d.ts.map +1 -1
  2. package/dist/buttons/SgFloatActionButton.js +5 -26
  3. package/dist/buttons/SgSplitButton.d.ts.map +1 -1
  4. package/dist/buttons/SgSplitButton.js +3 -1
  5. package/dist/buttons/fab-helpers.d.ts +6 -0
  6. package/dist/buttons/fab-helpers.d.ts.map +1 -0
  7. package/dist/buttons/fab-helpers.js +29 -0
  8. package/dist/commons/SgAvatar.d.ts.map +1 -1
  9. package/dist/commons/SgAvatar.js +6 -3
  10. package/dist/commons/SgBadge.d.ts.map +1 -1
  11. package/dist/commons/SgBadge.js +5 -2
  12. package/dist/commons/SgToast.d.ts.map +1 -1
  13. package/dist/commons/SgToast.js +3 -1
  14. package/dist/commons/SgToaster.d.ts.map +1 -1
  15. package/dist/commons/SgToaster.js +3 -1
  16. package/dist/environment/SgEnvironmentProvider.d.ts.map +1 -1
  17. package/dist/environment/SgEnvironmentProvider.js +10 -15
  18. package/dist/environment/persistent-state.d.ts +22 -0
  19. package/dist/environment/persistent-state.d.ts.map +1 -0
  20. package/dist/environment/persistent-state.js +33 -0
  21. package/dist/gadgets/calendar/SgCalendar.d.ts.map +1 -1
  22. package/dist/gadgets/calendar/SgCalendar.js +5 -23
  23. package/dist/gadgets/clock/SgClock.d.ts.map +1 -1
  24. package/dist/gadgets/clock/SgClock.js +12 -10
  25. package/dist/gadgets/clock/themes/SgClockThemePicker.d.ts +2 -1
  26. package/dist/gadgets/clock/themes/SgClockThemePicker.d.ts.map +1 -1
  27. package/dist/gadgets/clock/themes/SgClockThemePicker.js +23 -28
  28. package/dist/gadgets/clock/themes/search.d.ts +9 -0
  29. package/dist/gadgets/clock/themes/search.d.ts.map +1 -0
  30. package/dist/gadgets/clock/themes/search.js +15 -0
  31. package/dist/gadgets/gauge/SgLinearGauge.d.ts.map +1 -1
  32. package/dist/gadgets/gauge/SgLinearGauge.js +39 -28
  33. package/dist/gadgets/gauge/SgRadialGauge.d.ts.map +1 -1
  34. package/dist/gadgets/gauge/SgRadialGauge.js +44 -37
  35. package/dist/gadgets/gauge/math.d.ts +90 -0
  36. package/dist/gadgets/gauge/math.d.ts.map +1 -0
  37. package/dist/gadgets/gauge/math.js +81 -0
  38. package/dist/gadgets/qr-code/SgQRCode.d.ts.map +1 -1
  39. package/dist/gadgets/qr-code/SgQRCode.js +3 -1
  40. package/dist/i18n/en-US.d.ts.map +1 -1
  41. package/dist/i18n/en-US.js +97 -1
  42. package/dist/i18n/es.d.ts.map +1 -1
  43. package/dist/i18n/es.js +153 -57
  44. package/dist/i18n/fr.d.ts +3 -0
  45. package/dist/i18n/fr.d.ts.map +1 -0
  46. package/dist/i18n/fr.js +206 -0
  47. package/dist/i18n/index.d.ts +5 -1
  48. package/dist/i18n/index.d.ts.map +1 -1
  49. package/dist/i18n/index.js +50 -14
  50. package/dist/i18n/pt-BR.d.ts.map +1 -1
  51. package/dist/i18n/pt-BR.js +97 -1
  52. package/dist/i18n/pt-PT.d.ts.map +1 -1
  53. package/dist/i18n/pt-PT.js +97 -1
  54. package/dist/index.d.ts +12 -2
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +10 -2
  57. package/dist/inputs/SgAutocomplete.d.ts +1 -1
  58. package/dist/inputs/SgAutocomplete.d.ts.map +1 -1
  59. package/dist/inputs/SgAutocomplete.js +7 -4
  60. package/dist/inputs/SgCheckboxGroup.d.ts +2 -6
  61. package/dist/inputs/SgCheckboxGroup.d.ts.map +1 -1
  62. package/dist/inputs/SgCheckboxGroup.js +6 -6
  63. package/dist/inputs/SgCombobox.d.ts.map +1 -1
  64. package/dist/inputs/SgCombobox.js +11 -2
  65. package/dist/inputs/SgInputBirthDate.d.ts.map +1 -1
  66. package/dist/inputs/SgInputBirthDate.js +6 -1
  67. package/dist/inputs/SgInputCNPJ.d.ts +3 -1
  68. package/dist/inputs/SgInputCNPJ.d.ts.map +1 -1
  69. package/dist/inputs/SgInputCNPJ.js +4 -3
  70. package/dist/inputs/SgInputCPF.d.ts +3 -1
  71. package/dist/inputs/SgInputCPF.d.ts.map +1 -1
  72. package/dist/inputs/SgInputCPF.js +8 -3
  73. package/dist/inputs/SgInputCPFCNPJ.d.ts +3 -1
  74. package/dist/inputs/SgInputCPFCNPJ.d.ts.map +1 -1
  75. package/dist/inputs/SgInputCPFCNPJ.js +8 -3
  76. package/dist/inputs/SgInputCurrency.d.ts +3 -7
  77. package/dist/inputs/SgInputCurrency.d.ts.map +1 -1
  78. package/dist/inputs/SgInputCurrency.js +5 -2
  79. package/dist/inputs/SgInputDate.d.ts.map +1 -1
  80. package/dist/inputs/SgInputDate.js +6 -1
  81. package/dist/inputs/SgInputEmail.d.ts.map +1 -1
  82. package/dist/inputs/SgInputEmail.js +1 -1
  83. package/dist/inputs/SgInputNumber.d.ts +3 -7
  84. package/dist/inputs/SgInputNumber.d.ts.map +1 -1
  85. package/dist/inputs/SgInputNumber.js +5 -2
  86. package/dist/inputs/SgInputOTP.d.ts +5 -12
  87. package/dist/inputs/SgInputOTP.d.ts.map +1 -1
  88. package/dist/inputs/SgInputOTP.js +7 -4
  89. package/dist/inputs/SgInputPassword.d.ts.map +1 -1
  90. package/dist/inputs/SgInputPassword.js +1 -1
  91. package/dist/inputs/SgInputPhone.d.ts +3 -1
  92. package/dist/inputs/SgInputPhone.d.ts.map +1 -1
  93. package/dist/inputs/SgInputPhone.js +2 -1
  94. package/dist/inputs/SgInputPostalCode.d.ts.map +1 -1
  95. package/dist/inputs/SgInputPostalCode.js +2 -1
  96. package/dist/inputs/SgInputSelect.d.ts +4 -2
  97. package/dist/inputs/SgInputSelect.d.ts.map +1 -1
  98. package/dist/inputs/SgInputSelect.js +38 -3
  99. package/dist/inputs/SgInputText.d.ts +3 -7
  100. package/dist/inputs/SgInputText.d.ts.map +1 -1
  101. package/dist/inputs/SgInputText.js +5 -2
  102. package/dist/inputs/SgInputTextArea.d.ts +4 -2
  103. package/dist/inputs/SgInputTextArea.d.ts.map +1 -1
  104. package/dist/inputs/SgInputTextArea.js +37 -2
  105. package/dist/inputs/SgOrderList.d.ts +3 -1
  106. package/dist/inputs/SgOrderList.d.ts.map +1 -1
  107. package/dist/inputs/SgOrderList.js +19 -3
  108. package/dist/inputs/SgPickList.d.ts +3 -1
  109. package/dist/inputs/SgPickList.d.ts.map +1 -1
  110. package/dist/inputs/SgPickList.js +20 -4
  111. package/dist/inputs/SgRadioGroup.d.ts +2 -6
  112. package/dist/inputs/SgRadioGroup.d.ts.map +1 -1
  113. package/dist/inputs/SgRadioGroup.js +6 -6
  114. package/dist/inputs/SgRating.d.ts +2 -10
  115. package/dist/inputs/SgRating.d.ts.map +1 -1
  116. package/dist/inputs/SgRating.js +6 -3
  117. package/dist/inputs/SgSlider.d.ts +8 -2
  118. package/dist/inputs/SgSlider.d.ts.map +1 -1
  119. package/dist/inputs/SgSlider.js +62 -10
  120. package/dist/inputs/SgStepperInput.d.ts +8 -2
  121. package/dist/inputs/SgStepperInput.d.ts.map +1 -1
  122. package/dist/inputs/SgStepperInput.js +62 -8
  123. package/dist/inputs/SgTextEditor.d.ts +3 -1
  124. package/dist/inputs/SgTextEditor.d.ts.map +1 -1
  125. package/dist/inputs/SgTextEditor.js +24 -11
  126. package/dist/inputs/SgToggleSwitch.d.ts +3 -7
  127. package/dist/inputs/SgToggleSwitch.d.ts.map +1 -1
  128. package/dist/inputs/SgToggleSwitch.js +6 -3
  129. package/dist/layout/SgBreadcrumb.d.ts.map +1 -1
  130. package/dist/layout/SgBreadcrumb.js +7 -3
  131. package/dist/layout/SgCard.d.ts.map +1 -1
  132. package/dist/layout/SgCard.js +3 -1
  133. package/dist/layout/SgCarousel.d.ts.map +1 -1
  134. package/dist/layout/SgCarousel.js +3 -1
  135. package/dist/layout/SgExpandablePanel.d.ts.map +1 -1
  136. package/dist/layout/SgExpandablePanel.js +3 -1
  137. package/dist/layout/SgMenu.d.ts.map +1 -1
  138. package/dist/layout/SgMenu.js +173 -297
  139. package/dist/layout/SgPageControl.d.ts.map +1 -1
  140. package/dist/layout/SgPageControl.js +7 -3
  141. package/dist/layout/SgToolBar.d.ts.map +1 -1
  142. package/dist/layout/SgToolBar.js +19 -55
  143. package/dist/layout/SgTreeView.d.ts.map +1 -1
  144. package/dist/layout/SgTreeView.js +7 -3
  145. package/dist/layout/drag-position.d.ts +7 -0
  146. package/dist/layout/drag-position.d.ts.map +1 -0
  147. package/dist/layout/drag-position.js +30 -0
  148. package/dist/layout/menu-logic.d.ts +187 -0
  149. package/dist/layout/menu-logic.d.ts.map +1 -0
  150. package/dist/layout/menu-logic.js +349 -0
  151. package/dist/layout/toolbar-logic.d.ts +26 -0
  152. package/dist/layout/toolbar-logic.d.ts.map +1 -0
  153. package/dist/layout/toolbar-logic.js +38 -0
  154. package/dist/menus/SgDockMenu.d.ts.map +1 -1
  155. package/dist/menus/SgDockMenu.js +44 -120
  156. package/dist/menus/dock-menu-logic.d.ts +50 -0
  157. package/dist/menus/dock-menu-logic.d.ts.map +1 -0
  158. package/dist/menus/dock-menu-logic.js +113 -0
  159. package/dist/overlay/SgDialog.d.ts.map +1 -1
  160. package/dist/overlay/SgDialog.js +4 -2
  161. package/dist/overlay/SgPopup.d.ts.map +1 -1
  162. package/dist/overlay/SgPopup.js +4 -1
  163. package/dist/rhf.d.ts +8 -3
  164. package/dist/rhf.d.ts.map +1 -1
  165. package/dist/rhf.js +18 -1
  166. package/dist/sandbox.cjs +60 -60
  167. package/dist/wizard/SgWizard.d.ts.map +1 -1
  168. package/dist/wizard/SgWizard.js +20 -32
  169. package/dist/wizard/logic.d.ts +9 -0
  170. package/dist/wizard/logic.d.ts.map +1 -0
  171. package/dist/wizard/logic.js +20 -0
  172. package/package.json +8 -6
@@ -9,6 +9,8 @@ import { SgAutocomplete } from "../inputs/SgAutocomplete";
9
9
  import { SgCard } from "./SgCard";
10
10
  import { useSgDockLayout } from "./SgDockLayout";
11
11
  import { useHasSgEnvironmentProvider, useSgPersistence } from "../environment/SgEnvironmentProvider";
12
+ import { t, useComponentsI18n } from "../i18n";
13
+ import { buildMenuMaps, collectMenuSearchEntries, collectParentChain, computeActiveSets, filterMenuNodes, flattenVisibleNodes, mergeExpandedIdsForActivePath, resolveEffectiveActiveId, resolveExpandedIdsToggle, resolveHorizontalDockAlign, resolveMegaMenuActiveNode, resolveMegaMenuHoverActiveId, resolveMegaMenuInteraction, resolveMenuAutocompleteItems, resolveMenuHintPosition as resolveMenuHintPositionHelper, resolveMenuKeyboardAction, resolveMenuLayoutState, resolveMenuNodeActionIntent, resolveMenuSearchSelectionState, resolveTieredActiveState, resolveTieredHoverIntent, resolveTieredNodeClickIntent } from "./menu-logic";
12
14
  const ROOT_PARENT_ID = "__sg_menu_root__";
13
15
  function cn(...parts) {
14
16
  return parts.filter(Boolean).join(" ");
@@ -66,81 +68,6 @@ function useControllableState(value, defaultValue, onChange) {
66
68
  }, [isControlled, onChange]);
67
69
  return [current, setValue];
68
70
  }
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
71
  function firstChars(value, amount = 2) {
145
72
  const normalized = value.trim().replace(/\s+/g, " ");
146
73
  if (!normalized)
@@ -162,27 +89,6 @@ function sameStringArray(a, b) {
162
89
  }
163
90
  return true;
164
91
  }
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
92
  function elevationClass(elevation) {
187
93
  if (elevation === "sm")
188
94
  return "shadow-sm";
@@ -194,7 +100,9 @@ function resolveIcon(node) {
194
100
  return node.icon ?? null;
195
101
  }
196
102
  export function SgMenu(props) {
197
- const { id, menu, selection, brand, user, userMenu, menuStyle = "sidebar", menuVariantStyle = "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;
103
+ const i18n = useComponentsI18n();
104
+ const { id, menu, selection, brand, user, userMenu, menuStyle = "sidebar", menuVariantStyle = "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, keyboardNavigation = true, openSubmenuOnHover = false, search, elevation = "none", border = true, className, style, userSectionClassName, userSectionStyle, footer } = props;
105
+ const resolvedAriaLabel = ariaLabel ?? t(i18n, "components.menu.ariaLabel");
198
106
  const densityCfg = density === "compact"
199
107
  ? {
200
108
  row: "min-h-8 px-2 py-1 text-xs",
@@ -298,54 +206,47 @@ export function SgMenu(props) {
298
206
  const dockDragMovedRef = React.useRef(false);
299
207
  const dockHoverZoneRef = React.useRef(null);
300
208
  const searchEntries = React.useMemo(() => collectMenuSearchEntries(menu), [menu]);
301
- const autocompleteSource = React.useCallback((query) => {
302
- const q = query.trim().toLowerCase();
303
- const matches = !q
304
- ? searchEntries
305
- : searchEntries.filter((entry) => entry.label.toLowerCase().includes(q) ||
306
- entry.path.toLowerCase().includes(q));
307
- return matches.slice(0, 120).map((entry) => ({
308
- id: entry.id,
309
- label: entry.label,
310
- value: entry.path,
311
- group: entry.group,
312
- data: { nodeId: entry.id, path: entry.path }
313
- }));
314
- }, [searchEntries]);
209
+ const autocompleteSource = React.useCallback((query) => resolveMenuAutocompleteItems(searchEntries, query), [searchEntries]);
315
210
  const filteredMenu = React.useMemo(() => (searchEnabled ? filterMenuNodes(menu, searchValue) : menu), [menu, searchEnabled, searchValue]);
316
211
  const hasSearch = searchEnabled && searchValue.trim().length > 0;
317
212
  const effectiveMenuStyle = isCollapsed || hasSearch ? "panel" : resolvedMenuStyle;
318
- const resolvedPosition = dockMode && effectiveDockZone
319
- ? effectiveDockZone === "right"
320
- ? "right"
321
- : effectiveDockZone === "left"
322
- ? "left"
323
- : orientationDirection === "horizontal-right"
324
- ? "right"
325
- : "left"
326
- : position;
327
- const isHorizontalDockZone = effectiveDockZone === "top" || effectiveDockZone === "bottom";
328
- const isVerticalDockZone = effectiveDockZone === "left" || effectiveDockZone === "right";
329
- const tieredOpenToLeft = resolvedPosition === "right" ||
330
- (isHorizontalDockZone && horizontalDockAlign === "right");
213
+ const layoutState = React.useMemo(() => resolveMenuLayoutState({
214
+ dockMode,
215
+ effectiveDockZone,
216
+ orientationDirection,
217
+ position,
218
+ horizontalDockAlign,
219
+ isCollapsed,
220
+ menuStyle,
221
+ effectiveMenuStyle,
222
+ pinnedState,
223
+ expandedWidthCss,
224
+ collapsedWidthCss
225
+ }), [
226
+ collapsedWidthCss,
227
+ dockMode,
228
+ effectiveDockZone,
229
+ effectiveMenuStyle,
230
+ expandedWidthCss,
231
+ horizontalDockAlign,
232
+ isCollapsed,
233
+ menuStyle,
234
+ orientationDirection,
235
+ pinnedState,
236
+ position
237
+ ]);
238
+ const { resolvedPosition, isHorizontalDockZone, isVerticalDockZone, tieredOpenToLeft, isMegaMenuStyle, dockAlign, sidebarWidthCss } = layoutState;
331
239
  const hideMenuHint = React.useCallback(() => {
332
240
  menuHintAnchorRef.current = null;
333
241
  setMenuHint(null);
334
242
  }, []);
335
243
  const resolveMenuHintPosition = React.useCallback((anchor) => {
336
- if (!anchor.target.isConnected)
337
- return null;
338
244
  const rect = anchor.target.getBoundingClientRect();
339
- if (anchor.placement === "right") {
340
- return {
341
- x: rect.right + 8,
342
- y: rect.top + (rect.height / 2)
343
- };
344
- }
345
- return {
346
- x: rect.left + (rect.width / 2),
347
- y: rect.top - 8
348
- };
245
+ return resolveMenuHintPositionHelper({
246
+ isConnected: anchor.target.isConnected,
247
+ rect,
248
+ placement: anchor.placement
249
+ });
349
250
  }, []);
350
251
  const showMenuHint = React.useCallback((target, text, placement = "top") => {
351
252
  if (!text)
@@ -427,9 +328,12 @@ export function SgMenu(props) {
427
328
  React.useEffect(() => () => {
428
329
  clearDockDragVisual();
429
330
  }, [clearDockDragVisual]);
430
- const effectiveActiveId = selection?.activeId ??
431
- (selection?.activeUrl ? maps.firstByUrl.get(selection.activeUrl) : undefined) ??
432
- localActiveId;
331
+ const effectiveActiveId = resolveEffectiveActiveId({
332
+ activeId: selection?.activeId,
333
+ activeUrl: selection?.activeUrl,
334
+ localActiveId,
335
+ firstByUrl: maps.firstByUrl
336
+ });
433
337
  const effectiveActiveUrl = selection?.activeUrl;
434
338
  const activeSets = React.useMemo(() => computeActiveSets(menu, effectiveActiveId, effectiveActiveUrl), [menu, effectiveActiveId, effectiveActiveUrl]);
435
339
  React.useEffect(() => {
@@ -439,21 +343,20 @@ export function SgMenu(props) {
439
343
  if (chain.length === 0)
440
344
  return;
441
345
  setExpandedIds((prev) => {
442
- const next = Array.from(new Set([...prev, ...chain]));
346
+ const next = mergeExpandedIdsForActivePath(prev, chain);
443
347
  return sameStringArray(prev, next) ? prev : next;
444
348
  });
445
349
  }, [effectiveActiveId, maps.parentById, setExpandedIds]);
446
350
  React.useEffect(() => {
447
- if (!effectiveActiveId)
448
- return;
449
- const parentChain = collectParentChain(maps.parentById, effectiveActiveId).reverse();
450
- const activeNode = maps.nodeById.get(effectiveActiveId);
451
- const nextTieredPath = [...parentChain];
452
- if (activeNode?.children?.length)
453
- nextTieredPath.push(activeNode.id);
454
- setTieredPath(nextTieredPath);
455
- setMegaActiveId(parentChain[0] ?? activeNode?.id ?? menu[0]?.id);
456
- }, [effectiveActiveId, maps.nodeById, maps.parentById, menu]);
351
+ const nextState = resolveTieredActiveState({
352
+ effectiveActiveId,
353
+ maps,
354
+ menu,
355
+ rootParentId: ROOT_PARENT_ID
356
+ });
357
+ setTieredPath(nextState.tieredPath);
358
+ setMegaActiveId(nextState.megaActiveId);
359
+ }, [effectiveActiveId, maps, menu]);
457
360
  React.useEffect(() => {
458
361
  if (effectiveMenuStyle !== "tiered")
459
362
  return;
@@ -503,24 +406,14 @@ export function SgMenu(props) {
503
406
  hideMenuHint();
504
407
  }, [filteredMenu, effectiveMenuStyle, isCollapsed, drawerOpen, pinnedState, hideMenuHint]);
505
408
  const toggleExpanded = React.useCallback((nodeId) => {
506
- setExpandedIds((prev) => {
507
- const next = new Set(prev);
508
- const isOpen = next.has(nodeId);
509
- if (isOpen) {
510
- next.delete(nodeId);
511
- return Array.from(next);
512
- }
513
- if (mode === "accordion") {
514
- const parentId = maps.parentById.get(nodeId) ?? ROOT_PARENT_ID;
515
- const siblings = maps.childrenByParent.get(parentId) ?? [];
516
- for (const siblingId of siblings) {
517
- if (siblingId !== nodeId)
518
- next.delete(siblingId);
519
- }
520
- }
521
- next.add(nodeId);
522
- return Array.from(next);
523
- });
409
+ setExpandedIds((prev) => resolveExpandedIdsToggle({
410
+ currentIds: prev,
411
+ nodeId,
412
+ mode,
413
+ parentById: maps.parentById,
414
+ childrenByParent: maps.childrenByParent,
415
+ rootParentId: ROOT_PARENT_ID
416
+ }));
524
417
  }, [maps.childrenByParent, maps.parentById, mode, setExpandedIds]);
525
418
  const activateNode = React.useCallback((node) => {
526
419
  if (node.disabled)
@@ -615,7 +508,7 @@ export function SgMenu(props) {
615
508
  const zoneEl = dock.getZoneElement(zone);
616
509
  if (zoneEl) {
617
510
  const zoneRect = zoneEl.getBoundingClientRect();
618
- setHorizontalDockAlign(upEvent.clientX < zoneRect.left + zoneRect.width / 2 ? "left" : "right");
511
+ setHorizontalDockAlign(resolveHorizontalDockAlign(upEvent.clientX, zoneRect));
619
512
  }
620
513
  }
621
514
  else {
@@ -651,62 +544,30 @@ export function SgMenu(props) {
651
544
  const onListKeyDown = React.useCallback((event) => {
652
545
  if (!keyboardNavigation)
653
546
  return;
654
- if (visibleNodes.length === 0)
655
- return;
656
- const currentIndex = visibleNodes.findIndex((item) => item.node.id === focusedId);
657
- const activeIndex = currentIndex >= 0 ? currentIndex : 0;
658
- const current = visibleNodes[activeIndex];
659
- if (!current)
660
- return;
661
- const node = current.node;
662
- const hasChildren = !!node.children?.length;
663
- const isOpen = hasSearch || expandedSet.has(node.id);
664
- if (event.key === "ArrowDown") {
665
- event.preventDefault();
666
- const next = visibleNodes[Math.min(activeIndex + 1, visibleNodes.length - 1)];
667
- focusById(next?.node.id);
668
- return;
669
- }
670
- if (event.key === "ArrowUp") {
671
- event.preventDefault();
672
- const next = visibleNodes[Math.max(activeIndex - 1, 0)];
673
- focusById(next?.node.id);
547
+ const action = resolveMenuKeyboardAction({
548
+ key: event.key,
549
+ visibleNodes,
550
+ focusedId,
551
+ expandedIds: expandedSet,
552
+ hasSearch,
553
+ isCollapsed,
554
+ parentById: maps.parentById,
555
+ rootParentId: ROOT_PARENT_ID
556
+ });
557
+ if (action.type === "noop")
674
558
  return;
675
- }
676
- if (event.key === "ArrowRight") {
677
- if (hasChildren && !isCollapsed && !isOpen && !hasSearch) {
678
- event.preventDefault();
679
- toggleExpanded(node.id);
680
- return;
681
- }
682
- if (hasChildren && !isCollapsed) {
683
- event.preventDefault();
684
- const next = visibleNodes[activeIndex + 1];
685
- if (next && next.depth > current.depth)
686
- focusById(next.node.id);
687
- }
559
+ event.preventDefault();
560
+ if (action.type === "focus") {
561
+ focusById(action.id);
688
562
  return;
689
563
  }
690
- if (event.key === "ArrowLeft") {
691
- if (hasChildren && !isCollapsed && isOpen && !hasSearch) {
692
- event.preventDefault();
693
- toggleExpanded(node.id);
694
- return;
695
- }
696
- event.preventDefault();
697
- const parentId = maps.parentById.get(node.id);
698
- if (parentId && parentId !== ROOT_PARENT_ID)
699
- focusById(parentId);
564
+ if (action.type === "toggle") {
565
+ toggleExpanded(action.id);
700
566
  return;
701
567
  }
702
- if (event.key === "Enter" || event.key === " ") {
703
- event.preventDefault();
704
- if (hasChildren && !node.url && !node.onClick && !isCollapsed) {
705
- toggleExpanded(node.id);
706
- return;
707
- }
568
+ const node = maps.nodeById.get(action.id);
569
+ if (node)
708
570
  activateNode(node);
709
- }
710
571
  }, [
711
572
  activateNode,
712
573
  expandedSet,
@@ -715,6 +576,7 @@ export function SgMenu(props) {
715
576
  hasSearch,
716
577
  isCollapsed,
717
578
  keyboardNavigation,
579
+ maps.nodeById,
718
580
  maps.parentById,
719
581
  toggleExpanded,
720
582
  visibleNodes
@@ -740,12 +602,18 @@ export function SgMenu(props) {
740
602
  }, "data-sg-menu-node": node.id, type: "button", "aria-disabled": disabled || undefined, "aria-current": isExactActive ? "page" : undefined, onFocus: () => setFocusedId(node.id), onClick: () => {
741
603
  if (disabled)
742
604
  return;
743
- if (hasChildren && !node.url && !node.onClick && !isCollapsed) {
605
+ const intent = resolveMenuNodeActionIntent({
606
+ variant: "panel",
607
+ hasChildren,
608
+ hasActionTarget: Boolean(node.url || node.onClick),
609
+ isCollapsed
610
+ });
611
+ if (intent === "toggle") {
744
612
  toggleExpanded(node.id);
745
613
  return;
746
614
  }
747
615
  activateNode(node);
748
- }, className: cn("min-w-0 flex-1 rounded-md", "flex items-center text-left", 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));
616
+ }, className: cn("min-w-0 flex-1 rounded-md", "flex items-center text-left", 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 ? t(i18n, "components.actions.collapse") : t(i18n, "components.actions.expand"), 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));
749
617
  }, [
750
618
  activateNode,
751
619
  activeSets.branch,
@@ -770,7 +638,12 @@ export function SgMenu(props) {
770
638
  return (_jsxs("button", { type: "button", disabled: node.disabled, "aria-current": isExactActive ? "page" : undefined, onMouseEnter: node.hint ? (event) => showMenuHint(event.currentTarget, node.hint) : undefined, onMouseLeave: node.hint ? hideMenuHint : undefined, onClick: () => {
771
639
  if (node.disabled)
772
640
  return;
773
- if (hasChildren && !node.url && !node.onClick)
641
+ const intent = resolveMenuNodeActionIntent({
642
+ variant: "flat",
643
+ hasChildren,
644
+ hasActionTarget: Boolean(node.url || node.onClick)
645
+ });
646
+ if (intent === "noop")
774
647
  return;
775
648
  activateNode(node);
776
649
  }, className: cn("flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left text-sm transition-colors", isExactActive
@@ -794,27 +667,28 @@ export function SgMenu(props) {
794
667
  return (_jsxs("button", { type: "button", disabled: node.disabled, "aria-current": isExactActive ? "page" : undefined, onMouseEnter: (event) => {
795
668
  if (node.hint)
796
669
  showMenuHint(event.currentTarget, node.hint);
797
- if (!openSubmenuOnHover)
798
- return;
799
- if (!hasChildren) {
800
- setTieredPath((prev) => prev.slice(0, depth));
801
- return;
802
- }
803
- setTieredPath((prev) => [...prev.slice(0, depth), node.id]);
670
+ setTieredPath((prev) => resolveTieredHoverIntent({
671
+ currentPath: prev,
672
+ depth,
673
+ nodeId: node.id,
674
+ hasChildren,
675
+ openSubmenuOnHover
676
+ }) ?? prev);
804
677
  }, onMouseLeave: node.hint ? hideMenuHint : undefined, onClick: () => {
805
678
  if (node.disabled)
806
679
  return;
680
+ const nextState = resolveTieredNodeClickIntent({
681
+ currentPath: tieredPath,
682
+ depth,
683
+ nodeId: node.id,
684
+ hasChildren,
685
+ hasActionTarget: Boolean(node.url || node.onClick)
686
+ });
807
687
  if (hasChildren) {
808
- const isOpenAtDepth = tieredPath[depth] === node.id;
809
- setTieredPath((prev) => {
810
- const base = prev.slice(0, depth);
811
- return isOpenAtDepth ? base : [...base, node.id];
812
- });
813
- if (isOpenAtDepth)
814
- return;
815
- if (!node.url && !node.onClick)
816
- return;
688
+ setTieredPath(nextState.nextPath);
817
689
  }
690
+ if (!nextState.shouldActivateNode)
691
+ return;
818
692
  activateNode(node);
819
693
  }, 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));
820
694
  }) }), activeNode?.children?.length
@@ -832,13 +706,13 @@ export function SgMenu(props) {
832
706
  ]);
833
707
  const renderMegaColumns = React.useCallback((nodes) => {
834
708
  if (!nodes.length) {
835
- return _jsx("div", { className: "text-sm text-muted-foreground", children: "No items found." });
709
+ return _jsx("div", { className: "text-sm text-muted-foreground", children: t(i18n, "components.menu.empty") });
836
710
  }
837
711
  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))) }));
838
712
  }, [renderFlatAction]);
839
713
  const renderMenuTree = (items) => {
840
714
  if (items.length === 0) {
841
- return _jsx("div", { className: "px-3 py-2 text-xs text-muted-foreground", children: "No items found." });
715
+ return _jsx("div", { className: "px-3 py-2 text-xs text-muted-foreground", children: t(i18n, "components.menu.empty") });
842
716
  }
843
717
  if (effectiveMenuStyle === "panel") {
844
718
  return _jsx("div", { className: "space-y-0.5", children: items.map((node) => renderNode(node, 0)) });
@@ -846,8 +720,7 @@ export function SgMenu(props) {
846
720
  if (effectiveMenuStyle === "tiered") {
847
721
  return _jsx("div", { className: "relative min-h-[120px]", children: renderTieredLevels(items, 0) });
848
722
  }
849
- const activeMegaNode = items.find((node) => node.id === megaActiveId) ??
850
- items[0];
723
+ const activeMegaNode = resolveMegaMenuActiveNode(items, megaActiveId);
851
724
  if (effectiveMenuStyle === "mega-horizontal") {
852
725
  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) => {
853
726
  const hasChildren = !!node.children?.length;
@@ -856,15 +729,24 @@ export function SgMenu(props) {
856
729
  return (_jsxs("button", { type: "button", disabled: node.disabled, onMouseEnter: (event) => {
857
730
  if (node.hint)
858
731
  showMenuHint(event.currentTarget, node.hint);
859
- if (hasChildren)
860
- setMegaActiveId(node.id);
732
+ const nextMegaActiveId = resolveMegaMenuHoverActiveId({
733
+ nodeId: node.id,
734
+ hasChildren
735
+ });
736
+ if (nextMegaActiveId)
737
+ setMegaActiveId(nextMegaActiveId);
861
738
  }, onMouseLeave: node.hint ? hideMenuHint : undefined, onClick: () => {
862
739
  if (node.disabled)
863
740
  return;
864
- if (hasChildren) {
865
- setMegaActiveId(node.id);
866
- return;
741
+ const nextState = resolveMegaMenuInteraction({
742
+ nodeId: node.id,
743
+ hasChildren
744
+ });
745
+ if (nextState.nextMegaActiveId) {
746
+ setMegaActiveId(nextState.nextMegaActiveId);
867
747
  }
748
+ if (!nextState.shouldActivateNode)
749
+ return;
868
750
  activateNode(node);
869
751
  }, 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));
870
752
  }) }), _jsx("div", { className: "p-4", children: renderMegaColumns(activeMegaNode?.children ?? []) })] }));
@@ -876,15 +758,24 @@ export function SgMenu(props) {
876
758
  return (_jsxs("button", { type: "button", disabled: node.disabled, onMouseEnter: (event) => {
877
759
  if (node.hint)
878
760
  showMenuHint(event.currentTarget, node.hint);
879
- if (hasChildren)
880
- setMegaActiveId(node.id);
761
+ const nextMegaActiveId = resolveMegaMenuHoverActiveId({
762
+ nodeId: node.id,
763
+ hasChildren
764
+ });
765
+ if (nextMegaActiveId)
766
+ setMegaActiveId(nextMegaActiveId);
881
767
  }, onMouseLeave: node.hint ? hideMenuHint : undefined, onClick: () => {
882
768
  if (node.disabled)
883
769
  return;
884
- if (hasChildren) {
885
- setMegaActiveId(node.id);
886
- return;
770
+ const nextState = resolveMegaMenuInteraction({
771
+ nodeId: node.id,
772
+ hasChildren
773
+ });
774
+ if (nextState.nextMegaActiveId) {
775
+ setMegaActiveId(nextState.nextMegaActiveId);
887
776
  }
777
+ if (!nextState.shouldActivateNode)
778
+ return;
888
779
  activateNode(node);
889
780
  }, 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));
890
781
  }) }), _jsx("div", { className: "min-h-[220px] flex-1 p-4", children: renderMegaColumns(activeMegaNode?.children ?? []) })] }));
@@ -904,13 +795,13 @@ export function SgMenu(props) {
904
795
  setCollapsedState(!collapsedState);
905
796
  }, "aria-label": menuStyle === "hybrid"
906
797
  ? pinnedState
907
- ? "Unpin and collapse menu"
798
+ ? t(i18n, "components.menu.unpinAndCollapse")
908
799
  : drawerOpen
909
- ? "Close menu"
910
- : "Open menu"
800
+ ? t(i18n, "components.menu.close")
801
+ : t(i18n, "components.menu.open")
911
802
  : collapsedState
912
- ? "Expand menu"
913
- : "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: menuStyle === "hybrid" ? !(pinnedState || drawerOpen) : collapsedState, side: collapseIconSide }) })) : null;
803
+ ? t(i18n, "components.menu.expand")
804
+ : t(i18n, "components.menu.collapse"), 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: menuStyle === "hybrid" ? !(pinnedState || drawerOpen) : collapsedState, side: collapseIconSide }) })) : null;
914
805
  const showBrandContent = Boolean(brand && (!isCollapsed || isHorizontalDockZone));
915
806
  const showBrandSpacer = !showBrandContent && !isCollapsed;
916
807
  // In horizontal zones: collapse goes left when aligned left/center, right when aligned right.
@@ -918,13 +809,13 @@ export function SgMenu(props) {
918
809
  const collapseOnLeft = isHorizontalDockZone
919
810
  ? horizontalDockAlign !== "right"
920
811
  : resolvedPosition === "right";
921
- 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 rounded-md", isHorizontalDockZone ? "flex-none" : "flex-1", "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: cn("w-auto", isHorizontalDockZone ? "h-7 max-w-[120px]" : "h-12 max-w-[160px]") })) : null, brand.title ? (_jsx("span", { className: "truncate text-sm font-semibold", children: brand.title })) : null] })) : 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: () => {
812
+ 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 rounded-md", isHorizontalDockZone ? "flex-none" : "flex-1", "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 ?? t(i18n, "components.menu.brandAlt"), className: cn("w-auto", isHorizontalDockZone ? "h-7 max-w-[120px]" : "h-12 max-w-[160px]") })) : null, brand.title ? (_jsx("span", { className: "truncate text-sm font-semibold", children: brand.title })) : null] })) : showBrandSpacer ? (_jsx("div", { className: "flex-1" })) : null, showDockDragHandle ? (_jsx("button", { type: "button", onPointerDown: handleDockDragPointerDown, "aria-label": t(i18n, "components.menu.drag"), 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: () => {
922
813
  const next = !pinnedState;
923
814
  setPinnedState(next);
924
815
  if (menuStyle === "hybrid" && next)
925
816
  setDrawerOpen(false);
926
- }, "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;
927
- 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) => {
817
+ }, "aria-label": pinnedState ? t(i18n, "components.menu.unpin") : t(i18n, "components.menu.pin"), 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;
818
+ 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 ?? t(i18n, "components.actions.search"), placeholder: search?.placeholder ?? t(i18n, "components.actions.search"), value: searchValue, onChange: setSearchValue, source: autocompleteSource, minLengthForSearch: 0, openOnFocus: true, showDropDownButton: true, clearOnSelect: true, grouped: true, onSelect: (item) => {
928
819
  const nodeId = item.data?.nodeId;
929
820
  if (!nodeId)
930
821
  return;
@@ -933,25 +824,22 @@ export function SgMenu(props) {
933
824
  return;
934
825
  const parentChain = collectParentChain(maps.parentById, node.id);
935
826
  const rootToParent = [...parentChain].reverse();
936
- if (parentChain.length > 0) {
937
- setExpandedIds((prev) => {
938
- const next = Array.from(new Set([...prev, ...parentChain]));
939
- return sameStringArray(prev, next) ? prev : next;
940
- });
941
- }
942
- setTieredPath([
943
- ...rootToParent,
944
- ...(node.children?.length ? [node.id] : [])
945
- ]);
946
- setMegaActiveId(rootToParent[0] ?? node.id);
947
- if (node.children?.length && !node.url && !node.onClick) {
948
- setExpandedIds((prev) => {
949
- const nextSet = new Set(prev);
950
- nextSet.add(node.id);
951
- const next = Array.from(nextSet);
952
- return sameStringArray(prev, next) ? prev : next;
953
- });
954
- setLocalActiveId(node.id);
827
+ const nextState = resolveMenuSearchSelectionState({
828
+ currentExpandedIds: expandedIds,
829
+ parentChain,
830
+ rootToParent,
831
+ nodeId: node.id,
832
+ hasChildren: Boolean(node.children?.length),
833
+ hasActionTarget: Boolean(node.url || node.onClick)
834
+ });
835
+ setExpandedIds((prev) => {
836
+ const next = nextState.expandedIds;
837
+ return sameStringArray(prev, next) ? prev : next;
838
+ });
839
+ setTieredPath(nextState.tieredPath);
840
+ setMegaActiveId(nextState.megaActiveId);
841
+ if (!nextState.shouldActivateNode) {
842
+ setLocalActiveId(nextState.localActiveId);
955
843
  return;
956
844
  }
957
845
  activateNode(node);
@@ -961,28 +849,16 @@ export function SgMenu(props) {
961
849
  const node = data?.nodeId ? maps.nodeById.get(data.nodeId) : undefined;
962
850
  const iconNode = node ? resolveIcon(node) : null;
963
851
  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] })] }));
964
- } }) })) : 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", cardStyle: "outlined", size: density === "compact" ? "sm" : "md", collapsible: true, defaultOpen: false, title: user.name, description: user.subtitle, leading: (_jsx("button", { type: "button", onClick: (event) => {
852
+ } }) })) : null, _jsx("div", { className: cn("min-h-0 flex-1", effectiveMenuStyle === "panel" ? "overflow-auto" : "overflow-visible", densityCfg.section), role: "navigation", "aria-label": resolvedAriaLabel, 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", cardStyle: "outlined", size: density === "compact" ? "sm" : "md", collapsible: true, defaultOpen: false, title: user.name, description: user.subtitle, leading: (_jsx("button", { type: "button", onClick: (event) => {
965
853
  event.stopPropagation();
966
854
  user.onClick?.();
967
855
  }, 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] }));
968
856
  const shellBody = (_jsxs("div", { ref: menuRootRef, className: cn("flex h-full min-h-0 flex-col bg-background text-foreground"), children: [shellHeaderRow, shellContentArea] }));
969
- const isMegaMenuStyle = effectiveMenuStyle === "mega-horizontal" || effectiveMenuStyle === "mega-vertical";
970
- const dockAlignStyle = dockMode && effectiveDockZone === "right" && !isCollapsed
857
+ const dockAlignStyle = dockAlign === "end"
971
858
  ? { alignSelf: "flex-end" }
972
- : dockMode && effectiveDockZone === "left"
859
+ : dockAlign === "start"
973
860
  ? { alignSelf: "flex-start" }
974
861
  : undefined;
975
- const sidebarWidthCss = dockMode && isHorizontalDockZone && menuStyle !== "sidebar"
976
- ? "100%"
977
- : menuStyle === "inline" && isMegaMenuStyle
978
- ? "100%"
979
- : menuStyle === "hybrid"
980
- ? pinnedState
981
- ? expandedWidthCss
982
- : collapsedWidthCss
983
- : isCollapsed
984
- ? collapsedWidthCss
985
- : expandedWidthCss;
986
862
  const sidebarWidthStyle = sidebarWidthCss;
987
863
  const sidebarShell = (_jsx("aside", { ref: (node) => {
988
864
  sidebarShellRef.current = node;
@@ -1005,10 +881,10 @@ export function SgMenu(props) {
1005
881
  return (_jsxs(_Fragment, { children: [createPortal(shellForRender, portalTarget), menuHintNode] }));
1006
882
  }
1007
883
  if (menuStyle === "drawer") {
1008
- return (_jsxs(_Fragment, { children: [_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 }), menuHintNode] }));
884
+ return (_jsxs(_Fragment, { children: [_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: resolvedAriaLabel, role: "dialog", className: className, style: style, children: shellBody }), menuHintNode] }));
1009
885
  }
1010
886
  if (menuStyle === "hybrid") {
1011
- 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, menuStyle: "inline", menuVariantStyle: menuVariantStyle, 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) => {
887
+ 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: resolvedAriaLabel, role: "dialog", children: _jsx(SgMenu, { menu: menu, selection: { activeId: effectiveActiveId, activeUrl: effectiveActiveUrl }, brand: brand, user: user, userMenu: userMenu, menuStyle: "inline", menuVariantStyle: menuVariantStyle, 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) => {
1012
888
  setPinnedState(next);
1013
889
  if (next)
1014
890
  setDrawerOpen(false);
@@ -1023,7 +899,7 @@ export function SgMenu(props) {
1023
899
  }, onAction: onAction, onItemClick: (node) => {
1024
900
  setLocalActiveId(node.id);
1025
901
  onItemClick?.(node);
1026
- }, ariaLabel: ariaLabel, keyboardNavigation: keyboardNavigation, openSubmenuOnHover: openSubmenuOnHover, search: search, elevation: "none", border: false, footer: footer }) })) : null, menuHintNode] }));
902
+ }, ariaLabel: resolvedAriaLabel, keyboardNavigation: keyboardNavigation, openSubmenuOnHover: openSubmenuOnHover, search: search, elevation: "none", border: false, footer: footer }) })) : null, menuHintNode] }));
1027
903
  }
1028
904
  return (_jsxs(_Fragment, { children: [shellForRender, menuHintNode] }));
1029
905
  }