@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,164 @@
1
+ import { isValidElement, type CSSProperties, type ReactNode } from 'react';
2
+ import type { MainSidebarColorScheme, MainSidebarItem } from './types.js';
3
+
4
+ /**
5
+ * `MainSidebarItem.badge` accepts either a primitive (string/number) — in
6
+ * which case the variant wraps it in a default pill — or a fully-styled
7
+ * ReactElement (e.g. `<Badge variant="warning">12</Badge>`), which should
8
+ * render as-is to avoid double backgrounds.
9
+ */
10
+ export function isPrimitiveBadge(badge: ReactNode): badge is string | number {
11
+ return typeof badge === 'string' || typeof badge === 'number';
12
+ }
13
+
14
+ /**
15
+ * True when the badge should be rendered inside the variant's default pill
16
+ * wrapper. ReactElements bring their own styling and bypass the wrapper.
17
+ */
18
+ export function shouldWrapBadge(badge: ReactNode): boolean {
19
+ if (badge == null || badge === false) return false;
20
+ if (isValidElement(badge)) return false;
21
+ return true;
22
+ }
23
+
24
+ export function findItem(
25
+ items: MainSidebarItem[],
26
+ key: string | undefined,
27
+ ): MainSidebarItem | undefined {
28
+ if (!key) return undefined;
29
+ for (const item of items) {
30
+ if (item.key === key) return item;
31
+ const child = findItem(item.children ?? [], key);
32
+ if (child) return child;
33
+ }
34
+ return undefined;
35
+ }
36
+
37
+ /** True if `target` is `item.key` itself, OR appears somewhere in `item`'s subtree. */
38
+ export function isOnPath(item: MainSidebarItem, target: string | undefined): boolean {
39
+ if (!target) return false;
40
+ if (item.key === target) return true;
41
+ return (item.children ?? []).some((child) => isOnPath(child, target));
42
+ }
43
+
44
+ export function firstPanelItem(
45
+ items: MainSidebarItem[],
46
+ activeKey: string | undefined,
47
+ ) {
48
+ const active = findItem(items, activeKey);
49
+ if (active?.children?.length || active?.panel) return active;
50
+ return items.find((item) => item.children?.length || item.panel) ?? items[0];
51
+ }
52
+
53
+ export function opensPanel(item: MainSidebarItem) {
54
+ return Boolean(item.children?.length || item.panel);
55
+ }
56
+
57
+ export function defaultMatcher(item: MainSidebarItem, query: string) {
58
+ if (!query) return true;
59
+ return item.label.toLowerCase().includes(query.toLowerCase());
60
+ }
61
+
62
+ /** Filter a list non-recursively (used by per-level search). */
63
+ export function filterLevel(
64
+ items: MainSidebarItem[],
65
+ query: string,
66
+ matcher: (item: MainSidebarItem, query: string) => boolean = defaultMatcher,
67
+ ) {
68
+ if (!query) return items;
69
+ return items.filter((item) => matcher(item, query));
70
+ }
71
+
72
+ export interface FlatMatch {
73
+ item: MainSidebarItem;
74
+ /** Parent labels from root → immediate parent (excludes the matched item). */
75
+ breadcrumbs: string[];
76
+ }
77
+
78
+ /** Recursively flatten + filter the whole tree (used by command variant). */
79
+ export function flatMatchTree(
80
+ items: MainSidebarItem[],
81
+ query: string,
82
+ matcher: (item: MainSidebarItem, query: string) => boolean = defaultMatcher,
83
+ ): FlatMatch[] {
84
+ const out: FlatMatch[] = [];
85
+ const visit = (list: MainSidebarItem[], breadcrumbs: string[]) => {
86
+ for (const item of list) {
87
+ if (matcher(item, query)) {
88
+ out.push({ item, breadcrumbs });
89
+ }
90
+ if (item.children?.length) {
91
+ visit(item.children, [...breadcrumbs, item.label]);
92
+ }
93
+ }
94
+ };
95
+ visit(items, []);
96
+ return out;
97
+ }
98
+
99
+ /**
100
+ * Convert a `MainSidebarColorScheme` into a CSS-variable style payload.
101
+ *
102
+ * Strategy: override the design-system brand variables (`--color-primary`,
103
+ * `--color-accent`, `--color-foreground`, etc.) directly on the sidebar
104
+ * root. Every Tailwind class that resolves through those tokens
105
+ * (`bg-primary`, `text-primary-foreground`, `bg-accent`, …) gets the new
106
+ * colour automatically — no per-element rewrites, and the override applies
107
+ * to nested affordances too (tooltips, breadcrumbs, badges, expanded
108
+ * panel surfaces).
109
+ *
110
+ * Tokens are scoped because the inline style only applies within the
111
+ * sidebar root subtree, so the rest of the page keeps its brand defaults.
112
+ *
113
+ * Omitted tokens are NOT emitted, so the cascade falls through to the
114
+ * page-level defaults.
115
+ */
116
+ export function colorSchemeToStyle(
117
+ scheme: MainSidebarColorScheme | undefined,
118
+ ): CSSProperties {
119
+ if (!scheme) return {};
120
+ const out: Record<string, string> = {};
121
+
122
+ /* Granular tokens win over the bg/fg shorthands. */
123
+ const railBg = scheme.railBg ?? scheme.bg;
124
+ const railFg = scheme.railFg ?? scheme.fg;
125
+ const panelBg = scheme.panelBg ?? scheme.bg;
126
+ const panelFg = scheme.panelFg ?? scheme.fg;
127
+
128
+ if (railBg) out['--color-primary'] = railBg;
129
+ if (railFg) out['--color-primary-foreground'] = railFg;
130
+ if (panelBg) out['--color-card'] = panelBg;
131
+ if (panelFg) out['--color-card-foreground'] = panelFg;
132
+
133
+ if (scheme.accentBg) out['--color-accent'] = scheme.accentBg;
134
+ if (scheme.accentFg) out['--color-accent-foreground'] = scheme.accentFg;
135
+ if (scheme.border) out['--color-border'] = scheme.border;
136
+ if (scheme.mutedFg) out['--color-muted-foreground'] = scheme.mutedFg;
137
+ if (scheme.ring) out['--color-ring'] = scheme.ring;
138
+ /*
139
+ * Hover overlay — exposed under a sidebar-specific variable. Components
140
+ * read it via `bg-[var(--mihcm-sidebar-hover-bg,…)]` with a sensible
141
+ * fallback so the default theme still works when `hoverBg` isn't set.
142
+ */
143
+ if (scheme.hoverBg) out['--mihcm-sidebar-hover-bg'] = scheme.hoverBg;
144
+ /*
145
+ * Tooltip surface uses --color-foreground / --color-background. These
146
+ * are NOT touched by panelFg/railFg overrides — keeping them at the
147
+ * page's brand defaults lets the rail-icon tooltip flip cleanly with
148
+ * dark mode. Set `tooltipBg`/`tooltipFg` only when you need a tooltip
149
+ * surface distinct from the page brand.
150
+ */
151
+ if (scheme.tooltipBg) out['--color-foreground'] = scheme.tooltipBg;
152
+ if (scheme.tooltipFg) out['--color-background'] = scheme.tooltipFg;
153
+ return out as CSSProperties;
154
+ }
155
+
156
+ /** Resolve the active path stack into MainSidebarItem objects (filters missing ids). */
157
+ export function resolvePath(
158
+ items: MainSidebarItem[],
159
+ pathKeys: string[],
160
+ ): MainSidebarItem[] {
161
+ return pathKeys
162
+ .map((key) => findItem(items, key))
163
+ .filter((item): item is MainSidebarItem => Boolean(item));
164
+ }
@@ -0,0 +1,334 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * MainSidebar — `hover` variant.
5
+ *
6
+ * Rail stays in icon width permanently. Hovering an icon shows that icon's
7
+ * children in a floating mini-menu beside the rail. Clicking a child that
8
+ * itself has children drills the panel deeper — a breadcrumb at the top
9
+ * lets the user jump back to any ancestor in one click. Layout never
10
+ * reflows. Touch devices fall back to tap-toggle.
11
+ */
12
+ import { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
13
+ import { AnimatePresence, motion, useReducedMotion } from 'motion/react';
14
+ import { cn } from '../internal/cn.js';
15
+ import { Rail } from './rail.js';
16
+ import { MenuSearch } from './search.js';
17
+ import { BackButton } from './back-button.js';
18
+ import { PathBreadcrumb } from './breadcrumb.js';
19
+ import { defaultMatcher, filterLevel, findItem, opensPanel } from './helpers.js';
20
+ import { scaleVariants } from './motion.js';
21
+ import type { MainSidebarItem, MainSidebarProps } from './types.js';
22
+
23
+ export const HoverSidebar = forwardRef<HTMLElement, MainSidebarProps>(function HoverSidebar(
24
+ {
25
+ items,
26
+ activeKey,
27
+ onItemSelect,
28
+ header,
29
+ footer,
30
+ search = false,
31
+ searchPlaceholder,
32
+ onSearchChange,
33
+ side = 'left',
34
+ density = 'comfortable',
35
+ panelWidth = 260,
36
+ motionPreset = 'expressive',
37
+ hoverDelayMs = 120,
38
+ closeOnOutsideClick = true,
39
+ railClassName,
40
+ panelClassName,
41
+ itemClassName,
42
+ activeItemClassName,
43
+ backLabel = 'Back',
44
+ className,
45
+ ...rest
46
+ },
47
+ ref,
48
+ ) {
49
+ const reduceMotion = useReducedMotion();
50
+ const effectivePreset = reduceMotion ? 'subtle' : motionPreset;
51
+
52
+ /* `hoveredKey` = the root rail item that opened the panel.
53
+ * `panelPathKeys` = ancestors drilled into INSIDE the panel, starting
54
+ * from (but not including) hoveredKey. When empty, panel shows
55
+ * hoveredItem.children. When [childKey], panel shows child.children. */
56
+ const [hoveredKey, setHoveredKey] = useState<string | null>(null);
57
+ const [panelPathKeys, setPanelPathKeys] = useState<string[]>([]);
58
+ const [query, setQuery] = useState('');
59
+ const railRef = useRef<HTMLElement>(null);
60
+ const panelRef = useRef<HTMLDivElement>(null);
61
+ const hideTimer = useRef<number | undefined>(undefined);
62
+ const showTimer = useRef<number | undefined>(undefined);
63
+
64
+ const searchEnabled = search !== false;
65
+ const searchCfg = typeof search === 'object' ? search : undefined;
66
+ const matcher = searchCfg?.matcher ?? defaultMatcher;
67
+
68
+ const rootItem = useMemo(
69
+ () => (hoveredKey ? items.find((it) => it.key === hoveredKey) ?? null : null),
70
+ [items, hoveredKey],
71
+ );
72
+
73
+ /* Build the full breadcrumb path from rootItem down through the drilled
74
+ * panel ancestors. The deepest entry's children populate the panel. */
75
+ const path = useMemo<MainSidebarItem[]>(() => {
76
+ if (!rootItem) return [];
77
+ const stack: MainSidebarItem[] = [rootItem];
78
+ for (const key of panelPathKeys) {
79
+ const node = findItem(stack[stack.length - 1]?.children ?? [], key);
80
+ if (!node) break;
81
+ stack.push(node);
82
+ }
83
+ return stack;
84
+ }, [rootItem, panelPathKeys]);
85
+
86
+ const current = path[path.length - 1] ?? null;
87
+ const childList = current?.children ?? [];
88
+ const filtered = filterLevel(childList, query, matcher);
89
+
90
+ function cancelHide() {
91
+ if (hideTimer.current !== undefined) {
92
+ window.clearTimeout(hideTimer.current);
93
+ hideTimer.current = undefined;
94
+ }
95
+ }
96
+ function scheduleHide() {
97
+ cancelHide();
98
+ hideTimer.current = window.setTimeout(() => closePanel(), 200);
99
+ }
100
+ function cancelShow() {
101
+ if (showTimer.current !== undefined) {
102
+ window.clearTimeout(showTimer.current);
103
+ showTimer.current = undefined;
104
+ }
105
+ }
106
+ function scheduleShow(key: string) {
107
+ cancelShow();
108
+ showTimer.current = window.setTimeout(() => {
109
+ if (key !== hoveredKey) {
110
+ setHoveredKey(key);
111
+ setPanelPathKeys([]);
112
+ setQuery('');
113
+ }
114
+ }, hoverDelayMs);
115
+ }
116
+
117
+ function closePanel() {
118
+ setHoveredKey(null);
119
+ setPanelPathKeys([]);
120
+ setQuery('');
121
+ }
122
+
123
+ function selectRailItem(item: MainSidebarItem) {
124
+ if (item.disabled) return;
125
+ onItemSelect?.(item.key, item);
126
+ if (opensPanel(item)) {
127
+ cancelHide();
128
+ cancelShow();
129
+ if (hoveredKey === item.key) {
130
+ closePanel();
131
+ return;
132
+ }
133
+ setHoveredKey(item.key);
134
+ setPanelPathKeys([]);
135
+ setQuery('');
136
+ } else {
137
+ closePanel();
138
+ }
139
+ }
140
+
141
+ function selectPanelItem(item: MainSidebarItem) {
142
+ if (item.disabled) return;
143
+ onItemSelect?.(item.key, item);
144
+ if (opensPanel(item)) {
145
+ /* Drill deeper inside the panel. */
146
+ setPanelPathKeys((prev) => [...prev, item.key]);
147
+ setQuery('');
148
+ } else {
149
+ closePanel();
150
+ }
151
+ }
152
+
153
+ function goBack() {
154
+ setPanelPathKeys((prev) => prev.slice(0, -1));
155
+ setQuery('');
156
+ }
157
+
158
+ /* Esc + outside-click close panel. */
159
+ useEffect(() => {
160
+ if (!hoveredKey) return;
161
+ function onKey(e: KeyboardEvent) {
162
+ if (e.key === 'Escape') closePanel();
163
+ }
164
+ function onPointerDown(e: PointerEvent) {
165
+ if (!closeOnOutsideClick) return;
166
+ const target = e.target as Node;
167
+ if (panelRef.current?.contains(target) || railRef.current?.contains(target)) return;
168
+ closePanel();
169
+ }
170
+ document.addEventListener('keydown', onKey);
171
+ document.addEventListener('pointerdown', onPointerDown);
172
+ return () => {
173
+ document.removeEventListener('keydown', onKey);
174
+ document.removeEventListener('pointerdown', onPointerDown);
175
+ };
176
+ }, [hoveredKey, closeOnOutsideClick]);
177
+
178
+ /* Cleanup timers on unmount */
179
+ useEffect(() => {
180
+ return () => {
181
+ cancelHide();
182
+ cancelShow();
183
+ };
184
+ }, []);
185
+
186
+ return (
187
+ /*
188
+ * Per-surface mouse handlers (NOT one outer wrapper handler) — the panel
189
+ * is absolutely positioned outside the outer container's layout box, so
190
+ * an outer `onMouseLeave` fires the moment the cursor enters the gap
191
+ * between rail and panel, racing the panel's own `onMouseEnter` and
192
+ * causing a hide/show flicker.
193
+ *
194
+ * Instead each surface owns its own enter/leave:
195
+ * - rail-wrapper: enter → cancelHide, leave → scheduleHide
196
+ * - panel: enter → cancelHide, leave → scheduleHide
197
+ * The `hideTimer` is the bridge that absorbs the brief gap-traversal.
198
+ */
199
+ <div ref={ref as never} className={cn('relative h-full', className)} {...rest}>
200
+ <div
201
+ onMouseEnter={cancelHide}
202
+ onMouseLeave={scheduleHide}
203
+ onMouseMove={(e) => {
204
+ const target = (e.target as HTMLElement).closest('button');
205
+ if (!target) return;
206
+ const key = target.dataset.itemKey;
207
+ if (key && key !== hoveredKey) scheduleShow(key);
208
+ }}
209
+ >
210
+ <Rail
211
+ ref={railRef}
212
+ items={items.map((it) => ({ ...it }))}
213
+ activeKey={activeKey}
214
+ density={density}
215
+ side={side}
216
+ header={header}
217
+ footer={footer}
218
+ itemClassName={itemClassName}
219
+ activeItemClassName={activeItemClassName}
220
+ className={railClassName}
221
+ openPanelKey={hoveredKey ?? undefined}
222
+ onItemSelect={selectRailItem}
223
+ />
224
+ </div>
225
+
226
+ <AnimatePresence initial={false} mode="wait">
227
+ {current && (childList.length || current.panel) ? (
228
+ <motion.div
229
+ key={rootItem?.key ?? 'panel'}
230
+ ref={panelRef}
231
+ variants={scaleVariants(effectivePreset)}
232
+ initial="initial"
233
+ animate="animate"
234
+ exit="exit"
235
+ role="menu"
236
+ aria-label={current.label}
237
+ onMouseEnter={cancelHide}
238
+ onMouseLeave={scheduleHide}
239
+ style={{
240
+ width: typeof panelWidth === 'number' ? `${panelWidth}px` : panelWidth,
241
+ [side === 'right' ? 'right' : 'left']: density === 'compact' ? '3rem' : '3.5rem',
242
+ }}
243
+ className={cn(
244
+ 'absolute top-2 z-10 rounded-xl border border-border bg-card text-card-foreground shadow-xl',
245
+ panelClassName,
246
+ )}
247
+ >
248
+ <div className="border-b border-border px-3 py-2.5">
249
+ {path.length > 1 ? (
250
+ <>
251
+ <PathBreadcrumb
252
+ path={path}
253
+ onJump={(depth) => {
254
+ /* breadcrumb depth indexes into `path` (root=0). The
255
+ * panel stack starts from depth 1, so trim it to
256
+ * keep `depth` items beyond the root. */
257
+ setPanelPathKeys((prev) => prev.slice(0, depth));
258
+ setQuery('');
259
+ }}
260
+ className="mb-1"
261
+ />
262
+ <BackButton onClick={goBack} label={backLabel} className="mb-1 -ml-1.5" />
263
+ </>
264
+ ) : null}
265
+ <div className="truncate text-base font-semibold text-card-foreground">{current.label}</div>
266
+ {current.description ? (
267
+ <p className="mt-0.5 truncate text-xs text-card-foreground/70">{current.description}</p>
268
+ ) : null}
269
+ {searchEnabled ? (
270
+ <div className="mt-2">
271
+ <MenuSearch
272
+ value={query}
273
+ onValueChange={(v) => {
274
+ setQuery(v);
275
+ onSearchChange?.(v);
276
+ }}
277
+ placeholder={searchCfg?.placeholder ?? searchPlaceholder ?? `Search ${current.label}…`}
278
+
279
+ tone="onBrand"
280
+ />
281
+ </div>
282
+ ) : null}
283
+ </div>
284
+ <div className="max-h-[min(80vh,32rem)] overflow-y-auto p-1.5">
285
+ {filtered.length === 0 && query ? (
286
+ <div className="px-3 py-6 text-center text-xs text-card-foreground/70">
287
+ {searchCfg?.noResultsLabel ?? `No items match "${query}".`}
288
+ </div>
289
+ ) : (
290
+ filtered.map((item) => {
291
+ const hasChildren = opensPanel(item);
292
+ return (
293
+ <button
294
+ key={item.key}
295
+ type="button"
296
+ onClick={() => selectPanelItem(item)}
297
+ disabled={item.disabled}
298
+ className={cn(
299
+ 'group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-card-foreground transition-colors',
300
+ 'hover:bg-card-foreground/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
301
+ 'disabled:cursor-not-allowed disabled:opacity-50',
302
+ item.key === activeKey && 'text-card-foreground font-semibold',
303
+ itemClassName,
304
+ )}
305
+ >
306
+ {item.icon ? (
307
+ <span className="grid size-4 shrink-0 place-items-center text-card-foreground/70 [&_svg]:size-full" aria-hidden="true">
308
+ {item.icon}
309
+ </span>
310
+ ) : null}
311
+ <span className="min-w-0 flex-1 truncate">{item.label}</span>
312
+ {hasChildren ? (
313
+ <svg
314
+ viewBox="0 0 24 24"
315
+ fill="none"
316
+ stroke="currentColor"
317
+ strokeWidth="2"
318
+ className="size-3.5 shrink-0 text-card-foreground/70"
319
+ aria-hidden="true"
320
+ >
321
+ <path d="m9 18 6-6-6-6" strokeLinecap="round" strokeLinejoin="round" />
322
+ </svg>
323
+ ) : null}
324
+ </button>
325
+ );
326
+ })
327
+ )}
328
+ </div>
329
+ </motion.div>
330
+ ) : null}
331
+ </AnimatePresence>
332
+ </div>
333
+ );
334
+ });
@@ -0,0 +1,191 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * MainSidebar — variant dispatcher with shadcn-style collapse toggle.
5
+ *
6
+ * The sidebar has two top-level shapes (matching shadcn's `collapsible="icon"`):
7
+ *
8
+ * - **expanded** — wide vertical sidebar with icon + label rows and
9
+ * inline accordion expansion. Shared across every variant via
10
+ * `ExpandedSidebar`. This is what users see by default unless the call
11
+ * site opts into `defaultCollapsed`.
12
+ * - **collapsed** — the original narrow icon rail; behaviour is
13
+ * variant-specific. Five interaction models live here:
14
+ *
15
+ * - `drilldown` (default) — rail click reveals a panel; deeper levels
16
+ * replace it; back via breadcrumb. Layout overlays.
17
+ * - `floating` — panel floats on top of content; dismiss on
18
+ * outside-click, Esc, or repeated rail click.
19
+ * - `columns` — Finder-style Miller columns; each level keeps
20
+ * the previous column visible; horizontally scrolling.
21
+ * - `command` — search-first; typing filters the whole tree
22
+ * recursively; empty query falls back to drilldown.
23
+ * - `hover` — hovering an icon pops out a floating
24
+ * mini-menu. Touch falls back to tap-toggle.
25
+ *
26
+ * Toggle between the two shapes via the in-built collapse button (header of
27
+ * the expanded sidebar) or a floating expand chip rendered next to the rail
28
+ * when collapsed. Both can be hidden via `showCollapseToggle={false}`.
29
+ *
30
+ * Public barrel — implementations split under `./MainSidebar/*`. Wiki:
31
+ * docs/components/MainSidebar.md.
32
+ */
33
+ import { forwardRef, useState } from 'react';
34
+ import { cn } from '../internal/cn.js';
35
+ import { DrilldownSidebar } from './drilldown.js';
36
+ import { FloatingSidebar } from './floating.js';
37
+ import { ColumnsSidebar } from './columns.js';
38
+ import { CommandSidebar } from './command.js';
39
+ import { HoverSidebar } from './hover.js';
40
+ import { ExpandedSidebar } from './expanded.js';
41
+ import { MobileSidebar } from './mobile.js';
42
+ import { colorSchemeToStyle } from './helpers.js';
43
+ import type { MainSidebarProps } from './types.js';
44
+
45
+ export type {
46
+ IconSidebarItem,
47
+ IconSidebarProps,
48
+ MainSidebarCollapsible,
49
+ MainSidebarColorScheme,
50
+ MainSidebarDensity,
51
+ MainSidebarItem,
52
+ MainSidebarMotionPreset,
53
+ MainSidebarProps,
54
+ MainSidebarSearchConfig,
55
+ MainSidebarSide,
56
+ MainSidebarVariant,
57
+ } from './types.js';
58
+
59
+ export const MainSidebar = forwardRef<HTMLElement, MainSidebarProps>(function MainSidebar(props, ref) {
60
+ const {
61
+ collapsible = 'icon',
62
+ collapsed,
63
+ defaultCollapsed = true,
64
+ onCollapsedChange,
65
+ showCollapseToggle = true,
66
+ side = 'left',
67
+ expandedLabel = 'Expand menu',
68
+ /*
69
+ * Strip sidebar-meta props that no variant destructures. If left in
70
+ * `...rest`, they'd be spread onto the variant's root <div> and
71
+ * trigger "unknown DOM attribute" warnings (e.g. `mobile`).
72
+ */
73
+ mobile,
74
+ showLabelsWhenExpanded: _showLabelsWhenExpanded,
75
+ collapsedWidth: _collapsedWidth,
76
+ expandedWidth: _expandedWidth,
77
+ collapsedLabel: _collapsedLabel,
78
+ colorScheme,
79
+ style,
80
+ /* Variant-specific props — strip from `variantProps` and re-inject only
81
+ * for the variant that actually consumes them. Otherwise the prop
82
+ * leaks through into a variant that doesn't destructure it and lands
83
+ * on its root <div>, triggering "unknown DOM attribute" warnings. */
84
+ columnsMaxVisible,
85
+ hoverDelayMs,
86
+ ...variantProps
87
+ } = props;
88
+ void _showLabelsWhenExpanded;
89
+ void _collapsedWidth;
90
+ void _expandedWidth;
91
+ void _collapsedLabel;
92
+
93
+ /* Whitelabel theme — emitted as CSS-variable overrides on the wrapper so
94
+ * every nested Tailwind class (`bg-primary`, `text-primary-foreground`,
95
+ * `bg-accent`, `border-border`, etc.) inside the sidebar subtree
96
+ * resolves through the consumer's brand colours. */
97
+ const themeStyle = { ...colorSchemeToStyle(colorScheme), ...style };
98
+
99
+ const [internalCollapsed, setInternalCollapsed] = useState(defaultCollapsed);
100
+ const isCollapsed = collapsed ?? internalCollapsed;
101
+
102
+ function setCollapsed(next: boolean) {
103
+ if (collapsed === undefined) setInternalCollapsed(next);
104
+ onCollapsedChange?.(next);
105
+ }
106
+
107
+ /* Mobile / tablet — render a hamburger trigger that opens the sidebar in
108
+ * a Sheet overlay instead of taking inline page width. */
109
+ if (mobile) {
110
+ return (
111
+ <div style={themeStyle} className="contents">
112
+ <MobileSidebar {...variantProps} side={side} />
113
+ </div>
114
+ );
115
+ }
116
+
117
+ /* Expanded shape — universal across variants. */
118
+ if (collapsible !== 'none' && !isCollapsed) {
119
+ return (
120
+ <ExpandedSidebar
121
+ ref={ref}
122
+ {...variantProps}
123
+ side={side}
124
+ showCollapseToggle={showCollapseToggle}
125
+ onCollapse={() => setCollapsed(true)}
126
+ style={themeStyle}
127
+ />
128
+ );
129
+ }
130
+
131
+ /* Collapsed shape — variant-specific. Wrap in a relative shell so the
132
+ * floating expand chip can sit next to the rail without disturbing the
133
+ * variant's own layout. The wrapper carries the theme style so the
134
+ * floating expand chip also inherits the brand-override variables. */
135
+ const variantNode = (() => {
136
+ switch (props.variant) {
137
+ case 'floating':
138
+ return <FloatingSidebar ref={ref} {...variantProps} side={side} />;
139
+ case 'columns':
140
+ return (
141
+ <ColumnsSidebar
142
+ ref={ref}
143
+ {...variantProps}
144
+ side={side}
145
+ {...(columnsMaxVisible !== undefined ? { columnsMaxVisible } : {})}
146
+ />
147
+ );
148
+ case 'command':
149
+ return <CommandSidebar ref={ref} {...variantProps} side={side} />;
150
+ case 'hover':
151
+ return (
152
+ <HoverSidebar
153
+ ref={ref}
154
+ {...variantProps}
155
+ side={side}
156
+ {...(hoverDelayMs !== undefined ? { hoverDelayMs } : {})}
157
+ />
158
+ );
159
+ case 'drilldown':
160
+ case undefined:
161
+ default:
162
+ return <DrilldownSidebar ref={ref} {...variantProps} side={side} />;
163
+ }
164
+ })();
165
+
166
+ if (collapsible === 'none' || !showCollapseToggle) {
167
+ return <div style={themeStyle} className="contents">{variantNode}</div>;
168
+ }
169
+
170
+ return (
171
+ <div className="relative flex h-full" style={themeStyle}>
172
+ {variantNode}
173
+ <button
174
+ type="button"
175
+ onClick={() => setCollapsed(false)}
176
+ aria-label={expandedLabel}
177
+ className={cn(
178
+ 'absolute top-3 z-30 grid size-6 place-items-center rounded-full border border-border bg-card text-muted-foreground shadow-sm transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
179
+ side === 'right' ? 'left-[-12px]' : 'right-[-12px]',
180
+ )}
181
+ >
182
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="size-3.5" aria-hidden="true">
183
+ <path d={side === 'right' ? 'm15 18-6-6 6-6' : 'm9 18 6-6-6-6'} strokeLinecap="round" strokeLinejoin="round" />
184
+ </svg>
185
+ </button>
186
+ </div>
187
+ );
188
+ });
189
+
190
+ /** Backward-compat: IconSidebar was the original name. */
191
+ export { MainSidebar as IconSidebar };