@alepha/ui 0.10.6 → 0.11.0

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.
@@ -1,88 +1,249 @@
1
1
  import {
2
- type RouterGoOptions,
3
- type UseActiveOptions,
4
- useActive,
5
- useAlepha,
6
- useRouter,
2
+ type RouterGoOptions,
3
+ type UseActiveOptions,
4
+ useActive,
5
+ useAlepha,
6
+ useRouter,
7
7
  } from "@alepha/react";
8
8
  import { type FormModel, useFormState } from "@alepha/react-form";
9
- import { Button, type ButtonProps, Flex } from "@mantine/core";
9
+ import {
10
+ Button,
11
+ type ButtonProps,
12
+ Flex,
13
+ Menu,
14
+ Tooltip,
15
+ type TooltipProps,
16
+ } from "@mantine/core";
17
+ import { IconChevronRight } from "@tabler/icons-react";
10
18
  import { type ReactNode, useState } from "react";
11
19
 
20
+ export interface ActionMenuItem {
21
+ /**
22
+ * Menu item type
23
+ */
24
+ type?: "item" | "divider" | "label";
25
+
26
+ /**
27
+ * Label text for the menu item
28
+ */
29
+ label?: string;
30
+
31
+ /**
32
+ * Icon element to display before the label
33
+ */
34
+ icon?: ReactNode;
35
+
36
+ /**
37
+ * Click handler for menu items
38
+ */
39
+ onClick?: () => void;
40
+
41
+ /**
42
+ * Color for the menu item (e.g., "red" for danger actions)
43
+ */
44
+ color?: string;
45
+
46
+ /**
47
+ * Nested submenu items
48
+ */
49
+ children?: ActionMenuItem[];
50
+ }
51
+
52
+ export interface ActionMenuConfig {
53
+ /**
54
+ * Array of menu items to display
55
+ */
56
+ items: ActionMenuItem[];
57
+
58
+ /**
59
+ * Menu position relative to the button
60
+ */
61
+ position?:
62
+ | "bottom"
63
+ | "bottom-start"
64
+ | "bottom-end"
65
+ | "top"
66
+ | "top-start"
67
+ | "top-end"
68
+ | "left"
69
+ | "right";
70
+
71
+ /**
72
+ * Menu width
73
+ */
74
+ width?: number | string;
75
+
76
+ /**
77
+ * Menu shadow
78
+ */
79
+ shadow?: "xs" | "sm" | "md" | "lg" | "xl";
80
+ }
81
+
12
82
  export interface ActionCommonProps extends ButtonProps {
13
- children?: ReactNode;
14
- textVisibleFrom?: "xs" | "sm" | "md" | "lg" | "xl";
15
- // TODO
16
-
17
- /**
18
- * If set, a confirmation dialog will be shown before performing the action.
19
- * If `true`, a default title and message will be used.
20
- * If a string, it will be used as the message with a default title.
21
- * If an object, it can contain `title` and `message` properties to customize the dialog.
22
- */
23
- confirm?: boolean | string | { title?: string; message: string };
83
+ children?: ReactNode;
84
+ textVisibleFrom?: "xs" | "sm" | "md" | "lg" | "xl";
85
+
86
+ /**
87
+ * Tooltip to display on hover. Can be a string for simple tooltips
88
+ * or a TooltipProps object for advanced configuration.
89
+ */
90
+ tooltip?: string | TooltipProps;
91
+
92
+ /**
93
+ * Menu configuration. When provided, the action will display a dropdown menu.
94
+ */
95
+ menu?: ActionMenuConfig;
96
+
97
+ /**
98
+ * If set, a confirmation dialog will be shown before performing the action.
99
+ * If `true`, a default title and message will be used.
100
+ * If a string, it will be used as the message with a default title.
101
+ * If an object, it can contain `title` and `message` properties to customize the dialog.
102
+ */
103
+ confirm?: boolean | string | { title?: string; message: string };
24
104
  }
25
105
 
26
106
  export type ActionProps = ActionCommonProps &
27
- (ActiveHrefProps | ActionClickProps | ActionSubmitProps | {});
107
+ (ActiveHrefProps | ActionClickProps | ActionSubmitProps | {});
28
108
 
29
109
  // ---------------------------------------------------------------------------------------------------------------------
30
110
 
111
+ // Helper function to render menu items recursively
112
+ const renderMenuItem = (item: ActionMenuItem, index: number): ReactNode => {
113
+ // Render divider
114
+ if (item.type === "divider") {
115
+ return <Menu.Divider key={index} />;
116
+ }
117
+
118
+ // Render label
119
+ if (item.type === "label") {
120
+ return <Menu.Label key={index}>{item.label}</Menu.Label>;
121
+ }
122
+
123
+ // Render submenu if has children
124
+ if (item.children && item.children.length > 0) {
125
+ return (
126
+ <Menu key={index} trigger="hover" position="right-start" offset={2}>
127
+ <Menu.Target>
128
+ <Menu.Item
129
+ leftSection={item.icon}
130
+ rightSection={<IconChevronRight size={14} />}
131
+ >
132
+ {item.label}
133
+ </Menu.Item>
134
+ </Menu.Target>
135
+ <Menu.Dropdown>
136
+ {item.children.map((child, childIndex) =>
137
+ renderMenuItem(child, childIndex),
138
+ )}
139
+ </Menu.Dropdown>
140
+ </Menu>
141
+ );
142
+ }
143
+
144
+ // Render regular menu item
145
+ return (
146
+ <Menu.Item
147
+ key={index}
148
+ leftSection={item.icon}
149
+ onClick={item.onClick}
150
+ color={item.color}
151
+ >
152
+ {item.label}
153
+ </Menu.Item>
154
+ );
155
+ };
156
+
31
157
  const Action = (_props: ActionProps) => {
32
- const props = { variant: "subtle", ..._props };
33
-
34
- if (props.leftSection && !props.children) {
35
- props.className ??= "mantine-Action-iconOnly";
36
- props.p ??= "xs";
37
- }
38
-
39
- if (props.textVisibleFrom) {
40
- const { children, textVisibleFrom, leftSection, ...rest } = props;
41
- return (
42
- <>
43
- <Flex w={"100%"} visibleFrom={textVisibleFrom}>
44
- <Action flex={1} {...rest} leftSection={leftSection}>
45
- {children}
46
- </Action>
47
- </Flex>
48
- <Flex w={"100%"} hiddenFrom={textVisibleFrom}>
49
- <Action px={"xs"} {...rest}>
50
- {leftSection}
51
- </Action>
52
- </Flex>
53
- </>
54
- );
55
- }
56
-
57
- const renderAction = () => {
58
- if ("href" in props && props.href) {
59
- return (
60
- <ActionHref {...props} href={props.href}>
61
- {props.children}
62
- </ActionHref>
63
- );
64
- }
65
-
66
- if ("onClick" in props && props.onClick) {
67
- return (
68
- <ActionClick {...props} onClick={props.onClick}>
69
- {props.children}
70
- </ActionClick>
71
- );
72
- }
73
-
74
- if ("form" in props && props.form) {
75
- return (
76
- <ActionSubmit {...props} form={props.form}>
77
- {props.children}
78
- </ActionSubmit>
79
- );
80
- }
81
-
82
- return <Button {...(props as any)}>{props.children}</Button>;
83
- };
84
-
85
- return renderAction();
158
+ const props = { variant: "subtle", ..._props };
159
+ const { tooltip, menu, ...restProps } = props;
160
+
161
+ if (props.leftSection && !props.children) {
162
+ restProps.className ??= "mantine-Action-iconOnly";
163
+ restProps.p ??= "xs";
164
+ }
165
+
166
+ if (props.textVisibleFrom) {
167
+ const { children, textVisibleFrom, leftSection, ...rest } = restProps;
168
+ return (
169
+ <>
170
+ <Flex w={"100%"} visibleFrom={textVisibleFrom}>
171
+ <Action
172
+ flex={1}
173
+ {...rest}
174
+ leftSection={leftSection}
175
+ tooltip={tooltip}
176
+ menu={menu}
177
+ >
178
+ {children}
179
+ </Action>
180
+ </Flex>
181
+ <Flex w={"100%"} hiddenFrom={textVisibleFrom}>
182
+ <Action px={"xs"} {...rest} tooltip={tooltip} menu={menu}>
183
+ {leftSection}
184
+ </Action>
185
+ </Flex>
186
+ </>
187
+ );
188
+ }
189
+
190
+ const renderAction = () => {
191
+ if ("href" in restProps && restProps.href) {
192
+ return (
193
+ <ActionHref {...restProps} href={restProps.href}>
194
+ {restProps.children}
195
+ </ActionHref>
196
+ );
197
+ }
198
+
199
+ if ("onClick" in restProps && restProps.onClick) {
200
+ return (
201
+ <ActionClick {...restProps} onClick={restProps.onClick}>
202
+ {restProps.children}
203
+ </ActionClick>
204
+ );
205
+ }
206
+
207
+ if ("form" in restProps && restProps.form) {
208
+ return (
209
+ <ActionSubmit {...restProps} form={restProps.form}>
210
+ {restProps.children}
211
+ </ActionSubmit>
212
+ );
213
+ }
214
+
215
+ return <Button {...(restProps as any)}>{restProps.children}</Button>;
216
+ };
217
+
218
+ let actionElement = renderAction();
219
+
220
+ // Wrap with Menu if provided
221
+ if (menu) {
222
+ actionElement = (
223
+ <Menu
224
+ position={menu.position || "bottom-start"}
225
+ width={menu.width || 200}
226
+ shadow={menu.shadow || "md"}
227
+ >
228
+ <Menu.Target>{actionElement}</Menu.Target>
229
+ <Menu.Dropdown>
230
+ {menu.items.map((item, index) => renderMenuItem(item, index))}
231
+ </Menu.Dropdown>
232
+ </Menu>
233
+ );
234
+ }
235
+
236
+ // Wrap with Tooltip if provided
237
+ if (tooltip) {
238
+ const tooltipProps: TooltipProps =
239
+ typeof tooltip === "string"
240
+ ? { label: tooltip, children: actionElement }
241
+ : { ...tooltip, children: actionElement };
242
+
243
+ return <Tooltip {...tooltipProps} />;
244
+ }
245
+
246
+ return actionElement;
86
247
  };
87
248
 
88
249
  export default Action;
@@ -90,95 +251,95 @@ export default Action;
90
251
  // ---------------------------------------------------------------------------------------------------------------------
91
252
 
92
253
  export interface ActionSubmitProps extends ButtonProps {
93
- form: FormModel<any>;
254
+ form: FormModel<any>;
94
255
  }
95
256
 
96
257
  /**
97
258
  * Action button that submits a form with loading and disabled state handling.
98
259
  */
99
260
  const ActionSubmit = (props: ActionSubmitProps) => {
100
- const { form, ...buttonProps } = props;
101
- const state = useFormState(form);
102
- return (
103
- <Button
104
- {...buttonProps}
105
- loading={state.loading}
106
- disabled={state.loading || !state.dirty}
107
- type={"submit"}
108
- >
109
- {props.children}
110
- </Button>
111
- );
261
+ const { form, ...buttonProps } = props;
262
+ const state = useFormState(form);
263
+ return (
264
+ <Button
265
+ {...buttonProps}
266
+ loading={state.loading}
267
+ disabled={state.loading}
268
+ type={"submit"}
269
+ >
270
+ {props.children}
271
+ </Button>
272
+ );
112
273
  };
113
274
 
114
275
  // ---------------------------------------------------------------------------------------------------------------------
115
276
 
116
277
  export interface ActionClickProps extends ButtonProps {
117
- onClick: (e: any) => any;
278
+ onClick: (e: any) => any;
118
279
  }
119
280
 
120
281
  /**
121
282
  * Basic action button that handles click events with loading and error handling.
122
283
  */
123
284
  const ActionClick = (props: ActionClickProps) => {
124
- const [pending, setPending] = useState(false);
125
- const alepha = useAlepha();
126
-
127
- const onClick = async (e: any) => {
128
- setPending(true);
129
- try {
130
- await props.onClick(e);
131
- } catch (e) {
132
- console.error(e);
133
- await alepha.events.emit("form:submit:error", {
134
- id: "action",
135
- error: e as Error,
136
- });
137
- } finally {
138
- setPending(false);
139
- }
140
- };
141
-
142
- return (
143
- <Button
144
- {...props}
145
- disabled={pending || props.disabled}
146
- loading={pending}
147
- onClick={onClick}
148
- >
149
- {props.children}
150
- </Button>
151
- );
285
+ const [pending, setPending] = useState(false);
286
+ const alepha = useAlepha();
287
+
288
+ const onClick = async (e: any) => {
289
+ setPending(true);
290
+ try {
291
+ await props.onClick(e);
292
+ } catch (e) {
293
+ console.error(e);
294
+ await alepha.events.emit("form:submit:error", {
295
+ id: "action",
296
+ error: e as Error,
297
+ });
298
+ } finally {
299
+ setPending(false);
300
+ }
301
+ };
302
+
303
+ return (
304
+ <Button
305
+ {...props}
306
+ disabled={pending || props.disabled}
307
+ loading={pending}
308
+ onClick={onClick}
309
+ >
310
+ {props.children}
311
+ </Button>
312
+ );
152
313
  };
153
314
 
154
315
  // ---------------------------------------------------------------------------------------------------------------------
155
316
 
156
317
  export interface ActiveHrefProps extends ButtonProps {
157
- href: string;
158
- active?: Partial<UseActiveOptions> | false;
159
- routerGoOptions?: RouterGoOptions;
318
+ href: string;
319
+ active?: Partial<UseActiveOptions> | false;
320
+ routerGoOptions?: RouterGoOptions;
160
321
  }
161
322
 
162
323
  /**
163
324
  * Action for navigation with active state support.
164
325
  */
165
326
  const ActionHref = (props: ActiveHrefProps) => {
166
- const { active: options, routerGoOptions, ...buttonProps } = props;
167
- const router = useRouter();
168
- const { isPending, isActive } = useActive(
169
- options ? { href: props.href, ...options } : { href: props.href },
170
- );
171
- const anchorProps = router.anchor(props.href, routerGoOptions);
172
-
173
- return (
174
- <Button
175
- component={"a"}
176
- loading={isPending}
177
- {...anchorProps}
178
- {...buttonProps}
179
- variant={isActive && options !== false ? "filled" : "subtle"}
180
- >
181
- {props.children}
182
- </Button>
183
- );
327
+ const { active: options, routerGoOptions, ...buttonProps } = props;
328
+ const router = useRouter();
329
+ const { isPending, isActive } = useActive(
330
+ options ? { href: props.href, ...options } : { href: props.href },
331
+ );
332
+ const anchorProps = router.anchor(props.href, routerGoOptions);
333
+
334
+ return (
335
+ <Button
336
+ component={"a"}
337
+ loading={isPending}
338
+ {...anchorProps}
339
+ {...buttonProps}
340
+ variant={isActive && options !== false ? "filled" : "subtle"}
341
+ >
342
+ {props.children}
343
+ </Button>
344
+ );
184
345
  };
@@ -1,7 +1,7 @@
1
1
  import { NestedView, useRouterEvents } from "@alepha/react";
2
2
  import type {
3
- ColorSchemeScriptProps,
4
- MantineProviderProps,
3
+ ColorSchemeScriptProps,
4
+ MantineProviderProps,
5
5
  } from "@mantine/core";
6
6
  import { ColorSchemeScript, MantineProvider } from "@mantine/core";
7
7
  import { ModalsProvider, type ModalsProviderProps } from "@mantine/modals";
@@ -9,41 +9,44 @@ import { Notifications, type NotificationsProps } from "@mantine/notifications";
9
9
  import type { NavigationProgressProps } from "@mantine/nprogress";
10
10
  import { NavigationProgress, nprogress } from "@mantine/nprogress";
11
11
  import type { ReactNode } from "react";
12
+ import Omnibar, { type OmnibarProps } from "./Omnibar";
12
13
 
13
14
  export interface AlephaMantineProviderProps {
14
- children?: ReactNode;
15
- mantine?: MantineProviderProps;
16
- colorSchemeScript?: ColorSchemeScriptProps;
17
- navigationProgress?: NavigationProgressProps;
18
- notifications?: NotificationsProps;
19
- modals?: ModalsProviderProps;
15
+ children?: ReactNode;
16
+ mantine?: MantineProviderProps;
17
+ colorSchemeScript?: ColorSchemeScriptProps;
18
+ navigationProgress?: NavigationProgressProps;
19
+ notifications?: NotificationsProps;
20
+ modals?: ModalsProviderProps;
21
+ omnibar?: OmnibarProps;
20
22
  }
21
23
 
22
24
  const AlephaMantineProvider = (props: AlephaMantineProviderProps) => {
23
- useRouterEvents({
24
- onBegin: () => {
25
- nprogress.start();
26
- },
27
- onEnd: () => {
28
- nprogress.complete();
29
- },
30
- });
25
+ useRouterEvents({
26
+ onBegin: () => {
27
+ nprogress.start();
28
+ },
29
+ onEnd: () => {
30
+ nprogress.complete();
31
+ },
32
+ });
31
33
 
32
- return (
33
- <>
34
- <ColorSchemeScript
35
- defaultColorScheme={props.mantine?.defaultColorScheme}
36
- {...props.colorSchemeScript}
37
- />
38
- <MantineProvider {...props.mantine}>
39
- <Notifications {...props.notifications} />
40
- <NavigationProgress {...props.navigationProgress} />
41
- <ModalsProvider {...props.modals}>
42
- {props.children ?? <NestedView />}
43
- </ModalsProvider>
44
- </MantineProvider>
45
- </>
46
- );
34
+ return (
35
+ <>
36
+ <ColorSchemeScript
37
+ defaultColorScheme={props.mantine?.defaultColorScheme}
38
+ {...props.colorSchemeScript}
39
+ />
40
+ <MantineProvider {...props.mantine}>
41
+ <Notifications {...props.notifications} />
42
+ <NavigationProgress {...props.navigationProgress} />
43
+ <ModalsProvider {...props.modals}>
44
+ <Omnibar {...props.omnibar} />
45
+ {props.children ?? <NestedView />}
46
+ </ModalsProvider>
47
+ </MantineProvider>
48
+ </>
49
+ );
47
50
  };
48
51
 
49
52
  export default AlephaMantineProvider;