@alepha/ui 0.13.2 → 0.13.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 (122) hide show
  1. package/dist/admin/{AdminFiles-BjofP3OC.js → AdminFiles-8CC9mVsc.js} +3 -3
  2. package/dist/admin/{AdminFiles-BjofP3OC.js.map → AdminFiles-8CC9mVsc.js.map} +1 -1
  3. package/dist/admin/AdminFiles-BRLMP_7y.js +3 -0
  4. package/dist/admin/AdminLayout-Cm-Y4YTQ.js +396 -0
  5. package/dist/admin/AdminLayout-Cm-Y4YTQ.js.map +1 -0
  6. package/dist/admin/AdminLayout-D5M9kSiV.js +3 -0
  7. package/dist/admin/AdminNotifications-DxBKi2RO.js +3 -0
  8. package/dist/admin/AdminNotifications-d-gw5Uie.js +154 -0
  9. package/dist/admin/AdminNotifications-d-gw5Uie.js.map +1 -0
  10. package/dist/admin/{AdminSessions-CmDVneE2.js → AdminSessions-CpVusqmd.js} +37 -10
  11. package/dist/admin/AdminSessions-CpVusqmd.js.map +1 -0
  12. package/dist/admin/AdminSessions-DA285-5Q.js +3 -0
  13. package/dist/admin/AdminUserCreate-CQIrSslj.js +3 -0
  14. package/dist/admin/AdminUserCreate-DH7u_yJj.js +103 -0
  15. package/dist/admin/AdminUserCreate-DH7u_yJj.js.map +1 -0
  16. package/dist/admin/AdminUserDetails-DVmFCDsU.js +221 -0
  17. package/dist/admin/AdminUserDetails-DVmFCDsU.js.map +1 -0
  18. package/dist/admin/AdminUserDetails-T3nkXSdz.js +3 -0
  19. package/dist/admin/AdminUserLayout-DdtZGX8n.js +3 -0
  20. package/dist/admin/AdminUserLayout-gpOyn0Y7.js +153 -0
  21. package/dist/admin/AdminUserLayout-gpOyn0Y7.js.map +1 -0
  22. package/dist/admin/AdminUserSessions-CWYzjB3D.js +3 -0
  23. package/dist/admin/AdminUserSessions-CdVwoM-h.js +129 -0
  24. package/dist/admin/AdminUserSessions-CdVwoM-h.js.map +1 -0
  25. package/dist/admin/AdminUserSettings-S7gZvvjO.js +164 -0
  26. package/dist/admin/AdminUserSettings-S7gZvvjO.js.map +1 -0
  27. package/dist/admin/AdminUserSettings-jCzVYw_2.js +3 -0
  28. package/dist/admin/{AdminUsers-88De5pev.js → AdminUsers-9qEzxqAL.js} +33 -15
  29. package/dist/admin/AdminUsers-9qEzxqAL.js.map +1 -0
  30. package/dist/admin/AdminUsers-BcSUxV01.js +3 -0
  31. package/dist/admin/index.d.ts +5568 -418
  32. package/dist/admin/index.js +302 -42
  33. package/dist/admin/index.js.map +1 -1
  34. package/dist/auth/AuthLayout-BSL8ZHgr.js +19 -0
  35. package/dist/auth/AuthLayout-BSL8ZHgr.js.map +1 -0
  36. package/dist/auth/{Login-OCrvjs9U.js → Login-AlVPPqQp.js} +6 -5
  37. package/dist/auth/Login-AlVPPqQp.js.map +1 -0
  38. package/dist/auth/Login-otdWVvVU.js +4 -0
  39. package/dist/auth/{Register-Ei34GSba.js → Register-BxJmOqpF.js} +9 -6
  40. package/dist/auth/Register-BxJmOqpF.js.map +1 -0
  41. package/dist/auth/Register-D10MnlQc.js +4 -0
  42. package/dist/auth/{ResetPassword-tO0oMzfo.js → ResetPassword-BhyZ9ek4.js} +3 -3
  43. package/dist/auth/ResetPassword-BhyZ9ek4.js.map +1 -0
  44. package/dist/auth/ResetPassword-llBG-STp.js +3 -0
  45. package/dist/auth/VerifyEmail-BvOG-IUC.js +3 -0
  46. package/dist/auth/VerifyEmail-DeLct3oQ.js +131 -0
  47. package/dist/auth/VerifyEmail-DeLct3oQ.js.map +1 -0
  48. package/dist/auth/index.d.ts +2412 -2254
  49. package/dist/auth/index.js +97 -20
  50. package/dist/auth/index.js.map +1 -1
  51. package/dist/core/index.d.ts +280 -95
  52. package/dist/core/index.js +1375 -394
  53. package/dist/core/index.js.map +1 -1
  54. package/package.json +7 -6
  55. package/src/admin/AdminRouter.ts +116 -29
  56. package/src/admin/AdminSidebar.ts +31 -0
  57. package/src/admin/MainRouter.ts +23 -0
  58. package/src/admin/components/AdminLayout.tsx +66 -104
  59. package/src/admin/components/AdminNotifications.tsx +196 -12
  60. package/src/admin/components/AdminSessions.tsx +43 -7
  61. package/src/admin/components/AdminUserCreate.tsx +84 -0
  62. package/src/admin/components/AdminUserDetails.tsx +180 -0
  63. package/src/admin/components/AdminUserLayout.tsx +172 -0
  64. package/src/admin/components/AdminUserSessions.tsx +158 -0
  65. package/src/admin/components/AdminUserSettings.tsx +165 -0
  66. package/src/admin/components/AdminUsers.tsx +29 -9
  67. package/src/admin/index.ts +15 -3
  68. package/src/auth/AuthI18n.ts +22 -0
  69. package/src/auth/AuthRouter.ts +82 -8
  70. package/src/auth/components/AuthLayout.tsx +12 -0
  71. package/src/auth/components/Login.tsx +14 -12
  72. package/src/auth/components/Register.tsx +6 -5
  73. package/src/auth/components/ResetPassword.tsx +1 -1
  74. package/src/auth/components/VerifyEmail.tsx +102 -0
  75. package/src/auth/components/buttons/UserButton.tsx +12 -2
  76. package/src/auth/index.ts +1 -0
  77. package/src/core/components/buttons/ActionButton.tsx +12 -4
  78. package/src/core/components/buttons/DarkModeButton.tsx +1 -1
  79. package/src/core/components/buttons/ThemeButton.tsx +31 -0
  80. package/src/core/components/layout/AdminShell.tsx +4 -2
  81. package/src/core/components/layout/AlephaMantineProvider.tsx +10 -4
  82. package/src/core/components/layout/Omnibar.tsx +27 -15
  83. package/src/core/components/layout/Sidebar.tsx +33 -17
  84. package/src/core/components/table/DataTable.tsx +9 -5
  85. package/src/core/hooks/useTheme.ts +25 -0
  86. package/src/core/index.ts +8 -3
  87. package/src/core/providers/ThemeProvider.ts +90 -0
  88. package/src/core/themes/aurora.ts +107 -0
  89. package/src/core/themes/crystal.ts +107 -0
  90. package/src/core/themes/default.ts +7 -0
  91. package/src/core/themes/ember.ts +107 -0
  92. package/src/core/themes/index.ts +7 -0
  93. package/src/core/themes/midnight.ts +98 -0
  94. package/src/core/themes/remoraid.ts +278 -0
  95. package/src/core/themes/slate.ts +81 -0
  96. package/styles.css +84 -0
  97. package/dist/admin/AdminFiles-DldZB7oo.js +0 -3
  98. package/dist/admin/AdminJobs-BOq6AZOW.js +0 -3
  99. package/dist/admin/AdminJobs-CDnVxEv6.js +0 -125
  100. package/dist/admin/AdminJobs-CDnVxEv6.js.map +0 -1
  101. package/dist/admin/AdminLayout-Bgx25J8m.js +0 -3
  102. package/dist/admin/AdminLayout-CervL8LV.js +0 -88
  103. package/dist/admin/AdminLayout-CervL8LV.js.map +0 -1
  104. package/dist/admin/AdminNotifications-BDQXt3-e.js +0 -3
  105. package/dist/admin/AdminNotifications-DvI2989x.js +0 -40
  106. package/dist/admin/AdminNotifications-DvI2989x.js.map +0 -1
  107. package/dist/admin/AdminParameters-D_v0GAvI.js +0 -3
  108. package/dist/admin/AdminParameters-P1LB6ZI1.js +0 -40
  109. package/dist/admin/AdminParameters-P1LB6ZI1.js.map +0 -1
  110. package/dist/admin/AdminSessions-CmDVneE2.js.map +0 -1
  111. package/dist/admin/AdminSessions-Dkk_fzWK.js +0 -3
  112. package/dist/admin/AdminUsers-88De5pev.js.map +0 -1
  113. package/dist/admin/AdminUsers-oyAXqZ5l.js +0 -3
  114. package/dist/admin/AdminVerifications-D93TKymL.js +0 -3
  115. package/dist/admin/AdminVerifications-DBVEoqJe.js +0 -40
  116. package/dist/admin/AdminVerifications-DBVEoqJe.js.map +0 -1
  117. package/dist/auth/Login-BC2jTczq.js +0 -4
  118. package/dist/auth/Login-OCrvjs9U.js.map +0 -1
  119. package/dist/auth/Register-Dh0lsQmI.js +0 -4
  120. package/dist/auth/Register-Ei34GSba.js.map +0 -1
  121. package/dist/auth/ResetPassword-BnlAQAOE.js +0 -3
  122. package/dist/auth/ResetPassword-tO0oMzfo.js.map +0 -1
@@ -1,3 +1,4 @@
1
+ import { useRouter } from "@alepha/react";
1
2
  import { useAuth } from "@alepha/react/auth";
2
3
  import {
3
4
  ActionButton,
@@ -7,8 +8,9 @@ import {
7
8
  ui,
8
9
  } from "@alepha/ui";
9
10
  import { Avatar } from "@mantine/core";
10
- import { IconLogout, IconUser } from "@tabler/icons-react";
11
+ import { IconLogin2, IconLogout, IconUser } from "@tabler/icons-react";
11
12
  import type { ReactNode } from "react";
13
+ import type { AuthRouter } from "../../AuthRouter.ts";
12
14
 
13
15
  export interface UserButtonProps
14
16
  extends Omit<ActionProps, "menu" | "icon" | "onClick"> {
@@ -55,8 +57,16 @@ const UserButton = (props: UserButtonProps) => {
55
57
  picture?: string;
56
58
  }>();
57
59
 
60
+ const authRouter = useRouter<AuthRouter>();
61
+
58
62
  if (!auth.user) {
59
- return null;
63
+ return (
64
+ <ActionButton
65
+ {...buttonProps}
66
+ icon={IconLogin2}
67
+ href={authRouter.path("login")}
68
+ />
69
+ );
60
70
  }
61
71
 
62
72
  const userLabel = auth.user.username || auth.user.email;
package/src/auth/index.ts CHANGED
@@ -13,6 +13,7 @@ export { default as UserButton } from "./components/buttons/UserButton.tsx";
13
13
  export { default as Login } from "./components/Login.tsx";
14
14
  export { default as Register } from "./components/Register.tsx";
15
15
  export { default as ResetPassword } from "./components/ResetPassword.tsx";
16
+ export { default as VerifyEmail } from "./components/VerifyEmail.tsx";
16
17
 
17
18
  // ---------------------------------------------------------------------------------------------------------------------
18
19
 
@@ -144,6 +144,11 @@ export interface ActionCommonProps extends ButtonProps {
144
144
  * Additional props to pass to the ThemeIcon wrapping the icon.
145
145
  */
146
146
  themeIconProps?: ThemeIconProps;
147
+
148
+ /**
149
+ * Visual intent of the action button.
150
+ */
151
+ intent?: "primary" | "success" | "danger" | "warning" | "info";
147
152
  }
148
153
 
149
154
  export type ActionProps = ActionCommonProps &
@@ -234,9 +239,13 @@ const ActionMenuItem = (props: {
234
239
  };
235
240
 
236
241
  const ActionButton = (_props: ActionProps) => {
237
- const props = { variant: "default", ..._props };
242
+ const props = { variant: "subtle", ..._props };
238
243
  const { tooltip, menu, icon, ...restProps } = props;
239
244
 
245
+ // set default color to gray (not colored)
246
+ restProps.color ??= "gray";
247
+ restProps.c ??= "var(--mantine-color-text)";
248
+
240
249
  if (props.icon) {
241
250
  const icon = isComponentType(props.icon) ? (
242
251
  <props.icon size={ui.sizes.icon.md} />
@@ -254,15 +263,14 @@ const ActionButton = (_props: ActionProps) => {
254
263
 
255
264
  if (!props.children) {
256
265
  restProps.children = Children.only(icon);
257
- restProps.p ??= "xs";
266
+ restProps.px ??= "xs";
258
267
  } else {
259
268
  restProps.leftSection = icon;
260
269
  }
261
270
  }
262
271
 
263
272
  if (props.leftSection && !props.children) {
264
- restProps.className ??= "mantine-Action-iconOnly";
265
- restProps.p ??= "xs";
273
+ restProps.px ??= "xs";
266
274
  }
267
275
 
268
276
  if (props.textVisibleFrom) {
@@ -73,7 +73,7 @@ const DarkModeButton = (props: DarkModeButtonProps) => {
73
73
  return (
74
74
  <ActionButton
75
75
  onClick={toggleColorScheme}
76
- variant={props.variant ?? "default"}
76
+ variant={props.variant ?? "subtle"}
77
77
  size={props.size ?? "sm"}
78
78
  aria-label="Toggle color scheme"
79
79
  px={"xs"}
@@ -0,0 +1,31 @@
1
+ import { useInject } from "@alepha/react";
2
+ import { IconPalette } from "@tabler/icons-react";
3
+ import { useTheme } from "../../hooks/useTheme.ts";
4
+ import { ThemeProvider } from "../../providers/ThemeProvider.ts";
5
+ import ActionButton, { type ActionProps } from "./ActionButton.tsx";
6
+
7
+ export interface ThemeButtonProps {
8
+ actionProps?: Partial<ActionProps>;
9
+ }
10
+
11
+ const ThemeButton = (props: ThemeButtonProps) => {
12
+ const [theme, setTheme] = useTheme();
13
+ const themes = useInject(ThemeProvider).themes;
14
+
15
+ return (
16
+ <ActionButton
17
+ variant="subtle"
18
+ icon={IconPalette}
19
+ menu={{
20
+ items: themes.map((it) => ({
21
+ label: it.label,
22
+ onClick: () => setTheme(it),
23
+ active: theme.id === it.id,
24
+ })),
25
+ }}
26
+ {...props.actionProps}
27
+ />
28
+ );
29
+ };
30
+
31
+ export default ThemeButton;
@@ -93,18 +93,20 @@ const AdminShell = (props: AdminShellProps) => {
93
93
 
94
94
  return (
95
95
  <AppShell
96
+ w={"100%"}
97
+ flex={1}
96
98
  padding="md"
97
99
  header={hasAppBar ? { height: 60 } : undefined}
98
100
  navbar={
99
101
  hasSidebar
100
102
  ? {
101
- width: collapsed ? { base: 72 } : { base: 300 },
103
+ width: collapsed ? { base: 78 } : { base: 300 },
102
104
  breakpoint: "sm",
103
105
  collapsed: { mobile: !opened },
104
106
  }
105
107
  : undefined
106
108
  }
107
- footer={props.footer ? { height: 60 } : undefined}
109
+ footer={props.footer ? { height: 24 } : undefined}
108
110
  {...props.appShellProps}
109
111
  >
110
112
  <AppShell.Header bg={ui.colors.surface} {...props.appShellHeaderProps}>
@@ -1,15 +1,17 @@
1
1
  import { NestedView, useEvents } from "@alepha/react";
2
2
  import { FormValidationError } from "@alepha/react/form";
3
- import type {
4
- ColorSchemeScriptProps,
5
- MantineProviderProps,
3
+ import {
4
+ ColorSchemeScript,
5
+ type ColorSchemeScriptProps,
6
+ MantineProvider,
7
+ type MantineProviderProps,
6
8
  } from "@mantine/core";
7
- import { ColorSchemeScript, MantineProvider } from "@mantine/core";
8
9
  import { ModalsProvider, type ModalsProviderProps } from "@mantine/modals";
9
10
  import { Notifications, type NotificationsProps } from "@mantine/notifications";
10
11
  import type { NavigationProgressProps } from "@mantine/nprogress";
11
12
  import { NavigationProgress, nprogress } from "@mantine/nprogress";
12
13
  import type { ReactNode } from "react";
14
+ import { useTheme } from "../../hooks/useTheme.ts";
13
15
  import { useToast } from "../../hooks/useToast.ts";
14
16
  import Omnibar, { type OmnibarProps } from "./Omnibar.tsx";
15
17
 
@@ -25,6 +27,7 @@ export interface AlephaMantineProviderProps {
25
27
 
26
28
  const AlephaMantineProvider = (props: AlephaMantineProviderProps) => {
27
29
  const toast = useToast();
30
+ const [theme] = useTheme();
28
31
 
29
32
  useEvents(
30
33
  {
@@ -59,6 +62,9 @@ const AlephaMantineProvider = (props: AlephaMantineProviderProps) => {
59
62
  <MantineProvider
60
63
  {...props.mantine}
61
64
  theme={{
65
+ // Spread all theme properties from the selected theme
66
+ ...theme,
67
+ // User overrides take precedence
62
68
  ...props.mantine?.theme,
63
69
  }}
64
70
  >
@@ -1,7 +1,9 @@
1
- import { useRouter } from "@alepha/react";
1
+ import { useRouter, useStore } from "@alepha/react";
2
2
  import { Spotlight, type SpotlightActionData } from "@mantine/spotlight";
3
3
  import { IconSearch } from "@tabler/icons-react";
4
4
  import { type ReactNode, useMemo } from "react";
5
+ import { ui } from "../../constants/ui.ts";
6
+ import { renderIcon } from "../buttons/ActionButton.tsx";
5
7
 
6
8
  export interface OmnibarProps {
7
9
  shortcut?: string | string[];
@@ -14,21 +16,31 @@ const Omnibar = (props: OmnibarProps) => {
14
16
  const searchPlaceholder = props.searchPlaceholder ?? "Search...";
15
17
  const nothingFound = props.nothingFound ?? "Nothing found...";
16
18
  const router = useRouter();
19
+
20
+ // watch user to re-render on permission changes
21
+ const [user] = useStore("alepha.server.request.user");
22
+
17
23
  const actions: SpotlightActionData[] = useMemo(
18
24
  () =>
19
- router.concretePages.map((page) => ({
20
- id: page.name,
21
- label: page.label ?? page.name,
22
- description: page.description,
23
- onClick: () => {
24
- if (page.staticName) {
25
- return router.go(page.staticName, { params: page.params });
26
- }
27
- return router.go(page.name);
28
- },
29
- leftSection: page.icon,
30
- })),
31
- [],
25
+ router.concretePages
26
+ .filter((page) => {
27
+ if (page.can && !page.can()) return false;
28
+
29
+ return true;
30
+ })
31
+ .map((page) => ({
32
+ id: page.name,
33
+ label: page.label ?? page.name,
34
+ description: page.description,
35
+ onClick: () => {
36
+ if (page.staticName) {
37
+ return router.go(page.staticName, { params: page.params });
38
+ }
39
+ return router.go(page.name);
40
+ },
41
+ leftSection: renderIcon(page.icon),
42
+ })),
43
+ [user],
32
44
  );
33
45
 
34
46
  return (
@@ -37,7 +49,7 @@ const Omnibar = (props: OmnibarProps) => {
37
49
  shortcut={shortcut}
38
50
  limit={10}
39
51
  searchProps={{
40
- leftSection: <IconSearch size={20} />,
52
+ leftSection: <IconSearch size={ui.sizes.icon.md} />,
41
53
  placeholder: searchPlaceholder,
42
54
  }}
43
55
  nothingFound={nothingFound}
@@ -86,6 +86,11 @@ export const Sidebar = (props: SidebarProps) => {
86
86
  return <Flex key={key}>{item.element}</Flex>;
87
87
  }
88
88
 
89
+ // Check visibility control
90
+ if (item.can && !item.can()) {
91
+ return null;
92
+ }
93
+
89
94
  if (props.collapsed) {
90
95
  return (
91
96
  <SidebarCollapsedItem
@@ -224,7 +229,6 @@ export const SidebarItem = (props: SidebarItemProps) => {
224
229
  props.theme.button?.size ??
225
230
  (level === 0 ? "sm" : "xs")
226
231
  }
227
- color={"var(--alepha-text)"}
228
232
  variant={"subtle"}
229
233
  variantActive={"default"}
230
234
  radius={props.item.theme?.radius ?? props.theme.button?.radius ?? "md"}
@@ -271,15 +275,17 @@ export const SidebarItem = (props: SidebarItemProps) => {
271
275
  bottom: 16,
272
276
  }}
273
277
  />
274
- {item.children.map((child, index) => (
275
- <SidebarItem
276
- key={index}
277
- item={child}
278
- level={level + 1}
279
- onItemClick={props.onItemClick}
280
- theme={props.theme}
281
- />
282
- ))}
278
+ {item.children
279
+ .filter((child) => !child.can || child.can())
280
+ .map((child, index) => (
281
+ <SidebarItem
282
+ key={index}
283
+ item={child}
284
+ level={level + 1}
285
+ onItemClick={props.onItemClick}
286
+ theme={props.theme}
287
+ />
288
+ ))}
283
289
  </Flex>
284
290
  )}
285
291
  </Flex>
@@ -335,9 +341,16 @@ const SidebarCollapsedItem = (props: SidebarItemProps) => {
335
341
  props.theme.button?.size ??
336
342
  (level === 0 ? "sm" : "xs")
337
343
  }
338
- color={"var(--alepha-text)"}
339
344
  variant={"subtle"}
340
345
  variantActive={"default"}
346
+ tooltip={
347
+ item.children
348
+ ? undefined
349
+ : {
350
+ label: item.label,
351
+ position: "right",
352
+ }
353
+ }
341
354
  radius={props.item.theme?.radius ?? props.theme.button?.radius ?? "md"}
342
355
  onClick={handleItemClick}
343
356
  icon={renderIcon(item.icon) ?? <IconSquareRounded />}
@@ -348,12 +361,14 @@ const SidebarCollapsedItem = (props: SidebarItemProps) => {
348
361
  ? ({
349
362
  position: "right",
350
363
  on: "hover",
351
- items: item.children.map((child) => ({
352
- label: child.label,
353
- href: child.href,
354
- icon: renderIcon(child.icon),
355
- children: child.children,
356
- })),
364
+ items: item.children
365
+ .filter((child) => !child.can || child.can())
366
+ .map((child) => ({
367
+ label: child.label,
368
+ href: child.href,
369
+ icon: renderIcon(child.icon),
370
+ children: child.children?.filter((c) => !c.can || c.can()),
371
+ })),
357
372
  } as any)
358
373
  : undefined
359
374
  }
@@ -410,6 +425,7 @@ export interface SidebarMenuItem extends SidebarAbstractItem {
410
425
  rightSection?: ReactNode;
411
426
  theme?: SidebarButtonTheme;
412
427
  actionProps?: ActionProps;
428
+ can?: () => boolean; // Visibility control: true -> visible, false -> hidden
413
429
  }
414
430
 
415
431
  export interface SidebarButtonTheme {
@@ -1,6 +1,7 @@
1
1
  import { useInject } from "@alepha/react";
2
2
  import { type FormModel, useForm } from "@alepha/react/form";
3
3
  import {
4
+ Card,
4
5
  Flex,
5
6
  Pagination,
6
7
  Select,
@@ -20,6 +21,7 @@ import {
20
21
  } from "alepha";
21
22
  import { DateTimeProvider, type DurationLike } from "alepha/datetime";
22
23
  import { type ReactNode, useEffect, useState } from "react";
24
+ import { ui } from "../../constants/ui.ts";
23
25
  import ActionButton from "../buttons/ActionButton.tsx";
24
26
  import TypeForm, { type TypeFormProps } from "../form/TypeForm.tsx";
25
27
 
@@ -283,11 +285,13 @@ const DataTable = <T extends object, Filters extends TObject>(
283
285
  return (
284
286
  <Flex direction={"column"} gap={"sm"} flex={1}>
285
287
  {props.filters ? (
286
- <TypeForm
287
- {...props.typeFormProps}
288
- form={form as unknown as FormModel<Filters>}
289
- schema={schema}
290
- />
288
+ <Card withBorder p={"lg"} bg={ui.colors.elevated}>
289
+ <TypeForm
290
+ {...props.typeFormProps}
291
+ form={form as unknown as FormModel<Filters>}
292
+ schema={schema}
293
+ />
294
+ </Card>
291
295
  ) : null}
292
296
 
293
297
  <Flex className={"overflow-auto"}>
@@ -0,0 +1,25 @@
1
+ import { useInject, useStore } from "@alepha/react";
2
+ import {
3
+ type AlephaTheme,
4
+ mantineThemeAtom,
5
+ type Theme,
6
+ ThemeProvider,
7
+ } from "../providers/ThemeProvider.ts";
8
+
9
+ export const useTheme = () => {
10
+ useStore(mantineThemeAtom);
11
+
12
+ const themeService = useInject(ThemeProvider);
13
+ const currentTheme = themeService.getTheme();
14
+
15
+ // Find the full theme object from the themes array
16
+ const fullTheme =
17
+ themeService.themes.find((t) => t.id === currentTheme.id) ??
18
+ themeService.themes[0];
19
+
20
+ const applyTheme = (theme: Theme | AlephaTheme) => {
21
+ themeService.setTheme({ id: theme.id });
22
+ };
23
+
24
+ return [fullTheme, applyTheme] as const;
25
+ };
package/src/core/index.ts CHANGED
@@ -2,8 +2,9 @@ import { AlephaReactForm } from "@alepha/react/form";
2
2
  import { AlephaReactHead } from "@alepha/react/head";
3
3
  import { AlephaReactI18n } from "@alepha/react/i18n";
4
4
  import { $module } from "alepha";
5
- import type { ReactNode } from "react";
5
+ import type { ComponentType, ReactNode } from "react";
6
6
  import type { ControlProps } from "./components/form/Control.tsx";
7
+ import { ThemeProvider } from "./providers/ThemeProvider.ts";
7
8
  import { RootRouter } from "./RootRouter.ts";
8
9
  import { DialogService } from "./services/DialogService.tsx";
9
10
  import { ToastService } from "./services/ToastService.tsx";
@@ -27,6 +28,8 @@ export { default as ClipboardButton } from "./components/buttons/ClipboardButton
27
28
  export { default as DarkModeButton } from "./components/buttons/DarkModeButton.tsx";
28
29
  export { default as LanguageButton } from "./components/buttons/LanguageButton.tsx";
29
30
  export { default as OmnibarButton } from "./components/buttons/OmnibarButton.tsx";
31
+ export type { ThemeButtonProps } from "./components/buttons/ThemeButton.tsx";
32
+ export { default as ThemeButton } from "./components/buttons/ThemeButton.tsx";
30
33
  export { default as JsonViewer } from "./components/data/JsonViewer.tsx";
31
34
  export { default as AlertDialog } from "./components/dialogs/AlertDialog.tsx";
32
35
  export { default as ConfirmDialog } from "./components/dialogs/ConfirmDialog.tsx";
@@ -77,6 +80,7 @@ export { default as DataTable } from "./components/table/DataTable.tsx";
77
80
  export * from "./constants/ui.ts";
78
81
  export { useDialog } from "./hooks/useDialog.ts";
79
82
  export { useToast } from "./hooks/useToast.ts";
83
+ export * from "./providers/ThemeProvider.ts";
80
84
  export * from "./RootRouter.ts";
81
85
  export type {
82
86
  AlertDialogOptions,
@@ -121,7 +125,7 @@ declare module "@alepha/react" {
121
125
  /**
122
126
  * Optional icon for the page.
123
127
  */
124
- icon?: ReactNode;
128
+ icon?: ReactNode | ComponentType;
125
129
  }
126
130
  }
127
131
 
@@ -134,11 +138,12 @@ declare module "@alepha/react" {
134
138
  */
135
139
  export const AlephaUI = $module({
136
140
  name: "alepha.ui",
137
- services: [DialogService, ToastService, RootRouter],
141
+ services: [DialogService, ToastService, ThemeProvider, RootRouter],
138
142
  register: (alepha) => {
139
143
  alepha.with(AlephaReactI18n);
140
144
  alepha.with(AlephaReactHead);
141
145
  alepha.with(AlephaReactForm);
146
+ alepha.with(ThemeProvider);
142
147
  alepha.with(DialogService);
143
148
  alepha.with(ToastService);
144
149
  },
@@ -0,0 +1,90 @@
1
+ import { $head } from "@alepha/react/head";
2
+ import type { MantineThemeOverride } from "@mantine/core";
3
+ import { $atom, $inject, Alepha, type Static, t } from "alepha";
4
+ import { $cookie } from "alepha/server/cookies";
5
+ import {
6
+ auroraTheme,
7
+ crystalTheme,
8
+ defaultTheme,
9
+ emberTheme,
10
+ midnightTheme,
11
+ remoraidTheme,
12
+ slateTheme,
13
+ } from "../themes/index.ts";
14
+
15
+ export const mantineThemeAtom = $atom({
16
+ name: "alepha.ui.theme",
17
+ schema: t.object({
18
+ id: t.string(),
19
+ }),
20
+ default: {
21
+ id: "default",
22
+ },
23
+ });
24
+
25
+ export type Theme = Static<typeof mantineThemeAtom.schema>;
26
+
27
+ declare module "alepha" {
28
+ interface State {
29
+ [mantineThemeAtom.key]?: Theme;
30
+ }
31
+ }
32
+
33
+ export type AlephaTheme = MantineThemeOverride & {
34
+ id: string;
35
+ label: string;
36
+ description: string;
37
+ };
38
+
39
+ export class ThemeProvider {
40
+ protected readonly alepha = $inject(Alepha);
41
+ protected themeCookie = $cookie(mantineThemeAtom);
42
+
43
+ public themes: AlephaTheme[] = [
44
+ defaultTheme,
45
+ remoraidTheme,
46
+ midnightTheme,
47
+ slateTheme,
48
+ auroraTheme,
49
+ emberTheme,
50
+ crystalTheme,
51
+ ];
52
+
53
+ protected themeHead = $head(() => {
54
+ return {
55
+ htmlAttributes: {
56
+ "data-theme": this.getTheme().id,
57
+ },
58
+ };
59
+ });
60
+
61
+ public setTheme(theme: Theme) {
62
+ this.themeCookie.set(theme);
63
+ this.alepha.store.set(mantineThemeAtom, theme);
64
+
65
+ if (typeof document === "undefined") return;
66
+
67
+ document.documentElement.removeAttribute("data-theme");
68
+
69
+ if (theme.id !== "default") {
70
+ document.documentElement.setAttribute("data-theme", theme.id);
71
+ }
72
+ }
73
+
74
+ public getTheme() {
75
+ // TODO: make a safe cookie getter, today it crash when Cookie Server is called inside vite pre-render
76
+ try {
77
+ return (
78
+ this.themeCookie.get() ??
79
+ this.alepha.store.get(mantineThemeAtom) ??
80
+ mantineThemeAtom.options.default
81
+ );
82
+ } catch {
83
+ // TODO: atom should take default value if undefined ???
84
+ return (
85
+ this.alepha.store.get(mantineThemeAtom) ??
86
+ mantineThemeAtom.options.default
87
+ );
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,107 @@
1
+ import type { AlephaTheme } from "../providers/ThemeProvider.ts";
2
+
3
+ export const auroraTheme: AlephaTheme = {
4
+ id: "aurora",
5
+ label: "Aurora",
6
+ description: "Vibrant, playful with gradients and hover effects",
7
+ primaryColor: "violet",
8
+ primaryShade: { light: 5, dark: 4 },
9
+ fontFamily:
10
+ '"Nunito", "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
11
+ fontFamilyMonospace:
12
+ '"Fira Code", "JetBrains Mono", ui-monospace, Consolas, monospace',
13
+ headings: {
14
+ fontFamily:
15
+ '"Nunito", "Poppins", -apple-system, BlinkMacSystemFont, sans-serif',
16
+ fontWeight: "700",
17
+ textWrap: "wrap",
18
+ sizes: {
19
+ h1: { fontSize: "2.25rem", lineHeight: "1.3" },
20
+ h2: { fontSize: "1.75rem", lineHeight: "1.35" },
21
+ h3: { fontSize: "1.375rem", lineHeight: "1.4" },
22
+ h4: { fontSize: "1.125rem", lineHeight: "1.45" },
23
+ h5: { fontSize: "1rem", lineHeight: "1.5" },
24
+ h6: { fontSize: "0.875rem", lineHeight: "1.5" },
25
+ },
26
+ },
27
+ fontSizes: {
28
+ xs: "0.8rem",
29
+ sm: "0.9rem",
30
+ md: "1rem",
31
+ lg: "1.125rem",
32
+ xl: "1.3rem",
33
+ },
34
+ lineHeights: {
35
+ xs: "1.5",
36
+ sm: "1.55",
37
+ md: "1.6",
38
+ lg: "1.65",
39
+ xl: "1.7",
40
+ },
41
+ radius: {
42
+ xs: "6px",
43
+ sm: "8px",
44
+ md: "12px",
45
+ lg: "16px",
46
+ xl: "24px",
47
+ },
48
+ defaultRadius: "md",
49
+ shadows: {
50
+ xs: "0 2px 4px rgba(139, 92, 246, 0.08)",
51
+ sm: "0 4px 8px rgba(139, 92, 246, 0.1)",
52
+ md: "0 8px 16px rgba(139, 92, 246, 0.12)",
53
+ lg: "0 16px 32px rgba(139, 92, 246, 0.15)",
54
+ xl: "0 24px 48px rgba(139, 92, 246, 0.18)",
55
+ },
56
+ defaultGradient: { from: "violet", to: "pink", deg: 135 },
57
+ colors: {
58
+ dark: [
59
+ "#d4d0dc",
60
+ "#a8a3b3",
61
+ "#7c7689",
62
+ "#5c5568",
63
+ "#454050",
64
+ "#302c38",
65
+ "#252129",
66
+ "#1e1b24",
67
+ "#16141a",
68
+ "#0d0c10",
69
+ ],
70
+ gray: [
71
+ "#faf9fb",
72
+ "#f3f1f5",
73
+ "#e8e5ed",
74
+ "#d4d0dc",
75
+ "#a8a3b3",
76
+ "#7c7689",
77
+ "#5c5568",
78
+ "#454050",
79
+ "#302c38",
80
+ "#1e1b24",
81
+ ],
82
+ violet: [
83
+ "#f5f3ff",
84
+ "#ede9fe",
85
+ "#ddd6fe",
86
+ "#c4b5fd",
87
+ "#a78bfa",
88
+ "#8b5cf6",
89
+ "#7c3aed",
90
+ "#6d28d9",
91
+ "#5b21b6",
92
+ "#4c1d95",
93
+ ],
94
+ pink: [
95
+ "#fdf2f8",
96
+ "#fce7f3",
97
+ "#fbcfe8",
98
+ "#f9a8d4",
99
+ "#f472b6",
100
+ "#ec4899",
101
+ "#db2777",
102
+ "#be185d",
103
+ "#9d174d",
104
+ "#831843",
105
+ ],
106
+ },
107
+ };