@mihcm/ui 0.14.1 → 0.15.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 (300) hide show
  1. package/dist/CheckboxGrid.native.d.ts.map +1 -1
  2. package/dist/CheckboxGrid.native.js +2 -1
  3. package/dist/CheckboxGrid.native.js.map +1 -1
  4. package/dist/Combobox.native.d.ts.map +1 -1
  5. package/dist/Combobox.native.js +2 -1
  6. package/dist/Combobox.native.js.map +1 -1
  7. package/dist/DataTable/column-filter.d.ts +8 -0
  8. package/dist/DataTable/column-filter.d.ts.map +1 -0
  9. package/dist/DataTable/column-filter.js +67 -0
  10. package/dist/DataTable/column-filter.js.map +1 -0
  11. package/dist/DataTable/column-header.d.ts +16 -0
  12. package/dist/DataTable/column-header.d.ts.map +1 -0
  13. package/dist/DataTable/column-header.js +11 -0
  14. package/dist/DataTable/column-header.js.map +1 -0
  15. package/dist/DataTable/column-visibility.d.ts +7 -0
  16. package/dist/DataTable/column-visibility.d.ts.map +1 -0
  17. package/dist/DataTable/column-visibility.js +35 -0
  18. package/dist/DataTable/column-visibility.js.map +1 -0
  19. package/dist/DataTable/index.d.ts +5 -0
  20. package/dist/DataTable/index.d.ts.map +1 -0
  21. package/dist/DataTable/index.js +5 -0
  22. package/dist/DataTable/index.js.map +1 -0
  23. package/dist/DataTable/pinning.d.ts +13 -0
  24. package/dist/DataTable/pinning.d.ts.map +1 -0
  25. package/dist/DataTable/pinning.js +29 -0
  26. package/dist/DataTable/pinning.js.map +1 -0
  27. package/dist/DataTable.d.ts +3 -7
  28. package/dist/DataTable.d.ts.map +1 -1
  29. package/dist/DataTable.js +7 -126
  30. package/dist/DataTable.js.map +1 -1
  31. package/dist/Dialog.native.d.ts +3 -1
  32. package/dist/Dialog.native.d.ts.map +1 -1
  33. package/dist/Dialog.native.js +2 -2
  34. package/dist/Dialog.native.js.map +1 -1
  35. package/dist/Form/building-blocks.d.ts +26 -0
  36. package/dist/Form/building-blocks.d.ts.map +1 -0
  37. package/dist/Form/building-blocks.js +29 -0
  38. package/dist/Form/building-blocks.js.map +1 -0
  39. package/dist/Form/fields-choice.d.ts +72 -0
  40. package/dist/Form/fields-choice.d.ts.map +1 -0
  41. package/dist/Form/fields-choice.js +69 -0
  42. package/dist/Form/fields-choice.js.map +1 -0
  43. package/dist/Form/fields-complex.d.ts +28 -0
  44. package/dist/Form/fields-complex.d.ts.map +1 -0
  45. package/dist/Form/fields-complex.js +38 -0
  46. package/dist/Form/fields-complex.js.map +1 -0
  47. package/dist/Form/fields-date.d.ts +46 -0
  48. package/dist/Form/fields-date.d.ts.map +1 -0
  49. package/dist/Form/fields-date.js +41 -0
  50. package/dist/Form/fields-date.js.map +1 -0
  51. package/dist/Form/fields-text.d.ts +47 -0
  52. package/dist/Form/fields-text.d.ts.map +1 -0
  53. package/dist/Form/fields-text.js +46 -0
  54. package/dist/Form/fields-text.js.map +1 -0
  55. package/dist/Form/fields-toggle.d.ts +24 -0
  56. package/dist/Form/fields-toggle.d.ts.map +1 -0
  57. package/dist/Form/fields-toggle.js +32 -0
  58. package/dist/Form/fields-toggle.js.map +1 -0
  59. package/dist/Form/helpers.d.ts +66 -0
  60. package/dist/Form/helpers.d.ts.map +1 -0
  61. package/dist/Form/helpers.js +44 -0
  62. package/dist/Form/helpers.js.map +1 -0
  63. package/dist/Form/types.d.ts +25 -0
  64. package/dist/Form/types.d.ts.map +1 -0
  65. package/dist/Form/types.js +8 -0
  66. package/dist/Form/types.js.map +1 -0
  67. package/dist/Form.d.ts +24 -298
  68. package/dist/Form.d.ts.map +1 -1
  69. package/dist/Form.js +30 -246
  70. package/dist/Form.js.map +1 -1
  71. package/dist/IconSidebar.d.ts +6 -46
  72. package/dist/IconSidebar.d.ts.map +1 -1
  73. package/dist/IconSidebar.js +6 -116
  74. package/dist/IconSidebar.js.map +1 -1
  75. package/dist/MainSidebar/back-button.d.ts +14 -0
  76. package/dist/MainSidebar/back-button.d.ts.map +1 -0
  77. package/dist/MainSidebar/back-button.js +14 -0
  78. package/dist/MainSidebar/back-button.js.map +1 -0
  79. package/dist/MainSidebar/breadcrumb.d.ts +10 -0
  80. package/dist/MainSidebar/breadcrumb.d.ts.map +1 -0
  81. package/dist/MainSidebar/breadcrumb.js +24 -0
  82. package/dist/MainSidebar/breadcrumb.js.map +1 -0
  83. package/dist/MainSidebar/columns.d.ts +3 -0
  84. package/dist/MainSidebar/columns.d.ts.map +1 -0
  85. package/dist/MainSidebar/columns.js +198 -0
  86. package/dist/MainSidebar/columns.js.map +1 -0
  87. package/dist/MainSidebar/command.d.ts +3 -0
  88. package/dist/MainSidebar/command.d.ts.map +1 -0
  89. package/dist/MainSidebar/command.js +193 -0
  90. package/dist/MainSidebar/command.js.map +1 -0
  91. package/dist/MainSidebar/drilldown.d.ts +3 -0
  92. package/dist/MainSidebar/drilldown.d.ts.map +1 -0
  93. package/dist/MainSidebar/drilldown.js +154 -0
  94. package/dist/MainSidebar/drilldown.js.map +1 -0
  95. package/dist/MainSidebar/expanded.d.ts +7 -0
  96. package/dist/MainSidebar/expanded.d.ts.map +1 -0
  97. package/dist/MainSidebar/expanded.js +102 -0
  98. package/dist/MainSidebar/expanded.js.map +1 -0
  99. package/dist/MainSidebar/floating.d.ts +3 -0
  100. package/dist/MainSidebar/floating.d.ts.map +1 -0
  101. package/dist/MainSidebar/floating.js +116 -0
  102. package/dist/MainSidebar/floating.js.map +1 -0
  103. package/dist/MainSidebar/helpers.d.ts +50 -0
  104. package/dist/MainSidebar/helpers.d.ts.map +1 -0
  105. package/dist/MainSidebar/helpers.js +150 -0
  106. package/dist/MainSidebar/helpers.js.map +1 -0
  107. package/dist/MainSidebar/hover.d.ts +3 -0
  108. package/dist/MainSidebar/hover.d.ts.map +1 -0
  109. package/dist/MainSidebar/hover.js +177 -0
  110. package/dist/MainSidebar/hover.js.map +1 -0
  111. package/dist/MainSidebar/index.d.ts +6 -0
  112. package/dist/MainSidebar/index.d.ts.map +1 -0
  113. package/dist/MainSidebar/index.js +108 -0
  114. package/dist/MainSidebar/index.js.map +1 -0
  115. package/dist/MainSidebar/mobile.d.ts +29 -0
  116. package/dist/MainSidebar/mobile.d.ts.map +1 -0
  117. package/dist/MainSidebar/mobile.js +38 -0
  118. package/dist/MainSidebar/mobile.js.map +1 -0
  119. package/dist/MainSidebar/motion.d.ts +23 -0
  120. package/dist/MainSidebar/motion.d.ts.map +1 -0
  121. package/dist/MainSidebar/motion.js +40 -0
  122. package/dist/MainSidebar/motion.js.map +1 -0
  123. package/dist/MainSidebar/rail.d.ts +24 -0
  124. package/dist/MainSidebar/rail.d.ts.map +1 -0
  125. package/dist/MainSidebar/rail.js +29 -0
  126. package/dist/MainSidebar/rail.js.map +1 -0
  127. package/dist/MainSidebar/search.d.ts +19 -0
  128. package/dist/MainSidebar/search.d.ts.map +1 -0
  129. package/dist/MainSidebar/search.js +33 -0
  130. package/dist/MainSidebar/search.js.map +1 -0
  131. package/dist/MainSidebar/types.d.ts +161 -0
  132. package/dist/MainSidebar/types.d.ts.map +1 -0
  133. package/dist/MainSidebar/types.js +2 -0
  134. package/dist/MainSidebar/types.js.map +1 -0
  135. package/dist/MainSidebar.d.ts +6 -1
  136. package/dist/MainSidebar.d.ts.map +1 -1
  137. package/dist/MainSidebar.js +6 -1
  138. package/dist/MainSidebar.js.map +1 -1
  139. package/dist/NavigationMenu.js +1 -1
  140. package/dist/NavigationMenu.js.map +1 -1
  141. package/dist/RichTextEditor/theme.d.ts +44 -0
  142. package/dist/RichTextEditor/theme.d.ts.map +1 -0
  143. package/dist/RichTextEditor/theme.js +41 -0
  144. package/dist/RichTextEditor/theme.js.map +1 -0
  145. package/dist/RichTextEditor/toolbar-icons.d.ts +21 -0
  146. package/dist/RichTextEditor/toolbar-icons.d.ts.map +1 -0
  147. package/dist/RichTextEditor/toolbar-icons.js +21 -0
  148. package/dist/RichTextEditor/toolbar-icons.js.map +1 -0
  149. package/dist/RichTextEditor/toolbar.d.ts +5 -0
  150. package/dist/RichTextEditor/toolbar.d.ts.map +1 -0
  151. package/dist/RichTextEditor/toolbar.js +116 -0
  152. package/dist/RichTextEditor/toolbar.js.map +1 -0
  153. package/dist/RichTextEditor.d.ts +16 -9
  154. package/dist/RichTextEditor.d.ts.map +1 -1
  155. package/dist/RichTextEditor.js +18 -164
  156. package/dist/RichTextEditor.js.map +1 -1
  157. package/dist/Select/content.d.ts +9 -0
  158. package/dist/Select/content.d.ts.map +1 -0
  159. package/dist/Select/content.js +80 -0
  160. package/dist/Select/content.js.map +1 -0
  161. package/dist/Select/context.d.ts +27 -0
  162. package/dist/Select/context.d.ts.map +1 -0
  163. package/dist/Select/context.js +35 -0
  164. package/dist/Select/context.js.map +1 -0
  165. package/dist/Select/item.d.ts +13 -0
  166. package/dist/Select/item.d.ts.map +1 -0
  167. package/dist/Select/item.js +39 -0
  168. package/dist/Select/item.js.map +1 -0
  169. package/dist/Select/parts.d.ts +14 -0
  170. package/dist/Select/parts.d.ts.map +1 -0
  171. package/dist/Select/parts.js +17 -0
  172. package/dist/Select/parts.js.map +1 -0
  173. package/dist/Select/react-select.d.ts +25 -0
  174. package/dist/Select/react-select.d.ts.map +1 -0
  175. package/dist/Select/react-select.js +66 -0
  176. package/dist/Select/react-select.js.map +1 -0
  177. package/dist/Select/root.d.ts +15 -0
  178. package/dist/Select/root.d.ts.map +1 -0
  179. package/dist/Select/root.js +41 -0
  180. package/dist/Select/root.js.map +1 -0
  181. package/dist/Select/trigger.d.ts +15 -0
  182. package/dist/Select/trigger.d.ts.map +1 -0
  183. package/dist/Select/trigger.js +61 -0
  184. package/dist/Select/trigger.js.map +1 -0
  185. package/dist/Select.d.ts +14 -62
  186. package/dist/Select.d.ts.map +1 -1
  187. package/dist/Select.js +14 -293
  188. package/dist/Select.js.map +1 -1
  189. package/dist/Sidebar/context.d.ts +28 -0
  190. package/dist/Sidebar/context.d.ts.map +1 -0
  191. package/dist/Sidebar/context.js +37 -0
  192. package/dist/Sidebar/context.js.map +1 -0
  193. package/dist/Sidebar/group.d.ts +13 -0
  194. package/dist/Sidebar/group.d.ts.map +1 -0
  195. package/dist/Sidebar/group.js +20 -0
  196. package/dist/Sidebar/group.js.map +1 -0
  197. package/dist/Sidebar/icons.d.ts +7 -0
  198. package/dist/Sidebar/icons.d.ts.map +1 -0
  199. package/dist/Sidebar/icons.js +12 -0
  200. package/dist/Sidebar/icons.js.map +1 -0
  201. package/dist/Sidebar/layout.d.ts +9 -0
  202. package/dist/Sidebar/layout.d.ts.map +1 -0
  203. package/dist/Sidebar/layout.js +21 -0
  204. package/dist/Sidebar/layout.js.map +1 -0
  205. package/dist/Sidebar/menu.d.ts +29 -0
  206. package/dist/Sidebar/menu.d.ts.map +1 -0
  207. package/dist/Sidebar/menu.js +55 -0
  208. package/dist/Sidebar/menu.js.map +1 -0
  209. package/dist/Sidebar/provider.d.ts +33 -0
  210. package/dist/Sidebar/provider.d.ts.map +1 -0
  211. package/dist/Sidebar/provider.js +110 -0
  212. package/dist/Sidebar/provider.js.map +1 -0
  213. package/dist/Sidebar/sidebar.d.ts +17 -0
  214. package/dist/Sidebar/sidebar.d.ts.map +1 -0
  215. package/dist/Sidebar/sidebar.js +51 -0
  216. package/dist/Sidebar/sidebar.js.map +1 -0
  217. package/dist/Sidebar/submenu.d.ts +13 -0
  218. package/dist/Sidebar/submenu.d.ts.map +1 -0
  219. package/dist/Sidebar/submenu.js +17 -0
  220. package/dist/Sidebar/submenu.js.map +1 -0
  221. package/dist/Sidebar/trigger.d.ts +9 -0
  222. package/dist/Sidebar/trigger.d.ts.map +1 -0
  223. package/dist/Sidebar/trigger.js +33 -0
  224. package/dist/Sidebar/trigger.js.map +1 -0
  225. package/dist/Sidebar.d.ts +14 -104
  226. package/dist/Sidebar.d.ts.map +1 -1
  227. package/dist/Sidebar.js +14 -300
  228. package/dist/Sidebar.js.map +1 -1
  229. package/dist/StatCard.d.ts +67 -9
  230. package/dist/StatCard.d.ts.map +1 -1
  231. package/dist/StatCard.js +111 -9
  232. package/dist/StatCard.js.map +1 -1
  233. package/dist/TransferList.native.d.ts.map +1 -1
  234. package/dist/TransferList.native.js +2 -1
  235. package/dist/TransferList.native.js.map +1 -1
  236. package/package.json +2 -2
  237. package/src/CheckboxGrid.native.tsx +2 -1
  238. package/src/Combobox.native.tsx +2 -1
  239. package/src/DataTable/column-filter.tsx +134 -0
  240. package/src/DataTable/column-header.tsx +67 -0
  241. package/src/DataTable/column-visibility.tsx +87 -0
  242. package/src/DataTable/index.ts +4 -0
  243. package/src/DataTable/pinning.ts +40 -0
  244. package/src/DataTable.tsx +14 -297
  245. package/src/Dialog.native.tsx +4 -2
  246. package/src/Form/building-blocks.tsx +97 -0
  247. package/src/Form/fields-choice.tsx +312 -0
  248. package/src/Form/fields-complex.tsx +195 -0
  249. package/src/Form/fields-date.tsx +195 -0
  250. package/src/Form/fields-text.tsx +218 -0
  251. package/src/Form/fields-toggle.tsx +123 -0
  252. package/src/Form/helpers.tsx +189 -0
  253. package/src/Form/types.ts +26 -0
  254. package/src/Form.tsx +91 -1308
  255. package/src/IconSidebar.tsx +20 -442
  256. package/src/MainSidebar/back-button.tsx +58 -0
  257. package/src/MainSidebar/breadcrumb.tsx +53 -0
  258. package/src/MainSidebar/columns.tsx +350 -0
  259. package/src/MainSidebar/command.tsx +404 -0
  260. package/src/MainSidebar/drilldown.tsx +373 -0
  261. package/src/MainSidebar/expanded.tsx +414 -0
  262. package/src/MainSidebar/floating.tsx +268 -0
  263. package/src/MainSidebar/helpers.ts +166 -0
  264. package/src/MainSidebar/hover.tsx +334 -0
  265. package/src/MainSidebar/index.tsx +191 -0
  266. package/src/MainSidebar/mobile.tsx +117 -0
  267. package/src/MainSidebar/motion.ts +64 -0
  268. package/src/MainSidebar/rail.tsx +137 -0
  269. package/src/MainSidebar/search.tsx +99 -0
  270. package/src/MainSidebar/types.ts +208 -0
  271. package/src/MainSidebar.tsx +15 -4
  272. package/src/NavigationMenu.tsx +1 -1
  273. package/src/RichTextEditor/theme.ts +43 -0
  274. package/src/RichTextEditor/toolbar-icons.tsx +40 -0
  275. package/src/RichTextEditor/toolbar.tsx +271 -0
  276. package/src/RichTextEditor.tsx +23 -371
  277. package/src/Select/content.tsx +111 -0
  278. package/src/Select/context.tsx +66 -0
  279. package/src/Select/item.tsx +97 -0
  280. package/src/Select/parts.tsx +43 -0
  281. package/src/Select/react-select.tsx +216 -0
  282. package/src/Select/root.tsx +75 -0
  283. package/src/Select/trigger.tsx +122 -0
  284. package/src/Select.tsx +34 -692
  285. package/src/Sidebar/context.tsx +72 -0
  286. package/src/Sidebar/group.tsx +69 -0
  287. package/src/Sidebar/icons.tsx +42 -0
  288. package/src/Sidebar/layout.tsx +64 -0
  289. package/src/Sidebar/menu.tsx +171 -0
  290. package/src/Sidebar/provider.tsx +224 -0
  291. package/src/Sidebar/sidebar.tsx +178 -0
  292. package/src/Sidebar/submenu.tsx +58 -0
  293. package/src/Sidebar/trigger.tsx +104 -0
  294. package/src/Sidebar.tsx +44 -927
  295. package/src/StatCard.tsx +365 -20
  296. package/src/TransferList.native.tsx +2 -1
  297. package/dist/TiptapEditor.d.ts +0 -24
  298. package/dist/TiptapEditor.d.ts.map +0 -1
  299. package/dist/TiptapEditor.js +0 -84
  300. package/dist/TiptapEditor.js.map +0 -1
@@ -0,0 +1,404 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * MainSidebar — `command` variant.
5
+ *
6
+ * Two modes in one panel:
7
+ *
8
+ * - **Drilldown** when the search input is empty — rows with chevrons for
9
+ * items that have children; click to drill, back via chevron.
10
+ * - **Flat search** when the user types — recursive tree filter with
11
+ * breadcrumb paths on each match. Arrow keys + Enter pick by keyboard.
12
+ *
13
+ * The panel overlays the page rather than pushing layout.
14
+ */
15
+ import { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
16
+ import { AnimatePresence, motion, useReducedMotion } from 'motion/react';
17
+ import { cn } from '../internal/cn.js';
18
+ import { Rail } from './rail.js';
19
+ import { MenuSearch } from './search.js';
20
+ import { BackButton, CloseButton } from './back-button.js';
21
+ import { PathBreadcrumb } from './breadcrumb.js';
22
+ import { defaultMatcher, filterLevel, flatMatchTree, firstPanelItem, isOnPath, opensPanel, resolvePath, shouldWrapBadge } from './helpers.js';
23
+ import { slideVariants } from './motion.js';
24
+ import type { MainSidebarItem, MainSidebarProps } from './types.js';
25
+
26
+ export const CommandSidebar = forwardRef<HTMLElement, MainSidebarProps>(function CommandSidebar(
27
+ {
28
+ items,
29
+ activeKey,
30
+ expanded,
31
+ defaultExpanded = false,
32
+ onExpandedChange,
33
+ onItemSelect,
34
+ header,
35
+ footer,
36
+ panelHeader,
37
+ panelFooter,
38
+ search = true,
39
+ searchPlaceholder,
40
+ onSearchChange,
41
+ side = 'left',
42
+ density = 'comfortable',
43
+ panelWidth = 360,
44
+ motionPreset = 'expressive',
45
+ closeOnOutsideClick = true,
46
+ railClassName,
47
+ panelClassName,
48
+ itemClassName,
49
+ activeItemClassName,
50
+ backLabel = 'Back',
51
+ expandedLabel = 'Close menu',
52
+ className,
53
+ ...rest
54
+ },
55
+ ref,
56
+ ) {
57
+ const reduceMotion = useReducedMotion();
58
+ const effectivePreset = reduceMotion ? 'subtle' : motionPreset;
59
+
60
+ const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
61
+ const isExpanded = expanded ?? internalExpanded;
62
+
63
+ const [query, setQuery] = useState('');
64
+ const [focusIndex, setFocusIndex] = useState(0);
65
+ const inputRef = useRef<HTMLInputElement>(null);
66
+ const panelRef = useRef<HTMLElement>(null);
67
+ const railRef = useRef<HTMLElement>(null);
68
+
69
+ const searchCfg = typeof search === 'object' ? search : undefined;
70
+ const matcher = searchCfg?.matcher ?? defaultMatcher;
71
+
72
+ /* Drilldown state (active when query is empty). */
73
+ const rootItem = useMemo(() => firstPanelItem(items, activeKey), [activeKey, items]);
74
+ const [pathKeys, setPathKeys] = useState<string[]>([]);
75
+ const path = useMemo(() => {
76
+ const resolved = resolvePath(items, pathKeys);
77
+ return resolved.length ? resolved : rootItem ? [rootItem] : [];
78
+ }, [items, pathKeys, rootItem]);
79
+ const current = path.at(-1);
80
+ const drilldownItems = current?.children ?? items;
81
+
82
+ const searchActive = query.trim().length > 0;
83
+ const matches = useMemo(
84
+ () => (searchActive ? flatMatchTree(items, query, matcher) : []),
85
+ [items, query, matcher, searchActive],
86
+ );
87
+
88
+ const railOffset = density === 'compact' ? '3rem' : '3.5rem';
89
+
90
+ /*
91
+ * Skip auto-focus on initial mount so a `defaultExpanded` command
92
+ * sidebar doesn't yank the page scroll to its search input on page
93
+ * load. Pass `preventScroll: true` as belt-and-braces.
94
+ */
95
+ const focusInitialisedRef = useRef(false);
96
+ useEffect(() => {
97
+ if (!isExpanded) {
98
+ focusInitialisedRef.current = true;
99
+ return undefined;
100
+ }
101
+ setFocusIndex(0);
102
+ if (!focusInitialisedRef.current) {
103
+ focusInitialisedRef.current = true;
104
+ return undefined;
105
+ }
106
+ const id = window.setTimeout(() => inputRef.current?.focus({ preventScroll: true }), 0);
107
+ return () => window.clearTimeout(id);
108
+ }, [isExpanded]);
109
+
110
+ useEffect(() => {
111
+ setFocusIndex(0);
112
+ }, [query]);
113
+
114
+ function resetDeepState() {
115
+ setQuery('');
116
+ setPathKeys([]);
117
+ }
118
+
119
+ function setExpanded(next: boolean) {
120
+ if (expanded === undefined) setInternalExpanded(next);
121
+ if (!next) resetDeepState();
122
+ onExpandedChange?.(next);
123
+ }
124
+
125
+ function closePanel() {
126
+ resetDeepState();
127
+ setExpanded(false);
128
+ }
129
+
130
+ /* Outside-click closes the panel. */
131
+ useEffect(() => {
132
+ if (!isExpanded) return;
133
+ function onPointerDown(e: PointerEvent) {
134
+ if (!closeOnOutsideClick) return;
135
+ const target = e.target as Node;
136
+ if (panelRef.current?.contains(target) || railRef.current?.contains(target)) return;
137
+ closePanel();
138
+ }
139
+ document.addEventListener('pointerdown', onPointerDown);
140
+ return () => document.removeEventListener('pointerdown', onPointerDown);
141
+ // eslint-disable-next-line react-hooks/exhaustive-deps
142
+ }, [isExpanded, closeOnOutsideClick]);
143
+
144
+ function selectRailItem(item: MainSidebarItem) {
145
+ if (item.disabled) return;
146
+ onItemSelect?.(item.key, item);
147
+ if (opensPanel(item)) {
148
+ if (isExpanded && pathKeys[0] === item.key && !searchActive) {
149
+ setExpanded(false);
150
+ return;
151
+ }
152
+ setPathKeys([item.key]);
153
+ setQuery('');
154
+ setExpanded(true);
155
+ } else {
156
+ setExpanded(false);
157
+ }
158
+ }
159
+
160
+ function selectDrilldownItem(item: MainSidebarItem) {
161
+ if (item.disabled) return;
162
+ onItemSelect?.(item.key, item);
163
+ if (opensPanel(item)) {
164
+ setPathKeys((prev) => (prev.at(-1) === item.key ? prev : [...prev, item.key]));
165
+ }
166
+ }
167
+
168
+ function findStackTo(target: string, list: MainSidebarItem[] = items, acc: string[] = []): string[] | null {
169
+ for (const it of list) {
170
+ if (it.key === target) return [...acc, it.key];
171
+ if (it.children) {
172
+ const found = findStackTo(target, it.children, [...acc, it.key]);
173
+ if (found) return found;
174
+ }
175
+ }
176
+ return null;
177
+ }
178
+
179
+ function pickMatch(idx: number) {
180
+ const m = matches[idx];
181
+ if (!m || m.item.disabled) return;
182
+ onItemSelect?.(m.item.key, m.item);
183
+ if (opensPanel(m.item)) {
184
+ const stack = findStackTo(m.item.key) ?? [m.item.key];
185
+ setPathKeys(stack);
186
+ setQuery('');
187
+ } else {
188
+ setExpanded(false);
189
+ }
190
+ }
191
+
192
+ function goBack() {
193
+ setPathKeys((prev) => prev.slice(0, -1));
194
+ }
195
+
196
+ function onKey(e: React.KeyboardEvent<HTMLDivElement>) {
197
+ if (!searchActive) {
198
+ if (e.key === 'Escape') closePanel();
199
+ return;
200
+ }
201
+ if (e.key === 'ArrowDown') {
202
+ e.preventDefault();
203
+ setFocusIndex((i) => Math.min(matches.length - 1, i + 1));
204
+ } else if (e.key === 'ArrowUp') {
205
+ e.preventDefault();
206
+ setFocusIndex((i) => Math.max(0, i - 1));
207
+ } else if (e.key === 'Enter') {
208
+ e.preventDefault();
209
+ pickMatch(focusIndex);
210
+ } else if (e.key === 'Escape') {
211
+ e.preventDefault();
212
+ if (query) setQuery('');
213
+ else closePanel();
214
+ }
215
+ }
216
+
217
+ function updateQuery(next: string) {
218
+ setQuery(next);
219
+ onSearchChange?.(next);
220
+ }
221
+
222
+ return (
223
+ <div ref={ref as never} className={cn('relative h-full', className)} {...rest}>
224
+ <Rail
225
+ ref={railRef}
226
+ items={items}
227
+ activeKey={activeKey}
228
+ density={density}
229
+ side={side}
230
+ header={header}
231
+ footer={footer}
232
+ itemClassName={itemClassName}
233
+ activeItemClassName={activeItemClassName}
234
+ className={railClassName}
235
+ onItemSelect={selectRailItem}
236
+ openPanelKey={isExpanded ? current?.key : undefined}
237
+ />
238
+
239
+ <AnimatePresence initial={false} mode="wait">
240
+ {isExpanded ? (
241
+ <motion.aside
242
+ key="command"
243
+ ref={panelRef}
244
+ variants={slideVariants(effectivePreset, side === 'right' ? 'right' : 'left')}
245
+ initial="initial"
246
+ animate="animate"
247
+ exit="exit"
248
+ style={{
249
+ width: typeof panelWidth === 'number' ? `${panelWidth}px` : panelWidth,
250
+ [side === 'right' ? 'right' : 'left']: railOffset,
251
+ }}
252
+ className={cn(
253
+ 'absolute top-0 z-10 flex h-full min-w-0 flex-col bg-card shadow-xl',
254
+ side === 'right' ? 'border-l border-border' : 'border-r border-border',
255
+ panelClassName,
256
+ )}
257
+ onKeyDown={onKey}
258
+ >
259
+ <div className="border-b border-border px-4 py-3">
260
+ {panelHeader ?? (
261
+ <div className="flex items-start justify-between gap-3 pb-3">
262
+ <div className="min-w-0">
263
+ {!searchActive && path.length > 1 ? (
264
+ <>
265
+ <PathBreadcrumb
266
+ path={path}
267
+ onJump={(depth) => setPathKeys((prev) => prev.slice(0, depth + 1))}
268
+ className="mb-1"
269
+ />
270
+ <BackButton onClick={goBack} label={backLabel} className="mb-1 -ml-1.5" />
271
+ </>
272
+ ) : null}
273
+ <div className="truncate text-base font-semibold text-card-foreground">
274
+ {searchActive ? 'Quick navigation' : current?.label ?? 'Menu'}
275
+ </div>
276
+ {!searchActive && current?.description ? (
277
+ <p className="mt-0.5 truncate text-xs text-card-foreground/70">{current.description}</p>
278
+ ) : null}
279
+ </div>
280
+ <CloseButton onClick={closePanel} label={expandedLabel} />
281
+ </div>
282
+ )}
283
+ <MenuSearch
284
+ ref={inputRef}
285
+ value={query}
286
+ onValueChange={updateQuery}
287
+ placeholder={searchCfg?.placeholder ?? searchPlaceholder ?? 'Search all menus…'}
288
+
289
+ tone="onBrand"
290
+ />
291
+ {searchActive ? (
292
+ <div className="mt-2 flex items-center justify-between text-[11px] text-card-foreground/70">
293
+ <span>{matches.length} {matches.length === 1 ? 'match' : 'matches'}</span>
294
+ <span className="inline-flex items-center gap-1">
295
+ <kbd className="rounded border border-border bg-card-foreground/10 px-1.5 py-0.5 text-[10px] font-mono">↑↓</kbd>
296
+ <kbd className="rounded border border-border bg-card-foreground/10 px-1.5 py-0.5 text-[10px] font-mono">↵</kbd>
297
+ </span>
298
+ </div>
299
+ ) : null}
300
+ </div>
301
+
302
+ {searchActive ? (
303
+ <div role="listbox" aria-label="Menu search results" className="flex-1 overflow-y-auto p-2">
304
+ {matches.length === 0 ? (
305
+ <div className="px-4 py-8 text-center text-sm text-card-foreground/70">
306
+ {searchCfg?.noResultsLabel ?? `No items match "${query}".`}
307
+ </div>
308
+ ) : (
309
+ matches.map((m, idx) => (
310
+ <div
311
+ key={m.item.key + idx}
312
+ role="option"
313
+ aria-selected={idx === focusIndex}
314
+ onMouseEnter={() => setFocusIndex(idx)}
315
+ onClick={() => pickMatch(idx)}
316
+ className={cn(
317
+ 'group flex cursor-pointer items-center gap-3 rounded-md px-2.5 py-2 text-sm text-card-foreground transition-colors',
318
+ idx === focusIndex && 'bg-card-foreground/10',
319
+ m.item.disabled && 'cursor-not-allowed opacity-50',
320
+ itemClassName,
321
+ m.item.key === activeKey && 'text-card-foreground font-semibold',
322
+ m.item.key === activeKey && activeItemClassName,
323
+ )}
324
+ >
325
+ {m.item.icon ? (
326
+ <span className="grid size-5 shrink-0 place-items-center text-card-foreground/70 [&_svg]:size-full" aria-hidden="true">
327
+ {m.item.icon}
328
+ </span>
329
+ ) : null}
330
+ <div className="min-w-0 flex-1">
331
+ <div className="truncate font-medium">{m.item.label}</div>
332
+ {m.breadcrumbs.length ? (
333
+ <div className="truncate text-[11px] text-card-foreground/70">
334
+ {m.breadcrumbs.join(' › ')}
335
+ </div>
336
+ ) : null}
337
+ </div>
338
+ {m.item.badge != null && m.item.badge !== false ? (
339
+ shouldWrapBadge(m.item.badge) ? (
340
+ <span className="ml-auto inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-current/15 px-1.5 text-xs font-medium text-current">
341
+ {m.item.badge}
342
+ </span>
343
+ ) : (
344
+ <span className="ml-auto inline-flex shrink-0 items-center">{m.item.badge}</span>
345
+ )
346
+ ) : null}
347
+ </div>
348
+ ))
349
+ )}
350
+ </div>
351
+ ) : (
352
+ <div className="flex-1 overflow-y-auto p-2">
353
+ {filterLevel(drilldownItems, '').map((item) => {
354
+ const onActivePath = isOnPath(item, activeKey) || pathKeys.includes(item.key);
355
+ return (
356
+ <button
357
+ key={item.key}
358
+ type="button"
359
+ onClick={() => selectDrilldownItem(item)}
360
+ disabled={item.disabled}
361
+ className={cn(
362
+ 'group flex w-full items-center gap-2.5 rounded-md px-2.5 py-2 text-left text-sm text-card-foreground transition-colors',
363
+ 'hover:bg-card-foreground/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
364
+ 'disabled:cursor-not-allowed disabled:opacity-50',
365
+ onActivePath && 'bg-accent text-accent-foreground font-medium hover:bg-accent hover:text-accent-foreground',
366
+ itemClassName,
367
+ onActivePath && activeItemClassName,
368
+ )}
369
+ >
370
+ {item.icon ? (
371
+ <span className="grid size-5 shrink-0 place-items-center text-card-foreground/70 group-hover:text-card-foreground [&_svg]:size-full" aria-hidden="true">
372
+ {item.icon}
373
+ </span>
374
+ ) : null}
375
+ <span className="min-w-0 flex-1 truncate">{item.label}</span>
376
+ {item.badge != null && item.badge !== false ? (
377
+ shouldWrapBadge(item.badge) ? (
378
+ <span className="ml-auto inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-current/15 px-1.5 text-xs font-medium text-current">
379
+ {item.badge}
380
+ </span>
381
+ ) : (
382
+ <span className="ml-auto inline-flex shrink-0 items-center">{item.badge}</span>
383
+ )
384
+ ) : null}
385
+ {opensPanel(item) ? (
386
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="size-3.5 shrink-0 text-card-foreground/70" aria-hidden="true">
387
+ <path d="m9 18 6-6-6-6" strokeLinecap="round" strokeLinejoin="round" />
388
+ </svg>
389
+ ) : null}
390
+ </button>
391
+ );
392
+ })}
393
+ </div>
394
+ )}
395
+
396
+ {panelFooter ? (
397
+ <div className="border-t border-border px-4 py-3">{panelFooter}</div>
398
+ ) : null}
399
+ </motion.aside>
400
+ ) : null}
401
+ </AnimatePresence>
402
+ </div>
403
+ );
404
+ });