@modern-admin/react 0.1.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 (261) hide show
  1. package/dist/action-guard.d.ts +13 -0
  2. package/dist/action-guard.d.ts.map +1 -0
  3. package/dist/action-guard.js +15 -0
  4. package/dist/action-guard.js.map +1 -0
  5. package/dist/action-menu.d.ts +17 -0
  6. package/dist/action-menu.d.ts.map +1 -0
  7. package/dist/action-menu.jsx +80 -0
  8. package/dist/action-menu.jsx.map +1 -0
  9. package/dist/admin-app.d.ts +23 -0
  10. package/dist/admin-app.d.ts.map +1 -0
  11. package/dist/admin-app.jsx +407 -0
  12. package/dist/admin-app.jsx.map +1 -0
  13. package/dist/admin-router.d.ts +29 -0
  14. package/dist/admin-router.d.ts.map +1 -0
  15. package/dist/admin-router.jsx +215 -0
  16. package/dist/admin-router.jsx.map +1 -0
  17. package/dist/breadcrumbs.d.ts +17 -0
  18. package/dist/breadcrumbs.d.ts.map +1 -0
  19. package/dist/breadcrumbs.jsx +40 -0
  20. package/dist/breadcrumbs.jsx.map +1 -0
  21. package/dist/client.d.ts +526 -0
  22. package/dist/client.d.ts.map +1 -0
  23. package/dist/client.js +582 -0
  24. package/dist/client.js.map +1 -0
  25. package/dist/component-loader.d.ts +10 -0
  26. package/dist/component-loader.d.ts.map +1 -0
  27. package/dist/component-loader.js +23 -0
  28. package/dist/component-loader.js.map +1 -0
  29. package/dist/components/ai-assistant-widget.d.ts +3 -0
  30. package/dist/components/ai-assistant-widget.d.ts.map +1 -0
  31. package/dist/components/ai-assistant-widget.jsx +390 -0
  32. package/dist/components/ai-assistant-widget.jsx.map +1 -0
  33. package/dist/components/ai-fill-dialog.d.ts +9 -0
  34. package/dist/components/ai-fill-dialog.d.ts.map +1 -0
  35. package/dist/components/ai-fill-dialog.jsx +105 -0
  36. package/dist/components/ai-fill-dialog.jsx.map +1 -0
  37. package/dist/components/chart-builder-dialog.d.ts +10 -0
  38. package/dist/components/chart-builder-dialog.d.ts.map +1 -0
  39. package/dist/components/chart-builder-dialog.jsx +433 -0
  40. package/dist/components/chart-builder-dialog.jsx.map +1 -0
  41. package/dist/components/chart-widget.d.ts +12 -0
  42. package/dist/components/chart-widget.d.ts.map +1 -0
  43. package/dist/components/chart-widget.jsx +365 -0
  44. package/dist/components/chart-widget.jsx.map +1 -0
  45. package/dist/components/global-search-dialog.d.ts +7 -0
  46. package/dist/components/global-search-dialog.d.ts.map +1 -0
  47. package/dist/components/global-search-dialog.jsx +187 -0
  48. package/dist/components/global-search-dialog.jsx.map +1 -0
  49. package/dist/components/group-settings-dialog.d.ts +13 -0
  50. package/dist/components/group-settings-dialog.d.ts.map +1 -0
  51. package/dist/components/group-settings-dialog.jsx +53 -0
  52. package/dist/components/group-settings-dialog.jsx.map +1 -0
  53. package/dist/components/move-chart-dialog.d.ts +18 -0
  54. package/dist/components/move-chart-dialog.d.ts.map +1 -0
  55. package/dist/components/move-chart-dialog.jsx +68 -0
  56. package/dist/components/move-chart-dialog.jsx.map +1 -0
  57. package/dist/components/reference-multi-table-dialog.d.ts +12 -0
  58. package/dist/components/reference-multi-table-dialog.d.ts.map +1 -0
  59. package/dist/components/reference-multi-table-dialog.jsx +126 -0
  60. package/dist/components/reference-multi-table-dialog.jsx.map +1 -0
  61. package/dist/components/related-records-tabs.d.ts +8 -0
  62. package/dist/components/related-records-tabs.d.ts.map +1 -0
  63. package/dist/components/related-records-tabs.jsx +75 -0
  64. package/dist/components/related-records-tabs.jsx.map +1 -0
  65. package/dist/components/revisions-button.d.ts +7 -0
  66. package/dist/components/revisions-button.d.ts.map +1 -0
  67. package/dist/components/revisions-button.jsx +152 -0
  68. package/dist/components/revisions-button.jsx.map +1 -0
  69. package/dist/components/wizard-form.d.ts +43 -0
  70. package/dist/components/wizard-form.d.ts.map +1 -0
  71. package/dist/components/wizard-form.jsx +136 -0
  72. package/dist/components/wizard-form.jsx.map +1 -0
  73. package/dist/dashboard/time-series.d.ts +20 -0
  74. package/dist/dashboard/time-series.d.ts.map +1 -0
  75. package/dist/dashboard/time-series.js +108 -0
  76. package/dist/dashboard/time-series.js.map +1 -0
  77. package/dist/dialogs.d.ts +35 -0
  78. package/dist/dialogs.d.ts.map +1 -0
  79. package/dist/dialogs.jsx +152 -0
  80. package/dist/dialogs.jsx.map +1 -0
  81. package/dist/export.d.ts +39 -0
  82. package/dist/export.d.ts.map +1 -0
  83. package/dist/export.js +114 -0
  84. package/dist/export.js.map +1 -0
  85. package/dist/extension-registry.d.ts +122 -0
  86. package/dist/extension-registry.d.ts.map +1 -0
  87. package/dist/extension-registry.js +93 -0
  88. package/dist/extension-registry.js.map +1 -0
  89. package/dist/header-controls.d.ts +4 -0
  90. package/dist/header-controls.d.ts.map +1 -0
  91. package/dist/header-controls.jsx +70 -0
  92. package/dist/header-controls.jsx.map +1 -0
  93. package/dist/hooks.d.ts +104 -0
  94. package/dist/hooks.d.ts.map +1 -0
  95. package/dist/hooks.js +374 -0
  96. package/dist/hooks.js.map +1 -0
  97. package/dist/hotkey-help.d.ts +3 -0
  98. package/dist/hotkey-help.d.ts.map +1 -0
  99. package/dist/hotkey-help.jsx +32 -0
  100. package/dist/hotkey-help.jsx.map +1 -0
  101. package/dist/hotkey-registry.d.ts +18 -0
  102. package/dist/hotkey-registry.d.ts.map +1 -0
  103. package/dist/hotkey-registry.jsx +34 -0
  104. package/dist/hotkey-registry.jsx.map +1 -0
  105. package/dist/i18n.d.ts +74 -0
  106. package/dist/i18n.d.ts.map +1 -0
  107. package/dist/i18n.jsx +127 -0
  108. package/dist/i18n.jsx.map +1 -0
  109. package/dist/index.d.ts +35 -0
  110. package/dist/index.d.ts.map +1 -0
  111. package/dist/index.js +36 -0
  112. package/dist/index.js.map +1 -0
  113. package/dist/notify.d.ts +41 -0
  114. package/dist/notify.d.ts.map +1 -0
  115. package/dist/notify.jsx +58 -0
  116. package/dist/notify.jsx.map +1 -0
  117. package/dist/pages/ai-assistant-settings-section.d.ts +3 -0
  118. package/dist/pages/ai-assistant-settings-section.d.ts.map +1 -0
  119. package/dist/pages/ai-assistant-settings-section.jsx +126 -0
  120. package/dist/pages/ai-assistant-settings-section.jsx.map +1 -0
  121. package/dist/pages/audit-log-page.d.ts +3 -0
  122. package/dist/pages/audit-log-page.d.ts.map +1 -0
  123. package/dist/pages/audit-log-page.jsx +354 -0
  124. package/dist/pages/audit-log-page.jsx.map +1 -0
  125. package/dist/pages/edit-page.d.ts +7 -0
  126. package/dist/pages/edit-page.d.ts.map +1 -0
  127. package/dist/pages/edit-page.jsx +614 -0
  128. package/dist/pages/edit-page.jsx.map +1 -0
  129. package/dist/pages/export-dialog.d.ts +11 -0
  130. package/dist/pages/export-dialog.d.ts.map +1 -0
  131. package/dist/pages/export-dialog.jsx +102 -0
  132. package/dist/pages/export-dialog.jsx.map +1 -0
  133. package/dist/pages/home-page.d.ts +3 -0
  134. package/dist/pages/home-page.d.ts.map +1 -0
  135. package/dist/pages/home-page.jsx +211 -0
  136. package/dist/pages/home-page.jsx.map +1 -0
  137. package/dist/pages/list-page.d.ts +42 -0
  138. package/dist/pages/list-page.d.ts.map +1 -0
  139. package/dist/pages/list-page.jsx +1596 -0
  140. package/dist/pages/list-page.jsx.map +1 -0
  141. package/dist/pages/login-page.d.ts +11 -0
  142. package/dist/pages/login-page.d.ts.map +1 -0
  143. package/dist/pages/login-page.jsx +157 -0
  144. package/dist/pages/login-page.jsx.map +1 -0
  145. package/dist/pages/settings-page.d.ts +5 -0
  146. package/dist/pages/settings-page.d.ts.map +1 -0
  147. package/dist/pages/settings-page.jsx +787 -0
  148. package/dist/pages/settings-page.jsx.map +1 -0
  149. package/dist/pages/settings-shared.d.ts +51 -0
  150. package/dist/pages/settings-shared.d.ts.map +1 -0
  151. package/dist/pages/settings-shared.jsx +66 -0
  152. package/dist/pages/settings-shared.jsx.map +1 -0
  153. package/dist/pages/show-page.d.ts +7 -0
  154. package/dist/pages/show-page.d.ts.map +1 -0
  155. package/dist/pages/show-page.jsx +147 -0
  156. package/dist/pages/show-page.jsx.map +1 -0
  157. package/dist/pages/wizard-create-page.d.ts +14 -0
  158. package/dist/pages/wizard-create-page.d.ts.map +1 -0
  159. package/dist/pages/wizard-create-page.jsx +106 -0
  160. package/dist/pages/wizard-create-page.jsx.map +1 -0
  161. package/dist/property-renderer.d.ts +8 -0
  162. package/dist/property-renderer.d.ts.map +1 -0
  163. package/dist/property-renderer.jsx +690 -0
  164. package/dist/property-renderer.jsx.map +1 -0
  165. package/dist/provider.d.ts +20 -0
  166. package/dist/provider.d.ts.map +1 -0
  167. package/dist/provider.jsx +32 -0
  168. package/dist/provider.jsx.map +1 -0
  169. package/dist/realtime.d.ts +22 -0
  170. package/dist/realtime.d.ts.map +1 -0
  171. package/dist/realtime.js +38 -0
  172. package/dist/realtime.js.map +1 -0
  173. package/dist/reference.d.ts +52 -0
  174. package/dist/reference.d.ts.map +1 -0
  175. package/dist/reference.jsx +224 -0
  176. package/dist/reference.jsx.map +1 -0
  177. package/dist/relations.d.ts +11 -0
  178. package/dist/relations.d.ts.map +1 -0
  179. package/dist/relations.js +36 -0
  180. package/dist/relations.js.map +1 -0
  181. package/dist/router.d.ts +82 -0
  182. package/dist/router.d.ts.map +1 -0
  183. package/dist/router.jsx +187 -0
  184. package/dist/router.jsx.map +1 -0
  185. package/dist/show-when.d.ts +7 -0
  186. package/dist/show-when.d.ts.map +1 -0
  187. package/dist/show-when.js +77 -0
  188. package/dist/show-when.js.map +1 -0
  189. package/dist/types.d.ts +194 -0
  190. package/dist/types.d.ts.map +1 -0
  191. package/dist/types.js +18 -0
  192. package/dist/types.js.map +1 -0
  193. package/dist/use-dashboard-charts.d.ts +93 -0
  194. package/dist/use-dashboard-charts.d.ts.map +1 -0
  195. package/dist/use-dashboard-charts.js +263 -0
  196. package/dist/use-dashboard-charts.js.map +1 -0
  197. package/dist/use-hotkey.d.ts +17 -0
  198. package/dist/use-hotkey.d.ts.map +1 -0
  199. package/dist/use-hotkey.js +103 -0
  200. package/dist/use-hotkey.js.map +1 -0
  201. package/dist/user-directory.d.ts +18 -0
  202. package/dist/user-directory.d.ts.map +1 -0
  203. package/dist/user-directory.js +51 -0
  204. package/dist/user-directory.js.map +1 -0
  205. package/dist/validation.d.ts +22 -0
  206. package/dist/validation.d.ts.map +1 -0
  207. package/dist/validation.js +338 -0
  208. package/dist/validation.js.map +1 -0
  209. package/package.json +59 -0
  210. package/src/action-guard.ts +20 -0
  211. package/src/action-menu.tsx +161 -0
  212. package/src/admin-app.tsx +630 -0
  213. package/src/admin-router.tsx +273 -0
  214. package/src/breadcrumbs.tsx +75 -0
  215. package/src/client.ts +1093 -0
  216. package/src/component-loader.ts +33 -0
  217. package/src/components/ai-assistant-widget.tsx +565 -0
  218. package/src/components/ai-fill-dialog.tsx +143 -0
  219. package/src/components/chart-builder-dialog.tsx +618 -0
  220. package/src/components/chart-widget.tsx +654 -0
  221. package/src/components/global-search-dialog.tsx +272 -0
  222. package/src/components/group-settings-dialog.tsx +93 -0
  223. package/src/components/move-chart-dialog.tsx +130 -0
  224. package/src/components/reference-multi-table-dialog.tsx +196 -0
  225. package/src/components/related-records-tabs.tsx +130 -0
  226. package/src/components/revisions-button.tsx +237 -0
  227. package/src/components/wizard-form.tsx +302 -0
  228. package/src/dashboard/time-series.ts +125 -0
  229. package/src/dialogs.tsx +265 -0
  230. package/src/export.ts +140 -0
  231. package/src/extension-registry.ts +195 -0
  232. package/src/header-controls.tsx +125 -0
  233. package/src/hooks.ts +509 -0
  234. package/src/hotkey-help.tsx +56 -0
  235. package/src/hotkey-registry.tsx +60 -0
  236. package/src/i18n.tsx +267 -0
  237. package/src/index.ts +192 -0
  238. package/src/notify.tsx +94 -0
  239. package/src/pages/ai-assistant-settings-section.tsx +167 -0
  240. package/src/pages/audit-log-page.tsx +580 -0
  241. package/src/pages/edit-page.tsx +743 -0
  242. package/src/pages/export-dialog.tsx +154 -0
  243. package/src/pages/home-page.tsx +318 -0
  244. package/src/pages/list-page.tsx +2645 -0
  245. package/src/pages/login-page.tsx +242 -0
  246. package/src/pages/settings-page.tsx +1143 -0
  247. package/src/pages/settings-shared.tsx +134 -0
  248. package/src/pages/show-page.tsx +223 -0
  249. package/src/pages/wizard-create-page.tsx +164 -0
  250. package/src/property-renderer.tsx +1143 -0
  251. package/src/provider.tsx +70 -0
  252. package/src/realtime.ts +55 -0
  253. package/src/reference.tsx +386 -0
  254. package/src/relations.ts +55 -0
  255. package/src/router.tsx +211 -0
  256. package/src/show-when.ts +76 -0
  257. package/src/types.ts +198 -0
  258. package/src/use-dashboard-charts.ts +362 -0
  259. package/src/use-hotkey.ts +128 -0
  260. package/src/user-directory.ts +56 -0
  261. package/src/validation.ts +361 -0
@@ -0,0 +1,273 @@
1
+ // Code-based route tree wired up to @tanstack/react-router. Imported by
2
+ // `admin-app.tsx` and renders the entire authenticated shell as the root
3
+ // route's component. Each leaf route is a thin wrapper that pulls typed
4
+ // `params` and renders the existing page component (page props remain a
5
+ // public API surface — no changes to ResourceListPage/ResourceShowPage/etc.).
6
+ //
7
+ // History: browser (`createBrowserHistory`) — clean path-based URLs.
8
+ // Requires an SPA fallback rule on the server (e.g. `try_files ... index.html`
9
+ // in nginx, historyApiFallback in Vite). See `router.tsx` and `docs/frontend.md`.
10
+ //
11
+ // Basepath: `AdminRouterProvider` accepts a `basepath` prop (e.g. `/admin`)
12
+ // injected automatically from `window.__MODERN_ADMIN__.basePath`. The router
13
+ // is created with that basepath so TSR strips it from URLs before route
14
+ // matching. `BasepathContext` exposes it to `Link` and `useNavigate` so they
15
+ // can prepend it when pushing to browser history.
16
+ //
17
+ // Search params: TSR's default JSON-style parser would mangle our existing
18
+ // `filters[<path>]=<value>` URL format. We make `parseSearch`/`stringifySearch`
19
+ // no-ops (TSR keeps `searchStr` raw); `useRoute()` re-parses `searchStr` into
20
+ // `ListQueryState` via `parseLocation` in `router.tsx`.
21
+
22
+ import * as React from 'react'
23
+ import {
24
+ createBrowserHistory,
25
+ createRootRouteWithContext,
26
+ createRoute,
27
+ createRouter,
28
+ Outlet,
29
+ RouterProvider,
30
+ } from '@tanstack/react-router'
31
+ import { BasepathContext } from './router.js'
32
+ import { getRouteExtension } from './extension-registry.js'
33
+ import { ResourceListPage } from './pages/list-page.js'
34
+ import { ResourceShowPage } from './pages/show-page.js'
35
+ import { ResourceEditPage } from './pages/edit-page.js'
36
+ import { ResourceWizardCreatePage } from './pages/wizard-create-page.js'
37
+ import { HomePage } from './pages/home-page.js'
38
+ import { SettingsPage } from './pages/settings-page.js'
39
+ import { AuditLogPage } from './pages/audit-log-page.js'
40
+ import { useI18n } from './i18n.js'
41
+ import type { WizardStep } from './components/wizard-form.js'
42
+
43
+ // ─── Route tree ───────────────────────────────────────────────────────────────
44
+
45
+ interface RouterContext {
46
+ ShellLayout: React.ComponentType<{ children: React.ReactNode }>
47
+ }
48
+
49
+ const rootRoute = createRootRouteWithContext<RouterContext>()({
50
+ // The actual shell (sidebar + header + main) lives in `admin-app.tsx`.
51
+ // It's passed in via the router's context at provider time so we don't
52
+ // create a circular module dependency between admin-app and admin-router.
53
+ component: function RootRouteShell() {
54
+ const { ShellLayout } = rootRoute.useRouteContext()
55
+ return (
56
+ <ShellLayout>
57
+ <Outlet />
58
+ </ShellLayout>
59
+ )
60
+ },
61
+ })
62
+
63
+ const homeRoute = createRoute({
64
+ getParentRoute: () => rootRoute,
65
+ path: '/',
66
+ component: function HomeRouteComponent() {
67
+ return <HomePage />
68
+ },
69
+ })
70
+
71
+ const auditLogRoute = createRoute({
72
+ getParentRoute: () => rootRoute,
73
+ path: '/audit-log',
74
+ component: function AuditLogRouteComponent() {
75
+ return <AuditLogPage />
76
+ },
77
+ })
78
+
79
+ const resourceListRoute = createRoute({
80
+ getParentRoute: () => rootRoute,
81
+ path: '/resources/$resourceId',
82
+ component: function ResourceListRouteComponent() {
83
+ const { resourceId } = resourceListRoute.useParams()
84
+ return <ResourceListPage resourceId={resourceId} />
85
+ },
86
+ })
87
+
88
+ // Products new-record route uses a 3-step WizardForm as a showcase.
89
+ // Step 3 has no `properties` list — it becomes the catch-all for every
90
+ // property not claimed by steps 1 or 2 (thumbnail, accentColor, gallery, tags).
91
+ function ProductsNewPage(): React.ReactElement {
92
+ const { t } = useI18n()
93
+ const steps: WizardStep[] = [
94
+ { label: t('wizard:products.step1'), properties: ['name', 'sku', 'inStock'] },
95
+ { label: t('wizard:products.step2'), properties: ['price', 'currencyCode', 'quantity'] },
96
+ { label: t('wizard:products.step3') }, // catch-all: thumbnail, accentColor, gallery, tags
97
+ ]
98
+ return <ResourceWizardCreatePage resourceId="products" steps={steps} />
99
+ }
100
+
101
+ const resourceNewRoute = createRoute({
102
+ getParentRoute: () => rootRoute,
103
+ path: '/resources/$resourceId/new',
104
+ component: function ResourceNewRouteComponent() {
105
+ const { resourceId } = resourceNewRoute.useParams()
106
+ if (resourceId === 'products') return <ProductsNewPage />
107
+ return <ResourceEditPage resourceId={resourceId} />
108
+ },
109
+ })
110
+
111
+ const resourceShowRoute = createRoute({
112
+ getParentRoute: () => rootRoute,
113
+ path: '/resources/$resourceId/$recordId',
114
+ component: function ResourceShowRouteComponent() {
115
+ const { resourceId, recordId } = resourceShowRoute.useParams()
116
+ return <ResourceShowPage resourceId={resourceId} recordId={recordId} />
117
+ },
118
+ })
119
+
120
+ const resourceEditRoute = createRoute({
121
+ getParentRoute: () => rootRoute,
122
+ path: '/resources/$resourceId/$recordId/edit',
123
+ component: function ResourceEditRouteComponent() {
124
+ const { resourceId, recordId } = resourceEditRoute.useParams()
125
+ return <ResourceEditPage resourceId={resourceId} recordId={recordId} />
126
+ },
127
+ })
128
+
129
+ const settingsIndexRoute = createRoute({
130
+ getParentRoute: () => rootRoute,
131
+ path: '/settings',
132
+ component: function SettingsIndexRouteComponent() {
133
+ return <SettingsPage />
134
+ },
135
+ })
136
+
137
+ const settingsSectionRoute = createRoute({
138
+ getParentRoute: () => rootRoute,
139
+ path: '/settings/$section',
140
+ component: function SettingsSectionRouteComponent() {
141
+ const { section } = settingsSectionRoute.useParams()
142
+ return <SettingsPage section={section} />
143
+ },
144
+ })
145
+
146
+ // Route for Pro extension pages registered via `registerExtensionRoute`.
147
+ // The component reads `key` from params and looks up the registered
148
+ // extension at render time — so it works even if extensions are registered
149
+ // after module init but before the user navigates to the route.
150
+ const extensionRoute = createRoute({
151
+ getParentRoute: () => rootRoute,
152
+ path: '/ext/$extKey',
153
+ component: function ExtensionRouteComponent() {
154
+ const { extKey } = extensionRoute.useParams()
155
+ const ext = getRouteExtension(extKey)
156
+ if (!ext) {
157
+ // Extension was registered for this key in the sidebar but no component
158
+ // was provided. Render a minimal placeholder rather than a blank screen.
159
+ return (
160
+ <div className="flex flex-col items-center justify-center py-24 text-muted-foreground">
161
+ <p className="text-sm">Extension &quot;{extKey}&quot; not found.</p>
162
+ </div>
163
+ )
164
+ }
165
+ return <ext.component />
166
+ },
167
+ })
168
+
169
+ const routeTree = rootRoute.addChildren([
170
+ homeRoute,
171
+ auditLogRoute,
172
+ extensionRoute,
173
+ resourceNewRoute,
174
+ resourceEditRoute,
175
+ resourceShowRoute,
176
+ resourceListRoute,
177
+ settingsSectionRoute,
178
+ settingsIndexRoute,
179
+ ])
180
+
181
+ // ─── Router instance ──────────────────────────────────────────────────────────
182
+ //
183
+ // Search params: in TSR v1.169 `location.searchStr` is computed as
184
+ // `stringifySearch(parseSearch(rawUrlSearch))` — it's NOT the raw URL search
185
+ // string. Our list page reads `searchStr` to extract `filters[<path>]=<value>`
186
+ // pairs (see `parseLocation` in `router.tsx`), so the parse/stringify pair
187
+ // must be a flat-key identity round-trip rather than TSR's default
188
+ // JSON-encoded values (which would mangle the bracket notation) or no-ops
189
+ // (which would drop the search entirely — filters/sort never reach the API).
190
+
191
+ const flatParseSearch = (search: string): Record<string, string> => {
192
+ const str = search.startsWith('?') ? search.slice(1) : search
193
+ if (!str) return {}
194
+ const params = new URLSearchParams(str)
195
+ const out: Record<string, string> = {}
196
+ params.forEach((value, key) => {
197
+ out[key] = value
198
+ })
199
+ return out
200
+ }
201
+
202
+ const flatStringifySearch = (search: Record<string, unknown>): string => {
203
+ if (!search) return ''
204
+ const params = new URLSearchParams()
205
+ for (const [k, v] of Object.entries(search)) {
206
+ if (v == null || v === '') continue
207
+ params.set(k, typeof v === 'string' ? v : String(v))
208
+ }
209
+ const s = params.toString()
210
+ return s ? `?${s}` : ''
211
+ }
212
+
213
+ const noopRouterContext: RouterContext = {
214
+ // Replaced at provider mount time via the `context` prop on RouterProvider.
215
+ ShellLayout: () => null,
216
+ }
217
+
218
+ function makeRouter(basepath: string) {
219
+ return createRouter({
220
+ routeTree,
221
+ history: createBrowserHistory(),
222
+ basepath: basepath || '/',
223
+ parseSearch: flatParseSearch,
224
+ stringifySearch: flatStringifySearch,
225
+ context: noopRouterContext,
226
+ defaultPreload: false,
227
+ scrollRestoration: false,
228
+ })
229
+ }
230
+
231
+ // Type registration — gives typed `useParams()`/`useSearch()` and link
232
+ // validation across the package. The concrete router type uses the root
233
+ // router shape (basepath doesn't affect the generic types).
234
+ declare module '@tanstack/react-router' {
235
+ interface Register {
236
+ router: ReturnType<typeof makeRouter>
237
+ }
238
+ }
239
+
240
+ // ─── Provider ─────────────────────────────────────────────────────────────────
241
+
242
+ export interface AdminRouterProviderProps {
243
+ /** Component rendered as the authenticated shell. Receives `children` —
244
+ * must include an `<Outlet/>` slot (or a child element wrapping it) so
245
+ * routed page components can mount inside it. */
246
+ ShellLayout: React.ComponentType<{ children: React.ReactNode }>
247
+ /** URL prefix where the SPA is mounted (e.g. `/admin`). Defaults to `''`
248
+ * (root mount). Passed to TSR as the router `basepath` AND exposed to
249
+ * `Link`/`useNavigate` via `BasepathContext` so they push full paths. */
250
+ basepath?: string
251
+ }
252
+
253
+ /** Mounts the admin's route tree. Must be rendered only after the user is
254
+ * authenticated — login flow happens upstream in `AdminApp`. */
255
+ export function AdminRouterProvider({
256
+ ShellLayout,
257
+ basepath = '',
258
+ }: AdminRouterProviderProps): React.ReactElement {
259
+ // Normalise: strip trailing slash, treat '/' as 'no basepath' (root mount).
260
+ const normalised = React.useMemo(() => {
261
+ if (!basepath || basepath === '/') return ''
262
+ return basepath.endsWith('/') ? basepath.slice(0, -1) : basepath
263
+ }, [basepath])
264
+ // Create router lazily per basepath. Runtime config is stable across the
265
+ // app's lifetime, so in practice this runs exactly once per mount.
266
+ const router = React.useMemo(() => makeRouter(normalised), [normalised])
267
+ const context = React.useMemo<RouterContext>(() => ({ ShellLayout }), [ShellLayout])
268
+ return (
269
+ <BasepathContext.Provider value={normalised}>
270
+ <RouterProvider router={router} context={context} />
271
+ </BasepathContext.Provider>
272
+ )
273
+ }
@@ -0,0 +1,75 @@
1
+ // Page breadcrumb helper. Each page builds its own crumb chain (home →
2
+ // resource → record → action) and hands it to <PageBreadcrumbs />, which
3
+ // renders the shadcn primitives with separators, hash-router-aware Links
4
+ // and a Home icon on the root crumb.
5
+
6
+ import * as React from 'react'
7
+ import {
8
+ Breadcrumb,
9
+ BreadcrumbItem,
10
+ BreadcrumbLink,
11
+ BreadcrumbList,
12
+ BreadcrumbPage,
13
+ BreadcrumbSeparator,
14
+ } from '@modern-admin/ui'
15
+ import { Home } from 'lucide-react'
16
+ import { Link } from './router.js'
17
+ import type { Route } from './router.js'
18
+
19
+ export interface BreadcrumbItemSpec {
20
+ label: string
21
+ /** When set, renders a hash-router link; otherwise the crumb is plain text. */
22
+ to?: Route
23
+ /** Optional leading icon. The home crumb gets one by default. */
24
+ icon?: React.ReactNode
25
+ }
26
+
27
+ export interface PageBreadcrumbsProps {
28
+ items: BreadcrumbItemSpec[]
29
+ className?: string
30
+ }
31
+
32
+ export function PageBreadcrumbs({
33
+ items,
34
+ className,
35
+ }: PageBreadcrumbsProps): React.ReactElement | null {
36
+ if (items.length === 0) return null
37
+ return (
38
+ <Breadcrumb className={className}>
39
+ <BreadcrumbList>
40
+ {items.map((item, i) => {
41
+ const isLast = i === items.length - 1
42
+ const content = (
43
+ <span className="inline-flex items-center gap-1.5">
44
+ {item.icon}
45
+ <span className="truncate max-w-[8rem] sm:max-w-[16rem]">{item.label}</span>
46
+ </span>
47
+ )
48
+ return (
49
+ <React.Fragment key={`${i}-${item.label}`}>
50
+ <BreadcrumbItem>
51
+ {isLast || !item.to ? (
52
+ <BreadcrumbPage>{content}</BreadcrumbPage>
53
+ ) : (
54
+ <BreadcrumbLink asChild>
55
+ <Link to={item.to}>{content}</Link>
56
+ </BreadcrumbLink>
57
+ )}
58
+ </BreadcrumbItem>
59
+ {!isLast && <BreadcrumbSeparator />}
60
+ </React.Fragment>
61
+ )
62
+ })}
63
+ </BreadcrumbList>
64
+ </Breadcrumb>
65
+ )
66
+ }
67
+
68
+ /** Default home crumb — used by every page; pass `homeLabel` to localize. */
69
+ export function homeCrumb(label: string): BreadcrumbItemSpec {
70
+ return {
71
+ label,
72
+ to: { name: 'home' },
73
+ icon: <Home className="size-3.5" />,
74
+ }
75
+ }