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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "refine-mantine",
3
- "version": "1.7.0-dev.1",
3
+ "version": "1.7.0-dev.2",
4
4
  "type": "module",
5
5
  "exports": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -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
+ }