ding-react-admin 1.0.0

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 (237) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +3067 -0
  5. package/dist/src/app/AdminApp.d.ts +7 -0
  6. package/dist/src/app/AdminApp.d.ts.map +1 -0
  7. package/dist/src/components/AuthAlternateLink.d.ts +7 -0
  8. package/dist/src/components/AuthAlternateLink.d.ts.map +1 -0
  9. package/dist/src/components/DensitySwitch.d.ts +2 -0
  10. package/dist/src/components/DensitySwitch.d.ts.map +1 -0
  11. package/dist/src/components/ThemeSwitch.d.ts +2 -0
  12. package/dist/src/components/ThemeSwitch.d.ts.map +1 -0
  13. package/dist/src/components/ThemeToolbar.d.ts +2 -0
  14. package/dist/src/components/ThemeToolbar.d.ts.map +1 -0
  15. package/dist/src/components/index.d.ts +5 -0
  16. package/dist/src/components/index.d.ts.map +1 -0
  17. package/dist/src/context/AppThemeProvider.d.ts +13 -0
  18. package/dist/src/context/AppThemeProvider.d.ts.map +1 -0
  19. package/dist/src/context/AuthProvider.d.ts +18 -0
  20. package/dist/src/context/AuthProvider.d.ts.map +1 -0
  21. package/dist/src/context/DataProvider.d.ts +10 -0
  22. package/dist/src/context/DataProvider.d.ts.map +1 -0
  23. package/dist/src/context/PermissionsProvider.d.ts +18 -0
  24. package/dist/src/context/PermissionsProvider.d.ts.map +1 -0
  25. package/dist/src/crud/FilterBar.d.ts +6 -0
  26. package/dist/src/crud/FilterBar.d.ts.map +1 -0
  27. package/dist/src/crud/FormSteps.d.ts +20 -0
  28. package/dist/src/crud/FormSteps.d.ts.map +1 -0
  29. package/dist/src/crud/FormTabs.d.ts +13 -0
  30. package/dist/src/crud/FormTabs.d.ts.map +1 -0
  31. package/dist/src/crud/InlineFormSet.d.ts +29 -0
  32. package/dist/src/crud/InlineFormSet.d.ts.map +1 -0
  33. package/dist/src/crud/ListActionsBar.d.ts +15 -0
  34. package/dist/src/crud/ListActionsBar.d.ts.map +1 -0
  35. package/dist/src/crud/ResourceForm.d.ts +21 -0
  36. package/dist/src/crud/ResourceForm.d.ts.map +1 -0
  37. package/dist/src/crud/ResourceFormModal.d.ts +10 -0
  38. package/dist/src/crud/ResourceFormModal.d.ts.map +1 -0
  39. package/dist/src/crud/ResourceList.d.ts +38 -0
  40. package/dist/src/crud/ResourceList.d.ts.map +1 -0
  41. package/dist/src/crud/columns/BooleanColumn.d.ts +6 -0
  42. package/dist/src/crud/columns/BooleanColumn.d.ts.map +1 -0
  43. package/dist/src/crud/columns/CustomColumn.d.ts +9 -0
  44. package/dist/src/crud/columns/CustomColumn.d.ts.map +1 -0
  45. package/dist/src/crud/columns/DateColumn.d.ts +6 -0
  46. package/dist/src/crud/columns/DateColumn.d.ts.map +1 -0
  47. package/dist/src/crud/columns/NumberColumn.d.ts +6 -0
  48. package/dist/src/crud/columns/NumberColumn.d.ts.map +1 -0
  49. package/dist/src/crud/columns/ReferenceColumn.d.ts +6 -0
  50. package/dist/src/crud/columns/ReferenceColumn.d.ts.map +1 -0
  51. package/dist/src/crud/columns/ReferenceManyColumn.d.ts +6 -0
  52. package/dist/src/crud/columns/ReferenceManyColumn.d.ts.map +1 -0
  53. package/dist/src/crud/columns/TextColumn.d.ts +7 -0
  54. package/dist/src/crud/columns/TextColumn.d.ts.map +1 -0
  55. package/dist/src/crud/context/FilterContext.d.ts +17 -0
  56. package/dist/src/crud/context/FilterContext.d.ts.map +1 -0
  57. package/dist/src/crud/context/FormContext.d.ts +16 -0
  58. package/dist/src/crud/context/FormContext.d.ts.map +1 -0
  59. package/dist/src/crud/context/FormFieldsContext.d.ts +14 -0
  60. package/dist/src/crud/context/FormFieldsContext.d.ts.map +1 -0
  61. package/dist/src/crud/context/FormSectionContext.d.ts +7 -0
  62. package/dist/src/crud/context/FormSectionContext.d.ts.map +1 -0
  63. package/dist/src/crud/context/InlineFormContext.d.ts +16 -0
  64. package/dist/src/crud/context/InlineFormContext.d.ts.map +1 -0
  65. package/dist/src/crud/context/ListContext.d.ts +22 -0
  66. package/dist/src/crud/context/ListContext.d.ts.map +1 -0
  67. package/dist/src/crud/fields/BooleanField.d.ts +7 -0
  68. package/dist/src/crud/fields/BooleanField.d.ts.map +1 -0
  69. package/dist/src/crud/fields/DateField.d.ts +8 -0
  70. package/dist/src/crud/fields/DateField.d.ts.map +1 -0
  71. package/dist/src/crud/fields/FieldWrapper.d.ts +29 -0
  72. package/dist/src/crud/fields/FieldWrapper.d.ts.map +1 -0
  73. package/dist/src/crud/fields/NumberField.d.ts +12 -0
  74. package/dist/src/crud/fields/NumberField.d.ts.map +1 -0
  75. package/dist/src/crud/fields/PasswordField.d.ts +11 -0
  76. package/dist/src/crud/fields/PasswordField.d.ts.map +1 -0
  77. package/dist/src/crud/fields/ReferenceField.d.ts +18 -0
  78. package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -0
  79. package/dist/src/crud/fields/ReferenceManyField.d.ts +9 -0
  80. package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -0
  81. package/dist/src/crud/fields/SelectField.d.ts +10 -0
  82. package/dist/src/crud/fields/SelectField.d.ts.map +1 -0
  83. package/dist/src/crud/fields/TextField.d.ts +10 -0
  84. package/dist/src/crud/fields/TextField.d.ts.map +1 -0
  85. package/dist/src/crud/fields/useInlineOrFormField.d.ts +10 -0
  86. package/dist/src/crud/fields/useInlineOrFormField.d.ts.map +1 -0
  87. package/dist/src/crud/filters/BooleanFilter.d.ts +4 -0
  88. package/dist/src/crud/filters/BooleanFilter.d.ts.map +1 -0
  89. package/dist/src/crud/filters/DateFilter.d.ts +4 -0
  90. package/dist/src/crud/filters/DateFilter.d.ts.map +1 -0
  91. package/dist/src/crud/filters/NumberFilter.d.ts +4 -0
  92. package/dist/src/crud/filters/NumberFilter.d.ts.map +1 -0
  93. package/dist/src/crud/filters/ReferenceFilter.d.ts +13 -0
  94. package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -0
  95. package/dist/src/crud/filters/SelectFilter.d.ts +7 -0
  96. package/dist/src/crud/filters/SelectFilter.d.ts.map +1 -0
  97. package/dist/src/crud/filters/TextFilter.d.ts +6 -0
  98. package/dist/src/crud/filters/TextFilter.d.ts.map +1 -0
  99. package/dist/src/crud/index.d.ts +44 -0
  100. package/dist/src/crud/index.d.ts.map +1 -0
  101. package/dist/src/crud/types.d.ts +99 -0
  102. package/dist/src/crud/types.d.ts.map +1 -0
  103. package/dist/src/crud/utils/formErrors.d.ts +9 -0
  104. package/dist/src/crud/utils/formErrors.d.ts.map +1 -0
  105. package/dist/src/crud/utils/formSectionErrors.d.ts +6 -0
  106. package/dist/src/crud/utils/formSectionErrors.d.ts.map +1 -0
  107. package/dist/src/crud/utils/getByPath.d.ts +3 -0
  108. package/dist/src/crud/utils/getByPath.d.ts.map +1 -0
  109. package/dist/src/crud/utils/inlineArrayName.d.ts +3 -0
  110. package/dist/src/crud/utils/inlineArrayName.d.ts.map +1 -0
  111. package/dist/src/crud/utils/pickBySources.d.ts +6 -0
  112. package/dist/src/crud/utils/pickBySources.d.ts.map +1 -0
  113. package/dist/src/crud/utils/setByPath.d.ts +3 -0
  114. package/dist/src/crud/utils/setByPath.d.ts.map +1 -0
  115. package/dist/src/crud/utils/sortQueryParam.d.ts +7 -0
  116. package/dist/src/crud/utils/sortQueryParam.d.ts.map +1 -0
  117. package/dist/src/crud/utils/useChoices.d.ts +10 -0
  118. package/dist/src/crud/utils/useChoices.d.ts.map +1 -0
  119. package/dist/src/crud/utils/useListQueryState.d.ts +22 -0
  120. package/dist/src/crud/utils/useListQueryState.d.ts.map +1 -0
  121. package/dist/src/data/createMemoryResourceHandlers.d.ts +23 -0
  122. package/dist/src/data/createMemoryResourceHandlers.d.ts.map +1 -0
  123. package/dist/src/data/createRestResourceHandlers.d.ts +22 -0
  124. package/dist/src/data/createRestResourceHandlers.d.ts.map +1 -0
  125. package/dist/src/data/dataProviderTypes.d.ts +67 -0
  126. package/dist/src/data/dataProviderTypes.d.ts.map +1 -0
  127. package/dist/src/data/dataUtils.test.d.ts +2 -0
  128. package/dist/src/data/dataUtils.test.d.ts.map +1 -0
  129. package/dist/src/data/inMemoryList.d.ts +12 -0
  130. package/dist/src/data/inMemoryList.d.ts.map +1 -0
  131. package/dist/src/data/parseFormErrorHelpers.d.ts +35 -0
  132. package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -0
  133. package/dist/src/data/parseFormErrorHelpers.test.d.ts +2 -0
  134. package/dist/src/data/parseFormErrorHelpers.test.d.ts.map +1 -0
  135. package/dist/src/data/resourceHandlers.d.ts +33 -0
  136. package/dist/src/data/resourceHandlers.d.ts.map +1 -0
  137. package/dist/src/data/sortHelpers.d.ts +17 -0
  138. package/dist/src/data/sortHelpers.d.ts.map +1 -0
  139. package/dist/src/index.d.ts +31 -0
  140. package/dist/src/index.d.ts.map +1 -0
  141. package/dist/src/layouts/AdminLayout.d.ts +3 -0
  142. package/dist/src/layouts/AdminLayout.d.ts.map +1 -0
  143. package/dist/src/layouts/AuthPageLayout.d.ts +11 -0
  144. package/dist/src/layouts/AuthPageLayout.d.ts.map +1 -0
  145. package/dist/src/pages/LoginPage.d.ts +3 -0
  146. package/dist/src/pages/LoginPage.d.ts.map +1 -0
  147. package/dist/src/pages/PlaceholderPage.d.ts +4 -0
  148. package/dist/src/pages/PlaceholderPage.d.ts.map +1 -0
  149. package/dist/src/permissions/resourcePermissions.d.ts +22 -0
  150. package/dist/src/permissions/resourcePermissions.d.ts.map +1 -0
  151. package/dist/src/router/createAdminRouter.d.ts +3 -0
  152. package/dist/src/router/createAdminRouter.d.ts.map +1 -0
  153. package/dist/src/router/guards.d.ts +20 -0
  154. package/dist/src/router/guards.d.ts.map +1 -0
  155. package/dist/src/router/routeAccess.d.ts +21 -0
  156. package/dist/src/router/routeAccess.d.ts.map +1 -0
  157. package/dist/src/types.d.ts +123 -0
  158. package/dist/src/types.d.ts.map +1 -0
  159. package/package.json +58 -0
  160. package/src/app/AdminApp.tsx +34 -0
  161. package/src/components/AuthAlternateLink.tsx +23 -0
  162. package/src/components/DensitySwitch.tsx +31 -0
  163. package/src/components/ThemeSwitch.tsx +28 -0
  164. package/src/components/ThemeToolbar.tsx +34 -0
  165. package/src/components/index.ts +4 -0
  166. package/src/context/AppThemeProvider.tsx +128 -0
  167. package/src/context/AuthProvider.tsx +77 -0
  168. package/src/context/DataProvider.tsx +30 -0
  169. package/src/context/PermissionsProvider.tsx +57 -0
  170. package/src/crud/FilterBar.tsx +36 -0
  171. package/src/crud/FormSteps.tsx +135 -0
  172. package/src/crud/FormTabs.tsx +126 -0
  173. package/src/crud/InlineFormSet.tsx +231 -0
  174. package/src/crud/ListActionsBar.tsx +106 -0
  175. package/src/crud/ResourceForm.tsx +281 -0
  176. package/src/crud/ResourceFormModal.tsx +121 -0
  177. package/src/crud/ResourceList.tsx +633 -0
  178. package/src/crud/columns/BooleanColumn.tsx +32 -0
  179. package/src/crud/columns/CustomColumn.tsx +33 -0
  180. package/src/crud/columns/DateColumn.tsx +28 -0
  181. package/src/crud/columns/NumberColumn.tsx +31 -0
  182. package/src/crud/columns/ReferenceColumn.tsx +85 -0
  183. package/src/crud/columns/ReferenceManyColumn.tsx +67 -0
  184. package/src/crud/columns/TextColumn.tsx +38 -0
  185. package/src/crud/context/FilterContext.tsx +71 -0
  186. package/src/crud/context/FormContext.tsx +39 -0
  187. package/src/crud/context/FormFieldsContext.tsx +76 -0
  188. package/src/crud/context/FormSectionContext.tsx +22 -0
  189. package/src/crud/context/InlineFormContext.tsx +71 -0
  190. package/src/crud/context/ListContext.tsx +93 -0
  191. package/src/crud/fields/BooleanField.tsx +32 -0
  192. package/src/crud/fields/DateField.tsx +47 -0
  193. package/src/crud/fields/FieldWrapper.tsx +109 -0
  194. package/src/crud/fields/NumberField.tsx +50 -0
  195. package/src/crud/fields/PasswordField.tsx +98 -0
  196. package/src/crud/fields/ReferenceField.tsx +191 -0
  197. package/src/crud/fields/ReferenceManyField.tsx +70 -0
  198. package/src/crud/fields/SelectField.tsx +42 -0
  199. package/src/crud/fields/TextField.tsx +44 -0
  200. package/src/crud/fields/useInlineOrFormField.tsx +91 -0
  201. package/src/crud/filters/BooleanFilter.tsx +38 -0
  202. package/src/crud/filters/DateFilter.tsx +35 -0
  203. package/src/crud/filters/NumberFilter.tsx +33 -0
  204. package/src/crud/filters/ReferenceFilter.tsx +173 -0
  205. package/src/crud/filters/SelectFilter.tsx +44 -0
  206. package/src/crud/filters/TextFilter.tsx +36 -0
  207. package/src/crud/index.ts +73 -0
  208. package/src/crud/types.ts +130 -0
  209. package/src/crud/utils/formErrors.ts +38 -0
  210. package/src/crud/utils/formSectionErrors.ts +85 -0
  211. package/src/crud/utils/getByPath.ts +13 -0
  212. package/src/crud/utils/inlineArrayName.ts +4 -0
  213. package/src/crud/utils/pickBySources.ts +24 -0
  214. package/src/crud/utils/setByPath.ts +18 -0
  215. package/src/crud/utils/sortQueryParam.ts +37 -0
  216. package/src/crud/utils/useChoices.ts +182 -0
  217. package/src/crud/utils/useListQueryState.ts +191 -0
  218. package/src/data/createMemoryResourceHandlers.ts +78 -0
  219. package/src/data/createRestResourceHandlers.ts +57 -0
  220. package/src/data/dataProviderTypes.ts +101 -0
  221. package/src/data/dataUtils.test.ts +108 -0
  222. package/src/data/inMemoryList.ts +97 -0
  223. package/src/data/parseFormErrorHelpers.test.ts +84 -0
  224. package/src/data/parseFormErrorHelpers.ts +224 -0
  225. package/src/data/resourceHandlers.ts +124 -0
  226. package/src/data/sortHelpers.ts +48 -0
  227. package/src/index.ts +182 -0
  228. package/src/layouts/AdminLayout.tsx +381 -0
  229. package/src/layouts/AuthPageLayout.tsx +81 -0
  230. package/src/pages/LoginPage.tsx +80 -0
  231. package/src/pages/PlaceholderPage.tsx +9 -0
  232. package/src/permissions/resourcePermissions.ts +47 -0
  233. package/src/router/createAdminRouter.tsx +64 -0
  234. package/src/router/guards.tsx +63 -0
  235. package/src/router/routeAccess.ts +89 -0
  236. package/src/types.ts +135 -0
  237. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,23 @@
1
+ import { Typography } from "antd";
2
+ import { Link } from "react-router-dom";
3
+
4
+ export type AuthAlternateLinkProps = {
5
+ prompt: string;
6
+ linkText: string;
7
+ to: string;
8
+ };
9
+
10
+ export function AuthAlternateLink({
11
+ prompt,
12
+ linkText,
13
+ to,
14
+ }: AuthAlternateLinkProps) {
15
+ return (
16
+ <Typography.Paragraph
17
+ type="secondary"
18
+ style={{ textAlign: "center", marginBottom: 0 }}
19
+ >
20
+ {prompt} <Link to={to}>{linkText}</Link>
21
+ </Typography.Paragraph>
22
+ );
23
+ }
@@ -0,0 +1,31 @@
1
+ import { ColumnHeightOutlined, LayoutOutlined } from "@ant-design/icons";
2
+ import { Segmented } from "antd";
3
+ import type { SegmentedProps } from "antd";
4
+ import { useThemeMode } from "../context/AppThemeProvider";
5
+ import type { ThemeDensity } from "../types";
6
+
7
+ const OPTIONS: SegmentedProps["options"] = [
8
+ {
9
+ label: "Comfortable",
10
+ value: "comfortable",
11
+ icon: <LayoutOutlined />,
12
+ },
13
+ {
14
+ label: "Compact",
15
+ value: "compact",
16
+ icon: <ColumnHeightOutlined />,
17
+ },
18
+ ];
19
+
20
+ export function DensitySwitch() {
21
+ const { density, setDensity } = useThemeMode();
22
+
23
+ return (
24
+ <Segmented
25
+ size="small"
26
+ value={density}
27
+ options={OPTIONS}
28
+ onChange={(v) => setDensity(v as ThemeDensity)}
29
+ />
30
+ );
31
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +1,4 @@
1
+ export { AuthAlternateLink } from "./AuthAlternateLink";
2
+ export { ThemeToolbar } from "./ThemeToolbar";
3
+ export { ThemeSwitch } from "./ThemeSwitch";
4
+ export { DensitySwitch } from "./DensitySwitch";
@@ -0,0 +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
+ }
@@ -0,0 +1,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
+ 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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +1,57 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useMemo,
6
+ type ReactNode,
7
+ } from "react";
8
+
9
+ /**
10
+ * Check whether the current user has a permission string from your backend.
11
+ * Example: `can("users.create")` or `can("main.add_user")`.
12
+ */
13
+ export type PermissionsChecker = (permission: string) => boolean;
14
+
15
+ const PermissionsContext = createContext<PermissionsChecker | null>(null);
16
+
17
+ export type PermissionsProviderProps = {
18
+ children: ReactNode;
19
+ /** Return true if the current user has this permission string. */
20
+ can: PermissionsChecker;
21
+ };
22
+
23
+ export function PermissionsProvider({
24
+ children,
25
+ can,
26
+ }: PermissionsProviderProps) {
27
+ const memo = useMemo(() => can, [can]);
28
+ return (
29
+ <PermissionsContext.Provider value={memo}>
30
+ {children}
31
+ </PermissionsContext.Provider>
32
+ );
33
+ }
34
+
35
+ export function usePermissions(): PermissionsChecker {
36
+ const ctx = useContext(PermissionsContext);
37
+ if (!ctx) {
38
+ throw new Error(
39
+ "usePermissions must be used within PermissionsProvider",
40
+ );
41
+ }
42
+ return ctx;
43
+ }
44
+
45
+ /** Build a `can(permission)` checker from the user's permission strings (e.g. from login). */
46
+ export function createPermissionsChecker(
47
+ getPermissions: () => string[] | undefined,
48
+ ): PermissionsChecker {
49
+ return (permission: string) =>
50
+ getPermissions()?.includes(permission) ?? false;
51
+ }
52
+
53
+ /** Convenience: `const allowed = useCan("users.create")();` */
54
+ export function useCan(permission: string) {
55
+ const can = usePermissions();
56
+ return useCallback(() => can(permission), [can, permission]);
57
+ }
@@ -0,0 +1,36 @@
1
+ import { Space, Typography } from "antd";
2
+ import type { ReactNode } from "react";
3
+ import { useFilterContext } from "./context/FilterContext";
4
+
5
+ function FilterBarUI() {
6
+ const ctx = useFilterContext();
7
+ if (!ctx || ctx.filters.length === 0) return null;
8
+
9
+ return (
10
+ <Space wrap size="middle" style={{ marginBottom: 16 }}>
11
+ {ctx.filters.map((f) => (
12
+ <Space key={f.key} orientation="vertical" size={2}>
13
+ {f.label ? (
14
+ <Typography.Text type="secondary" style={{ fontSize: 12 }}>
15
+ {f.label}
16
+ </Typography.Text>
17
+ ) : null}
18
+ {f.render({
19
+ value: ctx.values[f.source],
20
+ onChange: (v) => ctx.setFilterValue(f.source, v),
21
+ })}
22
+ </Space>
23
+ ))}
24
+ </Space>
25
+ );
26
+ }
27
+
28
+ /** Wrap filter field components; renders filter controls above the table. */
29
+ export function FilterBar({ children }: { children: ReactNode }) {
30
+ return (
31
+ <>
32
+ {children}
33
+ <FilterBarUI />
34
+ </>
35
+ );
36
+ }
@@ -0,0 +1,135 @@
1
+ import { Button, Space, Steps, type StepsProps } from "antd";
2
+ import {
3
+ Children,
4
+ isValidElement,
5
+ useCallback,
6
+ useMemo,
7
+ useState,
8
+ type CSSProperties,
9
+ type ReactElement,
10
+ type ReactNode,
11
+ } from "react";
12
+ import { useFormContext, useFormState } from "react-hook-form";
13
+ import { FormSectionSourcesProvider } from "./context/FormSectionContext";
14
+ import {
15
+ sectionHasError,
16
+ useActivateFirstErrorSection,
17
+ useSectionSourceRefs,
18
+ } from "./utils/formSectionErrors";
19
+
20
+ export type FormStepProps = {
21
+ title: ReactNode;
22
+ description?: ReactNode;
23
+ children: ReactNode;
24
+ };
25
+
26
+ export function FormStep(_props: FormStepProps) {
27
+ return null;
28
+ }
29
+
30
+ function isFormStep(child: ReactNode): child is ReactElement<FormStepProps> {
31
+ return isValidElement(child) && child.type === FormStep;
32
+ }
33
+
34
+ export type FormStepsProps = {
35
+ children: ReactNode;
36
+ initialStep?: number;
37
+ /** Show Previous / Next controls below the active step. Default true. */
38
+ showNavigation?: boolean;
39
+ /** Allow clicking step headers to change the active step. Default false. */
40
+ allowStepSelect?: boolean;
41
+ stepsStyle?: CSSProperties;
42
+ navigationStyle?: CSSProperties;
43
+ } & Partial<Pick<StepsProps, "size" | "direction" | "type" | "status">>;
44
+
45
+ export function FormSteps({
46
+ children,
47
+ initialStep = 0,
48
+ showNavigation = true,
49
+ allowStepSelect = false,
50
+ stepsStyle,
51
+ navigationStyle,
52
+ size,
53
+ direction,
54
+ type,
55
+ status,
56
+ }: FormStepsProps) {
57
+ const steps = useMemo(
58
+ () => Children.toArray(children).filter(isFormStep),
59
+ [children],
60
+ );
61
+ const sectionRefs = useSectionSourceRefs(steps.length);
62
+ const [current, setCurrent] = useState(initialStep);
63
+ const lastIndex = steps.length - 1;
64
+
65
+ useActivateFirstErrorSection(sectionRefs, setCurrent);
66
+
67
+ const { control, getFieldState } = useFormContext();
68
+ const formState = useFormState({ control });
69
+
70
+ const stepItems = useMemo(
71
+ () =>
72
+ steps.map((step, index) => {
73
+ const hasError = sectionHasError(
74
+ sectionRefs[index].current,
75
+ getFieldState,
76
+ formState,
77
+ );
78
+
79
+ return {
80
+ title: step.props.title,
81
+ description: step.props.description,
82
+ status: hasError ? ("error" as const) : undefined,
83
+ };
84
+ }),
85
+ [formState, getFieldState, sectionRefs, steps],
86
+ );
87
+
88
+ const handleStepSelect = useCallback(
89
+ (next: number) => {
90
+ setCurrent(next);
91
+ },
92
+ [],
93
+ );
94
+
95
+ return (
96
+ <>
97
+ <Steps
98
+ current={current}
99
+ items={stepItems}
100
+ style={{ marginBottom: 24, ...stepsStyle }}
101
+ onChange={allowStepSelect ? handleStepSelect : undefined}
102
+ size={size}
103
+ direction={direction}
104
+ type={type}
105
+ status={status}
106
+ />
107
+
108
+ {steps.map((step, index) => (
109
+ <div
110
+ key={(step.key as string | undefined) ?? String(index)}
111
+ style={{ display: current === index ? undefined : "none" }}
112
+ >
113
+ <FormSectionSourcesProvider sourcesRef={sectionRefs[index]}>
114
+ {step.props.children}
115
+ </FormSectionSourcesProvider>
116
+ </div>
117
+ ))}
118
+
119
+ {showNavigation && steps.length > 1 ? (
120
+ <Space style={{ marginTop: 16, ...navigationStyle }}>
121
+ <Button disabled={current === 0} onClick={() => setCurrent((s) => s - 1)}>
122
+ Previous
123
+ </Button>
124
+ <Button
125
+ type="primary"
126
+ disabled={current === lastIndex}
127
+ onClick={() => setCurrent((s) => s + 1)}
128
+ >
129
+ Next
130
+ </Button>
131
+ </Space>
132
+ ) : null}
133
+ </>
134
+ );
135
+ }