@prenta/admin 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/design-system/tokens.css +17 -0
  3. package/design-system/tokens.json +35 -1
  4. package/design-system/tokens.scss +15 -0
  5. package/design-system/tokens.tailwind.cjs +14 -0
  6. package/dist/AdminRoot.d.ts.map +1 -1
  7. package/dist/AdminRoot.js +2 -2
  8. package/dist/AdminRoot.js.map +1 -1
  9. package/dist/__tests__/lib/batch-wizard.test.d.ts +2 -0
  10. package/dist/__tests__/lib/batch-wizard.test.d.ts.map +1 -0
  11. package/dist/__tests__/lib/batch-wizard.test.js +144 -0
  12. package/dist/__tests__/lib/batch-wizard.test.js.map +1 -0
  13. package/dist/__tests__/lib/job-drawer.test.js +2 -0
  14. package/dist/__tests__/lib/job-drawer.test.js.map +1 -1
  15. package/dist/__tests__/lib/job-view.test.js +100 -3
  16. package/dist/__tests__/lib/job-view.test.js.map +1 -1
  17. package/dist/__tests__/lib/jobs-service.test.js +56 -2
  18. package/dist/__tests__/lib/jobs-service.test.js.map +1 -1
  19. package/dist/__tests__/views/appearance-chrome.render.test.d.ts +2 -0
  20. package/dist/__tests__/views/appearance-chrome.render.test.d.ts.map +1 -0
  21. package/dist/__tests__/views/appearance-chrome.render.test.js +688 -0
  22. package/dist/__tests__/views/appearance-chrome.render.test.js.map +1 -0
  23. package/dist/__tests__/views/header-footer-form.test.d.ts +2 -0
  24. package/dist/__tests__/views/header-footer-form.test.d.ts.map +1 -0
  25. package/dist/__tests__/views/header-footer-form.test.js +266 -0
  26. package/dist/__tests__/views/header-footer-form.test.js.map +1 -0
  27. package/dist/__tests__/views/jobs-drawer.render.test.js +442 -2
  28. package/dist/__tests__/views/jobs-drawer.render.test.js.map +1 -1
  29. package/dist/__tests__/views/jobs-list.render.test.js +7 -5
  30. package/dist/__tests__/views/jobs-list.render.test.js.map +1 -1
  31. package/dist/__tests__/views/jobs-wizard.render.test.d.ts +2 -0
  32. package/dist/__tests__/views/jobs-wizard.render.test.d.ts.map +1 -0
  33. package/dist/__tests__/views/jobs-wizard.render.test.js +588 -0
  34. package/dist/__tests__/views/jobs-wizard.render.test.js.map +1 -0
  35. package/dist/lib/api.d.ts +2 -0
  36. package/dist/lib/api.d.ts.map +1 -1
  37. package/dist/lib/api.js +8 -1
  38. package/dist/lib/api.js.map +1 -1
  39. package/dist/lib/batch-wizard.d.ts +84 -0
  40. package/dist/lib/batch-wizard.d.ts.map +1 -0
  41. package/dist/lib/batch-wizard.js +122 -0
  42. package/dist/lib/batch-wizard.js.map +1 -0
  43. package/dist/lib/chrome-service.d.ts +69 -0
  44. package/dist/lib/chrome-service.d.ts.map +1 -0
  45. package/dist/lib/chrome-service.js +108 -0
  46. package/dist/lib/chrome-service.js.map +1 -0
  47. package/dist/lib/job-view.d.ts +43 -0
  48. package/dist/lib/job-view.d.ts.map +1 -1
  49. package/dist/lib/job-view.js +57 -1
  50. package/dist/lib/job-view.js.map +1 -1
  51. package/dist/lib/jobs-service.d.ts +19 -1
  52. package/dist/lib/jobs-service.d.ts.map +1 -1
  53. package/dist/lib/jobs-service.js +41 -0
  54. package/dist/lib/jobs-service.js.map +1 -1
  55. package/dist/prenta-admin.css +1 -1
  56. package/dist/views/AppearanceChrome.d.ts +22 -4
  57. package/dist/views/AppearanceChrome.d.ts.map +1 -1
  58. package/dist/views/AppearanceChrome.js +451 -15
  59. package/dist/views/AppearanceChrome.js.map +1 -1
  60. package/dist/views/Jobs/CancelJobDialog.d.ts +7 -1
  61. package/dist/views/Jobs/CancelJobDialog.d.ts.map +1 -1
  62. package/dist/views/Jobs/CancelJobDialog.js +13 -3
  63. package/dist/views/Jobs/CancelJobDialog.js.map +1 -1
  64. package/dist/views/Jobs/DrawerReview.d.ts +7 -0
  65. package/dist/views/Jobs/DrawerReview.d.ts.map +1 -1
  66. package/dist/views/Jobs/DrawerReview.js +1 -1
  67. package/dist/views/Jobs/DrawerReview.js.map +1 -1
  68. package/dist/views/Jobs/JobDrawer.d.ts +23 -3
  69. package/dist/views/Jobs/JobDrawer.d.ts.map +1 -1
  70. package/dist/views/Jobs/JobDrawer.js +30 -5
  71. package/dist/views/Jobs/JobDrawer.js.map +1 -1
  72. package/dist/views/Jobs/JobsTable.d.ts +4 -2
  73. package/dist/views/Jobs/JobsTable.d.ts.map +1 -1
  74. package/dist/views/Jobs/JobsTable.js +6 -5
  75. package/dist/views/Jobs/JobsTable.js.map +1 -1
  76. package/dist/views/Jobs/NewBatchJob.d.ts +34 -0
  77. package/dist/views/Jobs/NewBatchJob.d.ts.map +1 -0
  78. package/dist/views/Jobs/NewBatchJob.js +282 -0
  79. package/dist/views/Jobs/NewBatchJob.js.map +1 -0
  80. package/dist/views/Jobs/WorkerStrip.d.ts +3 -1
  81. package/dist/views/Jobs/WorkerStrip.d.ts.map +1 -1
  82. package/dist/views/Jobs/WorkerStrip.js +2 -2
  83. package/dist/views/Jobs/WorkerStrip.js.map +1 -1
  84. package/dist/views/Jobs/focus.d.ts +6 -0
  85. package/dist/views/Jobs/focus.d.ts.map +1 -1
  86. package/dist/views/Jobs/focus.js +10 -0
  87. package/dist/views/Jobs/focus.js.map +1 -1
  88. package/dist/views/Jobs.d.ts +12 -3
  89. package/dist/views/Jobs.d.ts.map +1 -1
  90. package/dist/views/Jobs.js +109 -31
  91. package/dist/views/Jobs.js.map +1 -1
  92. package/dist/views/header-footer/AnnouncementTab.d.ts +15 -0
  93. package/dist/views/header-footer/AnnouncementTab.d.ts.map +1 -0
  94. package/dist/views/header-footer/AnnouncementTab.js +43 -0
  95. package/dist/views/header-footer/AnnouncementTab.js.map +1 -0
  96. package/dist/views/header-footer/ChromeField.d.ts +25 -0
  97. package/dist/views/header-footer/ChromeField.d.ts.map +1 -0
  98. package/dist/views/header-footer/ChromeField.js +99 -0
  99. package/dist/views/header-footer/ChromeField.js.map +1 -0
  100. package/dist/views/header-footer/ChromePreview.d.ts +31 -0
  101. package/dist/views/header-footer/ChromePreview.d.ts.map +1 -0
  102. package/dist/views/header-footer/ChromePreview.js +195 -0
  103. package/dist/views/header-footer/ChromePreview.js.map +1 -0
  104. package/dist/views/header-footer/DeveloperTab.d.ts +17 -0
  105. package/dist/views/header-footer/DeveloperTab.d.ts.map +1 -0
  106. package/dist/views/header-footer/DeveloperTab.js +98 -0
  107. package/dist/views/header-footer/DeveloperTab.js.map +1 -0
  108. package/dist/views/header-footer/RegionTab.d.ts +27 -0
  109. package/dist/views/header-footer/RegionTab.d.ts.map +1 -0
  110. package/dist/views/header-footer/RegionTab.js +80 -0
  111. package/dist/views/header-footer/RegionTab.js.map +1 -0
  112. package/dist/views/header-footer/chrome-form.d.ts +137 -0
  113. package/dist/views/header-footer/chrome-form.d.ts.map +1 -0
  114. package/dist/views/header-footer/chrome-form.js +518 -0
  115. package/dist/views/header-footer/chrome-form.js.map +1 -0
  116. package/dist/views/header-footer/code-samples.d.ts +30 -0
  117. package/dist/views/header-footer/code-samples.d.ts.map +1 -0
  118. package/dist/views/header-footer/code-samples.js +127 -0
  119. package/dist/views/header-footer/code-samples.js.map +1 -0
  120. package/dist/views/header-footer/controls.d.ts +57 -0
  121. package/dist/views/header-footer/controls.d.ts.map +1 -0
  122. package/dist/views/header-footer/controls.js +43 -0
  123. package/dist/views/header-footer/controls.js.map +1 -0
  124. package/package.json +3 -3
  125. package/src/AdminRoot.tsx +16 -2
  126. package/src/__tests__/lib/batch-wizard.test.ts +199 -0
  127. package/src/__tests__/lib/job-drawer.test.ts +2 -0
  128. package/src/__tests__/lib/job-view.test.ts +123 -2
  129. package/src/__tests__/lib/jobs-service.test.ts +72 -3
  130. package/src/__tests__/views/appearance-chrome.render.test.tsx +818 -0
  131. package/src/__tests__/views/header-footer-form.test.ts +333 -0
  132. package/src/__tests__/views/jobs-drawer.render.test.tsx +508 -2
  133. package/src/__tests__/views/jobs-list.render.test.tsx +8 -5
  134. package/src/__tests__/views/jobs-wizard.render.test.tsx +707 -0
  135. package/src/lib/api.ts +12 -1
  136. package/src/lib/batch-wizard.ts +188 -0
  137. package/src/lib/chrome-service.ts +200 -0
  138. package/src/lib/job-view.ts +95 -1
  139. package/src/lib/jobs-service.ts +56 -1
  140. package/src/styles/prenta-tokens.css +17 -0
  141. package/src/styles/theme.css +15 -0
  142. package/src/views/AppearanceChrome.tsx +704 -46
  143. package/src/views/Jobs/CancelJobDialog.tsx +20 -2
  144. package/src/views/Jobs/DrawerReview.tsx +1 -1
  145. package/src/views/Jobs/JobDrawer.tsx +83 -3
  146. package/src/views/Jobs/JobsTable.tsx +11 -4
  147. package/src/views/Jobs/NewBatchJob.tsx +857 -0
  148. package/src/views/Jobs/WorkerStrip.tsx +16 -11
  149. package/src/views/Jobs/focus.ts +11 -0
  150. package/src/views/Jobs.tsx +163 -49
  151. package/src/views/header-footer/AnnouncementTab.tsx +231 -0
  152. package/src/views/header-footer/ChromeField.tsx +358 -0
  153. package/src/views/header-footer/ChromePreview.tsx +497 -0
  154. package/src/views/header-footer/DeveloperTab.tsx +331 -0
  155. package/src/views/header-footer/RegionTab.tsx +398 -0
  156. package/src/views/header-footer/chrome-form.ts +603 -0
  157. package/src/views/header-footer/code-samples.ts +146 -0
  158. package/src/views/header-footer/controls.tsx +166 -0
  159. package/dist/views/Jobs/placeholders.d.ts +0 -6
  160. package/dist/views/Jobs/placeholders.d.ts.map +0 -1
  161. package/dist/views/Jobs/placeholders.js +0 -20
  162. package/dist/views/Jobs/placeholders.js.map +0 -1
  163. package/src/views/Jobs/placeholders.tsx +0 -66
@@ -0,0 +1,358 @@
1
+ 'use client'
2
+
3
+ import { Plus, X } from 'lucide-react'
4
+ import { useId, type ReactNode } from 'react'
5
+ import type { FieldDef } from '@prenta/core'
6
+
7
+ import { Toggle } from '../../components/ui/Toggle.js'
8
+ import type { ChromeMenuOption } from '../../lib/chrome-service.js'
9
+ import {
10
+ SOCIAL_PLATFORMS,
11
+ fieldHelp,
12
+ fieldLabel,
13
+ humanizeKey,
14
+ isKnownField,
15
+ isToggled,
16
+ menuListSlots,
17
+ } from './chrome-form.js'
18
+ import { Help, INPUT, KeyChip, MONO, RadioPills, TEXTAREA } from './controls.js'
19
+
20
+ type Rec = Record<string, unknown>
21
+
22
+ function rec(value: unknown): Rec {
23
+ return typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Rec) : {}
24
+ }
25
+
26
+ function text(value: unknown): string {
27
+ return typeof value === 'string' ? value : ''
28
+ }
29
+
30
+ export interface ChromeFieldProps {
31
+ /** The region, for unique radio names. */
32
+ region: string
33
+ fieldKey: string
34
+ def: FieldDef
35
+ /** The stored-format value (brand fields have none). */
36
+ value: unknown
37
+ onChange: (value: unknown) => void
38
+ menus: ChromeMenuOption[]
39
+ siteTitle: string
40
+ disabled: boolean
41
+ problem?: string
42
+ /** Opens Settings for a brand field's "Change". */
43
+ onOpenBrand: () => void
44
+ brandHref: string
45
+ }
46
+
47
+ /** A menu picker. A saved slug the menus list doesn't have stays selectable. */
48
+ function MenuSelect({
49
+ value,
50
+ onChange,
51
+ menus,
52
+ label,
53
+ none,
54
+ disabled,
55
+ describedBy,
56
+ }: {
57
+ value: string
58
+ onChange: (slug: string) => void
59
+ menus: ChromeMenuOption[]
60
+ label: string
61
+ none?: boolean
62
+ disabled: boolean
63
+ describedBy?: string
64
+ }) {
65
+ const missing = value && !menus.some((menu) => menu.slug === value)
66
+ return (
67
+ <select
68
+ className={INPUT}
69
+ aria-label={label}
70
+ aria-describedby={describedBy}
71
+ value={value}
72
+ disabled={disabled}
73
+ onChange={(event) => onChange(event.target.value)}
74
+ >
75
+ {none || !value ? <option value="">{none ? 'None' : 'Pick a menu'}</option> : null}
76
+ {missing ? <option value={value}>{`${value} (not found)`}</option> : null}
77
+ {menus.map((menu) => (
78
+ <option key={menu.slug} value={menu.slug}>
79
+ {menu.name}
80
+ </option>
81
+ ))}
82
+ </select>
83
+ )
84
+ }
85
+
86
+ /**
87
+ * One field of a region, built from its definition (`.hf-f`): the head row
88
+ * with the label, help, key chip and (for optional and boolean fields) a
89
+ * switch, then the control for its type.
90
+ */
91
+ export function ChromeField({
92
+ region,
93
+ fieldKey,
94
+ def,
95
+ value,
96
+ onChange,
97
+ menus,
98
+ siteTitle,
99
+ disabled,
100
+ problem,
101
+ onOpenBrand,
102
+ brandHref,
103
+ }: ChromeFieldProps) {
104
+ const id = useId()
105
+ const label = fieldLabel(fieldKey, def)
106
+ const help = fieldHelp(def)
107
+ const helpId = help ? `${id}-help` : undefined
108
+ const problemId = problem ? `${id}-problem` : undefined
109
+ const describedBy = [helpId, problemId].filter(Boolean).join(' ') || undefined
110
+ const toggled = isToggled(def)
111
+ const on = toggled ? rec(value).on === true : true
112
+ const known = isKnownField(def)
113
+
114
+ const head = (
115
+ <div className="text-sm2 text-txt flex items-center gap-2 leading-[normal] font-semibold">
116
+ <span className="min-w-0 flex-1">
117
+ {label}
118
+ {help ? (
119
+ <small id={helpId} className="text-hint text-faint mt-px block font-normal">
120
+ {help}
121
+ </small>
122
+ ) : null}
123
+ </span>
124
+ <KeyChip>{fieldKey}</KeyChip>
125
+ {toggled || def.type === 'boolean' ? (
126
+ <Toggle
127
+ aria-label={label}
128
+ checked={def.type === 'boolean' ? value === true : on}
129
+ disabled={disabled}
130
+ onCheckedChange={(next) =>
131
+ onChange(def.type === 'boolean' ? next : { ...rec(value), on: next })
132
+ }
133
+ />
134
+ ) : null}
135
+ </div>
136
+ )
137
+
138
+ let body: ReactNode = null
139
+ if (!known) {
140
+ body = (
141
+ <Help>
142
+ {`This field's type (${String((def as { type: unknown }).type)}) needs a newer admin. Its saved value is kept.`}
143
+ </Help>
144
+ )
145
+ } else if (def.type === 'brand') {
146
+ body = (
147
+ <div className="rounded-9 border-bdr text-sm2 text-sub flex items-center gap-2.5 border border-dashed px-[11px] py-[9px] leading-[normal]">
148
+ <span className="text-txt text-[14px] font-extrabold tracking-[-0.3px]">{siteTitle}</span>
149
+ <span>From brand settings</span>
150
+ <a
151
+ href={brandHref}
152
+ onClick={(event) => {
153
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) return
154
+ event.preventDefault()
155
+ onOpenBrand()
156
+ }}
157
+ className="text-acc ml-auto font-semibold whitespace-nowrap no-underline"
158
+ >
159
+ Change
160
+ </a>
161
+ </div>
162
+ )
163
+ } else if (def.type === 'menu') {
164
+ if (def.list) {
165
+ const list = Array.isArray(value) ? (value as unknown[]) : []
166
+ const slots = menuListSlots(def, list.length)
167
+ body = (
168
+ <div className="flex flex-col gap-1.5">
169
+ {Array.from({ length: slots }, (_, index) => (
170
+ <MenuSelect
171
+ key={index}
172
+ none
173
+ label={`${label} ${index + 1}`}
174
+ value={text(list[index])}
175
+ menus={menus}
176
+ disabled={disabled}
177
+ describedBy={describedBy}
178
+ onChange={(slug) => {
179
+ const next = Array.from({ length: Math.max(list.length, index + 1) }, (_, i) =>
180
+ text(list[i]),
181
+ )
182
+ next[index] = slug
183
+ onChange(next)
184
+ }}
185
+ />
186
+ ))}
187
+ </div>
188
+ )
189
+ } else if (toggled) {
190
+ body = on ? (
191
+ <MenuSelect
192
+ label={label}
193
+ value={text(rec(value).value)}
194
+ menus={menus}
195
+ disabled={disabled}
196
+ describedBy={describedBy}
197
+ onChange={(slug) => onChange({ ...rec(value), value: slug })}
198
+ />
199
+ ) : null
200
+ } else {
201
+ body = (
202
+ <MenuSelect
203
+ label={label}
204
+ value={text(value)}
205
+ menus={menus}
206
+ disabled={disabled}
207
+ describedBy={describedBy}
208
+ onChange={onChange}
209
+ />
210
+ )
211
+ }
212
+ } else if (def.type === 'link') {
213
+ const link = rec(value)
214
+ body = on ? (
215
+ <div className="grid grid-cols-2 gap-2 [@media(width<=560px)]:grid-cols-1">
216
+ <input
217
+ className={INPUT}
218
+ aria-label={def.optional ? 'Button text' : `${label} text`}
219
+ aria-describedby={describedBy}
220
+ value={text(link.label)}
221
+ disabled={disabled}
222
+ onChange={(event) => onChange({ ...link, label: event.target.value })}
223
+ />
224
+ <input
225
+ className={`${INPUT} ${MONO}`}
226
+ aria-label={def.optional ? 'Links to' : `${label} URL`}
227
+ aria-describedby={describedBy}
228
+ value={text(link.url)}
229
+ disabled={disabled}
230
+ spellCheck={false}
231
+ onChange={(event) => onChange({ ...link, url: event.target.value })}
232
+ />
233
+ </div>
234
+ ) : null
235
+ } else if (def.type === 'text') {
236
+ const current = toggled ? text(rec(value).value) : text(value)
237
+ const input = (
238
+ <input
239
+ className={INPUT}
240
+ type={def.format === 'tel' ? 'tel' : def.format === 'email' ? 'email' : 'text'}
241
+ aria-label={label}
242
+ aria-describedby={describedBy}
243
+ aria-invalid={problem ? true : undefined}
244
+ value={current}
245
+ disabled={disabled}
246
+ onChange={(event) =>
247
+ onChange(toggled ? { ...rec(value), value: event.target.value } : event.target.value)
248
+ }
249
+ />
250
+ )
251
+ body = on ? input : null
252
+ } else if (def.type === 'textarea') {
253
+ body = (
254
+ <textarea
255
+ className={TEXTAREA}
256
+ aria-label={label}
257
+ aria-describedby={describedBy}
258
+ aria-invalid={problem ? true : undefined}
259
+ value={text(value)}
260
+ disabled={disabled}
261
+ onChange={(event) => onChange(event.target.value)}
262
+ />
263
+ )
264
+ } else if (def.type === 'select') {
265
+ body = (
266
+ <RadioPills
267
+ name={`hf-${region}-${fieldKey}`}
268
+ label={label}
269
+ options={def.options.map((option) => ({ id: option, label: humanizeKey(option) }))}
270
+ value={text(value)}
271
+ disabled={disabled}
272
+ onChange={onChange}
273
+ />
274
+ )
275
+ } else if (def.type === 'social') {
276
+ const rows = (Array.isArray(value) ? value : []).map(rec)
277
+ const update = (index: number, patch: Rec) =>
278
+ onChange(rows.map((row, i) => (i === index ? { ...row, ...patch } : row)))
279
+ body = (
280
+ <div className="flex flex-col gap-1.5">
281
+ {rows.map((row, index) => {
282
+ const platform = text(row.platform)
283
+ const platforms = SOCIAL_PLATFORMS.includes(platform)
284
+ ? SOCIAL_PLATFORMS
285
+ : [platform, ...SOCIAL_PLATFORMS]
286
+ return (
287
+ <div
288
+ key={index}
289
+ className="grid grid-cols-[120px_minmax(0,1fr)_28px] items-center gap-1.5 [@media(width<=560px)]:grid-cols-[96px_minmax(0,1fr)_28px]"
290
+ >
291
+ <select
292
+ className={INPUT}
293
+ aria-label="Platform"
294
+ value={platform}
295
+ disabled={disabled}
296
+ onChange={(event) => update(index, { platform: event.target.value })}
297
+ >
298
+ {platforms.map((option) => (
299
+ <option key={option} value={option}>
300
+ {option || 'Pick one'}
301
+ </option>
302
+ ))}
303
+ </select>
304
+ <input
305
+ className={`${INPUT} ${MONO}`}
306
+ aria-label={`${platform || 'Link'} URL`}
307
+ value={text(row.url)}
308
+ disabled={disabled}
309
+ spellCheck={false}
310
+ onChange={(event) => update(index, { url: event.target.value })}
311
+ />
312
+ <button
313
+ type="button"
314
+ aria-label={`Remove ${platform || 'link'}`}
315
+ disabled={disabled}
316
+ onClick={() => onChange(rows.filter((_, i) => i !== index))}
317
+ className="rounded-6 text-faint hover:bg-err-l hover:text-err flex h-7 w-7 cursor-pointer items-center justify-center border-0 bg-transparent p-0 disabled:cursor-not-allowed disabled:opacity-50"
318
+ >
319
+ <X size={14} strokeWidth={1.8} aria-hidden />
320
+ </button>
321
+ </div>
322
+ )
323
+ })}
324
+ <button
325
+ type="button"
326
+ disabled={disabled}
327
+ onClick={() =>
328
+ onChange([
329
+ ...rows,
330
+ {
331
+ platform:
332
+ SOCIAL_PLATFORMS.find((option) => !rows.some((row) => row.platform === option)) ??
333
+ SOCIAL_PLATFORMS[0],
334
+ url: 'https://',
335
+ },
336
+ ])
337
+ }
338
+ className="rounded-8 border-bdr bg-card text-meta text-txt hover:border-acc-m hover:bg-acc-l hover:text-acc inline-flex h-7 cursor-pointer items-center gap-[5px] self-start border px-[9px] leading-[normal] font-[550] whitespace-nowrap disabled:cursor-not-allowed disabled:opacity-45"
339
+ >
340
+ <Plus size={12} strokeWidth={1.8} aria-hidden />
341
+ Add link
342
+ </button>
343
+ </div>
344
+ )
345
+ }
346
+
347
+ return (
348
+ <div className="flex flex-col gap-1.5" data-field={fieldKey}>
349
+ {head}
350
+ {body}
351
+ {problem ? (
352
+ <Help id={problemId} tone="error">
353
+ {problem}
354
+ </Help>
355
+ ) : null}
356
+ </div>
357
+ )
358
+ }