refine-mantine 1.7.0-dev.1 → 1.7.0-dev.2
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/package.json
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Image } from "@mantine/core";
|
|
2
|
+
import type { Meta } from "@storybook/react";
|
|
3
|
+
import { Layout as LayoutMinimal } from "./LayoutMinimal";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: "Components/LayoutMinimal",
|
|
7
|
+
component: LayoutMinimal,
|
|
8
|
+
} satisfies Meta<typeof LayoutMinimal>;
|
|
9
|
+
|
|
10
|
+
export const FullLayout = () => <LayoutMinimal>Hello World</LayoutMinimal>;
|
|
11
|
+
|
|
12
|
+
export const WithLocaleChange = () => (
|
|
13
|
+
<LayoutMinimal
|
|
14
|
+
locales={[
|
|
15
|
+
{
|
|
16
|
+
label: "English",
|
|
17
|
+
lang: "en",
|
|
18
|
+
icon: <Image h={18} src="https://flagsapi.com/US/flat/64.png" />,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
label: "Deutsch",
|
|
22
|
+
lang: "de",
|
|
23
|
+
icon: <Image h={18} src="https://flagsapi.com/DE/flat/64.png" />,
|
|
24
|
+
},
|
|
25
|
+
]}
|
|
26
|
+
>
|
|
27
|
+
Hello World
|
|
28
|
+
</LayoutMinimal>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export const WithFooter = () => (
|
|
32
|
+
<LayoutMinimal
|
|
33
|
+
footer={
|
|
34
|
+
<div style={{ padding: 12, textAlign: "center" }}>
|
|
35
|
+
Footer content
|
|
36
|
+
</div>
|
|
37
|
+
}
|
|
38
|
+
footerProps={{ height: 48, withBorder: true }}
|
|
39
|
+
>
|
|
40
|
+
Hello World
|
|
41
|
+
</LayoutMinimal>
|
|
42
|
+
);
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActionIcon,
|
|
3
|
+
Anchor,
|
|
4
|
+
AppShell,
|
|
5
|
+
type AppShellFooterProps,
|
|
6
|
+
type AppShellHeaderProps,
|
|
7
|
+
type AppShellMainProps,
|
|
8
|
+
type AppShellNavbarConfiguration,
|
|
9
|
+
type AppShellNavbarProps,
|
|
10
|
+
type AppShellProps,
|
|
11
|
+
type AppShellSectionProps,
|
|
12
|
+
Avatar,
|
|
13
|
+
Burger,
|
|
14
|
+
Group,
|
|
15
|
+
Menu,
|
|
16
|
+
NavLink,
|
|
17
|
+
ScrollArea,
|
|
18
|
+
Stack,
|
|
19
|
+
Text,
|
|
20
|
+
Tooltip,
|
|
21
|
+
useComputedColorScheme,
|
|
22
|
+
useMantineColorScheme
|
|
23
|
+
} from "@mantine/core";
|
|
24
|
+
import { useDisclosure } from "@mantine/hooks";
|
|
25
|
+
import {
|
|
26
|
+
type BaseRecord,
|
|
27
|
+
CanAccess,
|
|
28
|
+
Link,
|
|
29
|
+
type TreeMenuItem,
|
|
30
|
+
useGetIdentity,
|
|
31
|
+
useLogout,
|
|
32
|
+
useMenu,
|
|
33
|
+
useNavigation,
|
|
34
|
+
useRefineOptions,
|
|
35
|
+
useTranslate,
|
|
36
|
+
useTranslation,
|
|
37
|
+
} from "@refinedev/core";
|
|
38
|
+
import { IconCheck, IconLanguage, IconList, IconLogout, IconMoon, IconSun } from "@tabler/icons-react";
|
|
39
|
+
import { type ReactNode, useCallback } from "react";
|
|
40
|
+
|
|
41
|
+
interface LayoutProps {
|
|
42
|
+
children: ReactNode;
|
|
43
|
+
shellProps?: AppShellProps;
|
|
44
|
+
headerProps?: AppShellHeaderProps;
|
|
45
|
+
navbarProps?: AppShellNavbarProps;
|
|
46
|
+
navbarConfiguration?: Partial<AppShellNavbarConfiguration>;
|
|
47
|
+
navbarMenuProps?: AppShellSectionProps;
|
|
48
|
+
navbarFooterProps?: AppShellSectionProps;
|
|
49
|
+
mainProps?: AppShellMainProps;
|
|
50
|
+
hideNavbar?: boolean;
|
|
51
|
+
footer?: ReactNode;
|
|
52
|
+
footerProps?: AppShellFooterProps;
|
|
53
|
+
locales?: LayoutLocale[];
|
|
54
|
+
renderHeader?: (toggle: ()=> void) => ReactNode;
|
|
55
|
+
renderMenu?: (params: ReturnType<typeof useMenu>) => ReactNode;
|
|
56
|
+
renderIdentity?: <T extends BaseRecord>(identity: T, logout: () => void) => ReactNode;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface LayoutLocale {
|
|
60
|
+
lang: string;
|
|
61
|
+
label: string;
|
|
62
|
+
icon?: ReactNode;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const Layout: React.FC<LayoutProps> = (p) => {
|
|
66
|
+
const [opened, { toggle, close }] = useDisclosure();
|
|
67
|
+
const { title: { icon: defaultIcon, text: defaultText } = {} } =
|
|
68
|
+
useRefineOptions();
|
|
69
|
+
const { data: identity } = useGetIdentity();
|
|
70
|
+
const menu = useMenu();
|
|
71
|
+
const { mutate: logout } = useLogout();
|
|
72
|
+
const { setColorScheme } = useMantineColorScheme();
|
|
73
|
+
const computedColorScheme = useComputedColorScheme('light', { getInitialValueInEffect: true });
|
|
74
|
+
const translate = useTranslate();
|
|
75
|
+
const languageLabel = translate("layout.navbar.languageLabel", "Language");
|
|
76
|
+
const colorSchemeLabel = computedColorScheme === 'dark'
|
|
77
|
+
? translate("layout.header.lightMode", 'Light mode')
|
|
78
|
+
: translate("layout.header.darkMode", 'Dark mode');
|
|
79
|
+
|
|
80
|
+
const handleLogout = useCallback(() => {
|
|
81
|
+
logout();
|
|
82
|
+
}, [logout]);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<AppShell
|
|
86
|
+
header={{
|
|
87
|
+
height: { base: 60, sm: 0 },
|
|
88
|
+
}}
|
|
89
|
+
navbar={{
|
|
90
|
+
width: { base: 260, sm: 80 },
|
|
91
|
+
breakpoint: "sm",
|
|
92
|
+
collapsed: {
|
|
93
|
+
mobile: !opened || p.hideNavbar,
|
|
94
|
+
desktop: p.hideNavbar,
|
|
95
|
+
},
|
|
96
|
+
...p.navbarConfiguration,
|
|
97
|
+
}}
|
|
98
|
+
padding="md"
|
|
99
|
+
{...p.shellProps}
|
|
100
|
+
>
|
|
101
|
+
<AppShell.Header p="md" {...p.headerProps} hiddenFrom="sm">
|
|
102
|
+
{p.renderHeader ? (
|
|
103
|
+
p.renderHeader(toggle)
|
|
104
|
+
) : (
|
|
105
|
+
<Group justify="space-between">
|
|
106
|
+
<Group>
|
|
107
|
+
<Burger opened={opened} onClick={toggle} size="sm" hidden={p.hideNavbar} />
|
|
108
|
+
<Anchor
|
|
109
|
+
component={Link as React.FC<{ to: string, children: ReactNode }>}
|
|
110
|
+
to="/"
|
|
111
|
+
style={{all: "unset"}}
|
|
112
|
+
>
|
|
113
|
+
<Group>
|
|
114
|
+
{defaultIcon}
|
|
115
|
+
{defaultText ? <Text>{defaultText}</Text> : null}
|
|
116
|
+
</Group>
|
|
117
|
+
</Anchor>
|
|
118
|
+
</Group>
|
|
119
|
+
</Group>
|
|
120
|
+
)}
|
|
121
|
+
</AppShell.Header>
|
|
122
|
+
|
|
123
|
+
<AppShell.Navbar {...p.navbarProps}>
|
|
124
|
+
<AppShell.Section visibleFrom="sm" p="md">
|
|
125
|
+
<Stack align="center" gap="xs">
|
|
126
|
+
<Tooltip label={defaultText ?? translate("layout.navbar.homeLabel", "Home")} position="right" transitionProps={{ duration: 0 }}>
|
|
127
|
+
<Anchor
|
|
128
|
+
component={Link as React.FC<{ to: string, children: ReactNode }>}
|
|
129
|
+
to="/"
|
|
130
|
+
style={{all: "unset"}}
|
|
131
|
+
>
|
|
132
|
+
<Stack align="center" gap={2}>
|
|
133
|
+
{defaultIcon}
|
|
134
|
+
</Stack>
|
|
135
|
+
</Anchor>
|
|
136
|
+
</Tooltip>
|
|
137
|
+
</Stack>
|
|
138
|
+
</AppShell.Section>
|
|
139
|
+
|
|
140
|
+
<AppShell.Section
|
|
141
|
+
component={ScrollArea}
|
|
142
|
+
grow
|
|
143
|
+
mt="xs"
|
|
144
|
+
{...p.navbarMenuProps}
|
|
145
|
+
visibleFrom="sm"
|
|
146
|
+
>
|
|
147
|
+
{p.renderMenu ? (
|
|
148
|
+
p.renderMenu(menu)
|
|
149
|
+
) : (
|
|
150
|
+
<Stack align="center" gap={0}>
|
|
151
|
+
{menu.menuItems.map((item) => (
|
|
152
|
+
<MenuItemIcon
|
|
153
|
+
item={item}
|
|
154
|
+
key={item.key}
|
|
155
|
+
selectedKey={menu.selectedKey}
|
|
156
|
+
onClick={close}
|
|
157
|
+
/>
|
|
158
|
+
))}
|
|
159
|
+
</Stack>
|
|
160
|
+
)}
|
|
161
|
+
</AppShell.Section>
|
|
162
|
+
|
|
163
|
+
<AppShell.Section
|
|
164
|
+
component={ScrollArea}
|
|
165
|
+
grow
|
|
166
|
+
mt="xs"
|
|
167
|
+
{...p.navbarMenuProps}
|
|
168
|
+
hiddenFrom="sm"
|
|
169
|
+
>
|
|
170
|
+
{p.renderMenu ? (
|
|
171
|
+
p.renderMenu(menu)
|
|
172
|
+
) : (
|
|
173
|
+
<Stack gap="xs">
|
|
174
|
+
{menu.menuItems.map((item) => (
|
|
175
|
+
<MenuItemFull
|
|
176
|
+
item={item}
|
|
177
|
+
key={item.key}
|
|
178
|
+
selectedKey={menu.selectedKey}
|
|
179
|
+
onClick={close}
|
|
180
|
+
/>
|
|
181
|
+
))}
|
|
182
|
+
</Stack>
|
|
183
|
+
)}
|
|
184
|
+
</AppShell.Section>
|
|
185
|
+
|
|
186
|
+
<AppShell.Section {...p.navbarFooterProps} visibleFrom="sm">
|
|
187
|
+
{p.renderIdentity ? (
|
|
188
|
+
p.renderIdentity(identity, handleLogout)
|
|
189
|
+
) : (
|
|
190
|
+
<Stack align="center" gap="xs">
|
|
191
|
+
{p.locales && (
|
|
192
|
+
<Locales locales={p.locales} variant="icon" label={languageLabel} />
|
|
193
|
+
)}
|
|
194
|
+
<Tooltip label={colorSchemeLabel} position="right" transitionProps={{ duration: 0 }}>
|
|
195
|
+
<ActionIcon
|
|
196
|
+
onClick={() => setColorScheme(computedColorScheme === 'light' ? 'dark' : 'light')}
|
|
197
|
+
aria-label={translate("layout.header.toggleColorScheme", "Toggle color scheme")}
|
|
198
|
+
variant="subtle"
|
|
199
|
+
size="xl"
|
|
200
|
+
>
|
|
201
|
+
{computedColorScheme === "light"
|
|
202
|
+
? <IconSun stroke={1.5} size={22} />
|
|
203
|
+
: <IconMoon stroke={1.5} size={22} />}
|
|
204
|
+
</ActionIcon>
|
|
205
|
+
</Tooltip>
|
|
206
|
+
<Tooltip label={translate("layout.navbar.signOutLabel", "Sign out")} position="right" transitionProps={{ duration: 0 }}>
|
|
207
|
+
<ActionIcon
|
|
208
|
+
onClick={handleLogout}
|
|
209
|
+
aria-label={translate("layout.navbar.signOutLabel", "Sign out")}
|
|
210
|
+
variant="subtle"
|
|
211
|
+
size="xl"
|
|
212
|
+
>
|
|
213
|
+
<IconLogout size={20} />
|
|
214
|
+
</ActionIcon>
|
|
215
|
+
</Tooltip>
|
|
216
|
+
</Stack>
|
|
217
|
+
)}
|
|
218
|
+
</AppShell.Section>
|
|
219
|
+
|
|
220
|
+
<AppShell.Section {...p.navbarFooterProps} hiddenFrom="sm">
|
|
221
|
+
<Stack gap="xs">
|
|
222
|
+
{p.locales && (
|
|
223
|
+
<Locales locales={p.locales} variant="full" label={languageLabel} />
|
|
224
|
+
)}
|
|
225
|
+
<NavLink
|
|
226
|
+
onClick={() => setColorScheme(computedColorScheme === 'light' ? 'dark' : 'light')}
|
|
227
|
+
leftSection={computedColorScheme === "light"
|
|
228
|
+
? <IconSun stroke={1.5} size={18} />
|
|
229
|
+
: <IconMoon stroke={1.5} size={18} />}
|
|
230
|
+
label={colorSchemeLabel}
|
|
231
|
+
variant="subtle"
|
|
232
|
+
/>
|
|
233
|
+
{p.renderIdentity ? (
|
|
234
|
+
p.renderIdentity(identity, handleLogout)
|
|
235
|
+
) : (
|
|
236
|
+
<NavLink
|
|
237
|
+
onClick={handleLogout}
|
|
238
|
+
leftSection={identity?.avatar ? <Avatar src={identity.avatar} /> : <IconLogout size={18} />}
|
|
239
|
+
label={translate("layout.navbar.signOutLabel", "Sign out")}
|
|
240
|
+
description={
|
|
241
|
+
identity?.email
|
|
242
|
+
? translate("layout.navbar.signOutDescription", { email: identity.email }, `Signed in as ${identity.email}`)
|
|
243
|
+
: undefined
|
|
244
|
+
}
|
|
245
|
+
variant="filled"
|
|
246
|
+
active
|
|
247
|
+
/>
|
|
248
|
+
)}
|
|
249
|
+
</Stack>
|
|
250
|
+
</AppShell.Section>
|
|
251
|
+
</AppShell.Navbar>
|
|
252
|
+
|
|
253
|
+
<AppShell.Main {...p.mainProps}>
|
|
254
|
+
{p.children}
|
|
255
|
+
</AppShell.Main>
|
|
256
|
+
|
|
257
|
+
<AppShell.Footer {...p.footerProps}>
|
|
258
|
+
{p.footer}
|
|
259
|
+
</AppShell.Footer>
|
|
260
|
+
</AppShell>
|
|
261
|
+
);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const MenuItemIcon = (p: {
|
|
265
|
+
item: TreeMenuItem;
|
|
266
|
+
selectedKey?: string;
|
|
267
|
+
onClick: () => void;
|
|
268
|
+
}) => {
|
|
269
|
+
const { listUrl } = useNavigation();
|
|
270
|
+
const isSelected = p.item.key === p.selectedKey;
|
|
271
|
+
const label = p.item.meta?.label ?? p.item.label ?? p.item.name;
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<CanAccess
|
|
275
|
+
key={p.item.key}
|
|
276
|
+
resource={p.item.name}
|
|
277
|
+
action="list"
|
|
278
|
+
params={{
|
|
279
|
+
resource: p.item,
|
|
280
|
+
}}
|
|
281
|
+
>
|
|
282
|
+
<Tooltip label={label} position="right" transitionProps={{ duration: 0 }}>
|
|
283
|
+
<ActionIcon
|
|
284
|
+
key={p.item.key}
|
|
285
|
+
component={Link as React.FC<{ to: string, onClick: () => void }>}
|
|
286
|
+
to={listUrl(p.item.name)}
|
|
287
|
+
variant={isSelected ? "filled" : "subtle"}
|
|
288
|
+
size="xl"
|
|
289
|
+
mb="xs"
|
|
290
|
+
bd={0}
|
|
291
|
+
onClick={p.onClick}
|
|
292
|
+
>
|
|
293
|
+
{p.item.meta?.icon ?? <IconList size={20} />}
|
|
294
|
+
</ActionIcon>
|
|
295
|
+
</Tooltip>
|
|
296
|
+
</CanAccess>
|
|
297
|
+
);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const MenuItemFull = (p: {
|
|
301
|
+
item: TreeMenuItem;
|
|
302
|
+
selectedKey?: string;
|
|
303
|
+
onClick: () => void;
|
|
304
|
+
}) => {
|
|
305
|
+
const { listUrl } = useNavigation();
|
|
306
|
+
const isSelected = p.item.key === p.selectedKey;
|
|
307
|
+
const label = p.item.meta?.label ?? p.item.label ?? p.item.name;
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<CanAccess
|
|
311
|
+
key={p.item.key}
|
|
312
|
+
resource={p.item.name}
|
|
313
|
+
action="list"
|
|
314
|
+
params={{
|
|
315
|
+
resource: p.item,
|
|
316
|
+
}}
|
|
317
|
+
>
|
|
318
|
+
<NavLink
|
|
319
|
+
key={p.item.key}
|
|
320
|
+
label={label}
|
|
321
|
+
leftSection={p.item.meta?.icon ?? <IconList size={18} />}
|
|
322
|
+
active={isSelected}
|
|
323
|
+
component={Link as React.FC<{ to: string, onClick: () => void }>}
|
|
324
|
+
to={listUrl(p.item.name)}
|
|
325
|
+
onClick={p.onClick}
|
|
326
|
+
/>
|
|
327
|
+
</CanAccess>
|
|
328
|
+
);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const Locales = (p: {
|
|
332
|
+
locales: LayoutLocale[];
|
|
333
|
+
variant: "icon" | "full";
|
|
334
|
+
label: string;
|
|
335
|
+
}) => {
|
|
336
|
+
const { changeLocale, getLocale } = useTranslation();
|
|
337
|
+
const locale = getLocale();
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<Menu shadow="md" width={200}>
|
|
341
|
+
<Menu.Target>
|
|
342
|
+
{p.variant === "icon" ? (
|
|
343
|
+
<Tooltip label={p.label} position="right" transitionProps={{ duration: 0 }}>
|
|
344
|
+
<ActionIcon aria-label={p.label} variant="subtle" size="xl">
|
|
345
|
+
<IconLanguage />
|
|
346
|
+
</ActionIcon>
|
|
347
|
+
</Tooltip>
|
|
348
|
+
) : (
|
|
349
|
+
<NavLink
|
|
350
|
+
label={p.label}
|
|
351
|
+
leftSection={<IconLanguage size={18} />}
|
|
352
|
+
component="button"
|
|
353
|
+
/>
|
|
354
|
+
)}
|
|
355
|
+
</Menu.Target>
|
|
356
|
+
<Menu.Dropdown>
|
|
357
|
+
{p.locales.map(({label, lang, icon}) => (
|
|
358
|
+
<Menu.Item
|
|
359
|
+
key={lang}
|
|
360
|
+
leftSection={icon}
|
|
361
|
+
onClick={() => changeLocale(lang)}
|
|
362
|
+
rightSection={lang === locale ? <IconCheck size={14} /> : undefined}
|
|
363
|
+
>
|
|
364
|
+
{label}
|
|
365
|
+
</Menu.Item>
|
|
366
|
+
))}
|
|
367
|
+
</Menu.Dropdown>
|
|
368
|
+
</Menu>
|
|
369
|
+
);
|
|
370
|
+
}
|