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