@spree/dashboard-core 0.10.1 → 0.10.2

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.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @spree/dashboard-core
2
2
 
3
- Spree Dashboard framework. Registries, providers, infra hooks, the admin SDK client, and the `defineDashboardPlugin` extension API.
3
+ Spree Dashboard framework — registries, providers, infra hooks, the admin SDK client singleton, and the `defineDashboardPlugin` extension API.
4
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.
5
+ This is the package plugin authors install. Use it to register nav entries, slot widgets, table column extensions, form fields, and settings pages from a separate npm package that `@spree/dashboard` activates at boot.
6
6
 
7
7
  ## Install
8
8
 
@@ -10,409 +10,42 @@ This is the package plugin authors install. Use it to register nav entries, slot
10
10
  pnpm add @spree/dashboard-core @spree/dashboard-ui
11
11
  ```
12
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`.
13
+ **List `@spree/dashboard-core` (and React, the TanStack packages, i18next, …) as peer dependencies in your plugin.** The registries are module singletons — a bundled second copy would write to Maps the app never reads, a silent no-op. The [plugin distribution guide](https://spreecommerce.org/docs/developer/dashboard/plugins/distributing) covers the full peer list.
14
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
15
+ ## Quick start
137
16
 
138
17
  ```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
- }
18
+ import { defineDashboardPlugin } from '@spree/dashboard-core/plugin'
19
+ import { PackageIcon } from 'lucide-react'
20
+ import { WishlistCard } from './wishlist-card'
190
21
 
191
22
  defineDashboardPlugin({
192
- nav: [
193
- { key: 'wishlists', label: 'Wishlists', path: '/wishlists', icon: BarChartIcon, position: 50 },
194
- ],
23
+ nav: [{ key: 'wishlists', label: 'Wishlists', path: '/wishlists', icon: PackageIcon }],
195
24
  slots: {
196
- 'product.form_sidebar': [
197
- { id: 'wishlist-count', component: WishlistsWidget, position: 50 },
198
- ],
25
+ 'product.form_sidebar': [{ id: 'wishlist-count', component: WishlistCard }],
199
26
  },
200
27
  tables: {
201
28
  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: [...],
29
+ add: [{ key: 'wishlist_count', label: 'Wishlists', render: (p) => p.wishlist_count }],
391
30
  remove: ['legacy_column'],
392
- update: { total: { label: 'Net total' } },
393
31
  },
394
32
  },
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 },
33
+ formFields: { product: [{ name: 'tech_specs', from: (p) => p?.tech_specs ?? '' }] },
403
34
  })
404
35
  ```
405
36
 
406
- Equivalent to calling the underlying registry methods directly. Use whichever feels right — the facade is purely ergonomic.
37
+ A second entry point, `@spree/dashboard-core/vite`, provides the Vite plugin that wires Tailwind v4 and plugin auto-discovery for custom hosts.
407
38
 
408
- ## What's NOT here
39
+ ## Documentation
409
40
 
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.
41
+ The full guides live on the docs site — this README intentionally stays short:
411
42
 
412
- Theme and toast UI live in `@spree/dashboard-ui` (they're rendering concerns, no Spree backend coupling): `ThemeProvider`, `useTheme`, `Toaster`.
43
+ - [Dashboard overview](https://spreecommerce.org/docs/developer/dashboard/overview) & [concepts](https://spreecommerce.org/docs/developer/dashboard/concepts)
44
+ - [Customization quickstart](https://spreecommerce.org/docs/developer/dashboard/customization/quickstart) — nav, routes, slots, tables, translations, permissions
45
+ - [Slots catalog](https://spreecommerce.org/docs/developer/dashboard/slots-catalog) — every named injection point
46
+ - [Plugin authoring](https://spreecommerce.org/docs/developer/dashboard/plugins/overview) — scaffolding, publishing, distributing
47
+ - [Public API](https://spreecommerce.org/docs/developer/dashboard/public-api) — everything this package exports, with stability guarantees
413
48
 
414
- ## Architectural notes
49
+ ## License
415
50
 
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.
51
+ MIT
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@spree/dashboard-core",
3
3
  "license": "MIT",
4
- "version": "0.10.1",
4
+ "version": "0.10.2",
5
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
6
  "author": "Vendo Connect Inc.",
7
7
  "homepage": "https://spreecommerce.org",
@@ -102,9 +102,12 @@ function NavItemContent({
102
102
  >
103
103
  <Link to={item.url}>
104
104
  <NavIcon icon={item.icon} isActive={itemIsActive} />
105
- <span>{item.title}</span>
105
+ {/* Explicitly hide in collapsed icon mode: a trailing badge span would
106
+ otherwise steal the `span:last-child` position the base button style
107
+ relies on to hide the label, leaving the title visible. */}
108
+ <span className="group-data-[collapsible=icon]:hidden">{item.title}</span>
106
109
  {item.badge && (
107
- <span className="ml-auto">
110
+ <span className="ml-auto group-data-[collapsible=icon]:hidden">
108
111
  <item.badge />
109
112
  </span>
110
113
  )}