@spree/dashboard-core 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 (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +418 -0
  3. package/package.json +84 -0
  4. package/src/client.ts +25 -0
  5. package/src/components/address-form-dialog.tsx +376 -0
  6. package/src/components/app-sidebar.tsx +70 -0
  7. package/src/components/bulk-action-bar.tsx +504 -0
  8. package/src/components/can.tsx +30 -0
  9. package/src/components/country-combobox.tsx +238 -0
  10. package/src/components/country-state-fields.tsx +86 -0
  11. package/src/components/currency-select.tsx +181 -0
  12. package/src/components/export-button.tsx +156 -0
  13. package/src/components/file-upload-field.tsx +265 -0
  14. package/src/components/image-upload-field.tsx +53 -0
  15. package/src/components/import-button.tsx +202 -0
  16. package/src/components/locale-select.tsx +268 -0
  17. package/src/components/market-combobox.tsx +89 -0
  18. package/src/components/nav-main.tsx +186 -0
  19. package/src/components/page-header.tsx +286 -0
  20. package/src/components/page-tabs.tsx +78 -0
  21. package/src/components/preferences-form.tsx +404 -0
  22. package/src/components/resource-combobox.tsx +126 -0
  23. package/src/components/resource-multi-autocomplete.tsx +176 -0
  24. package/src/components/resource-picker-sheet.tsx +235 -0
  25. package/src/components/resource-table.tsx +652 -0
  26. package/src/components/settings-sidebar.tsx +139 -0
  27. package/src/components/slot.tsx +50 -0
  28. package/src/components/store-date-picker.tsx +18 -0
  29. package/src/components/store-switcher.tsx +70 -0
  30. package/src/components/table-toolbar.tsx +818 -0
  31. package/src/components/tag-combobox.tsx +99 -0
  32. package/src/components/top-bar.tsx +210 -0
  33. package/src/hooks/use-auth.ts +10 -0
  34. package/src/hooks/use-command-palette.tsx +34 -0
  35. package/src/hooks/use-countries.ts +15 -0
  36. package/src/hooks/use-custom-fields.ts +142 -0
  37. package/src/hooks/use-direct-upload.ts +104 -0
  38. package/src/hooks/use-display-name.ts +42 -0
  39. package/src/hooks/use-export.ts +75 -0
  40. package/src/hooks/use-global-search.ts +79 -0
  41. package/src/hooks/use-host-form.ts +34 -0
  42. package/src/hooks/use-import.ts +62 -0
  43. package/src/hooks/use-resource-mutation.ts +99 -0
  44. package/src/hooks/use-switch-admin-locale.ts +55 -0
  45. package/src/index.ts +97 -0
  46. package/src/lib/custom-field-components.ts +56 -0
  47. package/src/lib/download.ts +49 -0
  48. package/src/lib/filters-to-ransack.ts +47 -0
  49. package/src/lib/form-errors.ts +103 -0
  50. package/src/lib/form-fields-registry.ts +102 -0
  51. package/src/lib/form-mappers.ts +30 -0
  52. package/src/lib/formatters.ts +19 -0
  53. package/src/lib/i18n.ts +210 -0
  54. package/src/lib/nav-registry.ts +224 -0
  55. package/src/lib/permissions.ts +55 -0
  56. package/src/lib/query-client.ts +11 -0
  57. package/src/lib/query-keys.ts +69 -0
  58. package/src/lib/route-registry.ts +158 -0
  59. package/src/lib/search-registry.ts +129 -0
  60. package/src/lib/settings-nav-registry.ts +158 -0
  61. package/src/lib/slot-registry.ts +116 -0
  62. package/src/lib/table-registry.ts +274 -0
  63. package/src/lib/timestamp-columns.tsx +58 -0
  64. package/src/locales/ar.json +629 -0
  65. package/src/locales/de.json +629 -0
  66. package/src/locales/en.json +629 -0
  67. package/src/locales/fr.json +629 -0
  68. package/src/locales/pl.json +629 -0
  69. package/src/locales/zh-CN.json +629 -0
  70. package/src/plugin.ts +222 -0
  71. package/src/providers/auth-provider.tsx +188 -0
  72. package/src/providers/permission-provider.tsx +118 -0
  73. package/src/providers/store-provider.tsx +111 -0
  74. package/src/virtual.d.ts +5 -0
  75. package/src/vite/discover.ts +170 -0
  76. package/src/vite/index.ts +270 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vendo Connect Inc., Vendo Sp. z o.o.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,418 @@
1
+ # @spree/dashboard-core
2
+
3
+ Spree Dashboard framework. Registries, providers, infra hooks, the admin SDK client, and the `defineDashboardPlugin` extension API.
4
+
5
+ This is the package plugin authors install. Use it to register nav entries, slot widgets, table column extensions, and settings pages from a separate npm package that gets imported into `@spree/dashboard` at boot.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @spree/dashboard-core @spree/dashboard-ui
11
+ ```
12
+
13
+ Peer dependencies that the consuming app provides: `react`, `react-dom`, `@spree/admin-sdk`, `@spree/dashboard-ui`, `@tanstack/react-query`, `@tanstack/react-router`, `@tanstack/react-hotkeys`, `react-hook-form`, `react-i18next`, `i18next`, `sonner`.
14
+
15
+ **Why peer deps:** the registries (nav, slot, table, settings-nav, form fields, custom field components) are module singletons. If your plugin bundles its own copy of `@spree/dashboard-core`, `registerSlot` writes to a different Map than the app's `<Slot>` reads — silent no-op. Same applies to React, the query client, the router instance, and the i18n singleton. pnpm dedupes these to a single instance as long as they're peers.
16
+
17
+ ## What's in the box
18
+
19
+ ```
20
+ @spree/dashboard-core
21
+ ├── adminClient # @spree/admin-sdk instance, baseUrl wired to VITE_SPREE_API_URL
22
+ │
23
+ ├── Providers (mount in your app shell, in this order)
24
+ │ ├── AuthProvider # JWT + refresh token, /auth/refresh on cold load
25
+ │ ├── PermissionProvider # /api/v3/admin/me → CanCanCan rules
26
+ │ └── StoreProvider # current store context (multi-store)
27
+ │
28
+ ├── Registries (the four extension points)
29
+ │ ├── nav # sidebar items
30
+ │ ├── settingsNav # settings sub-shell groups + items
31
+ │ ├── slot # named injection points (`product.form_sidebar`, ...)
32
+ │ └── tables # column add/remove/update per list view
33
+ │
34
+ ├── Infra hooks
35
+ │ ├── useAuth # { token, user, login(), logout() }
36
+ │ ├── usePermissions # CanCanCan rules + <Can> helper
37
+ │ ├── useStore # current store
38
+ │ ├── useResourceMutation # useMutation + toast + form-error mapping
39
+ │ ├── useDirectUpload # Active Storage direct-upload
40
+ │ ├── useGlobalSearch # cross-resource search
41
+ │ ├── useCommandPalette # ⌘K palette state
42
+ │ ├── useCopyToClipboard
43
+ │ ├── useScrolled
44
+ │ ├── useCountries # admin/countries (geo data, cached)
45
+ │ ├── useExport # CSV export jobs (polled)
46
+ │ └── useCustomFields # metafield definitions + values
47
+ │
48
+ ├── i18n (i18next + react-i18next)
49
+ │ ├── Base translation namespace (`admin.actions.*`, `admin.common.*`, `admin.validation.*`, …)
50
+ │ ├── Trans, useTranslation # re-exported from react-i18next
51
+ │ └── i18n # the i18next singleton (call addResourceBundle to extend)
52
+ │
53
+ ├── Helpers
54
+ │ ├── filtersToRansack # UI filter shape → Ransack params
55
+ │ ├── mapSpreeErrorsToForm # 422 → RHF setError
56
+ │ ├── formatPrice / getInitials
57
+ │ ├── Subject, Action # CanCanCan subject/action constants
58
+ │ └── queryClient # TanStack Query client singleton
59
+ │
60
+ └── defineDashboardPlugin # the one-call extension facade
61
+ ```
62
+
63
+ Additional entry point:
64
+
65
+ - `@spree/dashboard-core/vite` — Vite plugin that wires Tailwind v4. See [Vite integration](#vite-integration).
66
+
67
+ ## Vite integration
68
+
69
+ The dashboard relies on Tailwind v4, which doesn't scan `node_modules` by default and only accepts filesystem paths in `@source` directives (not bare package specifiers). The `spreeDashboardPlugin` from `@spree/dashboard-core/vite` resolves each Spree dashboard package and each host-named plugin through Node module resolution and injects matching `@source` directives into your CSS entry at build time. It also bundles `@tailwindcss/vite` itself so plugin ordering is guaranteed — hosts must NOT register `@tailwindcss/vite` separately.
70
+
71
+ ### Using the full `@spree/dashboard` app shell
72
+
73
+ ```ts
74
+ // host vite.config.ts
75
+ import { spreeDashboardPlugin } from '@spree/dashboard-core/vite'
76
+ import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
77
+ import react from '@vitejs/plugin-react'
78
+ import { defineConfig } from 'vite'
79
+
80
+ export default defineConfig({
81
+ plugins: [
82
+ spreeDashboardPlugin({
83
+ plugins: [
84
+ '@my-store/orders-dashboard-plugin',
85
+ '@my-store/wishlists-dashboard-plugin',
86
+ ],
87
+ }),
88
+ TanStackRouterVite(),
89
+ react(),
90
+ ],
91
+ })
92
+ ```
93
+
94
+ ```css
95
+ /* host src/styles.css */
96
+ @import "@spree/dashboard/styles.css";
97
+ ```
98
+
99
+ ### Building a custom dashboard on `@spree/dashboard-core` + `@spree/dashboard-ui`
100
+
101
+ If you're not using `@spree/dashboard`'s app shell — e.g. a vendor portal or B2B buyer admin built directly on the registries and primitives — point `cssEntry` at your own CSS file:
102
+
103
+ ```ts
104
+ // custom-dashboard vite.config.ts
105
+ import { spreeDashboardPlugin } from '@spree/dashboard-core/vite'
106
+
107
+ export default defineConfig({
108
+ plugins: [
109
+ spreeDashboardPlugin({
110
+ cssEntry: './src/admin.css',
111
+ plugins: ['@my-store/vendor-portal-plugin'],
112
+ }),
113
+ // host owns react, router, etc.
114
+ ],
115
+ })
116
+ ```
117
+
118
+ ```css
119
+ /* custom-dashboard src/admin.css */
120
+ @import "@spree/dashboard-ui/styles.css";
121
+ /* Tailwind auto-scans the host's own src/, so host-authored classes
122
+ work without configuration. The Vite plugin injects @source for
123
+ @spree/dashboard-core, @spree/dashboard-ui, and any named plugins. */
124
+ ```
125
+
126
+ ### `@spree/dashboard-ui` only (no `@spree/dashboard-core`)
127
+
128
+ If you only need design-system primitives and tokens, no registries, no providers — you don't need this Vite plugin at all. Install `@spree/dashboard-ui`, set up `@tailwindcss/vite` yourself, and import its stylesheet:
129
+
130
+ ```css
131
+ @import "@spree/dashboard-ui/styles.css";
132
+ ```
133
+
134
+ `dashboard-ui/styles.css` carries its own `@source` directives covering its components.
135
+
136
+ ### Options
137
+
138
+ ```ts
139
+ spreeDashboardPlugin({
140
+ /**
141
+ * Path to the host's CSS entry file, relative to the host's project root.
142
+ * The plugin injects @source directives into this file. Defaults to
143
+ * `./src/styles.css`.
144
+ */
145
+ cssEntry: './src/styles.css',
146
+
147
+ /**
148
+ * Names of installed dashboard plugin packages. Each must be a resolvable
149
+ * npm specifier. The plugin resolves each via Node module resolution and
150
+ * tells Tailwind to scan its source files.
151
+ */
152
+ plugins: ['@my-store/orders-plugin'],
153
+ })
154
+ ```
155
+
156
+ ### What plugin authors must do
157
+
158
+ Ship `src/` in their published package so Tailwind has source files to scan:
159
+
160
+ ```json
161
+ // plugin's package.json
162
+ {
163
+ "name": "@my-store/orders-dashboard-plugin",
164
+ "main": "./src/index.ts",
165
+ "types": "./src/index.ts",
166
+ "files": ["src", "README.md"]
167
+ }
168
+ ```
169
+
170
+ Plugin authors do NOT need to ship pre-built CSS, configure Tailwind, or care about the host's package manager — adding the plugin name to the host's `plugins: [...]` is enough.
171
+
172
+ ## Plugin authoring — the basics
173
+
174
+ Your plugin is an npm package. Its entry module calls `defineDashboardPlugin({...})` at import time. The dashboard app imports your entry module before first render — every registry handles late registration via `useSyncExternalStore`, so even if your plugin loads asynchronously the sidebar/slots re-render.
175
+
176
+ ```tsx
177
+ // my-spree-plugin/src/index.ts
178
+ import { defineDashboardPlugin, usePermissions } from '@spree/dashboard-core'
179
+ import { Card, CardHeader, CardTitle, CardContent } from '@spree/dashboard-ui'
180
+ import { BarChartIcon } from 'lucide-react'
181
+
182
+ function WishlistsWidget({ product }: { product: { id: string; wishlist_count: number } }) {
183
+ return (
184
+ <Card>
185
+ <CardHeader><CardTitle>Wishlists</CardTitle></CardHeader>
186
+ <CardContent>{product.wishlist_count} customers</CardContent>
187
+ </Card>
188
+ )
189
+ }
190
+
191
+ defineDashboardPlugin({
192
+ nav: [
193
+ { key: 'wishlists', label: 'Wishlists', path: '/wishlists', icon: BarChartIcon, position: 50 },
194
+ ],
195
+ slots: {
196
+ 'product.form_sidebar': [
197
+ { id: 'wishlist-count', component: WishlistsWidget, position: 50 },
198
+ ],
199
+ },
200
+ tables: {
201
+ products: {
202
+ add: [{ key: 'wishlist_count', label: 'Wishlists', sortable: true }],
203
+ },
204
+ },
205
+ })
206
+ ```
207
+
208
+ Then in the consuming app's entry point:
209
+
210
+ ```ts
211
+ // my-spree-app/src/main.tsx
212
+ import '@spree/dashboard-core/lib/i18n' // base translations
213
+ import 'my-spree-plugin' // your plugin registers extensions
214
+ // ...rest of normal dashboard bootstrap
215
+ ```
216
+
217
+ ## Translations — extending the base namespace
218
+
219
+ Plugin authors **inherit the framework's base translation namespace** (`admin.actions.save`, `admin.common.no_results`, `admin.validation.required`, `admin.fields.<simple>.label`, …) for free. To add your own keys, call `i18n.addResourceBundle` after the side-effect import.
220
+
221
+ ```ts
222
+ // my-spree-plugin/src/index.ts
223
+ import { i18n } from '@spree/dashboard-core'
224
+ import en from './locales/en.json'
225
+ //
226
+ // {
227
+ // "admin": {
228
+ // "wishlists": {
229
+ // "title": "Wishlists",
230
+ // "empty_state": "No wishlists yet"
231
+ // }
232
+ // }
233
+ // }
234
+
235
+ i18n.addResourceBundle('en', 'translation', en, true, true)
236
+ // `deep: true` + `overwrite: true` merge nested keys into the base namespace
237
+ // without dropping anything the framework provided.
238
+ ```
239
+
240
+ Now anywhere in your plugin you can mix base keys and your own:
241
+
242
+ ```tsx
243
+ import { useTranslation } from '@spree/dashboard-core'
244
+
245
+ function WishlistsHeader() {
246
+ const { t } = useTranslation()
247
+ return (
248
+ <header>
249
+ <h1>{t('admin.wishlists.title')}</h1>
250
+ <button>{t('admin.actions.create')}</button>{/* base */}
251
+ </header>
252
+ )
253
+ }
254
+ ```
255
+
256
+ ### When plugins should pick their own sub-namespace
257
+
258
+ `addResourceBundle` deep-merges into `admin.*`, so `admin.actions.save` from a plugin would **overwrite** the framework's `admin.actions.save`. To avoid collisions, put plugin-specific text under a vendor-prefixed key:
259
+
260
+ ```jsonc
261
+ {
262
+ "admin": {
263
+ "wishlists": { ... } // ok — fresh namespace under admin
264
+ }
265
+ }
266
+ ```
267
+
268
+ Not:
269
+
270
+ ```jsonc
271
+ {
272
+ "admin": {
273
+ "actions": {
274
+ "share": "Share" // collides if the framework ever adds `admin.actions.share`
275
+ }
276
+ }
277
+ }
278
+ ```
279
+
280
+ Prefer `admin.<plugin>.*` for everything you own. Use base keys (`admin.actions.*`, `admin.common.*`, `admin.fields.<simple>.*`) by *reading* them, not by extending them.
281
+
282
+ ### Multi-locale plugins
283
+
284
+ Same pattern, once per locale:
285
+
286
+ ```ts
287
+ import { i18n } from '@spree/dashboard-core'
288
+ import en from './locales/en.json'
289
+ import fr from './locales/fr.json'
290
+
291
+ i18n.addResourceBundle('en', 'translation', en, true, true)
292
+ i18n.addResourceBundle('fr', 'translation', fr, true, true)
293
+ ```
294
+
295
+ The framework currently ships English only; once Spree's locale list grows the same `addResourceBundle` pattern applies.
296
+
297
+ ## Extension surface — the registries
298
+
299
+ ### `nav` — main sidebar items
300
+
301
+ ```ts
302
+ import { nav } from '@spree/dashboard-core'
303
+ import { PackageIcon } from 'lucide-react'
304
+
305
+ nav.add({
306
+ key: 'wishlists',
307
+ label: 'Wishlists',
308
+ path: '/wishlists', // prefixed with /$storeId at render time
309
+ icon: PackageIcon,
310
+ position: 50, // built-ins use 100/200/300 — slot in between
311
+ subject: 'Spree::Wishlist', // CanCanCan subject — hides item without permission
312
+ })
313
+
314
+ // Or: insert relative to an existing entry
315
+ nav.insertAfter('orders', { key: 'wishlists', label: 'Wishlists', path: '/wishlists' })
316
+ nav.update('orders', { label: 'All Orders' })
317
+ nav.remove('legacy-thing')
318
+ ```
319
+
320
+ ### `settingsNav` — settings sub-shell
321
+
322
+ Settings entries cluster under groups:
323
+
324
+ ```ts
325
+ import { settingsNav } from '@spree/dashboard-core'
326
+
327
+ settingsNav.addGroup({ key: 'integrations', label: 'Integrations', position: 500 })
328
+ settingsNav.add({
329
+ key: 'wishlist-settings',
330
+ label: 'Wishlists',
331
+ path: '/wishlists', // prefixed with /$storeId/settings at render time
332
+ group: 'integrations',
333
+ position: 100,
334
+ })
335
+ ```
336
+
337
+ ### `registerSlot` — named injection points
338
+
339
+ Slots are typed by their context. A `product.form_sidebar` slot receives the product, ambient permissions, store, and user:
340
+
341
+ ```tsx
342
+ import { registerSlot } from '@spree/dashboard-core'
343
+ import { Card } from '@spree/dashboard-ui'
344
+
345
+ registerSlot<{ product: { id: string; wishlist_count: number } }>('product.form_sidebar', {
346
+ id: 'wishlist-count',
347
+ component: ({ product, permissions }) => {
348
+ if (!permissions?.can('manage', 'Spree::Wishlist')) return null
349
+ return <Card>Wishlists: {product.wishlist_count}</Card>
350
+ },
351
+ position: 50,
352
+ // Optional visibility predicate — receives the merged context
353
+ if: (ctx) => ctx.product.wishlist_count > 0,
354
+ })
355
+ ```
356
+
357
+ Built-in slot names follow a `resource.location` convention (e.g. `product.form_sidebar`, `order.page_body`, `product.actions`). The full list lives in the [slots catalog](https://spreecommerce.org/docs/developer/dashboard/slots-catalog).
358
+
359
+ ### `tables` — list-view column extensions
360
+
361
+ ```ts
362
+ import { tables } from '@spree/dashboard-core'
363
+
364
+ tables.products.addColumn({
365
+ key: 'wishlist_count',
366
+ label: 'Wishlists',
367
+ sortable: true,
368
+ render: (product) => product.wishlist_count,
369
+ })
370
+ tables.products.removeColumn('legacy_column')
371
+ tables.products.updateColumn('total', { label: 'Net total' })
372
+ ```
373
+
374
+ **Ordering:** built-in tables register lazily via side-effect imports in route files (`import '@/tables/products'` inside the products route), so calling `tables.products.addColumn(...)` from your plugin's entry module — which loads before any route mounts — would otherwise race the registration. The mutator handles this transparently: mutations on a not-yet-registered table are queued and replayed when `defineTable('products', ...)` runs. Mutations on tables that never register stay pending and never fire, which is the right behavior when your plugin extends an optional feature.
375
+
376
+ ## `defineDashboardPlugin` — one-call facade
377
+
378
+ Every registry in a single declarative config:
379
+
380
+ ```ts
381
+ import { defineDashboardPlugin } from '@spree/dashboard-core/plugin'
382
+
383
+ defineDashboardPlugin({
384
+ nav: [...],
385
+ settingsNavGroups: [...],
386
+ settingsNav: [...],
387
+ slots: { 'product.form_sidebar': [...] },
388
+ tables: {
389
+ products: {
390
+ add: [...],
391
+ remove: ['legacy_column'],
392
+ update: { total: { label: 'Net total' } },
393
+ },
394
+ },
395
+ // Extension fields on built-in forms — hydrate from the fetched resource,
396
+ // persist through the form's own Save. Render the input from a slot widget
397
+ // bound via useHostForm().
398
+ formFields: {
399
+ product: [{ name: 'tech_specs', from: (product) => product?.tech_specs ?? '' }],
400
+ },
401
+ // Replace the input widget for a specific custom field definition.
402
+ customFieldComponents: { 'specs.color': ColorPicker },
403
+ })
404
+ ```
405
+
406
+ Equivalent to calling the underlying registry methods directly. Use whichever feels right — the facade is purely ergonomic.
407
+
408
+ ## What's NOT here
409
+
410
+ Resource-specific hooks (`use-orders`, `use-products`, `use-customers`, …), Zod schemas, feature pages, route definitions, and per-feature locale strings live in `@spree/dashboard` (the app). We do not publish feature internals. Plugin authors compose new pages out of `@spree/dashboard-ui` primitives + `@spree/dashboard-core` infra, not out of forked feature internals.
411
+
412
+ Theme and toast UI live in `@spree/dashboard-ui` (they're rendering concerns, no Spree backend coupling): `ThemeProvider`, `useTheme`, `Toaster`.
413
+
414
+ ## Architectural notes
415
+
416
+ - **Client injection**: `adminClient` reads `import.meta.env.VITE_SPREE_API_URL` at build time. Three deployment topologies (dev proxy, prod single-origin, prod cross-origin) all share one code path. See `src/client.ts` for details.
417
+ - **i18n bootstrap**: `import '@spree/dashboard-core/lib/i18n'` initializes i18next with the base namespace. Must run before any component calls `useTranslation`. The dashboard's `i18n-setup.ts` does this + adds the app's resources via `addResourceBundle`. Plugins follow the same pattern.
418
+ - **Registries are module singletons**: see "Peer dependencies" above. Build your plugin with `@spree/dashboard-core` listed as a peer dep, not a regular dep.
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@spree/dashboard-core",
3
+ "license": "MIT",
4
+ "version": "0.1.0",
5
+ "description": "Spree Dashboard framework — registries (table, nav, slot, settings-nav), providers (auth, permission, store), admin SDK client, generic infra hooks, and the defineDashboardPlugin extension API. Developer Preview.",
6
+ "author": "Vendo Connect Inc.",
7
+ "homepage": "https://spreecommerce.org",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/spree/spree.git",
11
+ "directory": "packages/dashboard-core"
12
+ },
13
+ "keywords": [
14
+ "spree",
15
+ "commerce",
16
+ "ecommerce",
17
+ "admin",
18
+ "dashboard",
19
+ "react",
20
+ "plugin"
21
+ ],
22
+ "type": "module",
23
+ "main": "./src/index.ts",
24
+ "module": "./src/index.ts",
25
+ "types": "./src/index.ts",
26
+ "exports": {
27
+ ".": "./src/index.ts",
28
+ "./plugin": "./src/plugin.ts",
29
+ "./vite": "./src/vite/index.ts",
30
+ "./vite/discover": "./src/vite/discover.ts",
31
+ "./providers/*": "./src/providers/*.tsx",
32
+ "./hooks/*": "./src/hooks/*.ts",
33
+ "./hooks/use-command-palette": "./src/hooks/use-command-palette.tsx",
34
+ "./lib/*": "./src/lib/*.ts",
35
+ "./locales/en.json": "./src/locales/en.json"
36
+ },
37
+ "files": [
38
+ "src",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "peerDependencies": {
43
+ "@hookform/resolvers": "^5.2.2",
44
+ "@tanstack/react-hotkeys": "^0.10.0",
45
+ "@tanstack/react-query": "^5.100.9",
46
+ "@tanstack/react-router": "^1.169.2",
47
+ "i18next": "^26.2.0",
48
+ "react": "^19.2.6",
49
+ "react-dom": "^19.2.6",
50
+ "react-hook-form": "^7.75.0",
51
+ "react-i18next": "^17.0.8",
52
+ "sonner": "^2.0.7",
53
+ "@spree/admin-sdk": "^0.5.0",
54
+ "@spree/dashboard-ui": "^0.1.0"
55
+ },
56
+ "dependencies": {
57
+ "@dnd-kit/core": "^6.3.1",
58
+ "@dnd-kit/sortable": "^10.0.0",
59
+ "@dnd-kit/utilities": "^3.2.2",
60
+ "@tailwindcss/vite": "^4.2.4",
61
+ "date-fns": "^4.1.0",
62
+ "date-fns-tz": "^3.2.0",
63
+ "lucide-react": "^1.14.0",
64
+ "spark-md5": "^3.0.2",
65
+ "zod": "^4.4.3"
66
+ },
67
+ "devDependencies": {
68
+ "@types/react": "^19.2.14",
69
+ "@types/react-dom": "^19.2.3",
70
+ "@types/spark-md5": "^3.0.5",
71
+ "typescript": "~6.0.3",
72
+ "vite": "^8.0.16",
73
+ "vitest": "^4.1.5",
74
+ "@spree/dashboard-ui": "^0.1.0",
75
+ "@spree/admin-sdk": "^0.5.0"
76
+ },
77
+ "scripts": {
78
+ "lint": "biome check src",
79
+ "lint:fix": "biome check --fix src",
80
+ "format": "biome format --write src",
81
+ "typecheck": "tsc --noEmit",
82
+ "test": "vitest run"
83
+ }
84
+ }
package/src/client.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { createAdminClient } from '@spree/admin-sdk'
2
+
3
+ // `VITE_SPREE_API_URL` controls whether the SDK builds relative or absolute URLs.
4
+ // Three deployment topologies, one code path:
5
+ //
6
+ // 1. Dev (default, env unset) — Vite dev proxy forwards `/api/*` to
7
+ // Rails. Browser sees same-origin → no CORS, no preflight, the refresh-token
8
+ // cookie rides under `SameSite=Lax` without HTTPS.
9
+ //
10
+ // 2. Prod single-origin (env unset) — Static-host rewrite (Render/nginx/
11
+ // Cloudflare Worker) forwards `/api/*` to Rails. Same code path as dev,
12
+ // same security posture, no CORS config to maintain. This is the
13
+ // recommended topology for self-hosters.
14
+ //
15
+ // 3. Prod cross-origin (env set) — `VITE_SPREE_API_URL=https://api.shop.com`
16
+ // makes the SDK emit absolute URLs. Backend then needs the origin in
17
+ // `Spree::AllowedOrigin`; cookies switch to `SameSite=None; Secure`
18
+ // (handled server-side by Rails.env). Use when the admin lives on a CDN
19
+ // and you don't want an edge proxy.
20
+ const baseUrl = import.meta.env.VITE_SPREE_API_URL || ''
21
+
22
+ export const adminClient = createAdminClient({
23
+ baseUrl,
24
+ // No initial token — auth-provider bootstraps via /auth/refresh on mount.
25
+ })