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