@podoba/react 0.0.32 → 0.0.35
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/package.json +3 -3
- package/src/components/asset-masonry-grid.examples.tsx +9 -0
- package/src/components/asset-masonry-grid.tsx +62 -0
- package/src/components/asset-selection-surface.examples.tsx +32 -0
- package/src/components/asset-selection-surface.tsx +184 -0
- package/src/components/brand-page-header.tsx +297 -67
- package/src/components/button.tsx +27 -4
- package/src/components/combobox.tsx +1 -1
- package/src/components/compact-action-button.examples.tsx +7 -0
- package/src/components/compact-action-button.tsx +21 -0
- package/src/components/compact-settings-dialog.examples.tsx +15 -0
- package/src/components/compact-settings-dialog.tsx +48 -0
- package/src/components/context-action-glyph.examples.tsx +9 -0
- package/src/components/context-action-glyph.tsx +61 -0
- package/src/components/context-menu.tsx +191 -107
- package/src/components/context-search-panel.examples.tsx +9 -0
- package/src/components/context-search-panel.tsx +50 -0
- package/src/components/csv-binding-presentation.examples.tsx +14 -0
- package/src/components/csv-binding-presentation.tsx +40 -0
- package/src/components/cta-pill.tsx +19 -4
- package/src/components/dashboard-grid.tsx +1 -1
- package/src/components/date-picker.tsx +5 -3
- package/src/components/date-selection-calendar.examples.tsx +8 -0
- package/src/components/date-selection-calendar.tsx +47 -0
- package/src/components/delivery/send-to-print-modal.tsx +351 -84
- package/src/components/dialog-action-button.examples.tsx +7 -0
- package/src/components/dialog-action-button.tsx +22 -0
- package/src/components/dialog.tsx +32 -12
- package/src/components/document-upload-panel.examples.tsx +12 -0
- package/src/components/document-upload-panel.tsx +48 -0
- package/src/components/dropdown-menu.tsx +5 -3
- package/src/components/empty-panel-action.examples.tsx +7 -0
- package/src/components/empty-panel-action.tsx +8 -0
- package/src/components/field-appearance.ts +14 -0
- package/src/components/focus-field.tsx +3 -3
- package/src/components/icons.tsx +30 -0
- package/src/components/input.examples.tsx +8 -0
- package/src/components/input.tsx +13 -5
- package/src/components/media-asset-panel.examples.tsx +11 -0
- package/src/components/media-asset-panel.tsx +58 -0
- package/src/components/media-gallery.examples.tsx +12 -0
- package/src/components/media-gallery.tsx +53 -0
- package/src/components/media-settings-dialog.examples.tsx +21 -0
- package/src/components/media-settings-dialog.tsx +83 -0
- package/src/components/preview-info-card.examples.tsx +18 -0
- package/src/components/preview-info-card.tsx +27 -0
- package/src/components/reload-icon.examples.tsx +7 -0
- package/src/components/reload-icon.tsx +6 -0
- package/src/components/request-changes-modal.examples.tsx +26 -0
- package/src/components/request-changes-modal.tsx +36 -19
- package/src/components/rich-text-editor.tsx +1 -1
- package/src/components/select.examples.tsx +17 -0
- package/src/components/select.tsx +78 -22
- package/src/components/settings-dialog-surface.examples.tsx +44 -0
- package/src/components/settings-dialog-surface.tsx +240 -0
- package/src/components/side-panel.examples.tsx +80 -0
- package/src/components/side-panel.tsx +140 -0
- package/src/components/subtle.tsx +64 -8
- package/src/components/table.examples.tsx +9 -0
- package/src/components/table.tsx +29 -12
- package/src/components/template-catalog-card.examples.tsx +25 -0
- package/src/components/template-catalog-card.tsx +178 -0
- package/src/components/text.tsx +14 -1
- package/src/components/textarea.examples.tsx +8 -0
- package/src/components/textarea.tsx +16 -6
- package/src/components/tile.tsx +16 -17
- package/src/editor/block-editor.tsx +56 -14
- package/src/index.ts +20 -0
- package/src/layout/app-shell.tsx +13 -9
- package/src/layout/topbar.tsx +9 -12
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
// field is aria-invalid when invalid; the server error is role="alert"; confirm
|
|
22
22
|
// shows a pending state and disables while in flight / invalid.
|
|
23
23
|
|
|
24
|
-
import { useMemo, useState } from 'react'
|
|
24
|
+
import { useId, useMemo, useState } from 'react'
|
|
25
25
|
import { Button } from '../button'
|
|
26
|
-
import {
|
|
26
|
+
import { ModalDialog, ModalOverlay, ModalSurface } from '../dialog'
|
|
27
27
|
import { Input } from '../input'
|
|
28
28
|
|
|
29
29
|
/** The validated print spec the modal yields on confirm. */
|
|
@@ -36,6 +36,20 @@ export interface PrintSpec {
|
|
|
36
36
|
quantity: number
|
|
37
37
|
/** Delivery address — non-blank. */
|
|
38
38
|
address: string
|
|
39
|
+
/** Company / recipient. */
|
|
40
|
+
company?: string
|
|
41
|
+
/** Street address. */
|
|
42
|
+
street?: string
|
|
43
|
+
/** City. */
|
|
44
|
+
city?: string
|
|
45
|
+
/** ZIP / postcode. */
|
|
46
|
+
zip?: string
|
|
47
|
+
/** Contact person. */
|
|
48
|
+
contactName?: string
|
|
49
|
+
/** Contact phone. */
|
|
50
|
+
phone?: string
|
|
51
|
+
/** Optional print-production note. */
|
|
52
|
+
note?: string
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
/** Strings the send-to-print modal renders — supplied by the app (i18n). */
|
|
@@ -50,6 +64,7 @@ export interface SendToPrintModalLabels {
|
|
|
50
64
|
/** Material field label + placeholder. */
|
|
51
65
|
materialLabel: string
|
|
52
66
|
materialPlaceholder: string
|
|
67
|
+
materialDefault?: string
|
|
53
68
|
/** Quantity field label + placeholder. */
|
|
54
69
|
quantityLabel: string
|
|
55
70
|
quantityPlaceholder: string
|
|
@@ -64,6 +79,34 @@ export interface SendToPrintModalLabels {
|
|
|
64
79
|
confirm: string
|
|
65
80
|
/** Confirm button — pending state. */
|
|
66
81
|
submitting: string
|
|
82
|
+
/** Optional old-Manager section label. */
|
|
83
|
+
sectionLabel?: string
|
|
84
|
+
outputName?: string
|
|
85
|
+
outputDescription?: string
|
|
86
|
+
/** Optional no-material message. */
|
|
87
|
+
noMaterialConfigured?: string
|
|
88
|
+
companyLabel?: string
|
|
89
|
+
companyPlaceholder?: string
|
|
90
|
+
streetLabel?: string
|
|
91
|
+
streetPlaceholder?: string
|
|
92
|
+
cityLabel?: string
|
|
93
|
+
cityPlaceholder?: string
|
|
94
|
+
zipLabel?: string
|
|
95
|
+
zipPlaceholder?: string
|
|
96
|
+
contactNameLabel?: string
|
|
97
|
+
contactNamePlaceholder?: string
|
|
98
|
+
phoneLabel?: string
|
|
99
|
+
phonePlaceholder?: string
|
|
100
|
+
noteLabel?: string
|
|
101
|
+
notePlaceholder?: string
|
|
102
|
+
priceEyebrow?: string
|
|
103
|
+
priceIdle?: string
|
|
104
|
+
closeLabel?: string
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface PrintMaterialOption {
|
|
108
|
+
id: string
|
|
109
|
+
label: string
|
|
67
110
|
}
|
|
68
111
|
|
|
69
112
|
export interface SendToPrintModalProps {
|
|
@@ -79,6 +122,16 @@ export interface SendToPrintModalProps {
|
|
|
79
122
|
error?: string
|
|
80
123
|
/** App-supplied i18n strings. */
|
|
81
124
|
labels: SendToPrintModalLabels
|
|
125
|
+
/** Output title shown in the old Manager print-order summary card. */
|
|
126
|
+
outputName?: string
|
|
127
|
+
/** Output dimension / format line shown in the old Manager print-order summary card. */
|
|
128
|
+
outputDescription?: string
|
|
129
|
+
/** Available print materials. Empty means the old Manager disabled the order action. */
|
|
130
|
+
materialOptions?: PrintMaterialOption[]
|
|
131
|
+
/** Initial quantity. Old Manager opens print with a production-like quantity instead of blank. */
|
|
132
|
+
initialQuantity?: number
|
|
133
|
+
/** Modal family. Dashboard Workspace print orders use the old wider source panel. */
|
|
134
|
+
surfaceVariant?: 'standard' | 'dashboard'
|
|
82
135
|
/** Optional test id forwarded to the dialog. */
|
|
83
136
|
'data-testid'?: string
|
|
84
137
|
}
|
|
@@ -92,6 +145,12 @@ function parseQuantity(raw: string): number | null {
|
|
|
92
145
|
return value
|
|
93
146
|
}
|
|
94
147
|
|
|
148
|
+
function capitalizeFirstAddressLetter(value: string): string {
|
|
149
|
+
const index = value.search(/\p{L}/u)
|
|
150
|
+
if (index < 0) return value
|
|
151
|
+
return `${value.slice(0, index)}${value.charAt(index).toUpperCase()}${value.slice(index + 1)}`
|
|
152
|
+
}
|
|
153
|
+
|
|
95
154
|
/** Send-to-print modal with validated size / material / quantity / address. */
|
|
96
155
|
export function SendToPrintModal({
|
|
97
156
|
isOpen,
|
|
@@ -100,31 +159,69 @@ export function SendToPrintModal({
|
|
|
100
159
|
isPending = false,
|
|
101
160
|
error,
|
|
102
161
|
labels,
|
|
162
|
+
outputName,
|
|
163
|
+
outputDescription,
|
|
164
|
+
materialOptions = [],
|
|
165
|
+
initialQuantity = 500,
|
|
166
|
+
surfaceVariant = 'standard',
|
|
103
167
|
'data-testid': testId,
|
|
104
168
|
}: SendToPrintModalProps): React.ReactNode {
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
169
|
+
const titleId = useId()
|
|
170
|
+
const descriptionId = useId()
|
|
171
|
+
// `materialOptions` is normally fetched, so it is [] on mount. Seeding useState
|
|
172
|
+
// from it captures that empty first render forever; deriving the fallback each
|
|
173
|
+
// render selects the first option as soon as the list arrives. '' means
|
|
174
|
+
// "untouched", not "nothing selected".
|
|
175
|
+
const [materialId, setMaterialId] = useState('')
|
|
176
|
+
const [sizeRaw, setSizeRaw] = useState('')
|
|
177
|
+
const [quantityRaw, setQuantityRaw] = useState(String(initialQuantity))
|
|
178
|
+
const [company, setCompany] = useState('')
|
|
179
|
+
const [street, setStreet] = useState('')
|
|
180
|
+
const [city, setCity] = useState('')
|
|
181
|
+
const [zip, setZip] = useState('')
|
|
182
|
+
const [contactName, setContactName] = useState('')
|
|
183
|
+
const [phone, setPhone] = useState('')
|
|
184
|
+
const [note, setNote] = useState('')
|
|
109
185
|
|
|
110
186
|
const quantity = useMemo(() => parseQuantity(quantityRaw), [quantityRaw])
|
|
187
|
+
const effectiveMaterialId = materialId || (materialOptions[0]?.id ?? '')
|
|
188
|
+
const selectedMaterial = useMemo(
|
|
189
|
+
() => materialOptions.find((option) => option.id === effectiveMaterialId) ?? null,
|
|
190
|
+
[effectiveMaterialId, materialOptions],
|
|
191
|
+
)
|
|
192
|
+
// `size` is the caller's output description when it supplies one, and a collected
|
|
193
|
+
// field otherwise — the documented `print_specs` rule is that it is non-blank, and
|
|
194
|
+
// `outputDescription` is optional, so it cannot be the only source.
|
|
195
|
+
const derivedSize = outputDescription?.trim() ?? ''
|
|
196
|
+
const size = derivedSize || sizeRaw.trim()
|
|
111
197
|
// Surface the quantity error only once the user has typed something invalid
|
|
112
198
|
// (not on the initial blank state — that would scream before any input).
|
|
113
199
|
const quantityInvalid = quantityRaw.trim().length > 0 && quantity === null
|
|
114
200
|
|
|
115
201
|
const reset = (): void => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
setQuantityRaw(
|
|
119
|
-
|
|
202
|
+
setMaterialId('')
|
|
203
|
+
setSizeRaw('')
|
|
204
|
+
setQuantityRaw(String(initialQuantity))
|
|
205
|
+
setCompany('')
|
|
206
|
+
setStreet('')
|
|
207
|
+
setCity('')
|
|
208
|
+
setZip('')
|
|
209
|
+
setContactName('')
|
|
210
|
+
setPhone('')
|
|
211
|
+
setNote('')
|
|
120
212
|
}
|
|
121
213
|
|
|
122
214
|
const canConfirm =
|
|
123
215
|
!isPending &&
|
|
124
|
-
size.
|
|
125
|
-
material.trim().length > 0 &&
|
|
216
|
+
size.length > 0 &&
|
|
126
217
|
quantity !== null &&
|
|
127
|
-
|
|
218
|
+
selectedMaterial !== null &&
|
|
219
|
+
company.trim().length > 0 &&
|
|
220
|
+
street.trim().length > 0 &&
|
|
221
|
+
city.trim().length > 0 &&
|
|
222
|
+
zip.trim().length > 0 &&
|
|
223
|
+
contactName.trim().length > 0 &&
|
|
224
|
+
phone.trim().length > 0
|
|
128
225
|
|
|
129
226
|
const handleOpenChange = (open: boolean): void => {
|
|
130
227
|
if (!open) reset()
|
|
@@ -132,85 +229,255 @@ export function SendToPrintModal({
|
|
|
132
229
|
}
|
|
133
230
|
|
|
134
231
|
const handleConfirm = (): void => {
|
|
135
|
-
if (!canConfirm || quantity === null) return
|
|
232
|
+
if (!canConfirm || quantity === null || selectedMaterial === null) return
|
|
233
|
+
const address = [company, street, `${zip} ${city}`, contactName, phone]
|
|
234
|
+
.map((part) => part.trim())
|
|
235
|
+
.filter(Boolean)
|
|
236
|
+
.join(', ')
|
|
136
237
|
onConfirm({
|
|
137
|
-
size
|
|
138
|
-
material:
|
|
238
|
+
size,
|
|
239
|
+
material: selectedMaterial.label.trim(),
|
|
139
240
|
quantity,
|
|
140
|
-
address
|
|
241
|
+
address,
|
|
242
|
+
company: company.trim(),
|
|
243
|
+
street: street.trim(),
|
|
244
|
+
city: city.trim(),
|
|
245
|
+
zip: zip.trim(),
|
|
246
|
+
contactName: contactName.trim(),
|
|
247
|
+
phone: phone.trim(),
|
|
248
|
+
note: note.trim(),
|
|
141
249
|
})
|
|
142
250
|
}
|
|
143
251
|
|
|
252
|
+
const surfaceClassName = surfaceVariant === 'dashboard'
|
|
253
|
+
? 'box-border flex h-[min(90vh,72rem)] max-h-[min(90vh,72rem)] w-[min(50vw,72rem)] max-w-[min(50vw,72rem)] flex-col overflow-hidden p-0 max-[900px]:h-[90vh] max-[900px]:w-[92vw] max-[900px]:max-w-[92vw]'
|
|
254
|
+
: 'box-border flex h-[min(90vh,72rem)] max-h-[min(90vh,72rem)] w-[min(90vw,51rem)] max-w-[min(90vw,51rem)] flex-col overflow-hidden p-0 max-[900px]:h-[90vh] max-[900px]:w-[92vw] max-[900px]:max-w-[92vw]'
|
|
255
|
+
|
|
144
256
|
return (
|
|
145
|
-
<
|
|
146
|
-
title={labels.title}
|
|
147
|
-
data-testid={testId ?? 'delivery-print-modal'}
|
|
257
|
+
<ModalOverlay
|
|
148
258
|
isOpen={isOpen}
|
|
149
259
|
onOpenChange={handleOpenChange}
|
|
260
|
+
isDismissable
|
|
150
261
|
>
|
|
151
|
-
<
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
262
|
+
<ModalSurface className={surfaceClassName}>
|
|
263
|
+
<ModalDialog
|
|
264
|
+
aria-labelledby={titleId}
|
|
265
|
+
aria-describedby={descriptionId}
|
|
266
|
+
data-testid={testId ?? 'delivery-print-modal'}
|
|
267
|
+
className="flex min-h-0 flex-1 flex-col outline-none"
|
|
268
|
+
>
|
|
269
|
+
{({ close }) => (
|
|
270
|
+
<form
|
|
271
|
+
className="flex min-h-0 flex-1 flex-col gap-6 py-10"
|
|
272
|
+
onSubmit={(event) => {
|
|
273
|
+
event.preventDefault()
|
|
274
|
+
handleConfirm()
|
|
275
|
+
}}
|
|
276
|
+
noValidate
|
|
277
|
+
>
|
|
278
|
+
<div className="mx-auto box-border w-full max-w-[50rem] px-5">
|
|
279
|
+
<div className="flex items-start justify-between gap-4">
|
|
280
|
+
<h2 id={titleId} className="m-0 max-w-[19ch] text-heading1 font-medium leading-[1.02] tracking-tight text-fg">
|
|
281
|
+
{labels.title}
|
|
282
|
+
</h2>
|
|
283
|
+
<button
|
|
284
|
+
type="button"
|
|
285
|
+
onClick={close}
|
|
286
|
+
aria-label={labels.closeLabel ?? 'Close'}
|
|
287
|
+
className="-mr-1 -mt-1 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-fg-subtle outline-none transition-colors hover:bg-surface-muted hover:text-fg focus-visible:ring-2 focus-visible:ring-ring"
|
|
288
|
+
>
|
|
289
|
+
<svg
|
|
290
|
+
width="18"
|
|
291
|
+
height="18"
|
|
292
|
+
viewBox="0 0 24 24"
|
|
293
|
+
fill="none"
|
|
294
|
+
stroke="currentColor"
|
|
295
|
+
strokeWidth="2"
|
|
296
|
+
strokeLinecap="round"
|
|
297
|
+
aria-hidden="true"
|
|
298
|
+
>
|
|
299
|
+
<path d="M6 6l12 12M18 6 6 18" />
|
|
300
|
+
</svg>
|
|
301
|
+
</button>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<div className="min-h-0 flex-1 overflow-y-auto [scrollbar-gutter:stable]">
|
|
306
|
+
<div className="mx-auto box-border flex w-full max-w-[50rem] flex-col gap-6 px-5">
|
|
307
|
+
<div className="flex flex-col gap-1">
|
|
308
|
+
<p className="m-0 text-body font-medium text-fg">
|
|
309
|
+
{labels.sectionLabel ?? labels.title}
|
|
310
|
+
</p>
|
|
311
|
+
<p id={descriptionId} className="m-0 max-w-[48ch] text-body font-normal text-fg">
|
|
312
|
+
{labels.body}
|
|
313
|
+
</p>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<div className="flex flex-col gap-1 rounded-md bg-surface-card p-3 text-caption leading-4 text-fg-muted">
|
|
317
|
+
<span>{outputName?.trim() || labels.outputName || labels.sizeLabel}</span>
|
|
318
|
+
{outputDescription?.trim() || labels.outputDescription || labels.sizePlaceholder ? (
|
|
319
|
+
<strong className="font-medium text-fg">
|
|
320
|
+
{outputDescription?.trim() || labels.outputDescription || labels.sizePlaceholder}
|
|
321
|
+
</strong>
|
|
322
|
+
) : null}
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<div className="grid gap-3">
|
|
326
|
+
{derivedSize ? null : (
|
|
327
|
+
<Input
|
|
328
|
+
label={labels.sizeLabel}
|
|
329
|
+
placeholder={labels.sizePlaceholder}
|
|
330
|
+
value={sizeRaw}
|
|
331
|
+
onChange={setSizeRaw}
|
|
332
|
+
data-testid="delivery-print-size"
|
|
333
|
+
/>
|
|
334
|
+
)}
|
|
335
|
+
|
|
336
|
+
<Input
|
|
337
|
+
label={labels.quantityLabel}
|
|
338
|
+
placeholder={labels.quantityPlaceholder}
|
|
339
|
+
value={quantityRaw}
|
|
340
|
+
onChange={setQuantityRaw}
|
|
341
|
+
type="number"
|
|
342
|
+
inputMode="numeric"
|
|
343
|
+
isInvalid={quantityInvalid}
|
|
344
|
+
errorMessage={quantityInvalid ? labels.quantityError : undefined}
|
|
345
|
+
data-testid="delivery-print-quantity"
|
|
346
|
+
/>
|
|
347
|
+
|
|
348
|
+
{materialOptions.length > 1 ? (
|
|
349
|
+
<label className="grid gap-1.5 text-small font-medium text-fg">
|
|
350
|
+
<span>{labels.materialLabel}</span>
|
|
351
|
+
<select
|
|
352
|
+
value={effectiveMaterialId}
|
|
353
|
+
onChange={(event) => setMaterialId(event.currentTarget.value)}
|
|
354
|
+
data-testid="delivery-print-material"
|
|
355
|
+
className="h-12 rounded-md border border-border bg-surface px-3 text-body font-normal text-fg outline-none transition-colors hover:border-border-muted focus:border-fg focus:ring-2 focus:ring-ring"
|
|
356
|
+
>
|
|
357
|
+
{materialOptions.map((option) => (
|
|
358
|
+
<option key={option.id} value={option.id}>
|
|
359
|
+
{option.label}
|
|
360
|
+
</option>
|
|
361
|
+
))}
|
|
362
|
+
</select>
|
|
363
|
+
</label>
|
|
364
|
+
) : selectedMaterial ? (
|
|
365
|
+
<div
|
|
366
|
+
data-testid="delivery-print-material"
|
|
367
|
+
className="flex flex-col gap-1 rounded-md bg-surface-card p-3 text-caption leading-4 text-fg-muted"
|
|
368
|
+
>
|
|
369
|
+
<span>{labels.materialLabel}</span>
|
|
370
|
+
<strong className="font-medium text-fg">{selectedMaterial.label}</strong>
|
|
371
|
+
</div>
|
|
372
|
+
) : (
|
|
373
|
+
<p
|
|
374
|
+
role="note"
|
|
375
|
+
data-testid="delivery-print-material"
|
|
376
|
+
className="m-0 rounded-md bg-surface-card p-3 text-caption leading-4 text-fg-muted"
|
|
377
|
+
>
|
|
378
|
+
{labels.noMaterialConfigured ?? labels.materialPlaceholder}
|
|
379
|
+
</p>
|
|
380
|
+
)}
|
|
381
|
+
|
|
382
|
+
<Input
|
|
383
|
+
label={labels.companyLabel ?? labels.addressLabel}
|
|
384
|
+
placeholder={labels.companyPlaceholder ?? labels.addressPlaceholder}
|
|
385
|
+
value={company}
|
|
386
|
+
onChange={setCompany}
|
|
387
|
+
data-testid="delivery-print-company"
|
|
388
|
+
/>
|
|
389
|
+
<Input
|
|
390
|
+
label={labels.streetLabel ?? labels.addressLabel}
|
|
391
|
+
placeholder={labels.streetPlaceholder ?? labels.addressPlaceholder}
|
|
392
|
+
value={street}
|
|
393
|
+
onChange={(value) => setStreet(capitalizeFirstAddressLetter(value))}
|
|
394
|
+
data-testid="delivery-print-street"
|
|
395
|
+
/>
|
|
396
|
+
<div className="grid gap-4 sm:grid-cols-[1fr_10rem]">
|
|
397
|
+
<Input
|
|
398
|
+
label={labels.cityLabel ?? labels.addressLabel}
|
|
399
|
+
placeholder={labels.cityPlaceholder ?? labels.addressPlaceholder}
|
|
400
|
+
value={city}
|
|
401
|
+
onChange={setCity}
|
|
402
|
+
data-testid="delivery-print-city"
|
|
403
|
+
/>
|
|
404
|
+
<Input
|
|
405
|
+
label={labels.zipLabel ?? labels.addressLabel}
|
|
406
|
+
placeholder={labels.zipPlaceholder ?? labels.addressPlaceholder}
|
|
407
|
+
value={zip}
|
|
408
|
+
onChange={setZip}
|
|
409
|
+
data-testid="delivery-print-zip"
|
|
410
|
+
/>
|
|
411
|
+
</div>
|
|
412
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
413
|
+
<Input
|
|
414
|
+
label={labels.contactNameLabel ?? labels.addressLabel}
|
|
415
|
+
placeholder={labels.contactNamePlaceholder ?? labels.addressPlaceholder}
|
|
416
|
+
value={contactName}
|
|
417
|
+
onChange={setContactName}
|
|
418
|
+
data-testid="delivery-print-contact"
|
|
419
|
+
/>
|
|
420
|
+
<Input
|
|
421
|
+
label={labels.phoneLabel ?? labels.addressLabel}
|
|
422
|
+
placeholder={labels.phonePlaceholder ?? labels.addressPlaceholder}
|
|
423
|
+
value={phone}
|
|
424
|
+
onChange={setPhone}
|
|
425
|
+
type="tel"
|
|
426
|
+
data-testid="delivery-print-phone"
|
|
427
|
+
/>
|
|
428
|
+
</div>
|
|
429
|
+
<Input
|
|
430
|
+
label={labels.noteLabel ?? labels.addressLabel}
|
|
431
|
+
placeholder={labels.notePlaceholder ?? labels.addressPlaceholder}
|
|
432
|
+
value={note}
|
|
433
|
+
onChange={setNote}
|
|
434
|
+
data-testid="delivery-print-note"
|
|
435
|
+
/>
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
{error ? (
|
|
439
|
+
<p data-testid="delivery-print-error" role="alert" className="m-0 text-small text-danger">
|
|
440
|
+
{error}
|
|
441
|
+
</p>
|
|
442
|
+
) : null}
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
<div className="sticky bottom-0 mt-0 w-full border-border border-t bg-surface pt-4">
|
|
447
|
+
<div className="mx-auto box-border flex w-full max-w-[50rem] items-center justify-between gap-4 px-5">
|
|
448
|
+
<div className="flex min-w-0 flex-col gap-1 text-small text-fg-muted">
|
|
449
|
+
{/* No fallback: borrowing `sizeLabel` / `materialPlaceholder` put
|
|
450
|
+
"Size" and "e.g. Matte 250g" in a money slot for every consumer
|
|
451
|
+
that had not adopted the new label set. */}
|
|
452
|
+
{labels.priceEyebrow ? <span>{labels.priceEyebrow}</span> : null}
|
|
453
|
+
{labels.priceIdle ? <span className="truncate">{labels.priceIdle}</span> : null}
|
|
454
|
+
</div>
|
|
455
|
+
<div className="flex shrink-0 justify-end gap-2">
|
|
456
|
+
<Button
|
|
457
|
+
variant="secondary"
|
|
458
|
+
size="sm"
|
|
459
|
+
isDisabled={isPending}
|
|
460
|
+
onPress={() => handleOpenChange(false)}
|
|
461
|
+
>
|
|
462
|
+
{labels.cancel}
|
|
463
|
+
</Button>
|
|
464
|
+
<Button
|
|
465
|
+
data-testid="delivery-print-confirm"
|
|
466
|
+
variant="primary"
|
|
467
|
+
size="sm"
|
|
468
|
+
isDisabled={!canConfirm}
|
|
469
|
+
isPending={isPending}
|
|
470
|
+
onPress={handleConfirm}
|
|
471
|
+
>
|
|
472
|
+
{isPending ? labels.submitting : labels.confirm}
|
|
473
|
+
</Button>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
</form>
|
|
478
|
+
)}
|
|
479
|
+
</ModalDialog>
|
|
480
|
+
</ModalSurface>
|
|
481
|
+
</ModalOverlay>
|
|
215
482
|
)
|
|
216
483
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DialogActionButton } from './dialog-action-button'
|
|
2
|
+
export const examples = {
|
|
3
|
+
default: () => <DialogActionButton>Review Mapping</DialogActionButton>,
|
|
4
|
+
variants: () => <><DialogActionButton variant="secondary">Change CSV</DialogActionButton><DialogActionButton>Bind CSV</DialogActionButton></>,
|
|
5
|
+
states: () => <><DialogActionButton isDisabled>Bind CSV</DialogActionButton><DialogActionButton isPending>Binding CSV…</DialogActionButton></>,
|
|
6
|
+
}
|
|
7
|
+
export const meta = { category: 'Primitives', description: 'Source UI dialog action palette, typography and pointer/keyboard states, independent of dashboard CTAs.' }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Button as AriaButton } from 'react-aria-components'
|
|
2
|
+
import { uic } from '../utils/uic'
|
|
3
|
+
|
|
4
|
+
export const DIALOG_ACTION_VARIANTS = ['primary', 'secondary'] as const
|
|
5
|
+
export type DialogActionVariant = (typeof DIALOG_ACTION_VARIANTS)[number]
|
|
6
|
+
|
|
7
|
+
/** Source UI dialog action, distinct from the frontend dashboard CTA Button.
|
|
8
|
+
* Shares Button's semantic palette (brand-primary / surface-card) so the two never
|
|
9
|
+
* drift apart; only the geometry — pill radius, 24/12 padding — is specific here.
|
|
10
|
+
* Keyboard focus retains the shared RAC treatment.
|
|
11
|
+
*/
|
|
12
|
+
export const DialogActionButton = uic(AriaButton, {
|
|
13
|
+
displayName: 'DialogActionButton',
|
|
14
|
+
baseClass: 'inline-flex items-center justify-center rounded-full px-6 py-3 text-body font-medium leading-5 cursor-pointer transition-colors duration-200 motion-reduce:transition-none outline-none ' +
|
|
15
|
+
'data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring data-[focus-visible]:ring-offset-2 ' +
|
|
16
|
+
'data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed data-[pending]:cursor-progress',
|
|
17
|
+
variants: { variant: {
|
|
18
|
+
primary: 'border-0 bg-brand-primary text-fg-inverted data-[hovered]:bg-neutral-600 data-[pressed]:bg-brand-primary',
|
|
19
|
+
secondary: 'border border-border bg-surface-card text-fg data-[hovered]:bg-surface-muted data-[hovered]:border-border-muted data-[pressed]:bg-surface-muted',
|
|
20
|
+
} },
|
|
21
|
+
defaultVariants: { variant: 'primary' },
|
|
22
|
+
})
|
|
@@ -32,7 +32,7 @@ export const ModalOverlay = uic(RACModalOverlay, {
|
|
|
32
32
|
export const ModalSurface = uic(RACModal, {
|
|
33
33
|
displayName: 'DialogModal',
|
|
34
34
|
baseClass:
|
|
35
|
-
'rounded-lg bg-surface p-5 outline-none shadow-modal-surface ' +
|
|
35
|
+
'relative rounded-lg bg-surface p-5 outline-none shadow-modal-surface ' +
|
|
36
36
|
'data-[entering]:animate-modal-surface-in data-[exiting]:animate-modal-surface-out',
|
|
37
37
|
})
|
|
38
38
|
|
|
@@ -92,6 +92,10 @@ export type DialogProps = RACDialogProps & {
|
|
|
92
92
|
badge?: ReactNode
|
|
93
93
|
/** Width preset — `md` (default) for forms; `sm` for tight confirms; `lg`/`xl` for wide stepper content. */
|
|
94
94
|
size?: DialogSize
|
|
95
|
+
/** Optional form-specific width cap; the viewport width preset still applies. */
|
|
96
|
+
maxWidth?: number
|
|
97
|
+
/** Responsive intrinsic detail envelope, independent of title sizing. */
|
|
98
|
+
widthPreset?: 'responsive-detail' | 'custom-task-detail'
|
|
95
99
|
children: ReactNode | ((opts: { close: () => void }) => ReactNode)
|
|
96
100
|
/**
|
|
97
101
|
* Controlled open state. Provide together with `onOpenChange` to drive the
|
|
@@ -115,6 +119,8 @@ export const Dialog = ({
|
|
|
115
119
|
badge,
|
|
116
120
|
// gs DialogContent defaults to `md` (586px form width).
|
|
117
121
|
size = 'md',
|
|
122
|
+
maxWidth,
|
|
123
|
+
widthPreset,
|
|
118
124
|
children,
|
|
119
125
|
isOpen,
|
|
120
126
|
defaultOpen,
|
|
@@ -126,6 +132,11 @@ export const Dialog = ({
|
|
|
126
132
|
// `full` is a fixed-height flex canvas: the body fills and scrolls internally so
|
|
127
133
|
// the header (+ a footer the content pins) stay put. Other sizes are content-height.
|
|
128
134
|
const isFlex = size === 'full'
|
|
135
|
+
// The Manager header treatment — block layout, floated close, label-ramp
|
|
136
|
+
// description — belongs to the OPT-IN width presets, not to "any dialog that
|
|
137
|
+
// isn't `full`". Gating it on `isFlex` made every sm/md/lg/xl dialog render a
|
|
138
|
+
// display-scale title and stranded `TITLE_CLASS` on the single `full` size.
|
|
139
|
+
const isCompact = widthPreset !== undefined
|
|
129
140
|
return (
|
|
130
141
|
// `isOpen`/`defaultOpen`/`onOpenChange` go to the RAC ModalOverlay: when
|
|
131
142
|
// provided the dialog is controlled (renders independently of a DialogTrigger);
|
|
@@ -142,19 +153,28 @@ export const Dialog = ({
|
|
|
142
153
|
onOpenChange={onOpenChange}
|
|
143
154
|
isDismissable={isDismissable}
|
|
144
155
|
>
|
|
145
|
-
<ModalSurface
|
|
156
|
+
<ModalSurface
|
|
157
|
+
className={
|
|
158
|
+
widthPreset === 'responsive-detail'
|
|
159
|
+
? 'w-[90vw] max-w-125 md:max-w-150 lg:max-w-160 xl:max-w-180 max-h-[85vh] overflow-y-auto'
|
|
160
|
+
: widthPreset === 'custom-task-detail'
|
|
161
|
+
? 'w-[90vw] min-w-[400px] max-w-[500px] md:max-w-[600px] lg:max-w-[640px] xl:max-w-[720px] max-h-[85vh] overflow-y-auto max-[460px]:min-w-0'
|
|
162
|
+
: SIZE_CLASS[size]
|
|
163
|
+
}
|
|
164
|
+
style={maxWidth === undefined ? undefined : { maxWidth }}
|
|
165
|
+
>
|
|
146
166
|
<ModalDialog
|
|
147
167
|
{...props}
|
|
148
168
|
className={isFlex ? 'flex min-h-0 flex-1 flex-col outline-none' : 'outline-none'}
|
|
149
169
|
>
|
|
150
170
|
{(renderProps) => (
|
|
151
171
|
<>
|
|
152
|
-
<div className=
|
|
172
|
+
<div className={isCompact ? 'block' : 'mb-4 flex shrink-0 items-start justify-between gap-4'}>
|
|
153
173
|
{title || description ? (
|
|
154
174
|
<div className="min-w-0">
|
|
155
175
|
{title ? (
|
|
156
176
|
<div className="flex items-center gap-2">
|
|
157
|
-
<Heading slot="title" className={`${TITLE_CLASS[size]} text-fg`}>
|
|
177
|
+
<Heading slot="title" className={isCompact ? 'm-0 mb-4 max-w-5/6 text-heading1 font-medium tracking-normal text-fg' : `${TITLE_CLASS[size]} text-fg`}>
|
|
158
178
|
{title}
|
|
159
179
|
</Heading>
|
|
160
180
|
{badge ? (
|
|
@@ -165,8 +185,8 @@ export const Dialog = ({
|
|
|
165
185
|
</div>
|
|
166
186
|
) : null}
|
|
167
187
|
{description ? (
|
|
168
|
-
//
|
|
169
|
-
<p className=
|
|
188
|
+
// Source ordinary descriptions use the 14px/18px label ramp.
|
|
189
|
+
<p className={isCompact ? 'm-0 mb-6 max-w-5/6 text-small leading-4.5 font-normal text-fg-workflow-muted' : 'mt-1 text-body text-fg'}>{description}</p>
|
|
170
190
|
) : null}
|
|
171
191
|
</div>
|
|
172
192
|
) : (
|
|
@@ -179,19 +199,19 @@ export const Dialog = ({
|
|
|
179
199
|
// gs close: 32px square (`h-8 w-8`), medium radius (`rounded-md`),
|
|
180
200
|
// tertiary icon color (`text-fg-subtle`) → primary (`text-fg`) +
|
|
181
201
|
// subtle hover bg on hover. Smooth color transition.
|
|
182
|
-
className=
|
|
202
|
+
className={`${isCompact ? 'absolute top-[calc(var(--spacing)*3.375)] end-4 max-md:end-3' : '-mr-1 -mt-1'} inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-fg-subtle outline-none transition-colors hover:bg-surface-muted hover:text-fg focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none`}
|
|
183
203
|
>
|
|
184
204
|
<svg
|
|
185
|
-
width=
|
|
186
|
-
height=
|
|
187
|
-
viewBox=
|
|
205
|
+
width={isCompact ? 15 : 18}
|
|
206
|
+
height={isCompact ? 15 : 18}
|
|
207
|
+
viewBox={isCompact ? '0 0 15 15' : '0 0 24 24'}
|
|
188
208
|
fill="none"
|
|
189
|
-
stroke=
|
|
209
|
+
stroke={isCompact ? undefined : 'currentColor'}
|
|
190
210
|
strokeWidth="2"
|
|
191
211
|
strokeLinecap="round"
|
|
192
212
|
aria-hidden="true"
|
|
193
213
|
>
|
|
194
|
-
<path d=
|
|
214
|
+
<path fill={isCompact ? 'currentColor' : undefined} d={isCompact ? 'M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z' : 'M6 6l12 12M18 6 6 18'} />
|
|
195
215
|
</svg>
|
|
196
216
|
</button>
|
|
197
217
|
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DocumentUploadPanel } from './document-upload-panel'
|
|
2
|
+
|
|
3
|
+
const copy = { title: 'CSV file', actionLabel: 'Select CSV', helperText: 'No CSV file selected yet.\nSelect the CSV file for content mapping.' }
|
|
4
|
+
export const examples = {
|
|
5
|
+
default: () => <DocumentUploadPanel {...copy} />,
|
|
6
|
+
dragging: () => <DocumentUploadPanel {...copy} isDragging />,
|
|
7
|
+
disabled: () => <DocumentUploadPanel {...copy} isDisabled />,
|
|
8
|
+
}
|
|
9
|
+
export const meta = {
|
|
10
|
+
category: 'Form',
|
|
11
|
+
description: 'Upload tile with a centred keyboard-accessible action and a native drop surface. Focus/hover use the shared React Aria button; caller owns file validation.',
|
|
12
|
+
}
|