@spree/dashboard-core 6.0.2 → 6.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/vite/index.js +81 -5
  2. package/package.json +24 -24
  3. package/src/api-client.ts +7 -1
  4. package/src/client.ts +2 -0
  5. package/src/components/address-form-dialog.tsx +75 -28
  6. package/src/components/app-shell.tsx +5 -4
  7. package/src/components/app-sidebar.tsx +2 -1
  8. package/src/components/bulk-action-bar.tsx +6 -5
  9. package/src/components/country-state-fields.tsx +3 -0
  10. package/src/components/currency-select.tsx +6 -4
  11. package/src/components/developer-inspector-drawer.tsx +322 -0
  12. package/src/components/file-upload-field.tsx +64 -37
  13. package/src/components/image-upload-field.tsx +4 -0
  14. package/src/components/import-wizard-dialog.tsx +14 -12
  15. package/src/components/invitation-acceptance.tsx +15 -11
  16. package/src/components/media-editor-dialog.tsx +131 -0
  17. package/src/components/media-picker-dialog.tsx +866 -0
  18. package/src/components/media-picker-sheet.tsx +13 -300
  19. package/src/components/mobile-top-bar.tsx +1 -1
  20. package/src/components/nav-main.tsx +13 -2
  21. package/src/components/page-header.tsx +57 -13
  22. package/src/components/page-tabs.tsx +1 -1
  23. package/src/components/product-stock-summary.tsx +58 -0
  24. package/src/components/read-row.tsx +38 -0
  25. package/src/components/resource-combobox.tsx +1 -1
  26. package/src/components/resource-image-field.tsx +150 -0
  27. package/src/components/resource-picker-sheet.tsx +2 -2
  28. package/src/components/resource-table.tsx +161 -40
  29. package/src/components/section-heading.tsx +2 -2
  30. package/src/components/settings-container.tsx +19 -0
  31. package/src/components/settings-fields.tsx +165 -0
  32. package/src/components/settings-section.tsx +46 -0
  33. package/src/components/settings-sidebar.tsx +35 -18
  34. package/src/components/sheet-top-bar.tsx +7 -8
  35. package/src/components/sidebar-search.tsx +11 -12
  36. package/src/components/sidebar-user.tsx +39 -44
  37. package/src/components/skip-link.tsx +1 -1
  38. package/src/components/store-switcher.tsx +1 -1
  39. package/src/components/table-toolbar.tsx +194 -331
  40. package/src/components/timezone-combobox.tsx +119 -0
  41. package/src/components/top-bar-breadcrumbs.tsx +1 -1
  42. package/src/hooks/use-stock-locations.ts +6 -2
  43. package/src/hooks/use-table-density.ts +50 -0
  44. package/src/index.ts +20 -2
  45. package/src/lib/curl.test.ts +18 -0
  46. package/src/lib/curl.ts +28 -0
  47. package/src/lib/developer-inspector.test.ts +169 -0
  48. package/src/lib/developer-inspector.ts +134 -0
  49. package/src/lib/docs.ts +3 -2
  50. package/src/lib/fulfillment-items.test.ts +51 -0
  51. package/src/lib/fulfillment-items.ts +53 -0
  52. package/src/lib/request-snippets.ts +41 -0
  53. package/src/lib/table-registry.ts +13 -6
  54. package/src/lib/timestamp-columns.tsx +1 -1
  55. package/src/locales/ar.json +128 -14
  56. package/src/locales/de.json +128 -14
  57. package/src/locales/en.json +128 -14
  58. package/src/locales/es.json +128 -14
  59. package/src/locales/fr.json +128 -14
  60. package/src/locales/ja.json +128 -14
  61. package/src/locales/pl.json +130 -14
  62. package/src/locales/vi.json +128 -14
  63. package/src/locales/zh-CN.json +128 -14
  64. package/src/pages/settings-index.tsx +79 -48
  65. package/src/pages/stock-locations.tsx +14 -6
  66. package/src/products/digital-asset-edit-sheet.tsx +1 -1
  67. package/src/products/digital-asset-provider-sheet.tsx +1 -1
  68. package/src/products/digital-assets-card.tsx +7 -7
  69. package/src/products/inventory-section.tsx +2 -2
  70. package/src/products/media-edit-sheet.tsx +136 -133
  71. package/src/products/product-form-cards.tsx +204 -73
  72. package/src/products/product-form-mapping.test.ts +42 -1
  73. package/src/products/product-form-mapping.ts +20 -0
  74. package/src/products/product-schema.ts +8 -0
  75. package/src/products/product-types.ts +2 -0
  76. package/src/products/use-product-form-data.ts +30 -3
  77. package/src/products/variant-edit-sheet.tsx +3 -3
  78. package/src/products/variants-options-builder.tsx +13 -8
  79. package/src/products/variants-section.tsx +196 -4
  80. package/src/providers/auth-provider.tsx +2 -0
  81. package/src/providers/developer-inspector-provider.tsx +145 -0
  82. package/src/providers/json-link-resolver-provider.tsx +27 -0
  83. package/src/sellers/seller-schema.ts +107 -0
  84. package/src/tables/package-types.tsx +3 -3
  85. package/src/tables/stock-locations.tsx +15 -14
  86. package/src/virtual.d.ts +4 -0
  87. package/src/vite/index.ts +103 -5
@@ -197,7 +197,7 @@ function SignInForm({ invitationId, token, invitation, onAccepted }: FormProps)
197
197
  <>
198
198
  <InvitationIntro invitation={invitation} action={t('admin.invitation.sign_in_to_accept')} />
199
199
  <form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-6">
200
- {errors.root && <p className="text-sm text-destructive">{errors.root.message}</p>}
200
+ {errors.root && <p className="text-base text-destructive">{errors.root.message}</p>}
201
201
  <div className="grid gap-2">
202
202
  <Label htmlFor="invitee-email">{t('admin.fields.email.label')}</Label>
203
203
  <Input id="invitee-email" value={invitation.email} disabled />
@@ -211,7 +211,9 @@ function SignInForm({ invitationId, token, invitation, onAccepted }: FormProps)
211
211
  aria-invalid={!!errors.password || undefined}
212
212
  {...form.register('password')}
213
213
  />
214
- {errors.password && <p className="text-sm text-destructive">{errors.password.message}</p>}
214
+ {errors.password && (
215
+ <p className="text-base text-destructive">{errors.password.message}</p>
216
+ )}
215
217
  </div>
216
218
  <Button type="submit" className="w-full" disabled={isLoading}>
217
219
  {isLoading ? t('admin.invitation.accepting') : t('admin.invitation.sign_in_and_accept')}
@@ -248,7 +250,7 @@ function SignUpForm({ invitationId, token, invitation, onAccepted }: FormProps)
248
250
  <>
249
251
  <InvitationIntro invitation={invitation} action={t('admin.invitation.create_to_accept')} />
250
252
  <form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-6">
251
- {errors.root && <p className="text-sm text-destructive">{errors.root.message}</p>}
253
+ {errors.root && <p className="text-base text-destructive">{errors.root.message}</p>}
252
254
  <div className="grid gap-2">
253
255
  <Label htmlFor="invitee-email">{t('admin.fields.email.label')}</Label>
254
256
  <Input id="invitee-email" value={invitation.email} disabled />
@@ -263,7 +265,7 @@ function SignUpForm({ invitationId, token, invitation, onAccepted }: FormProps)
263
265
  {...form.register('first_name')}
264
266
  />
265
267
  {errors.first_name && (
266
- <p className="text-sm text-destructive">{errors.first_name.message}</p>
268
+ <p className="text-base text-destructive">{errors.first_name.message}</p>
267
269
  )}
268
270
  </div>
269
271
  <div className="grid gap-2">
@@ -274,7 +276,7 @@ function SignUpForm({ invitationId, token, invitation, onAccepted }: FormProps)
274
276
  {...form.register('last_name')}
275
277
  />
276
278
  {errors.last_name && (
277
- <p className="text-sm text-destructive">{errors.last_name.message}</p>
279
+ <p className="text-base text-destructive">{errors.last_name.message}</p>
278
280
  )}
279
281
  </div>
280
282
  </div>
@@ -286,7 +288,9 @@ function SignUpForm({ invitationId, token, invitation, onAccepted }: FormProps)
286
288
  aria-invalid={!!errors.password || undefined}
287
289
  {...form.register('password')}
288
290
  />
289
- {errors.password && <p className="text-sm text-destructive">{errors.password.message}</p>}
291
+ {errors.password && (
292
+ <p className="text-base text-destructive">{errors.password.message}</p>
293
+ )}
290
294
  </div>
291
295
  <div className="grid gap-2">
292
296
  <Label htmlFor="password_confirmation">
@@ -299,7 +303,7 @@ function SignUpForm({ invitationId, token, invitation, onAccepted }: FormProps)
299
303
  {...form.register('password_confirmation')}
300
304
  />
301
305
  {errors.password_confirmation && (
302
- <p className="text-sm text-destructive">{errors.password_confirmation.message}</p>
306
+ <p className="text-base text-destructive">{errors.password_confirmation.message}</p>
303
307
  )}
304
308
  </div>
305
309
  <Button type="submit" className="w-full" disabled={isLoading}>
@@ -336,7 +340,7 @@ function AcceptAsSignedIn({ invitationId, token, invitation, onAccepted }: FormP
336
340
  invitation={invitation}
337
341
  action={t('admin.invitation.signed_in_as_invitee', { email: user?.email })}
338
342
  />
339
- {error && <p className="text-sm text-destructive">{error}</p>}
343
+ {error && <p className="text-base text-destructive">{error}</p>}
340
344
  <Button className="w-full" disabled={isLoading} onClick={onAccept}>
341
345
  {isLoading ? t('admin.invitation.accepting') : t('admin.invitation.accept')}
342
346
  </Button>
@@ -354,7 +358,7 @@ function SignedInAsAnotherAccount({ invitation }: { invitation: InvitationSummar
354
358
  <h1 className="text-2xl font-bold">
355
359
  {t('admin.invitation.join_store', { store: invitation.target.name })}
356
360
  </h1>
357
- <p className="text-sm text-muted-foreground">
361
+ <p className="text-base text-muted-foreground">
358
362
  {t('admin.invitation.signed_in_as_another_account', {
359
363
  email: user?.email,
360
364
  invitee: invitation.email,
@@ -389,7 +393,7 @@ function InvitationIntro({
389
393
  <h1 className="text-2xl font-bold">
390
394
  {t('admin.invitation.join_store', { store: invitation.target.name })}
391
395
  </h1>
392
- <p className="text-sm text-muted-foreground">{`${invitedPart}${rolePart}${action}`}</p>
396
+ <p className="text-base text-muted-foreground">{`${invitedPart}${rolePart}${action}`}</p>
393
397
  </div>
394
398
  )
395
399
  }
@@ -405,7 +409,7 @@ function ErrorCard({ title, message }: { title: string; message: string }) {
405
409
  return (
406
410
  <div className="flex flex-col gap-2">
407
411
  <h1 className="text-2xl font-bold">{title}</h1>
408
- <p className="text-sm text-muted-foreground">{message}</p>
412
+ <p className="text-base text-muted-foreground">{message}</p>
409
413
  </div>
410
414
  )
411
415
  }
@@ -0,0 +1,131 @@
1
+ import {
2
+ Button,
3
+ cn,
4
+ Dialog,
5
+ DialogContent,
6
+ DialogDescription,
7
+ DialogTitle,
8
+ } from '@spree/dashboard-ui'
9
+ import { ArrowLeftIcon } from '@spree/dashboard-ui/icons'
10
+ import type { FormEvent, ReactNode } from 'react'
11
+ import { useTranslation } from 'react-i18next'
12
+
13
+ export interface MediaEditorDialogProps {
14
+ open: boolean
15
+ /** Closing goes through here — Escape, the back button — so a host can ask first. */
16
+ onOpenChange: (open: boolean) => void
17
+ title: ReactNode
18
+ /** Read by screen readers as the editor's description. */
19
+ description?: ReactNode
20
+ /** The top bar's end: Cancel / Save, Discard / Done. */
21
+ actions?: ReactNode
22
+ /** The file itself, centred on the stage — usually a `MediaPreview` with `fit="stage"`. */
23
+ preview: ReactNode
24
+ /** A pill floating under the picture, such as the focal point hint. */
25
+ hint?: ReactNode
26
+ /** Makes the editor a form, so Enter in a field and a submit button in `actions` save. */
27
+ onSubmit?: (event: FormEvent<HTMLFormElement>) => void
28
+ /** The side panel's sections — `MediaEditorSection`s. */
29
+ children: ReactNode
30
+ }
31
+
32
+ /**
33
+ * A full-screen editor for one media file: a top bar with the way out, the
34
+ * file's name and its save actions; the file on a dark stage, as large as the
35
+ * window allows; and a panel of sections beside it.
36
+ *
37
+ * Always dark, whatever the admin's theme — a picture is judged against a
38
+ * neutral ground, the way photo tools show it. Headless about saving: each
39
+ * host keeps its own model (the product form's Done, the library's Save).
40
+ */
41
+ export function MediaEditorDialog({
42
+ open,
43
+ onOpenChange,
44
+ title,
45
+ description,
46
+ actions,
47
+ preview,
48
+ hint,
49
+ onSubmit,
50
+ children,
51
+ }: MediaEditorDialogProps) {
52
+ const { t } = useTranslation()
53
+
54
+ const layout = (
55
+ <>
56
+ <header className="flex h-14 shrink-0 items-center gap-2 border-b border-border px-3">
57
+ <Button
58
+ type="button"
59
+ variant="ghost"
60
+ size="icon-sm"
61
+ onClick={() => onOpenChange(false)}
62
+ aria-label={t('admin.actions.close')}
63
+ >
64
+ <ArrowLeftIcon className="size-4 rtl:rotate-180" />
65
+ </Button>
66
+ <DialogTitle className="min-w-0 truncate font-medium text-base">{title}</DialogTitle>
67
+ {description && <DialogDescription className="sr-only">{description}</DialogDescription>}
68
+ <div className="ms-auto flex shrink-0 items-center gap-2">{actions}</div>
69
+ </header>
70
+
71
+ <div className="flex min-h-0 flex-1 flex-col overflow-y-auto md:flex-row md:overflow-hidden">
72
+ {/* A faint dot grid, so the edge of a picture with a dark or
73
+ transparent background still reads against the stage. */}
74
+ <div className="relative flex min-h-[50vh] flex-1 items-center justify-center bg-[radial-gradient(circle,oklch(1_0_0/0.07)_1px,transparent_1px)] bg-size-[20px_20px] p-6 pb-20 md:min-h-0 md:p-10 md:pb-20">
75
+ {preview}
76
+ {hint && (
77
+ <div className="absolute bottom-5 left-1/2 flex max-w-[calc(100%-2rem)] -translate-x-1/2 items-center gap-2 rounded-full border border-border bg-card px-3 py-1.5 text-base shadow-lg">
78
+ {hint}
79
+ </div>
80
+ )}
81
+ </div>
82
+
83
+ <aside className="flex w-full shrink-0 flex-col gap-3 border-border border-t p-3 md:w-80 md:overflow-y-auto md:border-t-0 md:border-s">
84
+ {children}
85
+ </aside>
86
+ </div>
87
+ </>
88
+ )
89
+
90
+ return (
91
+ <Dialog open={open} onOpenChange={onOpenChange}>
92
+ <DialogContent
93
+ fullScreen
94
+ showCloseButton={false}
95
+ // Edge to edge, and a dark scope for everything inside it.
96
+ className="dark inset-0 gap-0 rounded-none border-0 bg-background p-0 text-foreground"
97
+ >
98
+ {onSubmit ? (
99
+ <form onSubmit={onSubmit} className="flex min-h-0 flex-1 flex-col">
100
+ {layout}
101
+ </form>
102
+ ) : (
103
+ layout
104
+ )}
105
+ </DialogContent>
106
+ </Dialog>
107
+ )
108
+ }
109
+
110
+ /** One card of the editor's side panel. */
111
+ export function MediaEditorSection({
112
+ icon,
113
+ title,
114
+ children,
115
+ className,
116
+ }: {
117
+ icon?: ReactNode
118
+ title: ReactNode
119
+ children: ReactNode
120
+ className?: string
121
+ }) {
122
+ return (
123
+ <section className={cn('flex flex-col gap-4 rounded-xl bg-card p-4', className)}>
124
+ <h3 className="flex items-center gap-2 font-medium text-base [&_svg]:size-4 [&_svg]:text-muted-foreground">
125
+ {icon}
126
+ {title}
127
+ </h3>
128
+ {children}
129
+ </section>
130
+ )
131
+ }