@erxes/ui 3.2.25-1 → 3.2.25-3
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/components/combobox.d.ts +16 -0
- package/dist/modules/layout/components/AppLayout.d.ts +22 -0
- package/dist/modules/layout/index.d.ts +1 -0
- package/dist/types/UIConfig.d.ts +0 -2
- package/package.json +1 -1
- package/src/components/combobox.tsx +68 -1
- package/src/modules/layout/components/AppLayout.tsx +425 -0
- package/src/modules/layout/index.ts +1 -0
- package/src/types/UIConfig.ts +0 -2
|
@@ -18,6 +18,14 @@ export declare const ComboboxContent: React.ForwardRefExoticComponent<Omit<Omit<
|
|
|
18
18
|
export declare const ComboboxCheck: React.ForwardRefExoticComponent<Omit<import("@tabler/icons-react").IconProps & React.RefAttributes<import("@tabler/icons-react").Icon>, "ref"> & {
|
|
19
19
|
checked?: boolean;
|
|
20
20
|
} & React.RefAttributes<import("@tabler/icons-react").Icon>>;
|
|
21
|
+
export declare const ComboboxChipTrigger: ({ label, children, className, disabled, onClear, clearLabel, }: {
|
|
22
|
+
label?: string;
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
className?: string;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
onClear?: () => void;
|
|
27
|
+
clearLabel?: string;
|
|
28
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
21
29
|
export declare const ComboboxFetchMore: React.ForwardRefExoticComponent<Omit<Omit<{
|
|
22
30
|
children?: React.ReactNode;
|
|
23
31
|
} & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
@@ -53,6 +61,14 @@ export declare const Combobox: {
|
|
|
53
61
|
Check: React.ForwardRefExoticComponent<Omit<import("@tabler/icons-react").IconProps & React.RefAttributes<import("@tabler/icons-react").Icon>, "ref"> & {
|
|
54
62
|
checked?: boolean;
|
|
55
63
|
} & React.RefAttributes<import("@tabler/icons-react").Icon>>;
|
|
64
|
+
ChipTrigger: ({ label, children, className, disabled, onClear, clearLabel, }: {
|
|
65
|
+
label?: string;
|
|
66
|
+
children: React.ReactNode;
|
|
67
|
+
className?: string;
|
|
68
|
+
disabled?: boolean;
|
|
69
|
+
onClear?: () => void;
|
|
70
|
+
clearLabel?: string;
|
|
71
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
56
72
|
FetchMore: React.ForwardRefExoticComponent<Omit<Omit<{
|
|
57
73
|
children?: React.ReactNode;
|
|
58
74
|
} & Omit<Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* A link pinned to the bottom of an app sidebar (`AppLayout` `footer`), e.g.
|
|
4
|
+
* the plugin's own settings or the way back from them.
|
|
5
|
+
*/
|
|
6
|
+
export declare const AppLayoutFooterLink: ({ path, name, icon: Icon, }: {
|
|
7
|
+
path: string;
|
|
8
|
+
name: string;
|
|
9
|
+
icon?: React.ElementType;
|
|
10
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
/**
|
|
12
|
+
* Full-height layout for an app (plugin or core module) below the core top
|
|
13
|
+
* bar: the app owns its sidebar and pages. `id` keys the persisted
|
|
14
|
+
* open/width state, so use the plugin or module name.
|
|
15
|
+
*/
|
|
16
|
+
export declare const AppLayout: ({ id, sidebar, footer, title, children, }: {
|
|
17
|
+
id: string;
|
|
18
|
+
sidebar: React.ReactNode;
|
|
19
|
+
footer?: React.ReactNode;
|
|
20
|
+
title?: React.ReactNode;
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
package/dist/types/UIConfig.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Button } from './button';
|
|
3
3
|
import { cn } from '../lib/utils';
|
|
4
|
-
import { IconCheck, IconChevronDown } from '@tabler/icons-react';
|
|
4
|
+
import { IconCheck, IconChevronDown, IconX } from '@tabler/icons-react';
|
|
5
5
|
import { Popover } from './popover';
|
|
6
6
|
import { Command } from './command';
|
|
7
7
|
import { useInView } from 'react-intersection-observer';
|
|
@@ -139,6 +139,72 @@ export const ComboboxCheck = React.forwardRef<
|
|
|
139
139
|
|
|
140
140
|
ComboboxCheck.displayName = 'ComboboxCheck';
|
|
141
141
|
|
|
142
|
+
export const ComboboxChipTrigger = ({
|
|
143
|
+
label,
|
|
144
|
+
children,
|
|
145
|
+
className,
|
|
146
|
+
disabled,
|
|
147
|
+
onClear,
|
|
148
|
+
clearLabel,
|
|
149
|
+
}: {
|
|
150
|
+
label?: string;
|
|
151
|
+
children: React.ReactNode;
|
|
152
|
+
className?: string;
|
|
153
|
+
disabled?: boolean;
|
|
154
|
+
onClear?: () => void;
|
|
155
|
+
clearLabel?: string;
|
|
156
|
+
}) => {
|
|
157
|
+
const trigger = (
|
|
158
|
+
<ComboboxTriggerBase
|
|
159
|
+
className={cn(
|
|
160
|
+
'group w-fit max-w-sm h-7 rounded p-0 gap-0 overflow-hidden',
|
|
161
|
+
'data-[state=open]:border-primary/40',
|
|
162
|
+
className,
|
|
163
|
+
)}
|
|
164
|
+
disabled={disabled}
|
|
165
|
+
>
|
|
166
|
+
{label && (
|
|
167
|
+
<span className="flex h-full shrink-0 items-center whitespace-nowrap px-3 text-muted-foreground bg-muted/40 border-r">
|
|
168
|
+
{label}
|
|
169
|
+
</span>
|
|
170
|
+
)}
|
|
171
|
+
<span className="flex min-w-0 items-center gap-1.5 pl-3 pr-2.5 font-medium text-foreground">
|
|
172
|
+
<span className="flex min-w-0 items-center gap-1.5 overflow-hidden whitespace-nowrap [&_svg]:size-4 [&_svg]:shrink-0">
|
|
173
|
+
{children}
|
|
174
|
+
</span>
|
|
175
|
+
{onClear ? (
|
|
176
|
+
<span className="size-5 shrink-0" />
|
|
177
|
+
) : (
|
|
178
|
+
<IconChevronDown className="size-3.5 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
|
179
|
+
)}
|
|
180
|
+
</span>
|
|
181
|
+
</ComboboxTriggerBase>
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
if (!onClear) {
|
|
185
|
+
return trigger;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div className="relative w-fit max-w-sm">
|
|
190
|
+
{trigger}
|
|
191
|
+
<Button
|
|
192
|
+
variant="ghost"
|
|
193
|
+
size="icon"
|
|
194
|
+
aria-label={clearLabel}
|
|
195
|
+
title={clearLabel}
|
|
196
|
+
className="absolute right-2 top-1/2 size-5 -translate-y-1/2 p-0 text-muted-foreground"
|
|
197
|
+
onClick={(e) => {
|
|
198
|
+
e.stopPropagation();
|
|
199
|
+
onClear();
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
<IconX />
|
|
203
|
+
</Button>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
|
|
142
208
|
export const ComboboxFetchMore = React.forwardRef<
|
|
143
209
|
React.ElementRef<typeof Command.Item>,
|
|
144
210
|
React.ComponentPropsWithoutRef<typeof Command.Item> & {
|
|
@@ -207,6 +273,7 @@ export const Combobox = {
|
|
|
207
273
|
Value: ComboboxValue,
|
|
208
274
|
Content: ComboboxContent,
|
|
209
275
|
Check: ComboboxCheck,
|
|
276
|
+
ChipTrigger: ComboboxChipTrigger,
|
|
210
277
|
FetchMore: ComboboxFetchMore,
|
|
211
278
|
Empty: ComboboxEmpty,
|
|
212
279
|
};
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
IconLayoutSidebarLeftCollapse,
|
|
5
|
+
IconLayoutSidebarLeftExpand,
|
|
6
|
+
IconMenu2,
|
|
7
|
+
} from '@tabler/icons-react';
|
|
8
|
+
import { type PrimitiveAtom, useAtom } from 'jotai';
|
|
9
|
+
|
|
10
|
+
import { Button } from 'erxes-ui/components/button';
|
|
11
|
+
import { Separator } from 'erxes-ui/components/separator';
|
|
12
|
+
import { Sidebar } from 'erxes-ui/components/sidebar';
|
|
13
|
+
import { atomWithStorage } from 'jotai/utils';
|
|
14
|
+
import { Link, useLocation } from 'react-router-dom';
|
|
15
|
+
import { cn } from 'erxes-ui/lib/utils';
|
|
16
|
+
|
|
17
|
+
const APP_LAYOUT_DEFAULT_WIDTH = 240;
|
|
18
|
+
const APP_LAYOUT_MIN_WIDTH = 180;
|
|
19
|
+
const APP_LAYOUT_MAX_WIDTH = 480;
|
|
20
|
+
const APP_LAYOUT_KEYBOARD_STEP = 16;
|
|
21
|
+
|
|
22
|
+
type TAppLayoutAtoms = {
|
|
23
|
+
open: PrimitiveAtom<boolean>;
|
|
24
|
+
width: PrimitiveAtom<number | null>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const appLayoutAtoms = new Map<string, TAppLayoutAtoms>();
|
|
28
|
+
|
|
29
|
+
const getAppLayoutAtoms = (id: string) => {
|
|
30
|
+
const existing = appLayoutAtoms.get(id);
|
|
31
|
+
|
|
32
|
+
if (existing) {
|
|
33
|
+
return existing;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const atoms: TAppLayoutAtoms = {
|
|
37
|
+
open: atomWithStorage<boolean>(`app-layout:${id}:open`, true, undefined, {
|
|
38
|
+
getOnInit: true,
|
|
39
|
+
}),
|
|
40
|
+
width: atomWithStorage<number | null>(
|
|
41
|
+
`app-layout:${id}:width`,
|
|
42
|
+
null,
|
|
43
|
+
undefined,
|
|
44
|
+
{ getOnInit: true },
|
|
45
|
+
),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
appLayoutAtoms.set(id, atoms);
|
|
49
|
+
|
|
50
|
+
return atoms;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const clampWidth = (value: number) =>
|
|
54
|
+
Math.round(
|
|
55
|
+
Math.min(APP_LAYOUT_MAX_WIDTH, Math.max(APP_LAYOUT_MIN_WIDTH, value)),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const AppLayoutResizeHandle = ({
|
|
59
|
+
panelRef,
|
|
60
|
+
onResize,
|
|
61
|
+
onReset,
|
|
62
|
+
onResizingChange,
|
|
63
|
+
}: {
|
|
64
|
+
panelRef: React.RefObject<HTMLElement | null>;
|
|
65
|
+
onResize: (width: number) => void;
|
|
66
|
+
onReset: () => void;
|
|
67
|
+
onResizingChange: (resizing: boolean) => void;
|
|
68
|
+
}) => {
|
|
69
|
+
const drag = React.useRef<{ startX: number; startWidth: number } | null>(
|
|
70
|
+
null,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const stopDrag = (event: React.PointerEvent<HTMLButtonElement>) => {
|
|
74
|
+
if (!drag.current) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
drag.current = null;
|
|
79
|
+
|
|
80
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
81
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
onResizingChange(false);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
aria-label="Resize sidebar"
|
|
91
|
+
title="Resize sidebar"
|
|
92
|
+
className="group/resize absolute inset-y-0 right-0 z-30 w-2 cursor-col-resize touch-none outline-none"
|
|
93
|
+
onDoubleClick={onReset}
|
|
94
|
+
onKeyDown={(event) => {
|
|
95
|
+
const panel = panelRef.current;
|
|
96
|
+
|
|
97
|
+
if (
|
|
98
|
+
!panel ||
|
|
99
|
+
(event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')
|
|
100
|
+
) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
onResize(
|
|
106
|
+
clampWidth(
|
|
107
|
+
panel.offsetWidth +
|
|
108
|
+
(event.key === 'ArrowRight'
|
|
109
|
+
? APP_LAYOUT_KEYBOARD_STEP
|
|
110
|
+
: -APP_LAYOUT_KEYBOARD_STEP),
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
}}
|
|
114
|
+
onPointerCancel={stopDrag}
|
|
115
|
+
onPointerDown={(event) => {
|
|
116
|
+
const panel = panelRef.current;
|
|
117
|
+
|
|
118
|
+
if (event.button !== 0 || !panel) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
124
|
+
drag.current = { startX: event.clientX, startWidth: panel.offsetWidth };
|
|
125
|
+
onResizingChange(true);
|
|
126
|
+
}}
|
|
127
|
+
onPointerMove={(event) => {
|
|
128
|
+
if (!drag.current) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const { startX, startWidth } = drag.current;
|
|
133
|
+
|
|
134
|
+
onResize(clampWidth(startWidth + event.clientX - startX));
|
|
135
|
+
}}
|
|
136
|
+
onPointerUp={stopDrag}
|
|
137
|
+
>
|
|
138
|
+
<span className="absolute inset-y-0 right-0 w-0.5 bg-transparent transition-colors duration-150 group-hover/resize:bg-primary/40 group-focus-visible/resize:bg-primary group-active/resize:bg-primary" />
|
|
139
|
+
</button>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const AppLayoutFloatingButton = ({
|
|
144
|
+
label,
|
|
145
|
+
icon,
|
|
146
|
+
onClick,
|
|
147
|
+
}: {
|
|
148
|
+
label: string;
|
|
149
|
+
icon: React.ReactNode;
|
|
150
|
+
onClick: () => void;
|
|
151
|
+
}) => (
|
|
152
|
+
<div className="absolute top-0 left-0 z-30 flex h-12 w-10 items-center justify-center bg-sidebar">
|
|
153
|
+
<Button
|
|
154
|
+
aria-expanded={false}
|
|
155
|
+
aria-label={label}
|
|
156
|
+
className="size-7 shrink-0"
|
|
157
|
+
size="icon"
|
|
158
|
+
title={label}
|
|
159
|
+
type="button"
|
|
160
|
+
variant="ghost"
|
|
161
|
+
onClick={onClick}
|
|
162
|
+
>
|
|
163
|
+
{icon}
|
|
164
|
+
</Button>
|
|
165
|
+
<Separator.Inline className="absolute top-1/2 right-0 -translate-y-1/2" />
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Fade only: moving a full-height pane reads as a jolt.
|
|
170
|
+
const AppLayoutFooter = ({ children }: { children?: React.ReactNode }) =>
|
|
171
|
+
children ? (
|
|
172
|
+
<div className="shrink-0 border-t px-2 py-1.5">
|
|
173
|
+
<Sidebar.Menu>{children}</Sidebar.Menu>
|
|
174
|
+
</div>
|
|
175
|
+
) : null;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* A link pinned to the bottom of an app sidebar (`AppLayout` `footer`), e.g.
|
|
179
|
+
* the plugin's own settings or the way back from them.
|
|
180
|
+
*/
|
|
181
|
+
export const AppLayoutFooterLink = ({
|
|
182
|
+
path,
|
|
183
|
+
name,
|
|
184
|
+
icon: Icon,
|
|
185
|
+
}: {
|
|
186
|
+
path: string;
|
|
187
|
+
name: string;
|
|
188
|
+
icon?: React.ElementType;
|
|
189
|
+
}) => {
|
|
190
|
+
const { pathname } = useLocation();
|
|
191
|
+
const fullPath = `/${path.replace(/^\/+/, '')}`;
|
|
192
|
+
const isActive = pathname === fullPath || pathname.startsWith(`${fullPath}/`);
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<Sidebar.MenuItem>
|
|
196
|
+
<Sidebar.MenuButton asChild isActive={isActive}>
|
|
197
|
+
<Link to={fullPath}>
|
|
198
|
+
{Icon && <Icon />}
|
|
199
|
+
<span className="min-w-0 flex-1 truncate">{name}</span>
|
|
200
|
+
</Link>
|
|
201
|
+
</Sidebar.MenuButton>
|
|
202
|
+
</Sidebar.MenuItem>
|
|
203
|
+
);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const APP_LAYOUT_PAGE_ENTER: Keyframe[] = [{ opacity: 0.4 }, { opacity: 1 }];
|
|
207
|
+
|
|
208
|
+
const getPageKey = (pathname: string) =>
|
|
209
|
+
pathname.split('/').filter(Boolean).slice(0, 2).join('/');
|
|
210
|
+
|
|
211
|
+
/** The page pane: replays a short enter on route change, never the sidebar. */
|
|
212
|
+
const AppLayoutPage = ({ children }: { children: React.ReactNode }) => {
|
|
213
|
+
const { pathname } = useLocation();
|
|
214
|
+
const pageRef = React.useRef<HTMLDivElement>(null);
|
|
215
|
+
const pageKey = getPageKey(pathname);
|
|
216
|
+
const previousKey = React.useRef(pageKey);
|
|
217
|
+
|
|
218
|
+
React.useLayoutEffect(() => {
|
|
219
|
+
const page = pageRef.current;
|
|
220
|
+
|
|
221
|
+
if (previousKey.current === pageKey) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
previousKey.current = pageKey;
|
|
226
|
+
|
|
227
|
+
if (
|
|
228
|
+
!page?.animate ||
|
|
229
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
230
|
+
) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const animation = page.animate(APP_LAYOUT_PAGE_ENTER, {
|
|
235
|
+
duration: 160,
|
|
236
|
+
easing: 'ease-out',
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return () => animation.cancel();
|
|
240
|
+
}, [pageKey]);
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div
|
|
244
|
+
ref={pageRef}
|
|
245
|
+
className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden"
|
|
246
|
+
>
|
|
247
|
+
{children}
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const AppLayoutDesktop = ({
|
|
253
|
+
atoms,
|
|
254
|
+
children,
|
|
255
|
+
footer,
|
|
256
|
+
sidebar,
|
|
257
|
+
title,
|
|
258
|
+
}: {
|
|
259
|
+
atoms: TAppLayoutAtoms;
|
|
260
|
+
children: React.ReactNode;
|
|
261
|
+
sidebar: React.ReactNode;
|
|
262
|
+
footer?: React.ReactNode;
|
|
263
|
+
title?: React.ReactNode;
|
|
264
|
+
}) => {
|
|
265
|
+
const [open, setOpen] = useAtom(atoms.open);
|
|
266
|
+
const [width, setWidth] = useAtom(atoms.width);
|
|
267
|
+
const [resizing, setResizing] = React.useState(false);
|
|
268
|
+
const asideRef = React.useRef<HTMLElement>(null);
|
|
269
|
+
const currentWidth = width ?? APP_LAYOUT_DEFAULT_WIDTH;
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div
|
|
273
|
+
className={cn(
|
|
274
|
+
'relative flex min-h-0 min-w-0 flex-1',
|
|
275
|
+
!open && '[--navigation-panel-toggle-space:2.5rem]',
|
|
276
|
+
resizing && 'cursor-col-resize select-none',
|
|
277
|
+
)}
|
|
278
|
+
>
|
|
279
|
+
<aside
|
|
280
|
+
ref={asideRef}
|
|
281
|
+
aria-hidden={!open}
|
|
282
|
+
className={cn(
|
|
283
|
+
'relative flex shrink-0 flex-col overflow-hidden border-r bg-sidebar',
|
|
284
|
+
!resizing &&
|
|
285
|
+
'transition-[width] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none',
|
|
286
|
+
!open && 'border-r-0',
|
|
287
|
+
)}
|
|
288
|
+
style={{ width: open ? currentWidth : 0 }}
|
|
289
|
+
>
|
|
290
|
+
<div
|
|
291
|
+
className="flex h-full flex-col"
|
|
292
|
+
style={{ minWidth: currentWidth }}
|
|
293
|
+
>
|
|
294
|
+
<div className="flex h-12 shrink-0 items-center px-2">
|
|
295
|
+
<div className="min-w-0 flex-1 truncate px-2 text-sm font-medium">
|
|
296
|
+
{title}
|
|
297
|
+
</div>
|
|
298
|
+
<Button
|
|
299
|
+
aria-expanded
|
|
300
|
+
aria-label="Collapse sidebar"
|
|
301
|
+
className="size-8 shrink-0 rounded-md"
|
|
302
|
+
size="icon"
|
|
303
|
+
title="Collapse sidebar"
|
|
304
|
+
type="button"
|
|
305
|
+
variant="ghost"
|
|
306
|
+
onClick={() => setOpen(false)}
|
|
307
|
+
>
|
|
308
|
+
<IconLayoutSidebarLeftCollapse />
|
|
309
|
+
</Button>
|
|
310
|
+
</div>
|
|
311
|
+
<div
|
|
312
|
+
data-navigation-context
|
|
313
|
+
className="styled-scroll min-h-0 flex-1 overflow-x-hidden overflow-y-auto pb-1"
|
|
314
|
+
>
|
|
315
|
+
{sidebar}
|
|
316
|
+
</div>
|
|
317
|
+
<AppLayoutFooter>{footer}</AppLayoutFooter>
|
|
318
|
+
</div>
|
|
319
|
+
{open && (
|
|
320
|
+
<AppLayoutResizeHandle
|
|
321
|
+
panelRef={asideRef}
|
|
322
|
+
onResize={setWidth}
|
|
323
|
+
onReset={() => setWidth(null)}
|
|
324
|
+
onResizingChange={setResizing}
|
|
325
|
+
/>
|
|
326
|
+
)}
|
|
327
|
+
</aside>
|
|
328
|
+
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
|
329
|
+
{!open && (
|
|
330
|
+
<AppLayoutFloatingButton
|
|
331
|
+
label="Expand sidebar"
|
|
332
|
+
icon={<IconLayoutSidebarLeftExpand />}
|
|
333
|
+
onClick={() => setOpen(true)}
|
|
334
|
+
/>
|
|
335
|
+
)}
|
|
336
|
+
<AppLayoutPage>{children}</AppLayoutPage>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const AppLayoutMobile = ({
|
|
343
|
+
children,
|
|
344
|
+
footer,
|
|
345
|
+
sidebar,
|
|
346
|
+
title,
|
|
347
|
+
}: {
|
|
348
|
+
children: React.ReactNode;
|
|
349
|
+
sidebar: React.ReactNode;
|
|
350
|
+
footer?: React.ReactNode;
|
|
351
|
+
title?: React.ReactNode;
|
|
352
|
+
}) => {
|
|
353
|
+
const { toggleSidebar } = Sidebar.useSidebar();
|
|
354
|
+
|
|
355
|
+
return (
|
|
356
|
+
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden [--navigation-panel-toggle-space:2.5rem]">
|
|
357
|
+
<Sidebar collapsible="offcanvas">
|
|
358
|
+
{title && (
|
|
359
|
+
<Sidebar.Header className="px-4 text-sm font-medium">
|
|
360
|
+
{title}
|
|
361
|
+
</Sidebar.Header>
|
|
362
|
+
)}
|
|
363
|
+
<div
|
|
364
|
+
data-navigation-context
|
|
365
|
+
className="styled-scroll min-h-0 flex-1 overflow-y-auto pb-1"
|
|
366
|
+
>
|
|
367
|
+
{sidebar}
|
|
368
|
+
</div>
|
|
369
|
+
<AppLayoutFooter>{footer}</AppLayoutFooter>
|
|
370
|
+
</Sidebar>
|
|
371
|
+
<AppLayoutFloatingButton
|
|
372
|
+
label="Open sidebar"
|
|
373
|
+
icon={<IconMenu2 className="size-4" />}
|
|
374
|
+
onClick={toggleSidebar}
|
|
375
|
+
/>
|
|
376
|
+
<AppLayoutPage>{children}</AppLayoutPage>
|
|
377
|
+
</div>
|
|
378
|
+
);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const AppLayoutBody = (props: {
|
|
382
|
+
atoms: TAppLayoutAtoms;
|
|
383
|
+
children: React.ReactNode;
|
|
384
|
+
sidebar: React.ReactNode;
|
|
385
|
+
footer?: React.ReactNode;
|
|
386
|
+
title?: React.ReactNode;
|
|
387
|
+
}) => {
|
|
388
|
+
const { isMobile } = Sidebar.useSidebar();
|
|
389
|
+
|
|
390
|
+
return isMobile ? (
|
|
391
|
+
<AppLayoutMobile {...props} />
|
|
392
|
+
) : (
|
|
393
|
+
<AppLayoutDesktop {...props} />
|
|
394
|
+
);
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Full-height layout for an app (plugin or core module) below the core top
|
|
399
|
+
* bar: the app owns its sidebar and pages. `id` keys the persisted
|
|
400
|
+
* open/width state, so use the plugin or module name.
|
|
401
|
+
*/
|
|
402
|
+
export const AppLayout = ({
|
|
403
|
+
id,
|
|
404
|
+
sidebar,
|
|
405
|
+
footer,
|
|
406
|
+
title,
|
|
407
|
+
children,
|
|
408
|
+
}: {
|
|
409
|
+
id: string;
|
|
410
|
+
sidebar: React.ReactNode;
|
|
411
|
+
footer?: React.ReactNode;
|
|
412
|
+
title?: React.ReactNode;
|
|
413
|
+
children: React.ReactNode;
|
|
414
|
+
}) => (
|
|
415
|
+
<Sidebar.Provider className="min-h-0 flex-1" sidebarKeyboardShortcut={false}>
|
|
416
|
+
<AppLayoutBody
|
|
417
|
+
atoms={getAppLayoutAtoms(id)}
|
|
418
|
+
sidebar={sidebar}
|
|
419
|
+
footer={footer}
|
|
420
|
+
title={title}
|
|
421
|
+
>
|
|
422
|
+
{children}
|
|
423
|
+
</AppLayoutBody>
|
|
424
|
+
</Sidebar.Provider>
|
|
425
|
+
);
|