ding-react-admin 2.0.0 → 2.0.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 (169) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +254 -195
  3. package/dist/index.css +2 -0
  4. package/dist/index.js +1772 -1169
  5. package/dist/src/components/NavMenuLabel.d.ts +8 -0
  6. package/dist/src/components/NavMenuLabel.d.ts.map +1 -0
  7. package/dist/src/components/NavMenuSearch.d.ts +9 -0
  8. package/dist/src/components/NavMenuSearch.d.ts.map +1 -0
  9. package/dist/src/components/ScrollableArea.d.ts +10 -0
  10. package/dist/src/components/ScrollableArea.d.ts.map +1 -0
  11. package/dist/src/components/index.d.ts +3 -0
  12. package/dist/src/components/index.d.ts.map +1 -1
  13. package/dist/src/context/AuthProvider.d.ts +2 -0
  14. package/dist/src/context/AuthProvider.d.ts.map +1 -1
  15. package/dist/src/crud/FormGlobalErrorsAlert.d.ts +5 -0
  16. package/dist/src/crud/FormGlobalErrorsAlert.d.ts.map +1 -0
  17. package/dist/src/crud/ResourceForm.d.ts +1 -4
  18. package/dist/src/crud/ResourceForm.d.ts.map +1 -1
  19. package/dist/src/crud/ResourceFormModal.d.ts +7 -1
  20. package/dist/src/crud/ResourceFormModal.d.ts.map +1 -1
  21. package/dist/src/crud/ResourceRecordForm.d.ts +27 -0
  22. package/dist/src/crud/ResourceRecordForm.d.ts.map +1 -0
  23. package/dist/src/crud/fields/ReferenceField.d.ts +12 -2
  24. package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -1
  25. package/dist/src/crud/fields/ReferenceInputActions.d.ts +16 -0
  26. package/dist/src/crud/fields/ReferenceInputActions.d.ts.map +1 -0
  27. package/dist/src/crud/fields/ReferenceManyField.d.ts +10 -1
  28. package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -1
  29. package/dist/src/crud/filters/ReferenceFilter.d.ts +1 -1
  30. package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -1
  31. package/dist/src/crud/index.d.ts +5 -0
  32. package/dist/src/crud/index.d.ts.map +1 -1
  33. package/dist/src/crud/types.d.ts +12 -0
  34. package/dist/src/crud/types.d.ts.map +1 -1
  35. package/dist/src/crud/utils/choiceSelectionUtils.d.ts +10 -0
  36. package/dist/src/crud/utils/choiceSelectionUtils.d.ts.map +1 -0
  37. package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts +2 -0
  38. package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts.map +1 -0
  39. package/dist/src/crud/utils/formErrors.d.ts +18 -7
  40. package/dist/src/crud/utils/formErrors.d.ts.map +1 -1
  41. package/dist/src/crud/utils/formErrors.test.d.ts +2 -0
  42. package/dist/src/crud/utils/formErrors.test.d.ts.map +1 -0
  43. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts +1 -0
  44. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts.map +1 -1
  45. package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts +4 -0
  46. package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts.map +1 -0
  47. package/dist/src/crud/utils/toFormData.d.ts +1 -1
  48. package/dist/src/crud/utils/toFormData.d.ts.map +1 -1
  49. package/dist/src/crud/utils/uploadReferenceUtils.d.ts +5 -0
  50. package/dist/src/crud/utils/uploadReferenceUtils.d.ts.map +1 -0
  51. package/dist/src/crud/utils/useChoices.d.ts +27 -1
  52. package/dist/src/crud/utils/useChoices.d.ts.map +1 -1
  53. package/dist/src/crud/utils/useFormRecord.d.ts +9 -7
  54. package/dist/src/crud/utils/useFormRecord.d.ts.map +1 -1
  55. package/dist/src/data/parseFormErrorHelpers.d.ts +6 -27
  56. package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -1
  57. package/dist/src/index.d.ts +2 -2
  58. package/dist/src/index.d.ts.map +1 -1
  59. package/dist/src/layouts/AdminLayout.d.ts +1 -1
  60. package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
  61. package/dist/src/layouts/AuthPageLayout.d.ts.map +1 -1
  62. package/dist/src/layouts/navFilter.d.ts +5 -0
  63. package/dist/src/layouts/navFilter.d.ts.map +1 -0
  64. package/dist/src/layouts/navMenuItems.d.ts +6 -0
  65. package/dist/src/layouts/navMenuItems.d.ts.map +1 -0
  66. package/dist/src/types.d.ts +12 -0
  67. package/dist/src/types.d.ts.map +1 -1
  68. package/package.json +79 -59
  69. package/src/app/AdminApp.tsx +34 -34
  70. package/src/components/AuthAlternateLink.tsx +23 -23
  71. package/src/components/DensitySwitch.tsx +31 -31
  72. package/src/components/NavMenuLabel.tsx +26 -0
  73. package/src/components/NavMenuSearch.tsx +76 -0
  74. package/src/components/ScrollableArea.tsx +46 -0
  75. package/src/components/ThemeSwitch.tsx +28 -28
  76. package/src/components/ThemeToolbar.tsx +34 -34
  77. package/src/components/index.ts +7 -4
  78. package/src/components/navMenu.css +21 -0
  79. package/src/components/scrollable.css +28 -0
  80. package/src/context/AppThemeProvider.tsx +128 -128
  81. package/src/context/AuthProvider.tsx +89 -77
  82. package/src/context/DataProvider.tsx +30 -30
  83. package/src/context/PermissionsProvider.tsx +57 -57
  84. package/src/crud/FilterBar.tsx +36 -36
  85. package/src/crud/FormGlobalErrorsAlert.tsx +36 -0
  86. package/src/crud/FormSteps.tsx +135 -135
  87. package/src/crud/FormTabs.tsx +126 -126
  88. package/src/crud/InlineFormSet.tsx +162 -162
  89. package/src/crud/ListActionsBar.tsx +106 -106
  90. package/src/crud/ResourceForm.tsx +83 -160
  91. package/src/crud/ResourceFormModal.tsx +75 -133
  92. package/src/crud/ResourceRecordForm.tsx +166 -0
  93. package/src/crud/columns/BooleanColumn.tsx +32 -32
  94. package/src/crud/columns/CustomColumn.tsx +33 -33
  95. package/src/crud/columns/DateColumn.tsx +28 -28
  96. package/src/crud/columns/ImageColumn.tsx +58 -58
  97. package/src/crud/columns/NumberColumn.tsx +31 -31
  98. package/src/crud/columns/ReferenceColumn.tsx +85 -85
  99. package/src/crud/columns/ReferenceManyColumn.tsx +67 -67
  100. package/src/crud/columns/TextColumn.tsx +38 -38
  101. package/src/crud/context/FilterContext.tsx +71 -71
  102. package/src/crud/context/FormContext.tsx +39 -39
  103. package/src/crud/context/FormSectionContext.tsx +22 -22
  104. package/src/crud/context/InlineFieldsRegistry.tsx +61 -61
  105. package/src/crud/context/ListContext.tsx +93 -93
  106. package/src/crud/fields/BooleanField.tsx +38 -38
  107. package/src/crud/fields/DateField.tsx +53 -53
  108. package/src/crud/fields/FileField.tsx +123 -123
  109. package/src/crud/fields/ImageField.tsx +120 -120
  110. package/src/crud/fields/NumberField.tsx +52 -52
  111. package/src/crud/fields/PasswordField.tsx +99 -99
  112. package/src/crud/fields/ReferenceField.tsx +237 -96
  113. package/src/crud/fields/ReferenceInputActions.tsx +106 -0
  114. package/src/crud/fields/ReferenceManyField.tsx +200 -76
  115. package/src/crud/fields/SelectField.tsx +48 -48
  116. package/src/crud/fields/TextField.tsx +46 -46
  117. package/src/crud/fields/uploadFieldUtils.ts +21 -21
  118. package/src/crud/fields/useUploadPreviewUrl.ts +20 -20
  119. package/src/crud/filters/BooleanFilter.tsx +38 -38
  120. package/src/crud/filters/DateFilter.tsx +35 -35
  121. package/src/crud/filters/NumberFilter.tsx +33 -33
  122. package/src/crud/filters/ReferenceFilter.tsx +192 -173
  123. package/src/crud/filters/SelectFilter.tsx +44 -44
  124. package/src/crud/filters/TextFilter.tsx +36 -36
  125. package/src/crud/index.ts +99 -94
  126. package/src/crud/types.ts +146 -134
  127. package/src/crud/utils/buildFormPayload.ts +24 -24
  128. package/src/crud/utils/buildInlineRowsPayload.ts +37 -37
  129. package/src/crud/utils/buildResourceFormSubmitBody.ts +29 -29
  130. package/src/crud/utils/choiceSelectionUtils.test.ts +45 -0
  131. package/src/crud/utils/choiceSelectionUtils.ts +116 -0
  132. package/src/crud/utils/formErrors.test.ts +168 -0
  133. package/src/crud/utils/formErrors.ts +138 -42
  134. package/src/crud/utils/formSectionErrors.ts +85 -85
  135. package/src/crud/utils/getFormValue.ts +13 -13
  136. package/src/crud/utils/hasUploadValues.ts +9 -9
  137. package/src/crud/utils/nestedFieldPath.ts +8 -8
  138. package/src/crud/utils/prepareFormSubmitBody.ts +22 -17
  139. package/src/crud/utils/referenceSelectNotFoundContent.tsx +7 -0
  140. package/src/crud/utils/setFormValue.ts +18 -18
  141. package/src/crud/utils/sortQueryParam.ts +37 -37
  142. package/src/crud/utils/toFormData.ts +79 -81
  143. package/src/crud/utils/uploadReferenceUtils.ts +44 -0
  144. package/src/crud/utils/useAbortableEffect.ts +17 -17
  145. package/src/crud/utils/useChoices.ts +330 -182
  146. package/src/crud/utils/useFormRecord.ts +179 -156
  147. package/src/crud/utils/useListQueryState.ts +191 -191
  148. package/src/data/abortError.test.ts +19 -19
  149. package/src/data/abortError.ts +10 -10
  150. package/src/data/createMemoryResourceHandlers.ts +78 -78
  151. package/src/data/createRestResourceHandlers.ts +63 -63
  152. package/src/data/dataUtils.test.ts +228 -220
  153. package/src/data/inMemoryList.ts +97 -97
  154. package/src/data/parseFormErrorHelpers.test.ts +201 -138
  155. package/src/data/parseFormErrorHelpers.ts +376 -311
  156. package/src/data/sortHelpers.ts +48 -48
  157. package/src/index.ts +211 -207
  158. package/src/layouts/AdminLayout.tsx +530 -382
  159. package/src/layouts/AuthPageLayout.tsx +87 -81
  160. package/src/layouts/navFilter.ts +49 -0
  161. package/src/layouts/navMenuItems.tsx +44 -0
  162. package/src/pages/LoginPage.tsx +80 -80
  163. package/src/pages/PlaceholderPage.tsx +9 -9
  164. package/src/permissions/resourcePermissions.ts +47 -47
  165. package/src/router/createAdminRouter.tsx +64 -64
  166. package/src/router/guards.tsx +63 -63
  167. package/src/router/routeAccess.ts +89 -89
  168. package/src/types.ts +10 -0
  169. package/src/vite-env.d.ts +1 -1
@@ -0,0 +1,46 @@
1
+ import { theme } from "antd";
2
+ import { forwardRef } from "react";
3
+ import type { CSSProperties, ReactNode } from "react";
4
+ import "./scrollable.css";
5
+
6
+ export type ScrollableAreaProps = {
7
+ children: ReactNode;
8
+ className?: string;
9
+ style?: CSSProperties;
10
+ /** Use lighter thumbs on dark sidebar backgrounds. */
11
+ variant?: "default" | "on-dark";
12
+ };
13
+
14
+ export const ScrollableArea = forwardRef<HTMLDivElement, ScrollableAreaProps>(
15
+ function ScrollableArea(
16
+ { children, className, style, variant = "default" },
17
+ ref,
18
+ ) {
19
+ const { token } = theme.useToken();
20
+
21
+ const thumb =
22
+ variant === "on-dark"
23
+ ? "rgba(255, 255, 255, 0.22)"
24
+ : token.colorTextQuaternary;
25
+ const thumbHover =
26
+ variant === "on-dark"
27
+ ? "rgba(255, 255, 255, 0.38)"
28
+ : token.colorTextTertiary;
29
+
30
+ return (
31
+ <div
32
+ ref={ref}
33
+ className={["ding-admin-scroll", className].filter(Boolean).join(" ")}
34
+ style={
35
+ {
36
+ ...style,
37
+ "--ding-scroll-thumb": thumb,
38
+ "--ding-scroll-thumb-hover": thumbHover,
39
+ } as CSSProperties
40
+ }
41
+ >
42
+ {children}
43
+ </div>
44
+ );
45
+ },
46
+ );
@@ -1,28 +1,28 @@
1
- import {
2
- DesktopOutlined,
3
- MoonOutlined,
4
- SunOutlined,
5
- } from "@ant-design/icons";
6
- import { Segmented } from "antd";
7
- import type { SegmentedProps } from "antd";
8
- import type { ThemeMode } from "../types";
9
- import { useThemeMode } from "../context/AppThemeProvider";
10
-
11
- const OPTIONS: SegmentedProps["options"] = [
12
- { label: "Light", value: "light", icon: <SunOutlined /> },
13
- { label: "Dark", value: "dark", icon: <MoonOutlined /> },
14
- { label: "Auto", value: "system", icon: <DesktopOutlined /> },
15
- ];
16
-
17
- export function ThemeSwitch() {
18
- const { mode, setMode } = useThemeMode();
19
-
20
- return (
21
- <Segmented
22
- size="small"
23
- value={mode}
24
- options={OPTIONS}
25
- onChange={(v) => setMode(v as ThemeMode)}
26
- />
27
- );
28
- }
1
+ import {
2
+ DesktopOutlined,
3
+ MoonOutlined,
4
+ SunOutlined,
5
+ } from "@ant-design/icons";
6
+ import { Segmented } from "antd";
7
+ import type { SegmentedProps } from "antd";
8
+ import type { ThemeMode } from "../types";
9
+ import { useThemeMode } from "../context/AppThemeProvider";
10
+
11
+ const OPTIONS: SegmentedProps["options"] = [
12
+ { label: "Light", value: "light", icon: <SunOutlined /> },
13
+ { label: "Dark", value: "dark", icon: <MoonOutlined /> },
14
+ { label: "Auto", value: "system", icon: <DesktopOutlined /> },
15
+ ];
16
+
17
+ export function ThemeSwitch() {
18
+ const { mode, setMode } = useThemeMode();
19
+
20
+ return (
21
+ <Segmented
22
+ size="small"
23
+ value={mode}
24
+ options={OPTIONS}
25
+ onChange={(v) => setMode(v as ThemeMode)}
26
+ />
27
+ );
28
+ }
@@ -1,34 +1,34 @@
1
- import { SettingOutlined } from "@ant-design/icons";
2
- import { Button, Grid, Popover, Space, theme } from "antd";
3
- import { DensitySwitch } from "./DensitySwitch";
4
- import { ThemeSwitch } from "./ThemeSwitch";
5
-
6
- export function ThemeToolbar() {
7
- const { token } = theme.useToken();
8
- const screens = Grid.useBreakpoint();
9
- const placement = screens.lg ? "bottomRight" : "bottom";
10
-
11
- return (
12
- <Popover
13
- placement={placement}
14
- trigger="click"
15
- content={
16
- <Space
17
- orientation="vertical"
18
- size="middle"
19
- style={{ minWidth: 240, maxWidth: "min(92vw, 320px)" }}
20
- >
21
- <ThemeSwitch />
22
- <DensitySwitch />
23
- </Space>
24
- }
25
- styles={{ content: { padding: token.paddingSM } }}
26
- >
27
- <Button
28
- type="default"
29
- icon={<SettingOutlined />}
30
- aria-label="Display and theme settings"
31
- />
32
- </Popover>
33
- );
34
- }
1
+ import { SettingOutlined } from "@ant-design/icons";
2
+ import { Button, Grid, Popover, Space, theme } from "antd";
3
+ import { DensitySwitch } from "./DensitySwitch";
4
+ import { ThemeSwitch } from "./ThemeSwitch";
5
+
6
+ export function ThemeToolbar() {
7
+ const { token } = theme.useToken();
8
+ const screens = Grid.useBreakpoint();
9
+ const placement = screens.lg ? "bottomRight" : "bottom";
10
+
11
+ return (
12
+ <Popover
13
+ placement={placement}
14
+ trigger="click"
15
+ content={
16
+ <Space
17
+ orientation="vertical"
18
+ size="middle"
19
+ style={{ minWidth: 240, maxWidth: "min(92vw, 320px)" }}
20
+ >
21
+ <ThemeSwitch />
22
+ <DensitySwitch />
23
+ </Space>
24
+ }
25
+ styles={{ content: { padding: token.paddingSM } }}
26
+ >
27
+ <Button
28
+ type="default"
29
+ icon={<SettingOutlined />}
30
+ aria-label="Display and theme settings"
31
+ />
32
+ </Popover>
33
+ );
34
+ }
@@ -1,4 +1,7 @@
1
- export { AuthAlternateLink } from "./AuthAlternateLink";
2
- export { ThemeToolbar } from "./ThemeToolbar";
3
- export { ThemeSwitch } from "./ThemeSwitch";
4
- export { DensitySwitch } from "./DensitySwitch";
1
+ export { AuthAlternateLink } from "./AuthAlternateLink";
2
+ export { NavMenuLabel } from "./NavMenuLabel";
3
+ export { NavMenuSearch } from "./NavMenuSearch";
4
+ export { ScrollableArea } from "./ScrollableArea";
5
+ export { ThemeToolbar } from "./ThemeToolbar";
6
+ export { ThemeSwitch } from "./ThemeSwitch";
7
+ export { DensitySwitch } from "./DensitySwitch";
@@ -0,0 +1,21 @@
1
+ .ding-admin-nav-menu .ant-menu-title-content {
2
+ min-width: 0;
3
+ overflow: hidden;
4
+ }
5
+
6
+ .ding-admin-menu-label {
7
+ display: block;
8
+ width: 100%;
9
+ max-width: 100%;
10
+ min-width: 0;
11
+ overflow: hidden;
12
+ text-overflow: ellipsis;
13
+ white-space: nowrap;
14
+ }
15
+
16
+ /* Mobile drawer: wrap long labels instead of truncating */
17
+ .ant-drawer .ding-admin-menu-label {
18
+ white-space: normal;
19
+ overflow: visible;
20
+ text-overflow: clip;
21
+ }
@@ -0,0 +1,28 @@
1
+ .ding-admin-scroll {
2
+ scrollbar-width: thin;
3
+ scrollbar-color: var(--ding-scroll-thumb) transparent;
4
+ }
5
+
6
+ .ding-admin-scroll::-webkit-scrollbar {
7
+ width: 6px;
8
+ height: 6px;
9
+ }
10
+
11
+ .ding-admin-scroll::-webkit-scrollbar-track {
12
+ background: transparent;
13
+ }
14
+
15
+ .ding-admin-scroll::-webkit-scrollbar-thumb {
16
+ background-color: var(--ding-scroll-thumb);
17
+ border-radius: 999px;
18
+ border: 1px solid transparent;
19
+ background-clip: padding-box;
20
+ }
21
+
22
+ .ding-admin-scroll::-webkit-scrollbar-thumb:hover {
23
+ background-color: var(--ding-scroll-thumb-hover);
24
+ }
25
+
26
+ .ding-admin-scroll::-webkit-scrollbar-corner {
27
+ background: transparent;
28
+ }
@@ -1,128 +1,128 @@
1
- import { ConfigProvider, theme as antdTheme } from "antd";
2
- import {
3
- createContext,
4
- useContext,
5
- useEffect,
6
- useMemo,
7
- useState,
8
- } from "react";
9
- import type {
10
- AppThemeProviderProps,
11
- ThemeDensity,
12
- ThemeMode,
13
- } from "../types";
14
-
15
- export type { ThemeDensity, ThemeMode };
16
-
17
- type ThemeContextValue = {
18
- mode: ThemeMode;
19
- setMode: (mode: ThemeMode) => void;
20
- /** Resolved appearance for Ant Design (`system` → OS preference). */
21
- resolved: "light" | "dark";
22
- density: ThemeDensity;
23
- setDensity: (density: ThemeDensity) => void;
24
- };
25
-
26
- const ThemeContext = createContext<ThemeContextValue | null>(null);
27
-
28
- function readStoredMode(key: string): ThemeMode {
29
- try {
30
- const raw = localStorage.getItem(key);
31
- if (raw === "light" || raw === "dark" || raw === "system") return raw;
32
- } catch {
33
- /* ignore */
34
- }
35
- return "system";
36
- }
37
-
38
- function getSystemDark(): boolean {
39
- return window.matchMedia("(prefers-color-scheme: dark)").matches;
40
- }
41
-
42
- function readStoredDensity(key: string): ThemeDensity {
43
- try {
44
- const raw = localStorage.getItem(key);
45
- if (raw === "comfortable" || raw === "compact") return raw;
46
- } catch {
47
- /* ignore */
48
- }
49
- return "comfortable";
50
- }
51
-
52
- const DEFAULT_MODE_KEY = "ding-react-admin-theme-mode";
53
- const DEFAULT_DENSITY_KEY = "ding-react-admin-theme-density";
54
-
55
- export function AppThemeProvider({
56
- children,
57
- modeStorageKey = DEFAULT_MODE_KEY,
58
- densityStorageKey = DEFAULT_DENSITY_KEY,
59
- }: AppThemeProviderProps) {
60
- const [mode, setModeState] = useState<ThemeMode>(() =>
61
- readStoredMode(modeStorageKey),
62
- );
63
- const [density, setDensityState] = useState<ThemeDensity>(() =>
64
- readStoredDensity(densityStorageKey),
65
- );
66
- const [systemDark, setSystemDark] = useState(getSystemDark);
67
-
68
- useEffect(() => {
69
- if (mode !== "system") return;
70
- const mq = window.matchMedia("(prefers-color-scheme: dark)");
71
- const onChange = () => setSystemDark(mq.matches);
72
- onChange();
73
- mq.addEventListener("change", onChange);
74
- return () => mq.removeEventListener("change", onChange);
75
- }, [mode]);
76
-
77
- const setMode = (next: ThemeMode) => {
78
- setModeState(next);
79
- try {
80
- localStorage.setItem(modeStorageKey, next);
81
- } catch {
82
- /* ignore */
83
- }
84
- };
85
-
86
- const setDensity = (next: ThemeDensity) => {
87
- setDensityState(next);
88
- try {
89
- localStorage.setItem(densityStorageKey, next);
90
- } catch {
91
- /* ignore */
92
- }
93
- };
94
-
95
- const resolved: "light" | "dark" =
96
- mode === "system" ? (systemDark ? "dark" : "light") : mode;
97
-
98
- const configTheme = useMemo(() => {
99
- const base =
100
- resolved === "dark"
101
- ? antdTheme.darkAlgorithm
102
- : antdTheme.defaultAlgorithm;
103
- return {
104
- algorithm:
105
- density === "compact"
106
- ? [base, antdTheme.compactAlgorithm]
107
- : base,
108
- };
109
- }, [resolved, density]);
110
-
111
- const value = useMemo(
112
- () => ({ mode, setMode, resolved, density, setDensity }),
113
- [mode, resolved, density, setMode, setDensity],
114
- );
115
-
116
- return (
117
- <ThemeContext.Provider value={value}>
118
- <ConfigProvider theme={configTheme}>{children}</ConfigProvider>
119
- </ThemeContext.Provider>
120
- );
121
- }
122
-
123
- export function useThemeMode() {
124
- const ctx = useContext(ThemeContext);
125
- if (!ctx)
126
- throw new Error("useThemeMode must be used within AppThemeProvider");
127
- return ctx;
128
- }
1
+ import { ConfigProvider, theme as antdTheme } from "antd";
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useState,
8
+ } from "react";
9
+ import type {
10
+ AppThemeProviderProps,
11
+ ThemeDensity,
12
+ ThemeMode,
13
+ } from "../types";
14
+
15
+ export type { ThemeDensity, ThemeMode };
16
+
17
+ type ThemeContextValue = {
18
+ mode: ThemeMode;
19
+ setMode: (mode: ThemeMode) => void;
20
+ /** Resolved appearance for Ant Design (`system` → OS preference). */
21
+ resolved: "light" | "dark";
22
+ density: ThemeDensity;
23
+ setDensity: (density: ThemeDensity) => void;
24
+ };
25
+
26
+ const ThemeContext = createContext<ThemeContextValue | null>(null);
27
+
28
+ function readStoredMode(key: string): ThemeMode {
29
+ try {
30
+ const raw = localStorage.getItem(key);
31
+ if (raw === "light" || raw === "dark" || raw === "system") return raw;
32
+ } catch {
33
+ /* ignore */
34
+ }
35
+ return "system";
36
+ }
37
+
38
+ function getSystemDark(): boolean {
39
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
40
+ }
41
+
42
+ function readStoredDensity(key: string): ThemeDensity {
43
+ try {
44
+ const raw = localStorage.getItem(key);
45
+ if (raw === "comfortable" || raw === "compact") return raw;
46
+ } catch {
47
+ /* ignore */
48
+ }
49
+ return "comfortable";
50
+ }
51
+
52
+ const DEFAULT_MODE_KEY = "ding-react-admin-theme-mode";
53
+ const DEFAULT_DENSITY_KEY = "ding-react-admin-theme-density";
54
+
55
+ export function AppThemeProvider({
56
+ children,
57
+ modeStorageKey = DEFAULT_MODE_KEY,
58
+ densityStorageKey = DEFAULT_DENSITY_KEY,
59
+ }: AppThemeProviderProps) {
60
+ const [mode, setModeState] = useState<ThemeMode>(() =>
61
+ readStoredMode(modeStorageKey),
62
+ );
63
+ const [density, setDensityState] = useState<ThemeDensity>(() =>
64
+ readStoredDensity(densityStorageKey),
65
+ );
66
+ const [systemDark, setSystemDark] = useState(getSystemDark);
67
+
68
+ useEffect(() => {
69
+ if (mode !== "system") return;
70
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
71
+ const onChange = () => setSystemDark(mq.matches);
72
+ onChange();
73
+ mq.addEventListener("change", onChange);
74
+ return () => mq.removeEventListener("change", onChange);
75
+ }, [mode]);
76
+
77
+ const setMode = (next: ThemeMode) => {
78
+ setModeState(next);
79
+ try {
80
+ localStorage.setItem(modeStorageKey, next);
81
+ } catch {
82
+ /* ignore */
83
+ }
84
+ };
85
+
86
+ const setDensity = (next: ThemeDensity) => {
87
+ setDensityState(next);
88
+ try {
89
+ localStorage.setItem(densityStorageKey, next);
90
+ } catch {
91
+ /* ignore */
92
+ }
93
+ };
94
+
95
+ const resolved: "light" | "dark" =
96
+ mode === "system" ? (systemDark ? "dark" : "light") : mode;
97
+
98
+ const configTheme = useMemo(() => {
99
+ const base =
100
+ resolved === "dark"
101
+ ? antdTheme.darkAlgorithm
102
+ : antdTheme.defaultAlgorithm;
103
+ return {
104
+ algorithm:
105
+ density === "compact"
106
+ ? [base, antdTheme.compactAlgorithm]
107
+ : base,
108
+ };
109
+ }, [resolved, density]);
110
+
111
+ const value = useMemo(
112
+ () => ({ mode, setMode, resolved, density, setDensity }),
113
+ [mode, resolved, density, setMode, setDensity],
114
+ );
115
+
116
+ return (
117
+ <ThemeContext.Provider value={value}>
118
+ <ConfigProvider theme={configTheme}>{children}</ConfigProvider>
119
+ </ThemeContext.Provider>
120
+ );
121
+ }
122
+
123
+ export function useThemeMode() {
124
+ const ctx = useContext(ThemeContext);
125
+ if (!ctx)
126
+ throw new Error("useThemeMode must be used within AppThemeProvider");
127
+ return ctx;
128
+ }
@@ -1,77 +1,89 @@
1
- import {
2
- createContext,
3
- useCallback,
4
- useContext,
5
- useMemo,
6
- useState,
7
- type ReactNode,
8
- } from "react";
9
- import type { AuthAdapter, LoginCredentials } from "../types";
10
-
11
- type AuthContextValue = {
12
- isAuthenticated: boolean;
13
- login: (credentials: LoginCredentials) => Promise<void>;
14
- logout: () => void;
15
- };
16
-
17
- const AuthContext = createContext<AuthContextValue | null>(null);
18
-
19
- export type AuthProviderProps = {
20
- children: ReactNode;
21
- /** How login, logout, and token reads are implemented (API, sessionStorage, etc.). */
22
- adapter: AuthAdapter;
23
- };
24
-
25
- export function AuthProvider({ children, adapter }: AuthProviderProps) {
26
- const [token, setToken] = useState<string | null>(() => adapter.getToken());
27
-
28
- const login = useCallback(
29
- async (credentials: LoginCredentials) => {
30
- await adapter.login(credentials);
31
- setToken(adapter.getToken());
32
- },
33
- [adapter],
34
- );
35
-
36
- const logout = useCallback(() => {
37
- adapter.logout();
38
- setToken(adapter.getToken());
39
- }, [adapter]);
40
-
41
- const value = useMemo<AuthContextValue>(
42
- () => ({
43
- isAuthenticated: Boolean(token),
44
- login,
45
- logout,
46
- }),
47
- [token, login, logout],
48
- );
49
-
50
- return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
51
- }
52
-
53
- export function useAuth() {
54
- const ctx = useContext(AuthContext);
55
- if (!ctx) throw new Error("useAuth must be used within AuthProvider");
56
- return ctx;
57
- }
58
-
59
- const DEFAULT_SESSION_KEY = "ding-react-admin-auth";
60
-
61
- /** Store a non-empty token string in `sessionStorage` when “logged in”. */
62
- export function createSessionStorageAuthAdapter(
63
- storageKey: string = DEFAULT_SESSION_KEY,
64
- ): AuthAdapter {
65
- return {
66
- async login({ username, password }: LoginCredentials) {
67
- if (!username.trim() || !password) throw new Error("Invalid credentials");
68
- sessionStorage.setItem(storageKey, "1");
69
- },
70
- logout() {
71
- sessionStorage.removeItem(storageKey);
72
- },
73
- getToken() {
74
- return sessionStorage.getItem(storageKey);
75
- },
76
- };
77
- }
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useMemo,
6
+ useState,
7
+ type ReactNode,
8
+ } from "react";
9
+ import type { AuthAdapter, LoginCredentials } from "../types";
10
+
11
+ type AuthContextValue = {
12
+ isAuthenticated: boolean;
13
+ /** From `AuthAdapter.getUserLabel`; `"User"` when the adapter omits it or returns null. */
14
+ userLabel: string;
15
+ login: (credentials: LoginCredentials) => Promise<void>;
16
+ logout: () => void;
17
+ };
18
+
19
+ const AuthContext = createContext<AuthContextValue | null>(null);
20
+
21
+ export type AuthProviderProps = {
22
+ children: ReactNode;
23
+ /** How login, logout, and token reads are implemented (API, sessionStorage, etc.). */
24
+ adapter: AuthAdapter;
25
+ };
26
+
27
+ const DEFAULT_USER_LABEL = "User";
28
+
29
+ function readUserLabel(adapter: AuthAdapter): string {
30
+ return adapter.getUserLabel?.() ?? DEFAULT_USER_LABEL;
31
+ }
32
+
33
+ export function AuthProvider({ children, adapter }: AuthProviderProps) {
34
+ const [token, setToken] = useState<string | null>(() => adapter.getToken());
35
+ const [userLabel, setUserLabel] = useState(() => readUserLabel(adapter));
36
+
37
+ const login = useCallback(
38
+ async (credentials: LoginCredentials) => {
39
+ await adapter.login(credentials);
40
+ setToken(adapter.getToken());
41
+ setUserLabel(readUserLabel(adapter));
42
+ },
43
+ [adapter],
44
+ );
45
+
46
+ const logout = useCallback(() => {
47
+ adapter.logout();
48
+ setToken(adapter.getToken());
49
+ setUserLabel(readUserLabel(adapter));
50
+ }, [adapter]);
51
+
52
+ const value = useMemo<AuthContextValue>(
53
+ () => ({
54
+ isAuthenticated: Boolean(token),
55
+ userLabel,
56
+ login,
57
+ logout,
58
+ }),
59
+ [token, userLabel, login, logout],
60
+ );
61
+
62
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
63
+ }
64
+
65
+ export function useAuth() {
66
+ const ctx = useContext(AuthContext);
67
+ if (!ctx) throw new Error("useAuth must be used within AuthProvider");
68
+ return ctx;
69
+ }
70
+
71
+ const DEFAULT_SESSION_KEY = "ding-react-admin-auth";
72
+
73
+ /** Store a non-empty token string in `sessionStorage` when “logged in”. */
74
+ export function createSessionStorageAuthAdapter(
75
+ storageKey: string = DEFAULT_SESSION_KEY,
76
+ ): AuthAdapter {
77
+ return {
78
+ async login({ username, password }: LoginCredentials) {
79
+ if (!username.trim() || !password) throw new Error("Invalid credentials");
80
+ sessionStorage.setItem(storageKey, "1");
81
+ },
82
+ logout() {
83
+ sessionStorage.removeItem(storageKey);
84
+ },
85
+ getToken() {
86
+ return sessionStorage.getItem(storageKey);
87
+ },
88
+ };
89
+ }