@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,414 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * MainSidebar — shared "expanded" rendering.
5
+ *
6
+ * When `collapsed` is false, every variant routes through this component:
7
+ * a wide vertical sidebar showing icon + label rows, with inline accordion
8
+ * expansion for parent items. This is the shadcn `collapsible="icon"`
9
+ * expanded shape.
10
+ *
11
+ * When the user toggles the collapse button (or `collapsed` flips to true),
12
+ * the dispatcher swaps this out for the variant's narrow rail rendering.
13
+ */
14
+ import { forwardRef, useMemo, useState, type ReactNode } from 'react';
15
+ import { cn } from '../internal/cn.js';
16
+ import { MenuSearch } from './search.js';
17
+ import {
18
+ defaultMatcher,
19
+ flatMatchTree,
20
+ isOnPath,
21
+ opensPanel,
22
+ shouldWrapBadge,
23
+ } from './helpers.js';
24
+ import type {
25
+ MainSidebarItem,
26
+ MainSidebarProps,
27
+ } from './types.js';
28
+
29
+ interface ExpandedRowProps {
30
+ item: MainSidebarItem;
31
+ depth: number;
32
+ activeKey: string | undefined;
33
+ onSelect: (item: MainSidebarItem) => void;
34
+ expandedKeys: Set<string>;
35
+ toggleExpanded: (key: string) => void;
36
+ itemClassName: string | undefined;
37
+ activeItemClassName: string | undefined;
38
+ }
39
+
40
+ function ExpandedRow({
41
+ item,
42
+ depth,
43
+ activeKey,
44
+ onSelect,
45
+ expandedKeys,
46
+ toggleExpanded,
47
+ itemClassName,
48
+ activeItemClassName,
49
+ }: ExpandedRowProps) {
50
+ const hasChildren = opensPanel(item);
51
+ const onActivePath = isOnPath(item, activeKey);
52
+ const isActive = item.key === activeKey;
53
+ const isExpanded = expandedKeys.has(item.key);
54
+
55
+ return (
56
+ <>
57
+ <button
58
+ type="button"
59
+ disabled={item.disabled}
60
+ aria-current={isActive ? 'page' : undefined}
61
+ aria-expanded={hasChildren ? isExpanded : undefined}
62
+ onClick={() => {
63
+ if (hasChildren) toggleExpanded(item.key);
64
+ onSelect(item);
65
+ }}
66
+ /*
67
+ * Password-manager browser extensions (Dashlane, 1Password, etc.)
68
+ * decorate form/input buttons with their own `data-*` attributes
69
+ * after server render but before React hydration. That trips
70
+ * React's hydration mismatch warning. We're not rendering an
71
+ * input — just a navigation button — so suppress the diff on
72
+ * unrelated attribute injections.
73
+ */
74
+ suppressHydrationWarning
75
+ className={cn(
76
+ 'group flex w-full items-center gap-2 rounded-md py-2 pl-2.5 pr-2.5 text-left text-sm text-primary-foreground/90 transition-colors',
77
+ 'hover:bg-primary-foreground/10 hover:text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary-foreground/60',
78
+ 'disabled:cursor-not-allowed disabled:opacity-50',
79
+ /* Active or breadcrumb-on-path */
80
+ (isActive || onActivePath) && 'bg-accent text-accent-foreground font-medium hover:bg-accent hover:text-accent-foreground',
81
+ (isActive || onActivePath) && activeItemClassName,
82
+ itemClassName,
83
+ )}
84
+ >
85
+ {item.icon ? (
86
+ <span className="grid size-5 shrink-0 place-items-center text-inherit [&_svg]:size-full" aria-hidden="true">
87
+ {item.icon}
88
+ </span>
89
+ ) : null}
90
+ <span className="min-w-0 flex-1 truncate">{item.label}</span>
91
+ {item.badge != null && item.badge !== false ? (
92
+ shouldWrapBadge(item.badge) ? (
93
+ <span className="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">
94
+ {item.badge}
95
+ </span>
96
+ ) : (
97
+ <span className="inline-flex shrink-0 items-center">{item.badge}</span>
98
+ )
99
+ ) : null}
100
+ {hasChildren ? (
101
+ <svg
102
+ viewBox="0 0 24 24"
103
+ fill="none"
104
+ stroke="currentColor"
105
+ strokeWidth="2"
106
+ className={cn(
107
+ 'size-3.5 shrink-0 transition-transform duration-200',
108
+ isExpanded && 'rotate-90',
109
+ )}
110
+ aria-hidden="true"
111
+ >
112
+ <path d="m9 18 6-6-6-6" strokeLinecap="round" strokeLinejoin="round" />
113
+ </svg>
114
+ ) : null}
115
+ </button>
116
+ {hasChildren && isExpanded ? (
117
+ /*
118
+ * Indent the child group with a vertical guide line. The line sits
119
+ * under the parent icon column (ml-[1.0625rem] aligns it to the
120
+ * centre of the icon); content shifts right via pl-3 so the line
121
+ * stays a single 1px stroke. The same wrapper is used at every
122
+ * depth, so arbitrary nesting (level-3+) renders with consistent,
123
+ * stacking guide lines.
124
+ */
125
+ <div
126
+ role="group"
127
+ aria-label={`${item.label} sub-items`}
128
+ className="ml-[1.0625rem] border-l border-primary-foreground/15 pl-2 py-0.5"
129
+ >
130
+ {(item.children ?? []).map((child) => (
131
+ <ExpandedRow
132
+ key={child.key}
133
+ item={child}
134
+ depth={depth + 1}
135
+ activeKey={activeKey}
136
+ onSelect={onSelect}
137
+ expandedKeys={expandedKeys}
138
+ toggleExpanded={toggleExpanded}
139
+ itemClassName={itemClassName}
140
+ activeItemClassName={activeItemClassName}
141
+ />
142
+ ))}
143
+ </div>
144
+ ) : null}
145
+ </>
146
+ );
147
+ }
148
+
149
+ interface ExpandedSearchMatchProps {
150
+ item: MainSidebarItem;
151
+ breadcrumbs: string[];
152
+ activeKey: string | undefined;
153
+ onSelect: (item: MainSidebarItem) => void;
154
+ itemClassName: string | undefined;
155
+ }
156
+
157
+ function ExpandedSearchMatch({ item, breadcrumbs, activeKey, onSelect, itemClassName }: ExpandedSearchMatchProps) {
158
+ return (
159
+ <button
160
+ type="button"
161
+ disabled={item.disabled}
162
+ onClick={() => onSelect(item)}
163
+ suppressHydrationWarning
164
+ className={cn(
165
+ 'group flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left text-sm text-primary-foreground/90 transition-colors',
166
+ 'hover:bg-primary-foreground/10 hover:text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary-foreground/60',
167
+ 'disabled:cursor-not-allowed disabled:opacity-50',
168
+ item.key === activeKey && 'bg-accent text-accent-foreground font-medium hover:bg-accent hover:text-accent-foreground',
169
+ itemClassName,
170
+ )}
171
+ >
172
+ {item.icon ? (
173
+ <span className="grid size-5 shrink-0 place-items-center text-inherit [&_svg]:size-full" aria-hidden="true">
174
+ {item.icon}
175
+ </span>
176
+ ) : null}
177
+ <span className="min-w-0 flex-1">
178
+ <span className="block truncate">{item.label}</span>
179
+ {breadcrumbs.length ? (
180
+ <span className="block truncate text-[11px] text-current/60">
181
+ {breadcrumbs.join(' › ')}
182
+ </span>
183
+ ) : null}
184
+ </span>
185
+ {item.badge != null && item.badge !== false ? (
186
+ shouldWrapBadge(item.badge) ? (
187
+ <span className="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">
188
+ {item.badge}
189
+ </span>
190
+ ) : (
191
+ <span className="inline-flex shrink-0 items-center">{item.badge}</span>
192
+ )
193
+ ) : null}
194
+ </button>
195
+ );
196
+ }
197
+
198
+ interface ExpandedSidebarInternalProps extends MainSidebarProps {
199
+ onCollapse: () => void;
200
+ }
201
+
202
+ function CollapseChevron({
203
+ onClick,
204
+ label,
205
+ side,
206
+ }: {
207
+ onClick: () => void;
208
+ label: string;
209
+ side: 'left' | 'right';
210
+ }) {
211
+ return (
212
+ <button
213
+ type="button"
214
+ onClick={onClick}
215
+ aria-label={label}
216
+ className="grid size-7 shrink-0 place-items-center rounded-md text-primary-foreground/80 transition-colors hover:bg-primary-foreground/10 hover:text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-foreground/60"
217
+ >
218
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="size-4" aria-hidden="true">
219
+ <path d={side === 'right' ? 'm9 18 6-6-6-6' : 'm15 18-6-6 6-6'} strokeLinecap="round" strokeLinejoin="round" />
220
+ </svg>
221
+ </button>
222
+ );
223
+ }
224
+
225
+ export const ExpandedSidebar = forwardRef<HTMLElement, ExpandedSidebarInternalProps>(function ExpandedSidebar(
226
+ {
227
+ items,
228
+ activeKey,
229
+ onItemSelect,
230
+ header,
231
+ footer,
232
+ search = false,
233
+ searchPlaceholder,
234
+ onSearchChange,
235
+ side = 'left',
236
+ expandedWidth = '15rem',
237
+ showCollapseToggle = true,
238
+ onCollapse,
239
+ itemClassName,
240
+ activeItemClassName,
241
+ className,
242
+ style: outerStyle,
243
+ collapsedLabel = 'Collapse menu',
244
+ /* Strip non-DOM props that come from MainSidebarProps so they don't land on <nav>. */
245
+ variant: _variant,
246
+ expanded: _expanded,
247
+ defaultExpanded: _defaultExpanded,
248
+ onExpandedChange: _onExpandedChange,
249
+ panelHeader: _panelHeader,
250
+ panelFooter: _panelFooter,
251
+ density: _density,
252
+ panelWidth: _panelWidth,
253
+ motionPreset: _motionPreset,
254
+ showLabelsWhenExpanded: _showLabelsWhenExpanded,
255
+ collapsible: _collapsible,
256
+ collapsed: _collapsed,
257
+ defaultCollapsed: _defaultCollapsed,
258
+ onCollapsedChange: _onCollapsedChange,
259
+ collapsedWidth: _collapsedWidth,
260
+ closeOnOutsideClick: _closeOnOutsideClick,
261
+ columnsMaxVisible: _columnsMaxVisible,
262
+ hoverDelayMs: _hoverDelayMs,
263
+ railClassName: _railClassName,
264
+ panelClassName: _panelClassName,
265
+ expandedLabel: _expandedLabel,
266
+ backLabel: _backLabel,
267
+ mobile: _mobile,
268
+ ...rest
269
+ },
270
+ ref,
271
+ ) {
272
+ void _variant;
273
+ void _expanded;
274
+ void _defaultExpanded;
275
+ void _onExpandedChange;
276
+ void _panelHeader;
277
+ void _panelFooter;
278
+ void _density;
279
+ void _panelWidth;
280
+ void _motionPreset;
281
+ void _showLabelsWhenExpanded;
282
+ void _collapsible;
283
+ void _collapsed;
284
+ void _defaultCollapsed;
285
+ void _onCollapsedChange;
286
+ void _collapsedWidth;
287
+ void _closeOnOutsideClick;
288
+ void _columnsMaxVisible;
289
+ void _hoverDelayMs;
290
+ void _railClassName;
291
+ void _panelClassName;
292
+ void _expandedLabel;
293
+ void _backLabel;
294
+ void _mobile;
295
+ const searchEnabled = search !== false;
296
+ const searchCfg = typeof search === 'object' ? search : undefined;
297
+ const matcher = searchCfg?.matcher ?? defaultMatcher;
298
+
299
+ const [query, setQuery] = useState('');
300
+ const searchActive = query.trim().length > 0;
301
+ const matches = useMemo(
302
+ () => (searchActive ? flatMatchTree(items, query, matcher) : []),
303
+ [items, query, matcher, searchActive],
304
+ );
305
+
306
+ /* Every parent starts collapsed. The user explicitly opens what they need. */
307
+ const [expandedKeys, setExpandedKeys] = useState<Set<string>>(() => new Set());
308
+
309
+ function toggleExpanded(key: string) {
310
+ setExpandedKeys((prev) => {
311
+ const next = new Set(prev);
312
+ if (next.has(key)) next.delete(key);
313
+ else next.add(key);
314
+ return next;
315
+ });
316
+ }
317
+
318
+ function onSelect(item: MainSidebarItem) {
319
+ if (item.disabled) return;
320
+ onItemSelect?.(item.key, item);
321
+ }
322
+
323
+ function updateQuery(next: string) {
324
+ setQuery(next);
325
+ onSearchChange?.(next);
326
+ }
327
+
328
+ const widthValue = typeof expandedWidth === 'number' ? `${expandedWidth}px` : expandedWidth;
329
+
330
+ return (
331
+ <nav
332
+ ref={ref}
333
+ aria-label="Main navigation"
334
+ style={{ width: widthValue, ...outerStyle }}
335
+ className={cn(
336
+ 'relative flex h-full shrink-0 flex-col bg-primary text-primary-foreground',
337
+ side === 'right' ? 'border-l border-primary-700/40' : 'border-r border-primary-700/40',
338
+ className,
339
+ )}
340
+ {...rest}
341
+ >
342
+ {/*
343
+ When a header prop is provided, render the dedicated header band plus
344
+ an optional separate search row. When no header is provided, merge
345
+ the collapse button into the search row to avoid an empty top band.
346
+ */}
347
+ {header ? (
348
+ <div className="flex items-center justify-between gap-2 border-b border-primary-foreground/10 px-3 py-2.5">
349
+ <div className="min-w-0 flex-1">{header}</div>
350
+ {showCollapseToggle ? (
351
+ <CollapseChevron onClick={onCollapse} label={collapsedLabel} side={side} />
352
+ ) : null}
353
+ </div>
354
+ ) : null}
355
+
356
+ {(searchEnabled || (!header && showCollapseToggle)) ? (
357
+ <div className="flex items-center gap-2 border-b border-primary-foreground/10 px-3 py-2">
358
+ {searchEnabled ? (
359
+ <div className="min-w-0 flex-1">
360
+ <MenuSearch
361
+ value={query}
362
+ onValueChange={updateQuery}
363
+ placeholder={searchCfg?.placeholder ?? searchPlaceholder ?? 'Search menu…'}
364
+ tone="onBrand"
365
+ />
366
+ </div>
367
+ ) : null}
368
+ {!header && showCollapseToggle ? (
369
+ <CollapseChevron onClick={onCollapse} label={collapsedLabel} side={side} />
370
+ ) : null}
371
+ </div>
372
+ ) : null}
373
+
374
+ <div className="flex-1 overflow-y-auto p-2">
375
+ {searchActive ? (
376
+ matches.length === 0 ? (
377
+ <div className="px-4 py-8 text-center text-sm text-primary-foreground/70">
378
+ {searchCfg?.noResultsLabel ?? `No items match "${query}".`}
379
+ </div>
380
+ ) : (
381
+ matches.map((m) => (
382
+ <ExpandedSearchMatch
383
+ key={m.item.key + m.breadcrumbs.join('>')}
384
+ item={m.item}
385
+ breadcrumbs={m.breadcrumbs}
386
+ activeKey={activeKey}
387
+ onSelect={onSelect}
388
+ itemClassName={itemClassName}
389
+ />
390
+ ))
391
+ )
392
+ ) : (
393
+ items.map((item) => (
394
+ <ExpandedRow
395
+ key={item.key}
396
+ item={item}
397
+ depth={0}
398
+ activeKey={activeKey}
399
+ onSelect={onSelect}
400
+ expandedKeys={expandedKeys}
401
+ toggleExpanded={toggleExpanded}
402
+ itemClassName={itemClassName}
403
+ activeItemClassName={activeItemClassName}
404
+ />
405
+ ))
406
+ )}
407
+ </div>
408
+
409
+ {footer ? (
410
+ <div className="border-t border-primary-foreground/10 px-3 py-2.5">{footer}</div>
411
+ ) : null}
412
+ </nav>
413
+ );
414
+ });
@@ -0,0 +1,268 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * MainSidebar — `floating` variant.
5
+ *
6
+ * Rail click opens an overlay panel beside the rail. The panel floats on top
7
+ * of the page content (does NOT push layout). Dismisses on outside click,
8
+ * Esc, or repeated rail-icon click.
9
+ */
10
+ import { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
11
+ import { AnimatePresence, motion, useReducedMotion } from 'motion/react';
12
+ import { cn } from '../internal/cn.js';
13
+ import { Rail } from './rail.js';
14
+ import { MenuSearch } from './search.js';
15
+ import { BackButton, CloseButton } from './back-button.js';
16
+ import { PathBreadcrumb } from './breadcrumb.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 FloatingSidebar = forwardRef<HTMLElement, MainSidebarProps>(function FloatingSidebar(
22
+ {
23
+ items,
24
+ activeKey,
25
+ expanded,
26
+ defaultExpanded = false,
27
+ onExpandedChange,
28
+ onItemSelect,
29
+ header,
30
+ footer,
31
+ panelHeader,
32
+ panelFooter,
33
+ search = false,
34
+ searchPlaceholder,
35
+ onSearchChange,
36
+ side = 'left',
37
+ density = 'comfortable',
38
+ panelWidth = 288,
39
+ motionPreset = 'expressive',
40
+ closeOnOutsideClick = true,
41
+ railClassName,
42
+ panelClassName,
43
+ itemClassName,
44
+ activeItemClassName,
45
+ backLabel = 'Back',
46
+ expandedLabel = 'Close menu',
47
+ className,
48
+ ...rest
49
+ },
50
+ ref,
51
+ ) {
52
+ const reduceMotion = useReducedMotion();
53
+ const effectivePreset = reduceMotion ? 'subtle' : motionPreset;
54
+
55
+ const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
56
+ const isExpanded = expanded ?? internalExpanded;
57
+ const [pathKeys, setPathKeys] = useState<string[]>([]);
58
+ const path = useMemo(() => resolvePath(items, pathKeys), [items, pathKeys]);
59
+ const current = path.at(-1);
60
+
61
+ const [query, setQuery] = useState('');
62
+ const searchEnabled = search !== false;
63
+ const searchCfg = typeof search === 'object' ? search : undefined;
64
+ const matcher = searchCfg?.matcher ?? defaultMatcher;
65
+ const currentItems = current?.children ?? [];
66
+ const filtered = filterLevel(currentItems, query, matcher);
67
+
68
+ const panelRef = useRef<HTMLDivElement>(null);
69
+ const railRef = useRef<HTMLElement>(null);
70
+
71
+ function resetDeepState() {
72
+ setPathKeys([]);
73
+ setQuery('');
74
+ }
75
+
76
+ function setExpanded(next: boolean) {
77
+ if (expanded === undefined) setInternalExpanded(next);
78
+ if (!next) resetDeepState();
79
+ onExpandedChange?.(next);
80
+ }
81
+
82
+ function closePanel() {
83
+ resetDeepState();
84
+ setExpanded(false);
85
+ }
86
+
87
+ function selectRailItem(item: MainSidebarItem) {
88
+ if (item.disabled) return;
89
+ onItemSelect?.(item.key, item);
90
+ if (opensPanel(item)) {
91
+ if (current?.key === item.key && isExpanded) {
92
+ setExpanded(false);
93
+ return;
94
+ }
95
+ setPathKeys([item.key]);
96
+ setExpanded(true);
97
+ setQuery('');
98
+ } else {
99
+ setExpanded(false);
100
+ }
101
+ }
102
+
103
+ function selectPanelItem(item: MainSidebarItem) {
104
+ if (item.disabled) return;
105
+ onItemSelect?.(item.key, item);
106
+ if (opensPanel(item)) {
107
+ setPathKeys((prev) => (prev.at(-1) === item.key ? prev : [...prev, item.key]));
108
+ setQuery('');
109
+ }
110
+ }
111
+
112
+ /* Outside-click + Escape */
113
+ useEffect(() => {
114
+ if (!isExpanded) return;
115
+ function onPointerDown(e: PointerEvent) {
116
+ if (!closeOnOutsideClick) return;
117
+ const target = e.target as Node;
118
+ if (panelRef.current?.contains(target) || railRef.current?.contains(target)) return;
119
+ setExpanded(false);
120
+ }
121
+ function onKey(e: KeyboardEvent) {
122
+ if (e.key === 'Escape') closePanel();
123
+ }
124
+ document.addEventListener('pointerdown', onPointerDown);
125
+ document.addEventListener('keydown', onKey);
126
+ return () => {
127
+ document.removeEventListener('pointerdown', onPointerDown);
128
+ document.removeEventListener('keydown', onKey);
129
+ };
130
+ // eslint-disable-next-line react-hooks/exhaustive-deps
131
+ }, [isExpanded, closeOnOutsideClick]);
132
+
133
+ function updateQuery(next: string) {
134
+ setQuery(next);
135
+ onSearchChange?.(next);
136
+ }
137
+
138
+ function goBack() {
139
+ setPathKeys((prev) => prev.slice(0, -1));
140
+ setQuery('');
141
+ }
142
+
143
+ return (
144
+ <div
145
+ ref={ref as never}
146
+ className={cn('relative h-full', className)}
147
+ {...rest}
148
+ >
149
+ <Rail
150
+ ref={railRef}
151
+ items={items}
152
+ activeKey={activeKey}
153
+ density={density}
154
+ side={side}
155
+ header={header}
156
+ footer={footer}
157
+ itemClassName={itemClassName}
158
+ activeItemClassName={activeItemClassName}
159
+ className={railClassName}
160
+ onItemSelect={selectRailItem}
161
+ openPanelKey={isExpanded ? current?.key : undefined}
162
+ />
163
+
164
+ <AnimatePresence initial={false} mode="wait">
165
+ {isExpanded && current ? (
166
+ <motion.div
167
+ key={current.key + ':' + path.length}
168
+ ref={panelRef}
169
+ variants={slideVariants(effectivePreset, side === 'right' ? 'right' : 'left')}
170
+ initial="initial"
171
+ animate="animate"
172
+ exit="exit"
173
+ role="menu"
174
+ aria-label={current.label}
175
+ style={{
176
+ width: typeof panelWidth === 'number' ? `${panelWidth}px` : panelWidth,
177
+ [side === 'right' ? 'right' : 'left']: density === 'compact' ? '3rem' : '3.5rem',
178
+ }}
179
+ className={cn(
180
+ 'absolute top-0 z-10 flex h-full flex-col rounded-xl border border-border bg-card text-card-foreground shadow-xl',
181
+ 'm-2',
182
+ panelClassName,
183
+ )}
184
+ >
185
+ <div className="border-b border-border px-4 py-3">
186
+ {panelHeader ?? (
187
+ <div className="flex items-start justify-between gap-3">
188
+ <div className="min-w-0">
189
+ {path.length > 1 ? (
190
+ <>
191
+ <PathBreadcrumb
192
+ path={path}
193
+ onJump={(depth) => setPathKeys((prev) => prev.slice(0, depth + 1))}
194
+ className="mb-1"
195
+ />
196
+ <BackButton onClick={goBack} label={backLabel} className="mb-1 -ml-1.5" />
197
+ </>
198
+ ) : null}
199
+ <div className="truncate text-base font-semibold text-card-foreground">{current.label}</div>
200
+ {current.description ? (
201
+ <p className="mt-0.5 text-xs text-card-foreground/70">{current.description}</p>
202
+ ) : null}
203
+ </div>
204
+ <CloseButton onClick={closePanel} label={expandedLabel} />
205
+ </div>
206
+ )}
207
+ {searchEnabled ? (
208
+ <div className="mt-3">
209
+ <MenuSearch
210
+ value={query}
211
+ onValueChange={updateQuery}
212
+ placeholder={searchCfg?.placeholder ?? searchPlaceholder ?? `Search ${current.label}…`}
213
+
214
+ tone="onBrand"
215
+ />
216
+ </div>
217
+ ) : null}
218
+ </div>
219
+
220
+ <div className="flex-1 overflow-y-auto p-2">
221
+ {filtered.length === 0 && query ? (
222
+ <div className="px-4 py-8 text-center text-sm text-card-foreground/70">
223
+ {searchCfg?.noResultsLabel ?? `No items match "${query}".`}
224
+ </div>
225
+ ) : (
226
+ filtered.map((item) => {
227
+ const onActivePath = isOnPath(item, activeKey) || pathKeys.includes(item.key);
228
+ return (
229
+ <button
230
+ key={item.key}
231
+ type="button"
232
+ onClick={() => selectPanelItem(item)}
233
+ disabled={item.disabled}
234
+ className={cn(
235
+ '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',
236
+ 'hover:bg-card-foreground/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
237
+ 'disabled:cursor-not-allowed disabled:opacity-50',
238
+ onActivePath && 'bg-accent text-accent-foreground font-medium hover:bg-accent hover:text-accent-foreground',
239
+ itemClassName,
240
+ onActivePath && activeItemClassName,
241
+ )}
242
+ >
243
+ {item.icon ? (
244
+ <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">
245
+ {item.icon}
246
+ </span>
247
+ ) : null}
248
+ <span className="min-w-0 flex-1 truncate">{item.label}</span>
249
+ {opensPanel(item) ? (
250
+ <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">
251
+ <path d="m9 18 6-6-6-6" strokeLinecap="round" strokeLinejoin="round" />
252
+ </svg>
253
+ ) : null}
254
+ </button>
255
+ );
256
+ })
257
+ )}
258
+ </div>
259
+
260
+ {panelFooter ? (
261
+ <div className="border-t border-border px-4 py-3">{panelFooter}</div>
262
+ ) : null}
263
+ </motion.div>
264
+ ) : null}
265
+ </AnimatePresence>
266
+ </div>
267
+ );
268
+ });