@alepha/ui 0.13.1 → 0.13.3

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 (118) hide show
  1. package/dist/admin/AdminLayout-JakF7ESb.js +388 -0
  2. package/dist/admin/AdminLayout-JakF7ESb.js.map +1 -0
  3. package/dist/admin/AdminLayout-qNsIyl30.js +3 -0
  4. package/dist/admin/AdminNotifications-BPrxALdS.js +154 -0
  5. package/dist/admin/AdminNotifications-BPrxALdS.js.map +1 -0
  6. package/dist/admin/AdminNotifications-DV-35Fi3.js +3 -0
  7. package/dist/admin/{AdminSessions-CmDVneE2.js → AdminSessions-CMmBtbSw.js} +36 -9
  8. package/dist/admin/AdminSessions-CMmBtbSw.js.map +1 -0
  9. package/dist/admin/AdminSessions-Df2VYzlE.js +3 -0
  10. package/dist/admin/AdminUserCreate-Coa_yi6m.js +103 -0
  11. package/dist/admin/AdminUserCreate-Coa_yi6m.js.map +1 -0
  12. package/dist/admin/AdminUserCreate-DjiCcAk0.js +3 -0
  13. package/dist/admin/AdminUserDetails-BCFwOm9w.js +221 -0
  14. package/dist/admin/AdminUserDetails-BCFwOm9w.js.map +1 -0
  15. package/dist/admin/AdminUserDetails-C5yeJNa3.js +3 -0
  16. package/dist/admin/AdminUserLayout-B8ga5QvP.js +3 -0
  17. package/dist/admin/AdminUserLayout-CR2OqV9Z.js +153 -0
  18. package/dist/admin/AdminUserLayout-CR2OqV9Z.js.map +1 -0
  19. package/dist/admin/AdminUserSessions-A_5KkqTY.js +3 -0
  20. package/dist/admin/AdminUserSessions-Bcf6-rjG.js +129 -0
  21. package/dist/admin/AdminUserSessions-Bcf6-rjG.js.map +1 -0
  22. package/dist/admin/AdminUserSettings-DAsAhFjX.js +3 -0
  23. package/dist/admin/AdminUserSettings-DRYVdW6S.js +164 -0
  24. package/dist/admin/AdminUserSettings-DRYVdW6S.js.map +1 -0
  25. package/dist/admin/AdminUsers-Dd9a5UqO.js +3 -0
  26. package/dist/admin/{AdminUsers-88De5pev.js → AdminUsers-IN_2yHKt.js} +32 -14
  27. package/dist/admin/AdminUsers-IN_2yHKt.js.map +1 -0
  28. package/dist/admin/index.d.ts +6052 -908
  29. package/dist/admin/index.js +299 -41
  30. package/dist/admin/index.js.map +1 -1
  31. package/dist/auth/AuthLayout-BSL8ZHgr.js +19 -0
  32. package/dist/auth/AuthLayout-BSL8ZHgr.js.map +1 -0
  33. package/dist/auth/Login-DDsyCNAA.js +4 -0
  34. package/dist/auth/{Login-OCrvjs9U.js → Login-kBfaRgKG.js} +5 -4
  35. package/dist/auth/Login-kBfaRgKG.js.map +1 -0
  36. package/dist/auth/{Register-Ei34GSba.js → Register-BxJmOqpF.js} +9 -6
  37. package/dist/auth/Register-BxJmOqpF.js.map +1 -0
  38. package/dist/auth/Register-D10MnlQc.js +4 -0
  39. package/dist/auth/{ResetPassword-tO0oMzfo.js → ResetPassword-BhyZ9ek4.js} +3 -3
  40. package/dist/auth/ResetPassword-BhyZ9ek4.js.map +1 -0
  41. package/dist/auth/ResetPassword-llBG-STp.js +3 -0
  42. package/dist/auth/VerifyEmail-BvOG-IUC.js +3 -0
  43. package/dist/auth/VerifyEmail-DeLct3oQ.js +131 -0
  44. package/dist/auth/VerifyEmail-DeLct3oQ.js.map +1 -0
  45. package/dist/auth/index.d.ts +3773 -3568
  46. package/dist/auth/index.js +96 -20
  47. package/dist/auth/index.js.map +1 -1
  48. package/dist/core/index.d.ts +340 -155
  49. package/dist/core/index.js +1391 -395
  50. package/dist/core/index.js.map +1 -1
  51. package/package.json +11 -8
  52. package/src/admin/AdminRouter.ts +116 -29
  53. package/src/admin/MainRouter.ts +23 -0
  54. package/src/admin/components/AdminLayout.tsx +86 -103
  55. package/src/admin/components/AdminNotifications.tsx +196 -12
  56. package/src/admin/components/AdminParameters.tsx +1 -1
  57. package/src/admin/components/AdminSessions.tsx +43 -7
  58. package/src/admin/components/AdminUserCreate.tsx +84 -0
  59. package/src/admin/components/AdminUserDetails.tsx +180 -0
  60. package/src/admin/components/AdminUserLayout.tsx +172 -0
  61. package/src/admin/components/AdminUserSessions.tsx +158 -0
  62. package/src/admin/components/AdminUserSettings.tsx +165 -0
  63. package/src/admin/components/AdminUsers.tsx +29 -9
  64. package/src/admin/index.ts +12 -3
  65. package/src/auth/AuthI18n.ts +22 -0
  66. package/src/auth/AuthRouter.ts +82 -8
  67. package/src/auth/components/AuthLayout.tsx +12 -0
  68. package/src/auth/components/Login.tsx +13 -11
  69. package/src/auth/components/Register.tsx +6 -5
  70. package/src/auth/components/ResetPassword.tsx +1 -1
  71. package/src/auth/components/VerifyEmail.tsx +102 -0
  72. package/src/auth/components/buttons/UserButton.tsx +6 -2
  73. package/src/auth/index.ts +1 -0
  74. package/src/core/components/buttons/ActionButton.tsx +11 -4
  75. package/src/core/components/buttons/DarkModeButton.tsx +1 -1
  76. package/src/core/components/buttons/OmnibarButton.tsx +10 -5
  77. package/src/core/components/buttons/ThemeButton.tsx +31 -0
  78. package/src/core/components/layout/AdminShell.tsx +4 -2
  79. package/src/core/components/layout/AlephaMantineProvider.tsx +10 -4
  80. package/src/core/components/layout/Omnibar.tsx +27 -10
  81. package/src/core/components/layout/Sidebar.tsx +33 -15
  82. package/src/core/components/table/DataTable.tsx +9 -5
  83. package/src/core/hooks/useTheme.ts +25 -0
  84. package/src/core/index.ts +9 -4
  85. package/src/core/providers/ThemeProvider.ts +87 -0
  86. package/src/core/themes/aurora.ts +107 -0
  87. package/src/core/themes/crystal.ts +107 -0
  88. package/src/core/themes/default.ts +7 -0
  89. package/src/core/themes/ember.ts +107 -0
  90. package/src/core/themes/index.ts +7 -0
  91. package/src/core/themes/midnight.ts +104 -0
  92. package/src/core/themes/remoraid.ts +278 -0
  93. package/src/core/themes/slate.ts +81 -0
  94. package/dist/admin/AdminJobs-BOq6AZOW.js +0 -3
  95. package/dist/admin/AdminJobs-CDnVxEv6.js +0 -125
  96. package/dist/admin/AdminJobs-CDnVxEv6.js.map +0 -1
  97. package/dist/admin/AdminLayout-Bgx25J8m.js +0 -3
  98. package/dist/admin/AdminLayout-CervL8LV.js +0 -88
  99. package/dist/admin/AdminLayout-CervL8LV.js.map +0 -1
  100. package/dist/admin/AdminNotifications-BDQXt3-e.js +0 -3
  101. package/dist/admin/AdminNotifications-DvI2989x.js +0 -40
  102. package/dist/admin/AdminNotifications-DvI2989x.js.map +0 -1
  103. package/dist/admin/AdminParameters-CWi7crdn.js +0 -40
  104. package/dist/admin/AdminParameters-CWi7crdn.js.map +0 -1
  105. package/dist/admin/AdminParameters-DKRAVen3.js +0 -3
  106. package/dist/admin/AdminSessions-CmDVneE2.js.map +0 -1
  107. package/dist/admin/AdminSessions-Dkk_fzWK.js +0 -3
  108. package/dist/admin/AdminUsers-88De5pev.js.map +0 -1
  109. package/dist/admin/AdminUsers-oyAXqZ5l.js +0 -3
  110. package/dist/admin/AdminVerifications-D93TKymL.js +0 -3
  111. package/dist/admin/AdminVerifications-DBVEoqJe.js +0 -40
  112. package/dist/admin/AdminVerifications-DBVEoqJe.js.map +0 -1
  113. package/dist/auth/Login-BC2jTczq.js +0 -4
  114. package/dist/auth/Login-OCrvjs9U.js.map +0 -1
  115. package/dist/auth/Register-Dh0lsQmI.js +0 -4
  116. package/dist/auth/Register-Ei34GSba.js.map +0 -1
  117. package/dist/auth/ResetPassword-BnlAQAOE.js +0 -3
  118. 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,10 @@ 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 <ActionButton icon={IconLogin2} href={authRouter.path("login")} />;
60
64
  }
61
65
 
62
66
  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,12 @@ 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
+
240
248
  if (props.icon) {
241
249
  const icon = isComponentType(props.icon) ? (
242
250
  <props.icon size={ui.sizes.icon.md} />
@@ -254,15 +262,14 @@ const ActionButton = (_props: ActionProps) => {
254
262
 
255
263
  if (!props.children) {
256
264
  restProps.children = Children.only(icon);
257
- restProps.p ??= "xs";
265
+ restProps.px ??= "xs";
258
266
  } else {
259
267
  restProps.leftSection = icon;
260
268
  }
261
269
  }
262
270
 
263
271
  if (props.leftSection && !props.children) {
264
- restProps.className ??= "mantine-Action-iconOnly";
265
- restProps.p ??= "xs";
272
+ restProps.px ??= "xs";
266
273
  }
267
274
 
268
275
  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"}
@@ -12,18 +12,23 @@ const OmnibarButton = (props: OmnibarButtonProps) => {
12
12
  return (
13
13
  <ActionButton
14
14
  variant={"default"}
15
- miw={256}
16
15
  onClick={spotlight.open}
17
16
  justify={"space-between"}
18
- rightSection={<Kbd size={"sm"}>⌘+K</Kbd>}
17
+ rightSection={
18
+ <Kbd visibleFrom={"sm"} size={"sm"}>
19
+ ⌘+K
20
+ </Kbd>
21
+ }
19
22
  radius={"md"}
20
23
  {...props.actionProps}
21
24
  >
22
25
  <Flex align={"center"} gap={"xs"}>
23
26
  <IconSearch size={16} color={"gray"} />
24
- <Text size={"xs"} c={"dimmed"}>
25
- Search...
26
- </Text>
27
+ <Flex visibleFrom={"sm"} miw={192}>
28
+ <Text size={"xs"} c={"dimmed"}>
29
+ Search...
30
+ </Text>
31
+ </Flex>
27
32
  </Flex>
28
33
  </ActionButton>
29
34
  );
@@ -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,16 +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: () => router.go(page.name),
24
- leftSection: page.icon,
25
- })),
26
- [],
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],
27
44
  );
28
45
 
29
46
  return (
@@ -32,7 +49,7 @@ const Omnibar = (props: OmnibarProps) => {
32
49
  shortcut={shortcut}
33
50
  limit={10}
34
51
  searchProps={{
35
- leftSection: <IconSearch size={20} />,
52
+ leftSection: <IconSearch size={ui.sizes.icon.md} />,
36
53
  placeholder: searchPlaceholder,
37
54
  }}
38
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
@@ -271,15 +276,17 @@ export const SidebarItem = (props: SidebarItemProps) => {
271
276
  bottom: 16,
272
277
  }}
273
278
  />
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
- ))}
279
+ {item.children
280
+ .filter((child) => !child.can || child.can())
281
+ .map((child, index) => (
282
+ <SidebarItem
283
+ key={index}
284
+ item={child}
285
+ level={level + 1}
286
+ onItemClick={props.onItemClick}
287
+ theme={props.theme}
288
+ />
289
+ ))}
283
290
  </Flex>
284
291
  )}
285
292
  </Flex>
@@ -338,6 +345,14 @@ const SidebarCollapsedItem = (props: SidebarItemProps) => {
338
345
  color={"var(--alepha-text)"}
339
346
  variant={"subtle"}
340
347
  variantActive={"default"}
348
+ tooltip={
349
+ item.children
350
+ ? undefined
351
+ : {
352
+ label: item.label,
353
+ position: "right",
354
+ }
355
+ }
341
356
  radius={props.item.theme?.radius ?? props.theme.button?.radius ?? "md"}
342
357
  onClick={handleItemClick}
343
358
  icon={renderIcon(item.icon) ?? <IconSquareRounded />}
@@ -348,12 +363,14 @@ const SidebarCollapsedItem = (props: SidebarItemProps) => {
348
363
  ? ({
349
364
  position: "right",
350
365
  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
- })),
366
+ items: item.children
367
+ .filter((child) => !child.can || child.can())
368
+ .map((child) => ({
369
+ label: child.label,
370
+ href: child.href,
371
+ icon: renderIcon(child.icon),
372
+ children: child.children?.filter((c) => !c.can || c.can()),
373
+ })),
357
374
  } as any)
358
375
  : undefined
359
376
  }
@@ -410,6 +427,7 @@ export interface SidebarMenuItem extends SidebarAbstractItem {
410
427
  rightSection?: ReactNode;
411
428
  theme?: SidebarButtonTheme;
412
429
  actionProps?: ActionProps;
430
+ can?: () => boolean; // Visibility control: true -> visible, false -> hidden
413
431
  }
414
432
 
415
433
  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
+ type Theme,
5
+ ThemeProvider,
6
+ themeAtom,
7
+ } from "../providers/ThemeProvider.ts";
8
+
9
+ export const useTheme = () => {
10
+ useStore(themeAtom);
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,
@@ -102,7 +106,7 @@ declare module "typebox" {
102
106
  }
103
107
 
104
108
  declare module "@alepha/react" {
105
- interface PageDescriptorOptions {
109
+ interface PagePrimitiveOptions {
106
110
  /**
107
111
  * Human-readable title for the page.
108
112
  * - for Sidebar navigation
@@ -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,87 @@
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 themeAtom = $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 themeAtom.schema>;
26
+
27
+ declare module "alepha" {
28
+ interface State {
29
+ [themeAtom.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(themeAtom);
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(themeAtom, 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(themeAtom) ??
80
+ themeAtom.options.default
81
+ );
82
+ } catch {
83
+ // TODO: atom should take default value if undefined ???
84
+ return this.alepha.store.get(themeAtom) ?? themeAtom.options.default;
85
+ }
86
+ }
87
+ }
@@ -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
+ };