@mihcm/ui 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +148 -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 +164 -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,350 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * MainSidebar — `columns` variant.
5
+ *
6
+ * Miller columns (Finder-style). Each drill keeps the previous column
7
+ * visible; horizontally scrolling stacked columns. Each column has its
8
+ * own per-level search.
9
+ */
10
+ import { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
11
+ import { flushSync } from 'react-dom';
12
+ import { AnimatePresence, motion, useReducedMotion } from 'motion/react';
13
+ import { cn } from '../internal/cn.js';
14
+ import { Rail } from './rail.js';
15
+ import { MenuSearch } from './search.js';
16
+ import { CloseButton } from './back-button.js';
17
+ import { defaultMatcher, filterLevel, isOnPath, opensPanel, resolvePath } from './helpers.js';
18
+ import { slideVariants } from './motion.js';
19
+ import type { MainSidebarItem, MainSidebarProps } from './types.js';
20
+
21
+ export const ColumnsSidebar = forwardRef<HTMLElement, MainSidebarProps>(function ColumnsSidebar(
22
+ {
23
+ items,
24
+ activeKey,
25
+ expanded,
26
+ defaultExpanded = false,
27
+ onExpandedChange,
28
+ onItemSelect,
29
+ header,
30
+ footer,
31
+ search = false,
32
+ searchPlaceholder,
33
+ onSearchChange,
34
+ side = 'left',
35
+ density = 'comfortable',
36
+ panelWidth = 240,
37
+ motionPreset = 'expressive',
38
+ closeOnOutsideClick = true,
39
+ columnsMaxVisible = 3,
40
+ railClassName,
41
+ panelClassName,
42
+ itemClassName,
43
+ activeItemClassName,
44
+ expandedLabel = 'Close menu',
45
+ className,
46
+ ...rest
47
+ },
48
+ ref,
49
+ ) {
50
+ const reduceMotion = useReducedMotion();
51
+ const effectivePreset = reduceMotion ? 'subtle' : motionPreset;
52
+
53
+ const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
54
+ const isExpanded = expanded ?? internalExpanded;
55
+ const [pathKeys, setPathKeys] = useState<string[]>([]);
56
+ const path = useMemo(() => resolvePath(items, pathKeys), [items, pathKeys]);
57
+
58
+ const searchEnabled = search !== false;
59
+ const searchCfg = typeof search === 'object' ? search : undefined;
60
+ const matcher = searchCfg?.matcher ?? defaultMatcher;
61
+
62
+ /* One search query per column index. */
63
+ const [queries, setQueries] = useState<Record<number, string>>({});
64
+
65
+ /*
66
+ * Columns map one-to-one with the path nodes. Each column displays the
67
+ * CHILDREN of the corresponding path node:
68
+ *
69
+ * path = [payroll] → columns = [payroll.children]
70
+ * path = [people, onboarding] → columns = [people.children, onboarding.children]
71
+ *
72
+ * The rail itself surfaces the root-level items, so we don't repeat them
73
+ * as a column — clicking a rail icon jumps straight to its children.
74
+ */
75
+ const columns = useMemo(() => {
76
+ const cols: MainSidebarItem[][] = [];
77
+ for (const node of path) {
78
+ if (node.children?.length) cols.push(node.children);
79
+ }
80
+ return cols;
81
+ }, [path]);
82
+
83
+ /**
84
+ * Reset all deep state synchronously. Called from close handlers so the
85
+ * panel reopens with a fresh stack instead of remembering the previous
86
+ * drill-down path.
87
+ */
88
+ function resetDeepState() {
89
+ setPathKeys([]);
90
+ setQueries({});
91
+ }
92
+
93
+ function setExpanded(next: boolean) {
94
+ if (expanded === undefined) setInternalExpanded(next);
95
+ if (!next) resetDeepState();
96
+ onExpandedChange?.(next);
97
+ }
98
+
99
+ function closePanel() {
100
+ /*
101
+ * `flushSync` commits the column-stack reset BEFORE we flip
102
+ * `isExpanded`, so AnimatePresence captures the panel with just the
103
+ * root column (or none) for its slide-out exit — instead of all the
104
+ * deeply-drilled columns being frozen in the exit frame.
105
+ */
106
+ // eslint-disable-next-line @eslint-react/dom-no-flush-sync -- We need the column-stack reset to commit to the DOM BEFORE the panel slide-out is captured by AnimatePresence, otherwise stale deep columns linger in the exit frame.
107
+ flushSync(() => {
108
+ resetDeepState();
109
+ });
110
+ setExpanded(false);
111
+ }
112
+
113
+ const panelRef = useRef<HTMLDivElement>(null);
114
+ const railRef = useRef<HTMLElement>(null);
115
+
116
+ /* Esc + outside-click close the panel from anywhere. */
117
+ useEffect(() => {
118
+ if (!isExpanded) return;
119
+ function onKey(e: KeyboardEvent) {
120
+ if (e.key === 'Escape') closePanel();
121
+ }
122
+ function onPointerDown(e: PointerEvent) {
123
+ if (!closeOnOutsideClick) return;
124
+ const target = e.target as Node;
125
+ if (panelRef.current?.contains(target) || railRef.current?.contains(target)) return;
126
+ closePanel();
127
+ }
128
+ document.addEventListener('keydown', onKey);
129
+ document.addEventListener('pointerdown', onPointerDown);
130
+ return () => {
131
+ document.removeEventListener('keydown', onKey);
132
+ document.removeEventListener('pointerdown', onPointerDown);
133
+ };
134
+ // eslint-disable-next-line react-hooks/exhaustive-deps
135
+ }, [isExpanded, closeOnOutsideClick]);
136
+
137
+ /*
138
+ * Move focus into the panel when it opens — but skip the initial
139
+ * mount (covers `defaultExpanded` and SSR hydration), and pass
140
+ * `preventScroll: true` so the browser never yanks the page scroll
141
+ * to the just-focused element.
142
+ */
143
+ const focusInitialisedRef = useRef(false);
144
+ useEffect(() => {
145
+ if (!isExpanded) {
146
+ focusInitialisedRef.current = true;
147
+ return;
148
+ }
149
+ if (!focusInitialisedRef.current) {
150
+ focusInitialisedRef.current = true;
151
+ return;
152
+ }
153
+ const id = window.setTimeout(() => {
154
+ const focusable = panelRef.current?.querySelector<HTMLElement>(
155
+ 'input, [role="searchbox"], button:not([disabled])',
156
+ );
157
+ focusable?.focus({ preventScroll: true });
158
+ }, 0);
159
+ return () => window.clearTimeout(id);
160
+ }, [isExpanded]);
161
+
162
+ function selectRailItem(item: MainSidebarItem) {
163
+ if (item.disabled) return;
164
+ onItemSelect?.(item.key, item);
165
+ if (opensPanel(item)) {
166
+ /* Toggle close when clicking the rail icon for the panel that's
167
+ already open. Reuse `closePanel` so the deep-column reset is
168
+ flushed BEFORE the exit animation, otherwise the user sees stale
169
+ sub-columns slide out. */
170
+ if (isExpanded && pathKeys[0] === item.key) {
171
+ closePanel();
172
+ return;
173
+ }
174
+ setPathKeys([item.key]);
175
+ setExpanded(true);
176
+ setQueries({});
177
+ } else {
178
+ closePanel();
179
+ }
180
+ }
181
+
182
+ function selectColumnItem(colIndex: number, item: MainSidebarItem) {
183
+ if (item.disabled) return;
184
+ onItemSelect?.(item.key, item);
185
+ /*
186
+ * Column N shows path[N].children, so a click in column N adds a child
187
+ * AFTER path[N]. Truncate any deeper drilling and push the new key
188
+ * when the clicked item itself opens deeper panels.
189
+ */
190
+ const keepAncestors = path.slice(0, colIndex + 1).map((p) => p.key);
191
+ if (opensPanel(item)) {
192
+ setPathKeys([...keepAncestors, item.key]);
193
+ } else {
194
+ setPathKeys(keepAncestors);
195
+ }
196
+ }
197
+
198
+ function updateColumnQuery(colIndex: number, next: string) {
199
+ setQueries((prev) => ({ ...prev, [colIndex]: next }));
200
+ onSearchChange?.(next);
201
+ }
202
+
203
+ /*
204
+ * Column N shows path[N].children. The breadcrumb-active item in column N
205
+ * is the path node one step deeper (path[N + 1]), if the user drilled
206
+ * further. On the leaf column, fall back to the externally-controlled
207
+ * `activeKey` so the selected leaf stays highlighted.
208
+ */
209
+ function activeInColumn(colIndex: number) {
210
+ return path[colIndex + 1]?.key ?? (colIndex === path.length - 1 ? activeKey : undefined);
211
+ }
212
+
213
+ const widthPx = typeof panelWidth === 'number' ? `${panelWidth}px` : panelWidth;
214
+ const maxWidth = `calc(${columnsMaxVisible} * ${widthPx})`;
215
+ const railOffset = density === 'compact' ? '3rem' : '3.5rem';
216
+
217
+ return (
218
+ <div
219
+ ref={ref as never}
220
+ className={cn('relative h-full', className)}
221
+ {...rest}
222
+ >
223
+ <Rail
224
+ ref={railRef}
225
+ items={items}
226
+ activeKey={activeKey}
227
+ density={density}
228
+ side={side}
229
+ header={header}
230
+ footer={footer}
231
+ itemClassName={itemClassName}
232
+ activeItemClassName={activeItemClassName}
233
+ className={railClassName}
234
+ onItemSelect={selectRailItem}
235
+ openPanelKey={isExpanded ? path[0]?.key : undefined}
236
+ />
237
+
238
+ <AnimatePresence initial={false} mode="wait">
239
+ {isExpanded ? (
240
+ <motion.div
241
+ key="cols"
242
+ ref={panelRef}
243
+ variants={slideVariants(effectivePreset, side === 'right' ? 'right' : 'left')}
244
+ initial="initial"
245
+ animate="animate"
246
+ exit="exit"
247
+ role="menu"
248
+ aria-label={path[0]?.label ?? 'Navigation'}
249
+ style={{
250
+ maxWidth,
251
+ [side === 'right' ? 'right' : 'left']: railOffset,
252
+ }}
253
+ className={cn(
254
+ 'absolute top-0 z-10 flex h-full min-w-0 overflow-x-auto bg-card shadow-xl',
255
+ side === 'right' ? 'border-l border-border' : 'border-r border-border',
256
+ panelClassName,
257
+ )}
258
+ >
259
+ {columns.map((col, colIndex) => {
260
+ const q = queries[colIndex] ?? '';
261
+ const filtered = filterLevel(col, q, matcher);
262
+ /* Column N displays path[N]'s children, so path[N] is the parent header. */
263
+ const parent = path[colIndex];
264
+ const columnKey = parent?.key ?? `col-${colIndex}`;
265
+ return (
266
+ <div
267
+ key={columnKey}
268
+ style={{ width: widthPx, minWidth: widthPx }}
269
+ className={cn(
270
+ 'flex h-full shrink-0 flex-col',
271
+ colIndex < columns.length - 1 && 'border-r border-border',
272
+ )}
273
+ >
274
+ <div className="border-b border-border px-4 py-3">
275
+ <div className="flex items-start justify-between gap-3">
276
+ <div className="min-w-0">
277
+ <div className="truncate text-base font-semibold text-card-foreground">
278
+ {parent?.label ?? 'Menu'}
279
+ </div>
280
+ {parent?.description ? (
281
+ <p className="mt-0.5 text-xs text-card-foreground/70">{parent.description}</p>
282
+ ) : null}
283
+ </div>
284
+ {colIndex === 0 ? (
285
+ <CloseButton onClick={closePanel} label={expandedLabel} />
286
+ ) : null}
287
+ </div>
288
+ {searchEnabled ? (
289
+ <div className="mt-3">
290
+ <MenuSearch
291
+ value={q}
292
+ onValueChange={(v) => updateColumnQuery(colIndex, v)}
293
+ placeholder={searchCfg?.placeholder ?? searchPlaceholder ?? `Search ${parent?.label ?? 'menu'}…`}
294
+
295
+ tone="onBrand"
296
+ />
297
+ </div>
298
+ ) : null}
299
+ </div>
300
+ <div className="flex-1 overflow-y-auto p-1.5">
301
+ {filtered.length === 0 && q ? (
302
+ <div className="px-3 py-6 text-center text-xs text-card-foreground/70">
303
+ {searchCfg?.noResultsLabel ?? `No items match "${q}".`}
304
+ </div>
305
+ ) : (
306
+ filtered.map((item) => {
307
+ /* Highlight if it's the live activeKey leaf, or if it's the column-level breadcrumb, or if its subtree contains the active key. */
308
+ const isOnBreadcrumb = item.key === activeInColumn(colIndex);
309
+ const containsActive = isOnPath(item, activeKey);
310
+ const isActive = isOnBreadcrumb || containsActive;
311
+ return (
312
+ <button
313
+ key={item.key}
314
+ type="button"
315
+ onClick={() => selectColumnItem(colIndex, item)}
316
+ disabled={item.disabled}
317
+ className={cn(
318
+ 'group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-card-foreground transition-colors',
319
+ 'hover:bg-card-foreground/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
320
+ 'disabled:cursor-not-allowed disabled:opacity-50',
321
+ isActive && 'bg-primary text-primary-foreground hover:bg-primary',
322
+ itemClassName,
323
+ isActive && activeItemClassName,
324
+ )}
325
+ >
326
+ {item.icon ? (
327
+ <span className={cn('grid size-4 shrink-0 place-items-center [&_svg]:size-full', !isActive && 'text-card-foreground/70')} aria-hidden="true">
328
+ {item.icon}
329
+ </span>
330
+ ) : null}
331
+ <span className="min-w-0 flex-1 truncate">{item.label}</span>
332
+ {opensPanel(item) ? (
333
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="size-3 shrink-0" aria-hidden="true">
334
+ <path d="m9 18 6-6-6-6" strokeLinecap="round" strokeLinejoin="round" />
335
+ </svg>
336
+ ) : null}
337
+ </button>
338
+ );
339
+ })
340
+ )}
341
+ </div>
342
+ </div>
343
+ );
344
+ })}
345
+ </motion.div>
346
+ ) : null}
347
+ </AnimatePresence>
348
+ </div>
349
+ );
350
+ });