@alepha/ui 0.11.2 → 0.11.4

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.
Files changed (44) hide show
  1. package/dist/AlephaMantineProvider-CtIV-8MV.mjs +150 -0
  2. package/dist/AlephaMantineProvider-CtIV-8MV.mjs.map +1 -0
  3. package/dist/AlephaMantineProvider-D-vu9aCD.mjs +3 -0
  4. package/dist/{index.d.ts → index.d.mts} +290 -226
  5. package/dist/index.d.mts.map +1 -0
  6. package/dist/{index.js → index.mjs} +651 -730
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +14 -12
  9. package/src/RootRouter.ts +1 -1
  10. package/src/components/buttons/ActionButton.tsx +542 -0
  11. package/src/components/buttons/BurgerButton.tsx +20 -0
  12. package/src/components/{DarkModeButton.tsx → buttons/DarkModeButton.tsx} +27 -14
  13. package/src/components/buttons/LanguageButton.tsx +28 -0
  14. package/src/components/buttons/OmnibarButton.tsx +32 -0
  15. package/src/components/buttons/ToggleSidebarButton.tsx +28 -0
  16. package/src/components/dialogs/AlertDialog.tsx +10 -10
  17. package/src/components/dialogs/ConfirmDialog.tsx +18 -18
  18. package/src/components/dialogs/PromptDialog.tsx +5 -3
  19. package/src/components/{Control.tsx → form/Control.tsx} +6 -3
  20. package/src/components/{ControlDate.tsx → form/ControlDate.tsx} +4 -1
  21. package/src/components/{ControlSelect.tsx → form/ControlSelect.tsx} +4 -1
  22. package/src/components/{TypeForm.tsx → form/TypeForm.tsx} +8 -6
  23. package/src/components/layout/AdminShell.tsx +97 -0
  24. package/src/components/{AlephaMantineProvider.tsx → layout/AlephaMantineProvider.tsx} +30 -10
  25. package/src/components/layout/AppBar.tsx +133 -0
  26. package/src/components/layout/Omnibar.tsx +43 -0
  27. package/src/components/layout/Sidebar.tsx +410 -0
  28. package/src/components/table/DataTable.tsx +63 -0
  29. package/src/constants/ui.ts +8 -0
  30. package/src/index.ts +89 -24
  31. package/src/services/DialogService.tsx +13 -32
  32. package/src/services/ToastService.tsx +16 -4
  33. package/src/utils/parseInput.ts +1 -1
  34. package/dist/AlephaMantineProvider-DDbIijPF.js +0 -96
  35. package/dist/AlephaMantineProvider-DDbIijPF.js.map +0 -1
  36. package/dist/AlephaMantineProvider-pOu8hOzK.js +0 -3
  37. package/dist/index.d.ts.map +0 -1
  38. package/dist/index.js.map +0 -1
  39. package/src/components/Action.tsx +0 -345
  40. package/src/components/DataTable.css +0 -199
  41. package/src/components/DataTable.tsx +0 -724
  42. package/src/components/Omnibar.tsx +0 -77
  43. package/src/components/Sidebar.css +0 -217
  44. package/src/components/Sidebar.tsx +0 -255
@@ -0,0 +1,28 @@
1
+ import { useStore } from "@alepha/react";
2
+ import {
3
+ IconLayoutSidebarLeftCollapse,
4
+ IconLayoutSidebarRightCollapse,
5
+ } from "@tabler/icons-react";
6
+ import ActionButton from "./ActionButton.tsx";
7
+
8
+ const ToggleSidebarButton = () => {
9
+ const [collapsed, setCollapsed] = useStore("alepha.ui.sidebar.collapsed");
10
+
11
+ return (
12
+ <ActionButton
13
+ icon={
14
+ collapsed ? (
15
+ <IconLayoutSidebarRightCollapse />
16
+ ) : (
17
+ <IconLayoutSidebarLeftCollapse />
18
+ )
19
+ }
20
+ variant={"subtle"}
21
+ size={"md"}
22
+ onClick={() => setCollapsed(!collapsed)}
23
+ tooltip={collapsed ? "Expand sidebar" : "Collapse sidebar"}
24
+ />
25
+ );
26
+ };
27
+
28
+ export default ToggleSidebarButton;
@@ -1,13 +1,13 @@
1
1
  import { Button, Group, Text } from "@mantine/core";
2
2
  import type { AlertDialogProps } from "../../services/DialogService";
3
3
 
4
- export function AlertDialog({ options, onClose }: AlertDialogProps) {
5
- return (
6
- <>
7
- {options?.message && <Text mb="md">{options.message}</Text>}
8
- <Group justify="flex-end">
9
- <Button onClick={onClose}>{options?.okLabel || "OK"}</Button>
10
- </Group>
11
- </>
12
- );
13
- }
4
+ const AlertDialog = ({ options, onClose }: AlertDialogProps) => (
5
+ <>
6
+ {options?.message && <Text mb="md">{options.message}</Text>}
7
+ <Group justify="flex-end">
8
+ <Button onClick={onClose}>{options?.okLabel || "OK"}</Button>
9
+ </Group>
10
+ </>
11
+ );
12
+
13
+ export default AlertDialog;
@@ -1,21 +1,21 @@
1
1
  import { Button, Group, Text } from "@mantine/core";
2
2
  import type { ConfirmDialogProps } from "../../services/DialogService";
3
3
 
4
- export function ConfirmDialog({ options, onConfirm }: ConfirmDialogProps) {
5
- return (
6
- <>
7
- {options?.message && <Text mb="md">{options.message}</Text>}
8
- <Group justify="flex-end">
9
- <Button variant="subtle" onClick={() => onConfirm(false)}>
10
- {options?.cancelLabel || "Cancel"}
11
- </Button>
12
- <Button
13
- color={options?.confirmColor || "blue"}
14
- onClick={() => onConfirm(true)}
15
- >
16
- {options?.confirmLabel || "Confirm"}
17
- </Button>
18
- </Group>
19
- </>
20
- );
21
- }
4
+ const ConfirmDialog = ({ options, onConfirm }: ConfirmDialogProps) => (
5
+ <>
6
+ {options?.message && <Text mb="md">{options.message}</Text>}
7
+ <Group justify="flex-end">
8
+ <Button variant="subtle" onClick={() => onConfirm(false)}>
9
+ {options?.cancelLabel || "Cancel"}
10
+ </Button>
11
+ <Button
12
+ color={options?.confirmColor || "blue"}
13
+ onClick={() => onConfirm(true)}
14
+ >
15
+ {options?.confirmLabel || "Confirm"}
16
+ </Button>
17
+ </Group>
18
+ </>
19
+ );
20
+
21
+ export default ConfirmDialog;
@@ -2,12 +2,12 @@ import { Button, Group, Text, TextInput } from "@mantine/core";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import type { PromptDialogProps } from "../../services/DialogService";
4
4
 
5
- export function PromptDialog({ options, onSubmit }: PromptDialogProps) {
5
+ const PromptDialog = ({ options, onSubmit }: PromptDialogProps) => {
6
6
  const [value, setValue] = useState(options?.defaultValue || "");
7
7
  const inputRef = useRef<HTMLInputElement>(null);
8
8
 
9
9
  useEffect(() => {
10
- // Auto-focus the input when the dialog opens
10
+ // autofocus the input when the dialog opens
11
11
  inputRef.current?.focus();
12
12
  }, []);
13
13
 
@@ -49,4 +49,6 @@ export function PromptDialog({ options, onSubmit }: PromptDialogProps) {
49
49
  </Group>
50
50
  </>
51
51
  );
52
- }
52
+ };
53
+
54
+ export default PromptDialog;
@@ -23,9 +23,12 @@ import type {
23
23
  TimeInputProps,
24
24
  } from "@mantine/dates";
25
25
  import type { ComponentType } from "react";
26
- import { type GenericControlProps, parseInput } from "../utils/parseInput.ts";
27
- import ControlDate from "./ControlDate";
28
- import ControlSelect, { type ControlSelectProps } from "./ControlSelect";
26
+ import {
27
+ type GenericControlProps,
28
+ parseInput,
29
+ } from "../../utils/parseInput.ts";
30
+ import ControlDate from "./ControlDate.tsx";
31
+ import ControlSelect, { type ControlSelectProps } from "./ControlSelect.tsx";
29
32
 
30
33
  export interface ControlProps extends GenericControlProps {
31
34
  text?: TextInputProps;
@@ -7,7 +7,10 @@ import {
7
7
  TimeInput,
8
8
  type TimeInputProps,
9
9
  } from "@mantine/dates";
10
- import { type GenericControlProps, parseInput } from "../utils/parseInput.ts";
10
+ import {
11
+ type GenericControlProps,
12
+ parseInput,
13
+ } from "../../utils/parseInput.ts";
11
14
 
12
15
  export interface ControlDateProps extends GenericControlProps {
13
16
  date?: boolean | DateInputProps;
@@ -14,7 +14,10 @@ import {
14
14
  type TagsInputProps,
15
15
  } from "@mantine/core";
16
16
  import { useEffect, useState } from "react";
17
- import { type GenericControlProps, parseInput } from "../utils/parseInput.ts";
17
+ import {
18
+ type GenericControlProps,
19
+ parseInput,
20
+ } from "../../utils/parseInput.ts";
18
21
 
19
22
  export type SelectValueLabel =
20
23
  | string
@@ -2,8 +2,10 @@ import type { TObject } from "@alepha/core";
2
2
  import type { FormModel } from "@alepha/react-form";
3
3
  import { Flex, Grid } from "@mantine/core";
4
4
  import type { ReactNode } from "react";
5
- import Action, { type ActionSubmitProps } from "./Action";
6
- import Control, { type ControlProps } from "./Control";
5
+ import ActionButton, {
6
+ type ActionSubmitButtonProps,
7
+ } from "../buttons/ActionButton.tsx";
8
+ import Control, { type ControlProps } from "./Control.tsx";
7
9
 
8
10
  export interface TypeFormProps<T extends TObject> {
9
11
  form: FormModel<T>;
@@ -21,8 +23,8 @@ export interface TypeFormProps<T extends TObject> {
21
23
  controlProps?: Partial<Omit<ControlProps, "input">>;
22
24
  skipFormElement?: boolean;
23
25
  skipSubmitButton?: boolean;
24
- submitButtonProps?: Partial<Omit<ActionSubmitProps, "form">>;
25
- resetButtonProps?: Partial<Omit<ActionSubmitProps, "form">>;
26
+ submitButtonProps?: Partial<Omit<ActionSubmitButtonProps, "form">>;
27
+ resetButtonProps?: Partial<Omit<ActionSubmitButtonProps, "form">>;
26
28
  }
27
29
 
28
30
  /**
@@ -139,9 +141,9 @@ const TypeForm = <T extends TObject>(props: TypeFormProps<T>) => {
139
141
  {renderFields()}
140
142
  {!skipSubmitButton && (
141
143
  <Flex>
142
- <Action form={form} {...submitButtonProps}>
144
+ <ActionButton form={form} {...submitButtonProps}>
143
145
  {submitButtonProps?.children ?? "Submit"}
144
- </Action>
146
+ </ActionButton>
145
147
  <button type={"reset"}>Reset</button>
146
148
  </Flex>
147
149
  )}
@@ -0,0 +1,97 @@
1
+ import { NestedView, useEvents, useStore } from "@alepha/react";
2
+ import {
3
+ AppShell,
4
+ type AppShellFooterProps,
5
+ type AppShellHeaderProps,
6
+ type AppShellMainProps,
7
+ type AppShellNavbarProps,
8
+ type AppShellProps,
9
+ } from "@mantine/core";
10
+ import type { ReactNode } from "react";
11
+ import { ui } from "../../constants/ui.ts";
12
+ import AppBar, { type AppBarProps } from "./AppBar.tsx";
13
+ import { Sidebar, type SidebarProps } from "./Sidebar.tsx";
14
+
15
+ export interface AdminShellProps {
16
+ appShellProps?: Partial<AppShellProps>;
17
+ appShellMainProps?: Partial<AppShellMainProps>;
18
+ appShellHeaderProps?: Partial<AppShellHeaderProps>;
19
+ appShellNavbarProps?: Partial<AppShellNavbarProps>;
20
+ appShellFooterProps?: Partial<AppShellFooterProps>;
21
+ sidebarProps?: Partial<SidebarProps>;
22
+ appBarProps?: Partial<AppBarProps>;
23
+ header?: ReactNode;
24
+ footer?: ReactNode;
25
+ children?: ReactNode;
26
+ }
27
+
28
+ declare module "@alepha/core" {
29
+ interface State {
30
+ "alepha.ui.sidebar.opened"?: boolean;
31
+ "alepha.ui.sidebar.collapsed"?: boolean;
32
+ }
33
+ }
34
+
35
+ const AdminShell = (props: AdminShellProps) => {
36
+ const [opened, setOpened] = useStore("alepha.ui.sidebar.opened");
37
+ const [collapsed] = useStore(
38
+ "alepha.ui.sidebar.collapsed",
39
+ props.sidebarProps?.collapsed,
40
+ );
41
+
42
+ useEvents(
43
+ {
44
+ "react:transition:begin": () => {
45
+ setOpened(false);
46
+ },
47
+ },
48
+ [],
49
+ );
50
+
51
+ // Default AppBar items with burger button on the left
52
+ const defaultAppBarItems = [
53
+ { position: "left" as const, type: "burger" as const },
54
+ ];
55
+
56
+ return (
57
+ <AppShell
58
+ padding="md"
59
+ header={{ height: 60 }}
60
+ navbar={
61
+ props.sidebarProps !== undefined
62
+ ? {
63
+ width: collapsed ? { base: 72 } : { base: 300 },
64
+ breakpoint: "sm",
65
+ collapsed: { mobile: !opened },
66
+ }
67
+ : undefined
68
+ }
69
+ footer={props.footer ? { height: 60 } : undefined}
70
+ {...props.appShellProps}
71
+ >
72
+ <AppShell.Header bg={ui.colors.surface} {...props.appShellHeaderProps}>
73
+ {props.header ?? (
74
+ <AppBar items={defaultAppBarItems} {...props.appBarProps} />
75
+ )}
76
+ </AppShell.Header>
77
+
78
+ {props.sidebarProps !== undefined && (
79
+ <AppShell.Navbar bg={ui.colors.surface} {...props.appShellNavbarProps}>
80
+ <Sidebar collapsed={collapsed} {...props.sidebarProps} />
81
+ </AppShell.Navbar>
82
+ )}
83
+
84
+ <AppShell.Main {...props.appShellMainProps}>
85
+ {props.children ?? <NestedView />}
86
+ </AppShell.Main>
87
+
88
+ {props.footer && (
89
+ <AppShell.Footer bg={ui.colors.surface} {...props.appShellFooterProps}>
90
+ {props.footer}
91
+ </AppShell.Footer>
92
+ )}
93
+ </AppShell>
94
+ );
95
+ };
96
+
97
+ export default AdminShell;
@@ -1,4 +1,4 @@
1
- import { NestedView, useRouterEvents } from "@alepha/react";
1
+ import { NestedView, useEvents } from "@alepha/react";
2
2
  import type {
3
3
  ColorSchemeScriptProps,
4
4
  MantineProviderProps,
@@ -9,7 +9,8 @@ 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
+ import { useToast } from "../../hooks/useToast.ts";
13
+ import Omnibar, { type OmnibarProps } from "./Omnibar.tsx";
13
14
 
14
15
  export interface AlephaMantineProviderProps {
15
16
  children?: ReactNode;
@@ -22,14 +23,22 @@ export interface AlephaMantineProviderProps {
22
23
  }
23
24
 
24
25
  const AlephaMantineProvider = (props: AlephaMantineProviderProps) => {
25
- useRouterEvents({
26
- onBegin: () => {
27
- nprogress.start();
28
- },
29
- onEnd: () => {
30
- nprogress.complete();
26
+ const toast = useToast();
27
+
28
+ useEvents(
29
+ {
30
+ "react:transition:begin": () => {
31
+ nprogress.start();
32
+ },
33
+ "react:transition:end": () => {
34
+ nprogress.complete();
35
+ },
36
+ "react:action:error": () => {
37
+ toast.danger("An error occurred while processing your action.");
38
+ },
31
39
  },
32
- });
40
+ [],
41
+ );
33
42
 
34
43
  return (
35
44
  <>
@@ -37,7 +46,18 @@ const AlephaMantineProvider = (props: AlephaMantineProviderProps) => {
37
46
  defaultColorScheme={props.mantine?.defaultColorScheme}
38
47
  {...props.colorSchemeScript}
39
48
  />
40
- <MantineProvider {...props.mantine}>
49
+ <MantineProvider
50
+ {...props.mantine}
51
+ theme={{
52
+ primaryColor: "gray",
53
+ primaryShade: {
54
+ light: 9,
55
+ dark: 8,
56
+ },
57
+ cursorType: "pointer",
58
+ ...props.mantine?.theme,
59
+ }}
60
+ >
41
61
  <Notifications {...props.notifications} />
42
62
  <NavigationProgress {...props.navigationProgress} />
43
63
  <ModalsProvider {...props.modals}>
@@ -0,0 +1,133 @@
1
+ import { Divider, Flex, type FlexProps } from "@mantine/core";
2
+ import type { ReactNode } from "react";
3
+ import BurgerButton from "../buttons/BurgerButton.tsx";
4
+ import DarkModeButton, {
5
+ type DarkModeButtonProps,
6
+ } from "../buttons/DarkModeButton.tsx";
7
+ import LanguageButton, {
8
+ type LanguageButtonProps,
9
+ } from "../buttons/LanguageButton.tsx";
10
+ import OmnibarButton, {
11
+ type OmnibarButtonProps,
12
+ } from "../buttons/OmnibarButton.tsx";
13
+
14
+ export type AppBarItem =
15
+ | AppBarElement
16
+ | AppBarBurger
17
+ | AppBarDark
18
+ | AppBarSearch
19
+ | AppBarLang
20
+ | AppBarSpacer
21
+ | AppBarDivider;
22
+
23
+ export interface AppBarElement {
24
+ position: "left" | "center" | "right";
25
+ element: ReactNode;
26
+ }
27
+
28
+ export interface AppBarBurger {
29
+ position: "left" | "center" | "right";
30
+ type: "burger";
31
+ }
32
+
33
+ export interface AppBarDark {
34
+ position: "left" | "center" | "right";
35
+ type: "dark";
36
+ props?: DarkModeButtonProps;
37
+ }
38
+
39
+ export interface AppBarSearch {
40
+ position: "left" | "center" | "right";
41
+ type: "search";
42
+ props?: OmnibarButtonProps;
43
+ }
44
+
45
+ export interface AppBarLang {
46
+ position: "left" | "center" | "right";
47
+ type: "lang";
48
+ props?: LanguageButtonProps;
49
+ }
50
+
51
+ export interface AppBarSpacer {
52
+ position: "left" | "center" | "right";
53
+ type: "spacer";
54
+ }
55
+
56
+ export interface AppBarDivider {
57
+ position: "left" | "center" | "right";
58
+ type: "divider";
59
+ }
60
+
61
+ export interface AppBarProps {
62
+ flexProps?: FlexProps;
63
+ items?: AppBarItem[];
64
+ }
65
+
66
+ const AppBar = (props: AppBarProps) => {
67
+ const { items = [] } = props;
68
+
69
+ const renderItem = (item: AppBarItem, index: number) => {
70
+ if ("type" in item) {
71
+ if (item.type === "burger") {
72
+ return <BurgerButton key={index} />;
73
+ }
74
+ if (item.type === "dark") {
75
+ return <DarkModeButton key={index} {...item.props} />;
76
+ }
77
+ if (item.type === "search") {
78
+ return <OmnibarButton key={index} {...item.props} />;
79
+ }
80
+ if (item.type === "lang") {
81
+ return <LanguageButton key={index} {...item.props} />;
82
+ }
83
+ if (item.type === "spacer") {
84
+ return <Flex key={index} w={16} />;
85
+ }
86
+ if (item.type === "divider") {
87
+ return <Divider key={index} orientation="vertical" />;
88
+ }
89
+ }
90
+ if ("element" in item) {
91
+ return item.element;
92
+ }
93
+ return null;
94
+ };
95
+
96
+ const leftItems = items.filter((item) => item.position === "left");
97
+ const centerItems = items.filter((item) => item.position === "center");
98
+ const rightItems = items.filter((item) => item.position === "right");
99
+
100
+ return (
101
+ <Flex
102
+ h="100%"
103
+ align="center"
104
+ px="md"
105
+ justify="space-between"
106
+ {...props.flexProps}
107
+ >
108
+ <Flex flex={1}>
109
+ {leftItems.map((item, index) => (
110
+ <Flex key={index} ml={index === 0 ? 0 : "md"} align="center">
111
+ {renderItem(item, index)}
112
+ </Flex>
113
+ ))}
114
+ </Flex>
115
+ <Flex>
116
+ {centerItems.map((item, index) => (
117
+ <Flex key={index} mx="md" align="center">
118
+ {renderItem(item, index)}
119
+ </Flex>
120
+ ))}
121
+ </Flex>
122
+ <Flex flex={1} gap="md" align={"center"} justify={"end"}>
123
+ {rightItems.map((item, index) => (
124
+ <Flex key={index} ml={index === 0 ? 0 : "md"} align="center">
125
+ {renderItem(item, index)}
126
+ </Flex>
127
+ ))}
128
+ </Flex>
129
+ </Flex>
130
+ );
131
+ };
132
+
133
+ export default AppBar;
@@ -0,0 +1,43 @@
1
+ import { useRouter } from "@alepha/react";
2
+ import { Spotlight, type SpotlightActionData } from "@mantine/spotlight";
3
+ import { IconSearch } from "@tabler/icons-react";
4
+ import { type ReactNode, useMemo } from "react";
5
+
6
+ export interface OmnibarProps {
7
+ shortcut?: string | string[];
8
+ searchPlaceholder?: string;
9
+ nothingFound?: ReactNode;
10
+ }
11
+
12
+ const Omnibar = (props: OmnibarProps) => {
13
+ const shortcut = props.shortcut ?? "mod+K";
14
+ const searchPlaceholder = props.searchPlaceholder ?? "Search...";
15
+ const nothingFound = props.nothingFound ?? "Nothing found...";
16
+ const router = useRouter();
17
+ const actions: SpotlightActionData[] = useMemo(
18
+ () =>
19
+ router.concretePages.map((page) => ({
20
+ id: page.name,
21
+ label: page.label ?? page.name,
22
+ description: page.description,
23
+ onClick: () => router.go(page.path ?? page.name),
24
+ leftSection: page.icon,
25
+ })),
26
+ [],
27
+ );
28
+
29
+ return (
30
+ <Spotlight
31
+ actions={actions}
32
+ shortcut={shortcut}
33
+ limit={10}
34
+ searchProps={{
35
+ leftSection: <IconSearch size={20} />,
36
+ placeholder: searchPlaceholder,
37
+ }}
38
+ nothingFound={nothingFound}
39
+ />
40
+ );
41
+ };
42
+
43
+ export default Omnibar;