@erxes/ui 3.2.25-2 → 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.
@@ -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;
@@ -1 +1,2 @@
1
1
  export * from './components/Page';
2
+ export * from './components/AppLayout';
@@ -112,8 +112,6 @@ export type IUIConfig = {
112
112
  name: string;
113
113
  defaultPath?: string;
114
114
  icon: React.ElementType;
115
- content: () => React.ReactNode;
116
- subGroup?: () => React.ReactNode;
117
115
  };
118
116
  widgets?: {
119
117
  relationWidgets?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erxes/ui",
3
- "version": "3.2.25-2",
3
+ "version": "3.2.25-3",
4
4
  "description": "erxes design system: UI primitives, business modules, hooks and shared frontend state",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -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
+ );
@@ -1 +1,2 @@
1
1
  export * from './components/Page';
2
+ export * from './components/AppLayout';
@@ -137,8 +137,6 @@ export type IUIConfig = {
137
137
  name: string;
138
138
  defaultPath?: string;
139
139
  icon: React.ElementType;
140
- content: () => React.ReactNode;
141
- subGroup?: () => React.ReactNode;
142
140
  };
143
141
 
144
142
  widgets?: {