ding-react-admin 1.0.4 → 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 (230) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +254 -201
  3. package/dist/index.css +2 -0
  4. package/dist/index.js +2073 -1202
  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/InlineFormSet.d.ts +2 -17
  18. package/dist/src/crud/InlineFormSet.d.ts.map +1 -1
  19. package/dist/src/crud/ResourceForm.d.ts +2 -10
  20. package/dist/src/crud/ResourceForm.d.ts.map +1 -1
  21. package/dist/src/crud/ResourceFormModal.d.ts +7 -1
  22. package/dist/src/crud/ResourceFormModal.d.ts.map +1 -1
  23. package/dist/src/crud/ResourceList.d.ts.map +1 -1
  24. package/dist/src/crud/ResourceRecordForm.d.ts +27 -0
  25. package/dist/src/crud/ResourceRecordForm.d.ts.map +1 -0
  26. package/dist/src/crud/columns/ImageColumn.d.ts +14 -0
  27. package/dist/src/crud/columns/ImageColumn.d.ts.map +1 -0
  28. package/dist/src/crud/context/InlineFieldsRegistry.d.ts +15 -0
  29. package/dist/src/crud/context/InlineFieldsRegistry.d.ts.map +1 -0
  30. package/dist/src/crud/context/PayloadFieldsContext.d.ts +18 -0
  31. package/dist/src/crud/context/PayloadFieldsContext.d.ts.map +1 -0
  32. package/dist/src/crud/fields/FileField.d.ts +12 -0
  33. package/dist/src/crud/fields/FileField.d.ts.map +1 -0
  34. package/dist/src/crud/fields/ImageField.d.ts +14 -0
  35. package/dist/src/crud/fields/ImageField.d.ts.map +1 -0
  36. package/dist/src/crud/fields/ReferenceField.d.ts +12 -2
  37. package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -1
  38. package/dist/src/crud/fields/ReferenceInputActions.d.ts +16 -0
  39. package/dist/src/crud/fields/ReferenceInputActions.d.ts.map +1 -0
  40. package/dist/src/crud/fields/ReferenceManyField.d.ts +10 -1
  41. package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -1
  42. package/dist/src/crud/fields/uploadFieldUtils.d.ts +5 -0
  43. package/dist/src/crud/fields/uploadFieldUtils.d.ts.map +1 -0
  44. package/dist/src/crud/fields/useUploadPreviewUrl.d.ts +4 -0
  45. package/dist/src/crud/fields/useUploadPreviewUrl.d.ts.map +1 -0
  46. package/dist/src/crud/filters/ReferenceFilter.d.ts +1 -1
  47. package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -1
  48. package/dist/src/crud/index.d.ts +27 -9
  49. package/dist/src/crud/index.d.ts.map +1 -1
  50. package/dist/src/crud/types.d.ts +21 -7
  51. package/dist/src/crud/types.d.ts.map +1 -1
  52. package/dist/src/crud/utils/buildFormPayload.d.ts +6 -0
  53. package/dist/src/crud/utils/buildFormPayload.d.ts.map +1 -0
  54. package/dist/src/crud/utils/buildInlineRowsPayload.d.ts +6 -0
  55. package/dist/src/crud/utils/buildInlineRowsPayload.d.ts.map +1 -0
  56. package/dist/src/crud/utils/buildResourceFormSubmitBody.d.ts +5 -0
  57. package/dist/src/crud/utils/buildResourceFormSubmitBody.d.ts.map +1 -0
  58. package/dist/src/crud/utils/choiceSelectionUtils.d.ts +10 -0
  59. package/dist/src/crud/utils/choiceSelectionUtils.d.ts.map +1 -0
  60. package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts +2 -0
  61. package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts.map +1 -0
  62. package/dist/src/crud/utils/formErrors.d.ts +18 -7
  63. package/dist/src/crud/utils/formErrors.d.ts.map +1 -1
  64. package/dist/src/crud/utils/formErrors.test.d.ts +2 -0
  65. package/dist/src/crud/utils/formErrors.test.d.ts.map +1 -0
  66. package/dist/src/crud/utils/getFormValue.d.ts +3 -0
  67. package/dist/src/crud/utils/getFormValue.d.ts.map +1 -0
  68. package/dist/src/crud/utils/hasUploadValues.d.ts +3 -0
  69. package/dist/src/crud/utils/hasUploadValues.d.ts.map +1 -0
  70. package/dist/src/crud/utils/nestedFieldPath.d.ts +3 -0
  71. package/dist/src/crud/utils/nestedFieldPath.d.ts.map +1 -0
  72. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts +8 -0
  73. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts.map +1 -0
  74. package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts +4 -0
  75. package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts.map +1 -0
  76. package/dist/src/crud/utils/setFormValue.d.ts +3 -0
  77. package/dist/src/crud/utils/setFormValue.d.ts.map +1 -0
  78. package/dist/src/crud/utils/toFormData.d.ts +13 -0
  79. package/dist/src/crud/utils/toFormData.d.ts.map +1 -0
  80. package/dist/src/crud/utils/uploadReferenceUtils.d.ts +5 -0
  81. package/dist/src/crud/utils/uploadReferenceUtils.d.ts.map +1 -0
  82. package/dist/src/crud/utils/useChoices.d.ts +27 -1
  83. package/dist/src/crud/utils/useChoices.d.ts.map +1 -1
  84. package/dist/src/crud/utils/useFormRecord.d.ts +44 -0
  85. package/dist/src/crud/utils/useFormRecord.d.ts.map +1 -0
  86. package/dist/src/data/createRestResourceHandlers.d.ts +5 -5
  87. package/dist/src/data/createRestResourceHandlers.d.ts.map +1 -1
  88. package/dist/src/data/dataProviderTypes.d.ts +6 -5
  89. package/dist/src/data/dataProviderTypes.d.ts.map +1 -1
  90. package/dist/src/data/parseFormErrorHelpers.d.ts +23 -6
  91. package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -1
  92. package/dist/src/data/resourceHandlers.d.ts +1 -1
  93. package/dist/src/data/resourceHandlers.d.ts.map +1 -1
  94. package/dist/src/index.d.ts +4 -4
  95. package/dist/src/index.d.ts.map +1 -1
  96. package/dist/src/layouts/AdminLayout.d.ts +1 -1
  97. package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
  98. package/dist/src/layouts/AuthPageLayout.d.ts.map +1 -1
  99. package/dist/src/layouts/navFilter.d.ts +5 -0
  100. package/dist/src/layouts/navFilter.d.ts.map +1 -0
  101. package/dist/src/layouts/navMenuItems.d.ts +6 -0
  102. package/dist/src/layouts/navMenuItems.d.ts.map +1 -0
  103. package/dist/src/types.d.ts +12 -0
  104. package/dist/src/types.d.ts.map +1 -1
  105. package/package.json +79 -59
  106. package/src/app/AdminApp.tsx +34 -34
  107. package/src/components/AuthAlternateLink.tsx +23 -23
  108. package/src/components/DensitySwitch.tsx +31 -31
  109. package/src/components/NavMenuLabel.tsx +26 -0
  110. package/src/components/NavMenuSearch.tsx +76 -0
  111. package/src/components/ScrollableArea.tsx +46 -0
  112. package/src/components/ThemeSwitch.tsx +28 -28
  113. package/src/components/ThemeToolbar.tsx +34 -34
  114. package/src/components/index.ts +7 -4
  115. package/src/components/navMenu.css +21 -0
  116. package/src/components/scrollable.css +28 -0
  117. package/src/context/AppThemeProvider.tsx +128 -128
  118. package/src/context/AuthProvider.tsx +89 -77
  119. package/src/context/DataProvider.tsx +30 -30
  120. package/src/context/PermissionsProvider.tsx +57 -57
  121. package/src/crud/FilterBar.tsx +36 -36
  122. package/src/crud/FormGlobalErrorsAlert.tsx +36 -0
  123. package/src/crud/FormSteps.tsx +135 -135
  124. package/src/crud/FormTabs.tsx +126 -126
  125. package/src/crud/InlineFormSet.tsx +162 -219
  126. package/src/crud/ListActionsBar.tsx +106 -106
  127. package/src/crud/ResourceForm.tsx +83 -166
  128. package/src/crud/ResourceFormModal.tsx +75 -133
  129. package/src/crud/ResourceList.tsx +1 -0
  130. package/src/crud/ResourceRecordForm.tsx +166 -0
  131. package/src/crud/columns/BooleanColumn.tsx +32 -32
  132. package/src/crud/columns/CustomColumn.tsx +33 -33
  133. package/src/crud/columns/DateColumn.tsx +28 -28
  134. package/src/crud/columns/ImageColumn.tsx +58 -0
  135. package/src/crud/columns/NumberColumn.tsx +31 -31
  136. package/src/crud/columns/ReferenceColumn.tsx +85 -85
  137. package/src/crud/columns/ReferenceManyColumn.tsx +67 -67
  138. package/src/crud/columns/TextColumn.tsx +38 -38
  139. package/src/crud/context/FilterContext.tsx +71 -71
  140. package/src/crud/context/FormContext.tsx +39 -39
  141. package/src/crud/context/FormSectionContext.tsx +22 -22
  142. package/src/crud/context/InlineFieldsRegistry.tsx +61 -0
  143. package/src/crud/context/ListContext.tsx +93 -93
  144. package/src/crud/context/PayloadFieldsContext.tsx +62 -0
  145. package/src/crud/fields/BooleanField.tsx +38 -38
  146. package/src/crud/fields/DateField.tsx +53 -53
  147. package/src/crud/fields/FieldWrapper.tsx +87 -87
  148. package/src/crud/fields/FileField.tsx +123 -0
  149. package/src/crud/fields/ImageField.tsx +120 -0
  150. package/src/crud/fields/NumberField.tsx +52 -52
  151. package/src/crud/fields/PasswordField.tsx +99 -99
  152. package/src/crud/fields/ReferenceField.tsx +237 -96
  153. package/src/crud/fields/ReferenceInputActions.tsx +106 -0
  154. package/src/crud/fields/ReferenceManyField.tsx +200 -76
  155. package/src/crud/fields/SelectField.tsx +48 -48
  156. package/src/crud/fields/TextField.tsx +46 -46
  157. package/src/crud/fields/uploadFieldUtils.ts +21 -0
  158. package/src/crud/fields/useUploadPreviewUrl.ts +20 -0
  159. package/src/crud/filters/BooleanFilter.tsx +38 -38
  160. package/src/crud/filters/DateFilter.tsx +35 -35
  161. package/src/crud/filters/NumberFilter.tsx +33 -33
  162. package/src/crud/filters/ReferenceFilter.tsx +192 -173
  163. package/src/crud/filters/SelectFilter.tsx +44 -44
  164. package/src/crud/filters/TextFilter.tsx +36 -36
  165. package/src/crud/index.ts +99 -90
  166. package/src/crud/types.ts +146 -132
  167. package/src/crud/utils/buildFormPayload.ts +24 -0
  168. package/src/crud/utils/buildInlineRowsPayload.ts +37 -0
  169. package/src/crud/utils/buildResourceFormSubmitBody.ts +29 -0
  170. package/src/crud/utils/choiceSelectionUtils.test.ts +45 -0
  171. package/src/crud/utils/choiceSelectionUtils.ts +116 -0
  172. package/src/crud/utils/formErrors.test.ts +168 -0
  173. package/src/crud/utils/formErrors.ts +138 -38
  174. package/src/crud/utils/formSectionErrors.ts +85 -85
  175. package/src/crud/utils/{getByPath.ts → getFormValue.ts} +13 -13
  176. package/src/crud/utils/hasUploadValues.ts +9 -0
  177. package/src/crud/utils/nestedFieldPath.ts +8 -0
  178. package/src/crud/utils/prepareFormSubmitBody.ts +22 -0
  179. package/src/crud/utils/referenceSelectNotFoundContent.tsx +7 -0
  180. package/src/crud/utils/{setByPath.ts → setFormValue.ts} +18 -18
  181. package/src/crud/utils/sortQueryParam.ts +37 -37
  182. package/src/crud/utils/toFormData.ts +79 -0
  183. package/src/crud/utils/uploadReferenceUtils.ts +44 -0
  184. package/src/crud/utils/useAbortableEffect.ts +17 -17
  185. package/src/crud/utils/useChoices.ts +330 -182
  186. package/src/crud/utils/useFormRecord.ts +179 -0
  187. package/src/crud/utils/useListQueryState.ts +191 -191
  188. package/src/data/abortError.test.ts +19 -19
  189. package/src/data/abortError.ts +10 -10
  190. package/src/data/createMemoryResourceHandlers.ts +78 -78
  191. package/src/data/createRestResourceHandlers.ts +63 -57
  192. package/src/data/dataProviderTypes.ts +7 -5
  193. package/src/data/dataUtils.test.ts +228 -108
  194. package/src/data/inMemoryList.ts +97 -97
  195. package/src/data/parseFormErrorHelpers.test.ts +201 -84
  196. package/src/data/parseFormErrorHelpers.ts +376 -224
  197. package/src/data/resourceHandlers.ts +1 -1
  198. package/src/data/sortHelpers.ts +48 -48
  199. package/src/index.ts +211 -195
  200. package/src/layouts/AdminLayout.tsx +530 -381
  201. package/src/layouts/AuthPageLayout.tsx +87 -81
  202. package/src/layouts/navFilter.ts +49 -0
  203. package/src/layouts/navMenuItems.tsx +44 -0
  204. package/src/pages/LoginPage.tsx +80 -80
  205. package/src/pages/PlaceholderPage.tsx +9 -9
  206. package/src/permissions/resourcePermissions.ts +47 -47
  207. package/src/router/createAdminRouter.tsx +64 -64
  208. package/src/router/guards.tsx +63 -63
  209. package/src/router/routeAccess.ts +89 -89
  210. package/src/types.ts +10 -0
  211. package/src/vite-env.d.ts +1 -1
  212. package/dist/src/crud/context/SubmitFieldsContext.d.ts +0 -18
  213. package/dist/src/crud/context/SubmitFieldsContext.d.ts.map +0 -1
  214. package/dist/src/crud/utils/getByPath.d.ts +0 -3
  215. package/dist/src/crud/utils/getByPath.d.ts.map +0 -1
  216. package/dist/src/crud/utils/inlineArrayName.d.ts +0 -3
  217. package/dist/src/crud/utils/inlineArrayName.d.ts.map +0 -1
  218. package/dist/src/crud/utils/inlineFieldName.d.ts +0 -3
  219. package/dist/src/crud/utils/inlineFieldName.d.ts.map +0 -1
  220. package/dist/src/crud/utils/pickBySources.d.ts +0 -6
  221. package/dist/src/crud/utils/pickBySources.d.ts.map +0 -1
  222. package/dist/src/crud/utils/setByPath.d.ts +0 -3
  223. package/dist/src/crud/utils/setByPath.d.ts.map +0 -1
  224. package/dist/src/crud/utils/useResourceFormData.d.ts +0 -47
  225. package/dist/src/crud/utils/useResourceFormData.d.ts.map +0 -1
  226. package/src/crud/context/SubmitFieldsContext.tsx +0 -62
  227. package/src/crud/utils/inlineArrayName.ts +0 -4
  228. package/src/crud/utils/inlineFieldName.ts +0 -8
  229. package/src/crud/utils/pickBySources.ts +0 -24
  230. package/src/crud/utils/useResourceFormData.ts +0 -231
@@ -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
+ }
@@ -1,30 +1,30 @@
1
- import {
2
- createContext,
3
- useContext,
4
- useMemo,
5
- type ReactNode,
6
- } from "react";
7
- import type { DataProvider as DataProviderContract } from "../data/dataProviderTypes";
8
-
9
- const DataContext = createContext<DataProviderContract | null>(null);
10
-
11
- export type DataProviderProps = {
12
- children: ReactNode;
13
- /** Your API client (REST adapter, in-memory mock, etc.). */
14
- value: DataProviderContract;
15
- };
16
-
17
- export function DataProvider({ children, value }: DataProviderProps) {
18
- const memo = useMemo(() => value, [value]);
19
- return (
20
- <DataContext.Provider value={memo}>{children}</DataContext.Provider>
21
- );
22
- }
23
-
24
- export function useDataProvider() {
25
- const ctx = useContext(DataContext);
26
- if (!ctx) {
27
- throw new Error("useDataProvider must be used within DataProvider");
28
- }
29
- return ctx;
30
- }
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useMemo,
5
+ type ReactNode,
6
+ } from "react";
7
+ import type { DataProvider as DataProviderContract } from "../data/dataProviderTypes";
8
+
9
+ const DataContext = createContext<DataProviderContract | null>(null);
10
+
11
+ export type DataProviderProps = {
12
+ children: ReactNode;
13
+ /** Your API client (REST adapter, in-memory mock, etc.). */
14
+ value: DataProviderContract;
15
+ };
16
+
17
+ export function DataProvider({ children, value }: DataProviderProps) {
18
+ const memo = useMemo(() => value, [value]);
19
+ return (
20
+ <DataContext.Provider value={memo}>{children}</DataContext.Provider>
21
+ );
22
+ }
23
+
24
+ export function useDataProvider() {
25
+ const ctx = useContext(DataContext);
26
+ if (!ctx) {
27
+ throw new Error("useDataProvider must be used within DataProvider");
28
+ }
29
+ return ctx;
30
+ }