@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.
- package/dist/CheckboxGrid.native.d.ts.map +1 -1
- package/dist/CheckboxGrid.native.js +2 -1
- package/dist/CheckboxGrid.native.js.map +1 -1
- package/dist/Combobox.native.d.ts.map +1 -1
- package/dist/Combobox.native.js +2 -1
- package/dist/Combobox.native.js.map +1 -1
- package/dist/DataTable/column-filter.d.ts +8 -0
- package/dist/DataTable/column-filter.d.ts.map +1 -0
- package/dist/DataTable/column-filter.js +67 -0
- package/dist/DataTable/column-filter.js.map +1 -0
- package/dist/DataTable/column-header.d.ts +16 -0
- package/dist/DataTable/column-header.d.ts.map +1 -0
- package/dist/DataTable/column-header.js +11 -0
- package/dist/DataTable/column-header.js.map +1 -0
- package/dist/DataTable/column-visibility.d.ts +7 -0
- package/dist/DataTable/column-visibility.d.ts.map +1 -0
- package/dist/DataTable/column-visibility.js +35 -0
- package/dist/DataTable/column-visibility.js.map +1 -0
- package/dist/DataTable/index.d.ts +5 -0
- package/dist/DataTable/index.d.ts.map +1 -0
- package/dist/DataTable/index.js +5 -0
- package/dist/DataTable/index.js.map +1 -0
- package/dist/DataTable/pinning.d.ts +13 -0
- package/dist/DataTable/pinning.d.ts.map +1 -0
- package/dist/DataTable/pinning.js +29 -0
- package/dist/DataTable/pinning.js.map +1 -0
- package/dist/DataTable.d.ts +3 -7
- package/dist/DataTable.d.ts.map +1 -1
- package/dist/DataTable.js +7 -126
- package/dist/DataTable.js.map +1 -1
- package/dist/Dialog.native.d.ts +3 -1
- package/dist/Dialog.native.d.ts.map +1 -1
- package/dist/Dialog.native.js +2 -2
- package/dist/Dialog.native.js.map +1 -1
- package/dist/Form/building-blocks.d.ts +26 -0
- package/dist/Form/building-blocks.d.ts.map +1 -0
- package/dist/Form/building-blocks.js +29 -0
- package/dist/Form/building-blocks.js.map +1 -0
- package/dist/Form/fields-choice.d.ts +72 -0
- package/dist/Form/fields-choice.d.ts.map +1 -0
- package/dist/Form/fields-choice.js +69 -0
- package/dist/Form/fields-choice.js.map +1 -0
- package/dist/Form/fields-complex.d.ts +28 -0
- package/dist/Form/fields-complex.d.ts.map +1 -0
- package/dist/Form/fields-complex.js +38 -0
- package/dist/Form/fields-complex.js.map +1 -0
- package/dist/Form/fields-date.d.ts +46 -0
- package/dist/Form/fields-date.d.ts.map +1 -0
- package/dist/Form/fields-date.js +41 -0
- package/dist/Form/fields-date.js.map +1 -0
- package/dist/Form/fields-text.d.ts +47 -0
- package/dist/Form/fields-text.d.ts.map +1 -0
- package/dist/Form/fields-text.js +46 -0
- package/dist/Form/fields-text.js.map +1 -0
- package/dist/Form/fields-toggle.d.ts +24 -0
- package/dist/Form/fields-toggle.d.ts.map +1 -0
- package/dist/Form/fields-toggle.js +32 -0
- package/dist/Form/fields-toggle.js.map +1 -0
- package/dist/Form/helpers.d.ts +66 -0
- package/dist/Form/helpers.d.ts.map +1 -0
- package/dist/Form/helpers.js +44 -0
- package/dist/Form/helpers.js.map +1 -0
- package/dist/Form/types.d.ts +25 -0
- package/dist/Form/types.d.ts.map +1 -0
- package/dist/Form/types.js +8 -0
- package/dist/Form/types.js.map +1 -0
- package/dist/Form.d.ts +24 -298
- package/dist/Form.d.ts.map +1 -1
- package/dist/Form.js +30 -246
- package/dist/Form.js.map +1 -1
- package/dist/IconSidebar.d.ts +6 -46
- package/dist/IconSidebar.d.ts.map +1 -1
- package/dist/IconSidebar.js +6 -116
- package/dist/IconSidebar.js.map +1 -1
- package/dist/MainSidebar/back-button.d.ts +14 -0
- package/dist/MainSidebar/back-button.d.ts.map +1 -0
- package/dist/MainSidebar/back-button.js +14 -0
- package/dist/MainSidebar/back-button.js.map +1 -0
- package/dist/MainSidebar/breadcrumb.d.ts +10 -0
- package/dist/MainSidebar/breadcrumb.d.ts.map +1 -0
- package/dist/MainSidebar/breadcrumb.js +24 -0
- package/dist/MainSidebar/breadcrumb.js.map +1 -0
- package/dist/MainSidebar/columns.d.ts +3 -0
- package/dist/MainSidebar/columns.d.ts.map +1 -0
- package/dist/MainSidebar/columns.js +198 -0
- package/dist/MainSidebar/columns.js.map +1 -0
- package/dist/MainSidebar/command.d.ts +3 -0
- package/dist/MainSidebar/command.d.ts.map +1 -0
- package/dist/MainSidebar/command.js +193 -0
- package/dist/MainSidebar/command.js.map +1 -0
- package/dist/MainSidebar/drilldown.d.ts +3 -0
- package/dist/MainSidebar/drilldown.d.ts.map +1 -0
- package/dist/MainSidebar/drilldown.js +154 -0
- package/dist/MainSidebar/drilldown.js.map +1 -0
- package/dist/MainSidebar/expanded.d.ts +7 -0
- package/dist/MainSidebar/expanded.d.ts.map +1 -0
- package/dist/MainSidebar/expanded.js +102 -0
- package/dist/MainSidebar/expanded.js.map +1 -0
- package/dist/MainSidebar/floating.d.ts +3 -0
- package/dist/MainSidebar/floating.d.ts.map +1 -0
- package/dist/MainSidebar/floating.js +116 -0
- package/dist/MainSidebar/floating.js.map +1 -0
- package/dist/MainSidebar/helpers.d.ts +50 -0
- package/dist/MainSidebar/helpers.d.ts.map +1 -0
- package/dist/MainSidebar/helpers.js +148 -0
- package/dist/MainSidebar/helpers.js.map +1 -0
- package/dist/MainSidebar/hover.d.ts +3 -0
- package/dist/MainSidebar/hover.d.ts.map +1 -0
- package/dist/MainSidebar/hover.js +177 -0
- package/dist/MainSidebar/hover.js.map +1 -0
- package/dist/MainSidebar/index.d.ts +6 -0
- package/dist/MainSidebar/index.d.ts.map +1 -0
- package/dist/MainSidebar/index.js +108 -0
- package/dist/MainSidebar/index.js.map +1 -0
- package/dist/MainSidebar/mobile.d.ts +29 -0
- package/dist/MainSidebar/mobile.d.ts.map +1 -0
- package/dist/MainSidebar/mobile.js +38 -0
- package/dist/MainSidebar/mobile.js.map +1 -0
- package/dist/MainSidebar/motion.d.ts +23 -0
- package/dist/MainSidebar/motion.d.ts.map +1 -0
- package/dist/MainSidebar/motion.js +40 -0
- package/dist/MainSidebar/motion.js.map +1 -0
- package/dist/MainSidebar/rail.d.ts +24 -0
- package/dist/MainSidebar/rail.d.ts.map +1 -0
- package/dist/MainSidebar/rail.js +29 -0
- package/dist/MainSidebar/rail.js.map +1 -0
- package/dist/MainSidebar/search.d.ts +19 -0
- package/dist/MainSidebar/search.d.ts.map +1 -0
- package/dist/MainSidebar/search.js +33 -0
- package/dist/MainSidebar/search.js.map +1 -0
- package/dist/MainSidebar/types.d.ts +161 -0
- package/dist/MainSidebar/types.d.ts.map +1 -0
- package/dist/MainSidebar/types.js +2 -0
- package/dist/MainSidebar/types.js.map +1 -0
- package/dist/MainSidebar.d.ts +6 -1
- package/dist/MainSidebar.d.ts.map +1 -1
- package/dist/MainSidebar.js +6 -1
- package/dist/MainSidebar.js.map +1 -1
- package/dist/NavigationMenu.js +1 -1
- package/dist/NavigationMenu.js.map +1 -1
- package/dist/RichTextEditor/theme.d.ts +44 -0
- package/dist/RichTextEditor/theme.d.ts.map +1 -0
- package/dist/RichTextEditor/theme.js +41 -0
- package/dist/RichTextEditor/theme.js.map +1 -0
- package/dist/RichTextEditor/toolbar-icons.d.ts +21 -0
- package/dist/RichTextEditor/toolbar-icons.d.ts.map +1 -0
- package/dist/RichTextEditor/toolbar-icons.js +21 -0
- package/dist/RichTextEditor/toolbar-icons.js.map +1 -0
- package/dist/RichTextEditor/toolbar.d.ts +5 -0
- package/dist/RichTextEditor/toolbar.d.ts.map +1 -0
- package/dist/RichTextEditor/toolbar.js +116 -0
- package/dist/RichTextEditor/toolbar.js.map +1 -0
- package/dist/RichTextEditor.d.ts +16 -9
- package/dist/RichTextEditor.d.ts.map +1 -1
- package/dist/RichTextEditor.js +18 -164
- package/dist/RichTextEditor.js.map +1 -1
- package/dist/Select/content.d.ts +9 -0
- package/dist/Select/content.d.ts.map +1 -0
- package/dist/Select/content.js +80 -0
- package/dist/Select/content.js.map +1 -0
- package/dist/Select/context.d.ts +27 -0
- package/dist/Select/context.d.ts.map +1 -0
- package/dist/Select/context.js +35 -0
- package/dist/Select/context.js.map +1 -0
- package/dist/Select/item.d.ts +13 -0
- package/dist/Select/item.d.ts.map +1 -0
- package/dist/Select/item.js +39 -0
- package/dist/Select/item.js.map +1 -0
- package/dist/Select/parts.d.ts +14 -0
- package/dist/Select/parts.d.ts.map +1 -0
- package/dist/Select/parts.js +17 -0
- package/dist/Select/parts.js.map +1 -0
- package/dist/Select/react-select.d.ts +25 -0
- package/dist/Select/react-select.d.ts.map +1 -0
- package/dist/Select/react-select.js +66 -0
- package/dist/Select/react-select.js.map +1 -0
- package/dist/Select/root.d.ts +15 -0
- package/dist/Select/root.d.ts.map +1 -0
- package/dist/Select/root.js +41 -0
- package/dist/Select/root.js.map +1 -0
- package/dist/Select/trigger.d.ts +15 -0
- package/dist/Select/trigger.d.ts.map +1 -0
- package/dist/Select/trigger.js +61 -0
- package/dist/Select/trigger.js.map +1 -0
- package/dist/Select.d.ts +14 -62
- package/dist/Select.d.ts.map +1 -1
- package/dist/Select.js +14 -293
- package/dist/Select.js.map +1 -1
- package/dist/Sidebar/context.d.ts +28 -0
- package/dist/Sidebar/context.d.ts.map +1 -0
- package/dist/Sidebar/context.js +37 -0
- package/dist/Sidebar/context.js.map +1 -0
- package/dist/Sidebar/group.d.ts +13 -0
- package/dist/Sidebar/group.d.ts.map +1 -0
- package/dist/Sidebar/group.js +20 -0
- package/dist/Sidebar/group.js.map +1 -0
- package/dist/Sidebar/icons.d.ts +7 -0
- package/dist/Sidebar/icons.d.ts.map +1 -0
- package/dist/Sidebar/icons.js +12 -0
- package/dist/Sidebar/icons.js.map +1 -0
- package/dist/Sidebar/layout.d.ts +9 -0
- package/dist/Sidebar/layout.d.ts.map +1 -0
- package/dist/Sidebar/layout.js +21 -0
- package/dist/Sidebar/layout.js.map +1 -0
- package/dist/Sidebar/menu.d.ts +29 -0
- package/dist/Sidebar/menu.d.ts.map +1 -0
- package/dist/Sidebar/menu.js +55 -0
- package/dist/Sidebar/menu.js.map +1 -0
- package/dist/Sidebar/provider.d.ts +33 -0
- package/dist/Sidebar/provider.d.ts.map +1 -0
- package/dist/Sidebar/provider.js +110 -0
- package/dist/Sidebar/provider.js.map +1 -0
- package/dist/Sidebar/sidebar.d.ts +17 -0
- package/dist/Sidebar/sidebar.d.ts.map +1 -0
- package/dist/Sidebar/sidebar.js +51 -0
- package/dist/Sidebar/sidebar.js.map +1 -0
- package/dist/Sidebar/submenu.d.ts +13 -0
- package/dist/Sidebar/submenu.d.ts.map +1 -0
- package/dist/Sidebar/submenu.js +17 -0
- package/dist/Sidebar/submenu.js.map +1 -0
- package/dist/Sidebar/trigger.d.ts +9 -0
- package/dist/Sidebar/trigger.d.ts.map +1 -0
- package/dist/Sidebar/trigger.js +33 -0
- package/dist/Sidebar/trigger.js.map +1 -0
- package/dist/Sidebar.d.ts +14 -104
- package/dist/Sidebar.d.ts.map +1 -1
- package/dist/Sidebar.js +14 -300
- package/dist/Sidebar.js.map +1 -1
- package/dist/StatCard.d.ts +67 -9
- package/dist/StatCard.d.ts.map +1 -1
- package/dist/StatCard.js +111 -9
- package/dist/StatCard.js.map +1 -1
- package/dist/TransferList.native.d.ts.map +1 -1
- package/dist/TransferList.native.js +2 -1
- package/dist/TransferList.native.js.map +1 -1
- package/package.json +2 -2
- package/src/CheckboxGrid.native.tsx +2 -1
- package/src/Combobox.native.tsx +2 -1
- package/src/DataTable/column-filter.tsx +134 -0
- package/src/DataTable/column-header.tsx +67 -0
- package/src/DataTable/column-visibility.tsx +87 -0
- package/src/DataTable/index.ts +4 -0
- package/src/DataTable/pinning.ts +40 -0
- package/src/DataTable.tsx +14 -297
- package/src/Dialog.native.tsx +4 -2
- package/src/Form/building-blocks.tsx +97 -0
- package/src/Form/fields-choice.tsx +312 -0
- package/src/Form/fields-complex.tsx +195 -0
- package/src/Form/fields-date.tsx +195 -0
- package/src/Form/fields-text.tsx +218 -0
- package/src/Form/fields-toggle.tsx +123 -0
- package/src/Form/helpers.tsx +189 -0
- package/src/Form/types.ts +26 -0
- package/src/Form.tsx +91 -1308
- package/src/IconSidebar.tsx +20 -442
- package/src/MainSidebar/back-button.tsx +58 -0
- package/src/MainSidebar/breadcrumb.tsx +53 -0
- package/src/MainSidebar/columns.tsx +350 -0
- package/src/MainSidebar/command.tsx +404 -0
- package/src/MainSidebar/drilldown.tsx +373 -0
- package/src/MainSidebar/expanded.tsx +414 -0
- package/src/MainSidebar/floating.tsx +268 -0
- package/src/MainSidebar/helpers.ts +164 -0
- package/src/MainSidebar/hover.tsx +334 -0
- package/src/MainSidebar/index.tsx +191 -0
- package/src/MainSidebar/mobile.tsx +117 -0
- package/src/MainSidebar/motion.ts +64 -0
- package/src/MainSidebar/rail.tsx +137 -0
- package/src/MainSidebar/search.tsx +99 -0
- package/src/MainSidebar/types.ts +208 -0
- package/src/MainSidebar.tsx +15 -4
- package/src/NavigationMenu.tsx +1 -1
- package/src/RichTextEditor/theme.ts +43 -0
- package/src/RichTextEditor/toolbar-icons.tsx +40 -0
- package/src/RichTextEditor/toolbar.tsx +271 -0
- package/src/RichTextEditor.tsx +23 -371
- package/src/Select/content.tsx +111 -0
- package/src/Select/context.tsx +66 -0
- package/src/Select/item.tsx +97 -0
- package/src/Select/parts.tsx +43 -0
- package/src/Select/react-select.tsx +216 -0
- package/src/Select/root.tsx +75 -0
- package/src/Select/trigger.tsx +122 -0
- package/src/Select.tsx +34 -692
- package/src/Sidebar/context.tsx +72 -0
- package/src/Sidebar/group.tsx +69 -0
- package/src/Sidebar/icons.tsx +42 -0
- package/src/Sidebar/layout.tsx +64 -0
- package/src/Sidebar/menu.tsx +171 -0
- package/src/Sidebar/provider.tsx +224 -0
- package/src/Sidebar/sidebar.tsx +178 -0
- package/src/Sidebar/submenu.tsx +58 -0
- package/src/Sidebar/trigger.tsx +104 -0
- package/src/Sidebar.tsx +44 -927
- package/src/StatCard.tsx +365 -20
- package/src/TransferList.native.tsx +2 -1
- package/dist/TiptapEditor.d.ts +0 -24
- package/dist/TiptapEditor.d.ts.map +0 -1
- package/dist/TiptapEditor.js +0 -84
- 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 };
|