@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.
- package/dist/vite/index.js +81 -5
- package/package.json +24 -24
- package/src/api-client.ts +7 -1
- package/src/client.ts +2 -0
- package/src/components/address-form-dialog.tsx +75 -28
- package/src/components/app-shell.tsx +5 -4
- package/src/components/app-sidebar.tsx +2 -1
- package/src/components/bulk-action-bar.tsx +6 -5
- package/src/components/country-state-fields.tsx +3 -0
- package/src/components/currency-select.tsx +6 -4
- package/src/components/developer-inspector-drawer.tsx +322 -0
- package/src/components/file-upload-field.tsx +64 -37
- package/src/components/image-upload-field.tsx +4 -0
- package/src/components/import-wizard-dialog.tsx +14 -12
- package/src/components/invitation-acceptance.tsx +15 -11
- package/src/components/media-editor-dialog.tsx +131 -0
- package/src/components/media-picker-dialog.tsx +866 -0
- package/src/components/media-picker-sheet.tsx +13 -300
- package/src/components/mobile-top-bar.tsx +1 -1
- package/src/components/nav-main.tsx +13 -2
- package/src/components/page-header.tsx +57 -13
- package/src/components/page-tabs.tsx +1 -1
- package/src/components/product-stock-summary.tsx +58 -0
- package/src/components/read-row.tsx +38 -0
- package/src/components/resource-combobox.tsx +1 -1
- package/src/components/resource-image-field.tsx +150 -0
- package/src/components/resource-picker-sheet.tsx +2 -2
- package/src/components/resource-table.tsx +161 -40
- package/src/components/section-heading.tsx +2 -2
- package/src/components/settings-container.tsx +19 -0
- package/src/components/settings-fields.tsx +165 -0
- package/src/components/settings-section.tsx +46 -0
- package/src/components/settings-sidebar.tsx +35 -18
- package/src/components/sheet-top-bar.tsx +7 -8
- package/src/components/sidebar-search.tsx +11 -12
- package/src/components/sidebar-user.tsx +39 -44
- package/src/components/skip-link.tsx +1 -1
- package/src/components/store-switcher.tsx +1 -1
- package/src/components/table-toolbar.tsx +194 -331
- package/src/components/timezone-combobox.tsx +119 -0
- package/src/components/top-bar-breadcrumbs.tsx +1 -1
- package/src/hooks/use-stock-locations.ts +6 -2
- package/src/hooks/use-table-density.ts +50 -0
- package/src/index.ts +20 -2
- package/src/lib/curl.test.ts +18 -0
- package/src/lib/curl.ts +28 -0
- package/src/lib/developer-inspector.test.ts +169 -0
- package/src/lib/developer-inspector.ts +134 -0
- package/src/lib/docs.ts +3 -2
- package/src/lib/fulfillment-items.test.ts +51 -0
- package/src/lib/fulfillment-items.ts +53 -0
- package/src/lib/request-snippets.ts +41 -0
- package/src/lib/table-registry.ts +13 -6
- package/src/lib/timestamp-columns.tsx +1 -1
- package/src/locales/ar.json +128 -14
- package/src/locales/de.json +128 -14
- package/src/locales/en.json +128 -14
- package/src/locales/es.json +128 -14
- package/src/locales/fr.json +128 -14
- package/src/locales/ja.json +128 -14
- package/src/locales/pl.json +130 -14
- package/src/locales/vi.json +128 -14
- package/src/locales/zh-CN.json +128 -14
- package/src/pages/settings-index.tsx +79 -48
- package/src/pages/stock-locations.tsx +14 -6
- package/src/products/digital-asset-edit-sheet.tsx +1 -1
- package/src/products/digital-asset-provider-sheet.tsx +1 -1
- package/src/products/digital-assets-card.tsx +7 -7
- package/src/products/inventory-section.tsx +2 -2
- package/src/products/media-edit-sheet.tsx +136 -133
- package/src/products/product-form-cards.tsx +204 -73
- package/src/products/product-form-mapping.test.ts +42 -1
- package/src/products/product-form-mapping.ts +20 -0
- package/src/products/product-schema.ts +8 -0
- package/src/products/product-types.ts +2 -0
- package/src/products/use-product-form-data.ts +30 -3
- package/src/products/variant-edit-sheet.tsx +3 -3
- package/src/products/variants-options-builder.tsx +13 -8
- package/src/products/variants-section.tsx +196 -4
- package/src/providers/auth-provider.tsx +2 -0
- package/src/providers/developer-inspector-provider.tsx +145 -0
- package/src/providers/json-link-resolver-provider.tsx +27 -0
- package/src/sellers/seller-schema.ts +107 -0
- package/src/tables/package-types.tsx +3 -3
- package/src/tables/stock-locations.tsx +15 -14
- package/src/virtual.d.ts +4 -0
- package/src/vite/index.ts +103 -5
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cn,
|
|
3
|
+
Field,
|
|
4
|
+
FieldContent,
|
|
5
|
+
FieldError,
|
|
6
|
+
FieldGroup,
|
|
7
|
+
FieldLabel,
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue,
|
|
13
|
+
Switch,
|
|
14
|
+
} from '@spree/dashboard-ui'
|
|
15
|
+
import type { ComponentProps, ReactNode } from 'react'
|
|
16
|
+
import { type Control, Controller, type FieldPath, type FieldValues } from 'react-hook-form'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A settings card's rows, with a rule between each. Rows put the label on the
|
|
20
|
+
* left and the control on the right once the card is wide enough, and stack
|
|
21
|
+
* on a narrow screen.
|
|
22
|
+
*/
|
|
23
|
+
export function SettingsFieldGroup({ className, ...props }: ComponentProps<typeof FieldGroup>) {
|
|
24
|
+
return (
|
|
25
|
+
<FieldGroup
|
|
26
|
+
className={cn(
|
|
27
|
+
'gap-0 divide-y divide-border-subtle *:py-4 *:first:pt-0 *:last:pb-0',
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One setting: its label and help on the left, its control on the right.
|
|
37
|
+
* `horizontal` keeps it a row at any width, for a narrow column whose
|
|
38
|
+
* controls are short anyway.
|
|
39
|
+
*/
|
|
40
|
+
export function SettingsRow({
|
|
41
|
+
label,
|
|
42
|
+
htmlFor,
|
|
43
|
+
help,
|
|
44
|
+
error,
|
|
45
|
+
orientation = 'responsive',
|
|
46
|
+
children,
|
|
47
|
+
}: {
|
|
48
|
+
label: ReactNode
|
|
49
|
+
htmlFor?: string
|
|
50
|
+
help?: ReactNode
|
|
51
|
+
error?: { message?: string }
|
|
52
|
+
orientation?: 'responsive' | 'horizontal'
|
|
53
|
+
children: ReactNode
|
|
54
|
+
}) {
|
|
55
|
+
const horizontal = orientation === 'horizontal'
|
|
56
|
+
return (
|
|
57
|
+
// Centred against the control, which the field would otherwise top-align
|
|
58
|
+
// beside a label and its help.
|
|
59
|
+
<Field
|
|
60
|
+
orientation={orientation}
|
|
61
|
+
className={
|
|
62
|
+
horizontal
|
|
63
|
+
? 'gap-4 has-[>[data-slot=field-content]]:items-center'
|
|
64
|
+
: '@md/field-group:gap-6 @md/field-group:has-[>[data-slot=field-content]]:items-center'
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
<FieldContent>
|
|
68
|
+
<FieldLabel htmlFor={htmlFor}>{label}</FieldLabel>
|
|
69
|
+
{help && <span className="text-xs text-muted-foreground">{help}</span>}
|
|
70
|
+
<FieldError errors={[error]} />
|
|
71
|
+
</FieldContent>
|
|
72
|
+
<div
|
|
73
|
+
className={cn(
|
|
74
|
+
'flex min-w-0 shrink-0 items-center gap-2',
|
|
75
|
+
horizontal ? 'max-w-[60%]' : '@md/field-group:max-w-[50%]',
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</div>
|
|
80
|
+
</Field>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface SelectFieldProps<TValues extends FieldValues> {
|
|
85
|
+
id: string
|
|
86
|
+
label: string
|
|
87
|
+
placeholder?: string
|
|
88
|
+
help?: string
|
|
89
|
+
name: FieldPath<TValues>
|
|
90
|
+
control: Control<TValues>
|
|
91
|
+
options: ReadonlyArray<{ value: string; label: string }>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function SettingsSelectField<TValues extends FieldValues>({
|
|
95
|
+
id,
|
|
96
|
+
label,
|
|
97
|
+
placeholder,
|
|
98
|
+
help,
|
|
99
|
+
name,
|
|
100
|
+
control,
|
|
101
|
+
options,
|
|
102
|
+
}: SelectFieldProps<TValues>) {
|
|
103
|
+
return (
|
|
104
|
+
<Controller
|
|
105
|
+
name={name}
|
|
106
|
+
control={control}
|
|
107
|
+
render={({ field, fieldState }) => (
|
|
108
|
+
<SettingsRow label={label} htmlFor={id} help={help} error={fieldState.error}>
|
|
109
|
+
<Select items={options as never} value={field.value} onValueChange={field.onChange}>
|
|
110
|
+
{/* As wide as its value in a row, rather than half the card. */}
|
|
111
|
+
<SelectTrigger
|
|
112
|
+
id={id}
|
|
113
|
+
aria-invalid={!!fieldState.error || undefined}
|
|
114
|
+
className="@md/field-group:w-auto @md/field-group:min-w-40"
|
|
115
|
+
>
|
|
116
|
+
<SelectValue placeholder={placeholder} />
|
|
117
|
+
</SelectTrigger>
|
|
118
|
+
<SelectContent>
|
|
119
|
+
{options.map((o) => (
|
|
120
|
+
<SelectItem key={o.value} value={o.value}>
|
|
121
|
+
{o.label}
|
|
122
|
+
</SelectItem>
|
|
123
|
+
))}
|
|
124
|
+
</SelectContent>
|
|
125
|
+
</Select>
|
|
126
|
+
</SettingsRow>
|
|
127
|
+
)}
|
|
128
|
+
/>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface SwitchFieldProps<TValues extends FieldValues> {
|
|
133
|
+
id: string
|
|
134
|
+
label: string
|
|
135
|
+
help?: string
|
|
136
|
+
name: FieldPath<TValues>
|
|
137
|
+
control: Control<TValues>
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function SettingsSwitchField<TValues extends FieldValues>({
|
|
141
|
+
id,
|
|
142
|
+
label,
|
|
143
|
+
help,
|
|
144
|
+
name,
|
|
145
|
+
control,
|
|
146
|
+
}: SwitchFieldProps<TValues>) {
|
|
147
|
+
return (
|
|
148
|
+
// A switch sits beside its label at every width.
|
|
149
|
+
<Field orientation="horizontal" className="gap-4 has-[>[data-slot=field-content]]:items-center">
|
|
150
|
+
<FieldContent>
|
|
151
|
+
<FieldLabel htmlFor={id} className="cursor-pointer">
|
|
152
|
+
{label}
|
|
153
|
+
</FieldLabel>
|
|
154
|
+
{help && <span className="text-xs text-muted-foreground">{help}</span>}
|
|
155
|
+
</FieldContent>
|
|
156
|
+
<Controller
|
|
157
|
+
name={name}
|
|
158
|
+
control={control}
|
|
159
|
+
render={({ field }) => (
|
|
160
|
+
<Switch id={id} checked={!!field.value} onCheckedChange={field.onChange} />
|
|
161
|
+
)}
|
|
162
|
+
/>
|
|
163
|
+
</Field>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Card, SectionTitle } from '@spree/dashboard-ui'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
export interface SettingsSectionProps {
|
|
5
|
+
title: ReactNode
|
|
6
|
+
/** An icon element set in a tile before the title, like a `Section` heading. */
|
|
7
|
+
icon?: ReactNode
|
|
8
|
+
description?: ReactNode
|
|
9
|
+
/** Sits at the end of the heading row, such as an Add or Edit button. */
|
|
10
|
+
action?: ReactNode
|
|
11
|
+
/** Classes for the card that holds `children`. */
|
|
12
|
+
className?: string
|
|
13
|
+
children: ReactNode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One block of a settings page: a heading above the card that holds its
|
|
18
|
+
* fields, with any description at the top of the card. The heading sits
|
|
19
|
+
* outside the card, so a page of several blocks reads as titled sections
|
|
20
|
+
* rather than a stack of boxes each carrying its own title bar.
|
|
21
|
+
*/
|
|
22
|
+
export function SettingsSection({
|
|
23
|
+
title,
|
|
24
|
+
icon,
|
|
25
|
+
description,
|
|
26
|
+
action,
|
|
27
|
+
className,
|
|
28
|
+
children,
|
|
29
|
+
}: SettingsSectionProps) {
|
|
30
|
+
return (
|
|
31
|
+
<section className="flex flex-col gap-3">
|
|
32
|
+
<div className="flex items-center justify-between gap-4">
|
|
33
|
+
<SectionTitle>
|
|
34
|
+
{icon}
|
|
35
|
+
{title}
|
|
36
|
+
</SectionTitle>
|
|
37
|
+
{action && <div className="shrink-0">{action}</div>}
|
|
38
|
+
</div>
|
|
39
|
+
<Card className={className}>
|
|
40
|
+
{/* Inside the card, as the first line of what it holds. */}
|
|
41
|
+
{description && <p className="px-4 pt-4 text-muted-foreground text-base">{description}</p>}
|
|
42
|
+
{children}
|
|
43
|
+
</Card>
|
|
44
|
+
</section>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -75,17 +75,22 @@ export function SettingsSidebar({
|
|
|
75
75
|
aria-hidden={!open}
|
|
76
76
|
data-state={open ? 'open' : 'closed'}
|
|
77
77
|
className={cn(
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
'z-30 hidden vh-full shrink-0 overflow-hidden
|
|
83
|
-
open
|
|
78
|
+
// Part of the chrome, beside the icon rail and outside the content
|
|
79
|
+
// sheet: it takes the sidebar's ground. The sheet's own edge divides
|
|
80
|
+
// it from the page; a hairline of the same weight divides it from the
|
|
81
|
+
// rail.
|
|
82
|
+
'z-30 hidden vh-full shrink-0 overflow-hidden text-sidebar-foreground transition-[width,border-color] duration-200 ease-out lg:block',
|
|
83
|
+
open
|
|
84
|
+
? 'border-s border-sidebar-border lg:w-(--spacing-sidebar-width)'
|
|
85
|
+
: 'lg:w-0 border-s-0 border-transparent',
|
|
84
86
|
)}
|
|
85
87
|
>
|
|
86
88
|
<div
|
|
87
89
|
className={cn(
|
|
88
|
-
|
|
90
|
+
// `pt-2` is the inset sidebar's own padding, which the icon rail and
|
|
91
|
+
// the sheet both sit inside, so the header lines up with the store
|
|
92
|
+
// switcher and the sheet's top bar.
|
|
93
|
+
'flex h-full w-(--spacing-sidebar-width) flex-col pt-2 transition-opacity duration-200',
|
|
89
94
|
open ? 'opacity-100 delay-100' : 'pointer-events-none opacity-0',
|
|
90
95
|
)}
|
|
91
96
|
>
|
|
@@ -97,7 +102,7 @@ export function SettingsSidebar({
|
|
|
97
102
|
fade below is a `mask-image` on the scrolling element, and a mask
|
|
98
103
|
applies to sticky descendants too — a header inside it would
|
|
99
104
|
dissolve along with the rows it is meant to stay above. */}
|
|
100
|
-
<SidebarHeader className="shrink-0 gap-2 pb-2">
|
|
105
|
+
<SidebarHeader className="shrink-0 gap-2 px-3 pb-2">
|
|
101
106
|
{/* `h-rail-header-height` matches the store switcher opposite, so the
|
|
102
107
|
two line up. */}
|
|
103
108
|
<div className="flex h-rail-header-height items-center gap-1">
|
|
@@ -109,7 +114,7 @@ export function SettingsSidebar({
|
|
|
109
114
|
>
|
|
110
115
|
<ArrowLeftIcon className="size-4" />
|
|
111
116
|
</Link>
|
|
112
|
-
<span className="truncate font-medium text-
|
|
117
|
+
<span className="truncate font-medium text-base">{t('admin.settings_page.title')}</span>
|
|
113
118
|
</div>
|
|
114
119
|
|
|
115
120
|
<SettingsNavSearch value={query} onValueChange={setQuery} tabIndex={open ? 0 : -1} />
|
|
@@ -124,7 +129,10 @@ export function SettingsSidebar({
|
|
|
124
129
|
top until there is something scrolled past, clear again at the
|
|
125
130
|
bottom once the end is reached. Falls back to a static fade on both
|
|
126
131
|
edges where scroll-driven animations are unsupported. */}
|
|
127
|
-
|
|
132
|
+
{/* `px-1` on top of the groups' own 8px gives the rows the header's
|
|
133
|
+
12px gutter, the main nav's. Set here rather than on the groups,
|
|
134
|
+
which the phone sheet shares at its own spacing. */}
|
|
135
|
+
<div className="quiet-scrollbar scroll-fade min-h-0 flex-1 overflow-y-auto px-1">
|
|
128
136
|
<SettingsNavBody
|
|
129
137
|
tabIndex={open ? 0 : -1}
|
|
130
138
|
tenantId={tenantId}
|
|
@@ -148,14 +156,16 @@ export function SettingsSidebar({
|
|
|
148
156
|
* touch sizing the primary drawer uses.
|
|
149
157
|
*/
|
|
150
158
|
/**
|
|
151
|
-
*
|
|
159
|
+
* Whether the settings rail is on screen: the `lg` breakpoint the settings
|
|
160
|
+
* sheet is bounded by, and below which the settings index is the area's only
|
|
161
|
+
* navigation. `useIsMobile`
|
|
152
162
|
* cannot stand in: it is fixed at 768px, and closing the sheet there would
|
|
153
163
|
* strand it open across the 768-1024px band where it is still the only way to
|
|
154
164
|
* navigate settings.
|
|
155
165
|
*/
|
|
156
166
|
const SETTINGS_SHEET_BREAKPOINT = 1024
|
|
157
167
|
|
|
158
|
-
function
|
|
168
|
+
export function useSettingsRailShown() {
|
|
159
169
|
return useSyncExternalStore(
|
|
160
170
|
(onChange) => {
|
|
161
171
|
const query = window.matchMedia(`(min-width: ${SETTINGS_SHEET_BREAKPOINT}px)`)
|
|
@@ -184,7 +194,7 @@ export function SettingsNavSheet({
|
|
|
184
194
|
// focus trap armed, with nothing on screen to close it. Widening past the
|
|
185
195
|
// breakpoint while the sheet is open also reports the close, so the caller's
|
|
186
196
|
// state does not stay stuck open behind a desktop layout.
|
|
187
|
-
const isDesktop =
|
|
197
|
+
const isDesktop = useSettingsRailShown()
|
|
188
198
|
|
|
189
199
|
useEffect(() => {
|
|
190
200
|
if (isDesktop && open) onOpenChange(false)
|
|
@@ -246,7 +256,10 @@ function SettingsNavSearch({
|
|
|
246
256
|
aria-label={t('admin.settings_page.search_placeholder')}
|
|
247
257
|
clearLabel={t('admin.common.clear')}
|
|
248
258
|
tabIndex={tabIndex}
|
|
249
|
-
|
|
259
|
+
// The main nav's search bar, as a real field: its corner, edge, fill,
|
|
260
|
+
// icon inset and text size, and no shadow, so the two rails' search
|
|
261
|
+
// boxes read as one. Phones keep the larger touch size.
|
|
262
|
+
className="h-8 shadow-none [&>[data-align=inline-start]]:pl-2.5 [&>input]:pl-2! [&>input]:text-base in-data-[mobile=true]:h-11 in-data-[mobile=true]:[&>input]:text-base"
|
|
250
263
|
/>
|
|
251
264
|
)
|
|
252
265
|
}
|
|
@@ -308,14 +321,18 @@ function SettingsNavBody({
|
|
|
308
321
|
)}
|
|
309
322
|
|
|
310
323
|
{visible.groups.length === 0 && (
|
|
311
|
-
<p className="px-4 py-2 text-
|
|
324
|
+
<p className="px-4 py-2 text-base text-sidebar-foreground/70">
|
|
312
325
|
{t('admin.settings_page.no_results', { query: query.trim() })}
|
|
313
326
|
</p>
|
|
314
327
|
)}
|
|
315
328
|
|
|
316
329
|
{visible.groups.map(({ group, entries }) => (
|
|
317
|
-
|
|
318
|
-
|
|
330
|
+
// Labels sit a size and a shade under the links, with air above each
|
|
331
|
+
// group, so the list reads as sections rather than one long column.
|
|
332
|
+
<SidebarGroup key={group.key} className="not-first:pt-3">
|
|
333
|
+
<SidebarGroupLabel className="h-7 font-normal text-muted-foreground">
|
|
334
|
+
{resolveNavLabel(group, t)}
|
|
335
|
+
</SidebarGroupLabel>
|
|
319
336
|
<SidebarMenu>
|
|
320
337
|
{entries.map((entry) => (
|
|
321
338
|
<SettingsItem
|
|
@@ -357,7 +374,7 @@ function SettingsItem({
|
|
|
357
374
|
via `useSidebar()`, which would fire spuriously in this secondary nav. */}
|
|
358
375
|
<SidebarMenuButton asChild isActive={isActive}>
|
|
359
376
|
<Link to={url} tabIndex={tabIndex}>
|
|
360
|
-
<NavIcon icon={Icon} isActive={isActive} />
|
|
377
|
+
<NavIcon icon={Icon} isActive={isActive} muted />
|
|
361
378
|
{/* `truncate` so a long translated label gives way to the badge
|
|
362
379
|
rather than pushing it out of the sidebar. */}
|
|
363
380
|
<span className="truncate">{label}</span>
|
|
@@ -21,23 +21,22 @@ export function SheetTopBar({
|
|
|
21
21
|
}: {
|
|
22
22
|
tenantId?: string
|
|
23
23
|
/**
|
|
24
|
-
* Hides the
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* Hides the bar. Inside settings the settings nav marks where you are and
|
|
25
|
+
* every record page carries its own back button, so a trail would only
|
|
26
|
+
* repeat them. The rail trigger goes too: the rail is collapsed by the app
|
|
27
|
+
* there, and a control offering to re-expand a rail the area will just
|
|
28
|
+
* re-collapse would not do what it says.
|
|
28
29
|
*/
|
|
29
30
|
inSettings?: boolean
|
|
30
31
|
}) {
|
|
31
32
|
const { isMobile, state } = useSidebar()
|
|
32
33
|
const collapsed = state === 'collapsed' && !isMobile
|
|
33
34
|
|
|
34
|
-
if (!collapsed) return null
|
|
35
|
+
if (!collapsed || inSettings) return null
|
|
35
36
|
|
|
36
37
|
return (
|
|
37
38
|
<div className="hidden min-h-8 items-center gap-2 px-4 pt-3 md:flex">
|
|
38
|
-
|
|
39
|
-
<SidebarTrigger className="-ms-1 shrink-0 text-muted-foreground hover:bg-accent hover:text-foreground" />
|
|
40
|
-
)}
|
|
39
|
+
<SidebarTrigger className="-ms-1 shrink-0 text-muted-foreground hover:bg-accent hover:text-foreground" />
|
|
41
40
|
<TopBarBreadcrumbs tenantId={tenantId} />
|
|
42
41
|
</div>
|
|
43
42
|
)
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
TooltipTrigger,
|
|
7
7
|
useSidebar,
|
|
8
8
|
} from '@spree/dashboard-ui'
|
|
9
|
-
import {
|
|
9
|
+
import { SearchIcon } from '@spree/dashboard-ui/icons'
|
|
10
10
|
import { useTranslation } from 'react-i18next'
|
|
11
11
|
import { useOptionalCommandPalette } from '../hooks/use-command-palette'
|
|
12
12
|
|
|
@@ -30,10 +30,9 @@ export function SidebarSearch() {
|
|
|
30
30
|
const palette = useOptionalCommandPalette()
|
|
31
31
|
const { isMobile, state } = useSidebar()
|
|
32
32
|
const isCollapsed = state === 'collapsed' && !isMobile
|
|
33
|
-
//
|
|
34
|
-
// and the
|
|
35
|
-
|
|
36
|
-
const label = t('admin.components.command_palette.search_label')
|
|
33
|
+
// Just "Search": the bar is an affordance, not a field, so it names the
|
|
34
|
+
// action and leaves the palette's own placeholder to say what is searchable.
|
|
35
|
+
const label = t('admin.actions.search')
|
|
37
36
|
|
|
38
37
|
if (!palette) return null
|
|
39
38
|
|
|
@@ -46,25 +45,25 @@ export function SidebarSearch() {
|
|
|
46
45
|
type="button"
|
|
47
46
|
onClick={() => palette.setOpen(true)}
|
|
48
47
|
aria-label={isCollapsed ? label : undefined}
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
48
|
+
// A field's fill and edge, without its shadow: it reads as the
|
|
49
|
+
// search box it opens, and stays flat on the rail. Focus is the
|
|
50
|
+
// inputs' soft blue glow from `--ring`.
|
|
52
51
|
className={
|
|
53
52
|
isCollapsed
|
|
54
53
|
? // `size-10` matches what a nav item collapses to, so the rail reads
|
|
55
54
|
// as one column of equal targets rather than a search box that
|
|
56
55
|
// shrank. The glyph stays 16px, as `NavIcon` draws it.
|
|
57
56
|
'flex size-10 items-center justify-center rounded-lg text-muted-foreground outline-none transition-colors duration-100 ease-out hover:bg-sidebar-accent-hover hover:text-foreground focus-visible:shadow-[0_0_0_3px_color-mix(in_srgb,var(--ring)_15%,transparent)]'
|
|
58
|
-
: 'flex w-full cursor-pointer items-center gap-2 rounded-
|
|
57
|
+
: 'flex w-full cursor-pointer items-center gap-2 rounded-xl border border-field-border bg-card py-1.5 ps-2.5 pe-2.5 text-muted-foreground text-base outline-none transition-[color,border-color,box-shadow] duration-100 ease-out hover:text-foreground focus-visible:border-ring focus-visible:shadow-[0_0_0_3px_color-mix(in_srgb,var(--ring)_15%,transparent)]'
|
|
59
58
|
}
|
|
60
59
|
>
|
|
61
60
|
<SearchIcon className="size-4 shrink-0" />
|
|
62
61
|
{!isCollapsed && (
|
|
63
62
|
<>
|
|
64
63
|
<span className="flex-1 truncate text-left">{label}</span>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
{/* The shortcut as quiet text, not a keycap: a hint, not a control. */}
|
|
65
|
+
<kbd className="hidden font-sans text-xs tabular-nums text-muted-foreground sm:inline">
|
|
66
|
+
{IS_MAC ? '⌘K' : 'Ctrl K'}
|
|
68
67
|
</kbd>
|
|
69
68
|
</>
|
|
70
69
|
)}
|
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
DropdownMenu,
|
|
6
6
|
DropdownMenuContent,
|
|
7
7
|
DropdownMenuItem,
|
|
8
|
-
DropdownMenuLabel,
|
|
9
8
|
DropdownMenuSeparator,
|
|
9
|
+
DropdownMenuShortcut,
|
|
10
10
|
DropdownMenuTrigger,
|
|
11
11
|
LanguageMenuItems,
|
|
12
12
|
SidebarMenu,
|
|
@@ -16,21 +16,24 @@ import {
|
|
|
16
16
|
} from '@spree/dashboard-ui'
|
|
17
17
|
import {
|
|
18
18
|
BookOpenIcon,
|
|
19
|
-
ExternalLinkIcon,
|
|
20
19
|
LogOutIcon,
|
|
21
|
-
MailIcon,
|
|
22
20
|
MessageCircleIcon,
|
|
21
|
+
MessageSquareIcon,
|
|
23
22
|
MoreHorizontalIcon,
|
|
24
|
-
|
|
23
|
+
TerminalIcon,
|
|
24
|
+
UserPenIcon,
|
|
25
25
|
} from '@spree/dashboard-ui/icons'
|
|
26
|
+
import { useFormatForDisplay } from '@tanstack/react-hotkeys'
|
|
26
27
|
import { useTranslation } from 'react-i18next'
|
|
27
28
|
import { useAuth } from '../hooks/use-auth'
|
|
28
29
|
import { useSwitchAdminLocale } from '../hooks/use-switch-admin-locale'
|
|
29
|
-
import { COMMUNITY_URL,
|
|
30
|
+
import { COMMUNITY_URL, DOCS_BASE_URL } from '../lib/docs'
|
|
30
31
|
import { getInitials } from '../lib/formatters'
|
|
31
32
|
import { i18n } from '../lib/i18n'
|
|
32
|
-
import {
|
|
33
|
-
|
|
33
|
+
import {
|
|
34
|
+
DEVELOPER_INSPECTOR_HOTKEY,
|
|
35
|
+
useOptionalDeveloperInspector,
|
|
36
|
+
} from '../providers/developer-inspector-provider'
|
|
34
37
|
|
|
35
38
|
/**
|
|
36
39
|
* The account menu, sitting at the foot of the primary sidebar.
|
|
@@ -39,10 +42,6 @@ import { useOptionalStore } from '../providers/store-provider'
|
|
|
39
42
|
* language, the support links and sign out — because removing that bar left
|
|
40
43
|
* the account with nowhere else to live, and the foot of the nav is where a
|
|
41
44
|
* merchant already looks for it.
|
|
42
|
-
*
|
|
43
|
-
* `useOptionalStore` rather than `useStore`: the seller panel mounts this
|
|
44
|
-
* under a `TenantProvider` with no store at all, and a hard `useStore` would
|
|
45
|
-
* throw there. A panel without a store simply gets no "View store" entry.
|
|
46
45
|
*/
|
|
47
46
|
export function SidebarUser({
|
|
48
47
|
uiLocales = [],
|
|
@@ -62,8 +61,9 @@ export function SidebarUser({
|
|
|
62
61
|
const { isMobile, state } = useSidebar()
|
|
63
62
|
const { user, logout } = useAuth()
|
|
64
63
|
const switchAdminLocale = useSwitchAdminLocale()
|
|
65
|
-
|
|
66
|
-
const
|
|
64
|
+
// Only panels that mount the inspector offer it.
|
|
65
|
+
const inspector = useOptionalDeveloperInspector()
|
|
66
|
+
const formatHotkey = useFormatForDisplay()
|
|
67
67
|
|
|
68
68
|
// The desktop rail's icon mode. The mobile drawer is full width whatever
|
|
69
69
|
// that says, so honouring it there would hide the name for no reason.
|
|
@@ -100,7 +100,7 @@ export function SidebarUser({
|
|
|
100
100
|
</Avatar>
|
|
101
101
|
{!inBar && !isCollapsed && (
|
|
102
102
|
<>
|
|
103
|
-
<span className="min-w-0 flex-1 truncate text-left font-medium text-
|
|
103
|
+
<span className="min-w-0 flex-1 truncate text-left font-medium text-base text-foreground">
|
|
104
104
|
{user.full_name || user.email}
|
|
105
105
|
</span>
|
|
106
106
|
<MoreHorizontalIcon className="size-4 shrink-0 text-muted-foreground" />
|
|
@@ -129,7 +129,7 @@ export function SidebarUser({
|
|
|
129
129
|
{initials}
|
|
130
130
|
</AvatarFallback>
|
|
131
131
|
</Avatar>
|
|
132
|
-
<div className="grid min-w-0 flex-1 text-
|
|
132
|
+
<div className="grid min-w-0 flex-1 text-base leading-tight">
|
|
133
133
|
<span className="truncate font-medium text-foreground">
|
|
134
134
|
{user.full_name || user.email}
|
|
135
135
|
</span>
|
|
@@ -139,9 +139,15 @@ export function SidebarUser({
|
|
|
139
139
|
</div>
|
|
140
140
|
</div>
|
|
141
141
|
<DropdownMenuSeparator />
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
{onEditProfile && (
|
|
143
|
+
<>
|
|
144
|
+
<DropdownMenuItem onClick={onEditProfile}>
|
|
145
|
+
<UserPenIcon className="size-4" />
|
|
146
|
+
{t('admin.account.edit_profile')}
|
|
147
|
+
</DropdownMenuItem>
|
|
148
|
+
<DropdownMenuSeparator />
|
|
149
|
+
</>
|
|
150
|
+
)}
|
|
145
151
|
<ThemeMenuItems />
|
|
146
152
|
{/* Select-style pill; self-hides when < 2 languages are installed. */}
|
|
147
153
|
<LanguageMenuItems
|
|
@@ -150,30 +156,19 @@ export function SidebarUser({
|
|
|
150
156
|
value={i18n.language}
|
|
151
157
|
onSelect={handleSelectLocale}
|
|
152
158
|
/>
|
|
153
|
-
|
|
154
|
-
{onEditProfile && (
|
|
159
|
+
{inspector && (
|
|
155
160
|
<>
|
|
156
|
-
<DropdownMenuItem onClick={onEditProfile}>
|
|
157
|
-
<UserIcon className="size-4" />
|
|
158
|
-
{t('admin.account.edit_profile')}
|
|
159
|
-
</DropdownMenuItem>
|
|
160
161
|
<DropdownMenuSeparator />
|
|
162
|
+
<DropdownMenuItem onClick={() => inspector.setOpen(true)}>
|
|
163
|
+
<TerminalIcon className="size-4" />
|
|
164
|
+
{t('admin.components.developer_inspector.menu_open')}
|
|
165
|
+
<DropdownMenuShortcut>
|
|
166
|
+
{formatHotkey(DEVELOPER_INSPECTOR_HOTKEY)}
|
|
167
|
+
</DropdownMenuShortcut>
|
|
168
|
+
</DropdownMenuItem>
|
|
161
169
|
</>
|
|
162
170
|
)}
|
|
163
|
-
|
|
164
|
-
belongs with the other outbound links rather than as a button
|
|
165
|
-
competing with the nav. */}
|
|
166
|
-
{viewStoreHref && (
|
|
167
|
-
<DropdownMenuItem asChild>
|
|
168
|
-
<a href={viewStoreHref} target="_blank" rel="noopener noreferrer">
|
|
169
|
-
<ExternalLinkIcon className="size-4" />
|
|
170
|
-
{t('admin.account.view_store')}
|
|
171
|
-
</a>
|
|
172
|
-
</DropdownMenuItem>
|
|
173
|
-
)}
|
|
174
|
-
{/* All three carried over from the old top bar as inert rows — they
|
|
175
|
-
looked like links and did nothing. Destinations live in `lib/docs`
|
|
176
|
-
beside the docs base so a domain move is one edit. */}
|
|
171
|
+
<DropdownMenuSeparator />
|
|
177
172
|
<DropdownMenuItem asChild>
|
|
178
173
|
<a href={DOCS_BASE_URL} target="_blank" rel="noopener noreferrer">
|
|
179
174
|
<BookOpenIcon className="size-4" />
|
|
@@ -186,12 +181,12 @@ export function SidebarUser({
|
|
|
186
181
|
{t('admin.account.community')}
|
|
187
182
|
</a>
|
|
188
183
|
</DropdownMenuItem>
|
|
189
|
-
|
|
190
|
-
<
|
|
191
|
-
<
|
|
192
|
-
{t('admin.account.
|
|
193
|
-
</
|
|
194
|
-
|
|
184
|
+
{inspector?.sendFeedback && (
|
|
185
|
+
<DropdownMenuItem onClick={() => inspector.sendFeedback?.()}>
|
|
186
|
+
<MessageSquareIcon className="size-4" />
|
|
187
|
+
{t('admin.account.send_feedback')}
|
|
188
|
+
</DropdownMenuItem>
|
|
189
|
+
)}
|
|
195
190
|
<DropdownMenuSeparator />
|
|
196
191
|
<DropdownMenuItem onClick={logout}>
|
|
197
192
|
<LogOutIcon className="size-4" />
|
|
@@ -24,7 +24,7 @@ export function SkipLink({ targetId = 'main-content' }: { targetId?: string }) {
|
|
|
24
24
|
'sr-only',
|
|
25
25
|
'focus-visible:not-sr-only focus-visible:fixed focus-visible:top-3 focus-visible:left-3',
|
|
26
26
|
'focus-visible:z-50 focus-visible:rounded-lg focus-visible:bg-card',
|
|
27
|
-
'focus-visible:px-4 focus-visible:py-2 focus-visible:text-
|
|
27
|
+
'focus-visible:px-4 focus-visible:py-2 focus-visible:text-base focus-visible:font-medium',
|
|
28
28
|
'focus-visible:text-foreground focus-visible:shadow-md',
|
|
29
29
|
'focus-visible:border focus-visible:border-border-control',
|
|
30
30
|
'focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50',
|
|
@@ -51,7 +51,7 @@ export function StoreSwitcher() {
|
|
|
51
51
|
<AvatarFallback>{storeInitials}</AvatarFallback>
|
|
52
52
|
</Avatar>
|
|
53
53
|
{!isCollapsed && (
|
|
54
|
-
<div className="grid flex-1 text-left text-
|
|
54
|
+
<div className="grid flex-1 text-left text-base leading-tight">
|
|
55
55
|
<span className="truncate font-medium text-foreground">{store?.name}</span>
|
|
56
56
|
</div>
|
|
57
57
|
)}
|