@vertebr/react 0.11.0 → 0.13.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 (34) hide show
  1. package/dist/components/admin/AdminContent.d.ts.map +1 -1
  2. package/dist/components/admin/AdminContent.js +7 -10
  3. package/dist/components/admin/VertebrAdmin.d.ts.map +1 -1
  4. package/dist/components/admin/VertebrAdmin.js +6 -4
  5. package/dist/components/config/DescriptionInput.d.ts +3 -1
  6. package/dist/components/config/DescriptionInput.d.ts.map +1 -1
  7. package/dist/components/config/DescriptionInput.js +2 -2
  8. package/dist/components/config/DocumentAttachmentField.d.ts +17 -0
  9. package/dist/components/config/DocumentAttachmentField.d.ts.map +1 -0
  10. package/dist/components/config/DocumentAttachmentField.js +32 -0
  11. package/dist/components/config/ImagePickerModal.d.ts +12 -1
  12. package/dist/components/config/ImagePickerModal.d.ts.map +1 -1
  13. package/dist/components/config/ImagePickerModal.js +52 -23
  14. package/dist/components/config/MarkdownDocumentModal.d.ts +19 -0
  15. package/dist/components/config/MarkdownDocumentModal.d.ts.map +1 -0
  16. package/dist/components/config/MarkdownDocumentModal.js +34 -0
  17. package/dist/components/config/form/ConfigField.d.ts.map +1 -1
  18. package/dist/components/config/form/ConfigField.js +129 -15
  19. package/dist/components/config/form/RecordConfigForm.d.ts.map +1 -1
  20. package/dist/components/config/form/RecordConfigForm.js +24 -10
  21. package/dist/components/fonts/FontLibraryBoard.d.ts.map +1 -1
  22. package/dist/components/fonts/FontLibraryBoard.js +1 -1
  23. package/dist/components/gallery/GalleryBoard.d.ts.map +1 -1
  24. package/dist/components/gallery/GalleryBoard.js +37 -5
  25. package/dist/components/resources/ImageUploadControl.d.ts +3 -1
  26. package/dist/components/resources/ImageUploadControl.d.ts.map +1 -1
  27. package/dist/components/resources/ImageUploadControl.js +5 -3
  28. package/dist/lib/documents.d.ts +14 -0
  29. package/dist/lib/documents.d.ts.map +1 -0
  30. package/dist/lib/documents.js +42 -0
  31. package/dist/styles/form.css +28 -0
  32. package/dist/styles/gallery.css +237 -0
  33. package/dist/styles/modal.css +2 -1
  34. package/package.json +1 -1
@@ -1,37 +1,126 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
2
3
  import { Controller } from "react-hook-form";
3
4
  import * as Label from "@radix-ui/react-label";
4
5
  import * as Switch from "@radix-ui/react-switch";
5
6
  import { VisibilityEyeButton } from "../VisibilityEye";
6
7
  import { isDescriptionField } from "../../../lib/description";
7
8
  import { collectRecordImages, isImageField, urlsFromValue, } from "../../../lib/images";
9
+ import { isDocumentField } from "../../../lib/documents";
8
10
  import { isCentsField, isPriceField } from "../../../lib/price";
9
11
  import { FontPickerField } from "../../fonts/FontPickerField";
10
12
  import { ImageUploadControl } from "../../resources/ImageUploadControl";
11
13
  import { DescriptionInput } from "../DescriptionInput";
14
+ import { DocumentAttachmentField } from "../DocumentAttachmentField";
15
+ import { MarkdownDocumentModal } from "../MarkdownDocumentModal";
12
16
  import { DatetimeInput } from "../DatetimeInput";
13
17
  import { EnumInput } from "../EnumInput";
14
18
  import { EnumListInput } from "../EnumListInput";
19
+ import { ImagePickerModal } from "../ImagePickerModal";
15
20
  import { PriceInput } from "../PriceInput";
16
21
  import { ReferenceInput } from "../ReferenceInput";
17
22
  import { displayLabel, firstImageUrl, isMultilineField, fieldVisibilityKey, spanFor, } from "./helpers";
23
+ function findMarkdownBodyKey(schema, currentKey) {
24
+ const preferred = ["body", "content", "policyBody", "text"];
25
+ for (const key of preferred) {
26
+ const field = schema[key];
27
+ if (!field || key === currentKey)
28
+ continue;
29
+ if (field.type === "richtext" || isDescriptionField(key, field))
30
+ return key;
31
+ }
32
+ for (const [key, field] of Object.entries(schema)) {
33
+ if (key === currentKey)
34
+ continue;
35
+ if (field.type === "richtext" || isDescriptionField(key, field))
36
+ return key;
37
+ }
38
+ return null;
39
+ }
18
40
  export function ConfigField({ fieldKey, field, groupLabel, variant, schema, watched, form, uploads, onUpload, galleryUpload, onListGallery, onUploadGallery, fonts, fontsLoading, onListFonts, onRefreshFonts, hideImagePreview, hideUpload, visibilityGroup, settingsRow, client, }) {
41
+ const [pickerOpen, setPickerOpen] = useState(false);
42
+ const [docModalOpen, setDocModalOpen] = useState(false);
19
43
  const label = displayLabel(fieldKey, field, groupLabel);
44
+ const documentField = isDocumentField(fieldKey, field);
45
+ const markdownBodyKey = documentField
46
+ ? findMarkdownBodyKey(schema, fieldKey)
47
+ : null;
48
+ const markdownBodyField = markdownBodyKey
49
+ ? schema[markdownBodyKey]
50
+ : undefined;
51
+ const markdownBodyValue = markdownBodyKey && typeof watched?.[markdownBodyKey] === "string"
52
+ ? watched[markdownBodyKey]
53
+ : markdownBodyKey
54
+ ? ""
55
+ : undefined;
20
56
  const visibilityKey = fieldVisibilityKey(fieldKey, schema, visibilityGroup ?? groupLabel);
21
57
  const visibilityOn = !visibilityKey || watched?.[visibilityKey] !== false;
22
58
  const showInlinePreview = !hideImagePreview &&
23
59
  field.type !== "file[]" &&
24
60
  isImageField(fieldKey, field) &&
25
61
  Boolean(firstImageUrl(watched?.[fieldKey]));
62
+ const canPickFiles = !field.readOnly &&
63
+ Boolean(onListGallery) &&
64
+ (field.type === "file[]" ||
65
+ isImageField(fieldKey, field) ||
66
+ documentField);
67
+ const canLocalUpload = !hideUpload &&
68
+ Boolean(uploads?.[fieldKey] && onUpload) &&
69
+ !field.readOnly;
70
+ const canGalleryUpload = Boolean(onUploadGallery);
71
+ const applyPickedImages = (images) => {
72
+ const urls = images.map((image) => image.url).filter(Boolean);
73
+ if (!urls.length)
74
+ return;
75
+ if (field.type === "file[]") {
76
+ form.setValue(fieldKey, [...urlsFromValue(form.getValues(fieldKey)), ...urls], { shouldDirty: true, shouldTouch: true });
77
+ }
78
+ else {
79
+ form.setValue(fieldKey, urls[0], {
80
+ shouldDirty: true,
81
+ shouldTouch: true,
82
+ });
83
+ }
84
+ setPickerOpen(false);
85
+ };
26
86
  if (field.type === "boolean") {
27
87
  if (settingsRow) {
28
88
  return (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsxs("li", { className: "vertebr-settings-list-row", "data-on": Boolean(controllerField.value) ? "true" : "false", children: [_jsxs("div", { className: "vertebr-settings-list-copy", children: [_jsx(Label.Root, { htmlFor: fieldKey, children: label }), field.description ? (_jsx("p", { className: "vertebr-settings-list-desc", children: field.description })) : null] }), _jsx(Switch.Root, { className: "vertebr-switch", id: fieldKey, checked: Boolean(controllerField.value), onCheckedChange: controllerField.onChange, children: _jsx(Switch.Thumb, { className: "vertebr-switch-thumb" }) })] })) }));
29
89
  }
30
90
  return (_jsxs("div", { className: "vertebr-switch-row", children: [_jsxs("div", { className: "vertebr-switch-row-copy", children: [_jsx(Label.Root, { htmlFor: fieldKey, children: label }), field.description ? (_jsx("p", { className: "vertebr-switch-row-desc", children: field.description })) : null] }), _jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(Switch.Root, { className: "vertebr-switch", id: fieldKey, checked: Boolean(controllerField.value), onCheckedChange: controllerField.onChange, children: _jsx(Switch.Thumb, { className: "vertebr-switch-thumb" }) })) })] }));
31
91
  }
32
- return (_jsxs("div", { className: "vertebr-field", "data-span": spanFor(fieldKey, field, variant), "data-kind": isImageField(fieldKey, field) ? "image" : undefined, "data-storefront": visibilityKey ? (visibilityOn ? "on" : "off") : undefined, children: [_jsxs("div", { className: "vertebr-field-head", children: [_jsx(Label.Root, { htmlFor: fieldKey, children: label }), visibilityKey ? (_jsx(VisibilityEyeButton, { name: visibilityKey, label: label, form: form })) : null] }), showInlinePreview ? (_jsxs("div", { className: "vertebr-field-media", children: [_jsx("img", { src: firstImageUrl(watched[fieldKey]) ?? "", alt: "" }), _jsx("input", { id: fieldKey, type: "text", readOnly: field.readOnly, ...form.register(fieldKey) })] })) : field.type === "richtext" || isDescriptionField(fieldKey, field) ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(DescriptionInput, { id: fieldKey, name: controllerField.name, inputRef: controllerField.ref, value: typeof controllerField.value === "string"
92
+ return (_jsxs("div", { className: "vertebr-field", "data-span": spanFor(fieldKey, field, variant), "data-kind": isImageField(fieldKey, field)
93
+ ? "image"
94
+ : documentField
95
+ ? "document"
96
+ : undefined, "data-storefront": visibilityKey ? (visibilityOn ? "on" : "off") : undefined, children: [_jsxs("div", { className: "vertebr-field-head", children: [_jsx(Label.Root, { htmlFor: fieldKey, children: label }), visibilityKey ? (_jsx(VisibilityEyeButton, { name: visibilityKey, label: label, form: form })) : null] }), showInlinePreview ? (_jsxs("div", { className: "vertebr-field-media", children: [_jsx("img", { src: firstImageUrl(watched[fieldKey]) ?? "", alt: "" }), _jsx("input", { id: fieldKey, type: "text", readOnly: field.readOnly, ...form.register(fieldKey) })] })) : documentField ? (_jsx(DocumentAttachmentField, { id: fieldKey, label: label, value: typeof watched?.[fieldKey] === "string" ? watched[fieldKey] : "", readOnly: field.readOnly, description: field.description, markdownBody: markdownBodyValue, canPickFiles: canPickFiles, canUpload: canLocalUpload || canGalleryUpload, onChangeUrl: (next) => form.setValue(fieldKey, next, {
97
+ shouldDirty: true,
98
+ shouldTouch: true,
99
+ }), onOpenPicker: () => setPickerOpen(true), onUploadFiles: async (files) => {
100
+ if (canLocalUpload) {
101
+ const uploaded = await onUpload(fieldKey, files);
102
+ const url = uploaded[0]?.url;
103
+ if (url) {
104
+ form.setValue(fieldKey, url, {
105
+ shouldDirty: true,
106
+ shouldTouch: true,
107
+ });
108
+ }
109
+ return;
110
+ }
111
+ if (!onUploadGallery)
112
+ return;
113
+ const items = await onUploadGallery(files);
114
+ const url = items[0]?.url;
115
+ if (url) {
116
+ form.setValue(fieldKey, url, {
117
+ shouldDirty: true,
118
+ shouldTouch: true,
119
+ });
120
+ }
121
+ }, onEditMarkdown: markdownBodyKey ? () => setDocModalOpen(true) : undefined })) : field.type === "richtext" || isDescriptionField(fieldKey, field) ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(DescriptionInput, { id: fieldKey, name: controllerField.name, inputRef: controllerField.ref, value: typeof controllerField.value === "string"
33
122
  ? controllerField.value
34
- : "", readOnly: field.readOnly, recordImages: collectRecordImages(schema, watched), galleryUpload: galleryUpload, onListGallery: onListGallery, onUploadGallery: onUploadGallery, onBlur: controllerField.onBlur, onChange: controllerField.onChange })) })) : field.type === "file[]" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => {
123
+ : "", readOnly: field.readOnly, recordImages: collectRecordImages(schema, watched), galleryUpload: galleryUpload, onListGallery: onListGallery, onUploadGallery: onUploadGallery, onBlur: controllerField.onBlur, onChange: controllerField.onChange, onOpenDocumentView: () => setDocModalOpen(true) })) })) : field.type === "file[]" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => {
35
124
  const urls = urlsFromValue(controllerField.value);
36
125
  return (_jsx("div", { className: "vertebr-field-files", children: urls.length > 0 ? (_jsx("ul", { className: "vertebr-field-files-list", children: urls.map((url) => (_jsxs("li", { children: [_jsx("img", { src: url, alt: "" }), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", onClick: () => controllerField.onChange(urls.filter((item) => item !== url)), children: "Remove" })] }, url))) })) : (_jsx("p", { className: "vertebr-field-files-empty", children: "No photos yet" })) }));
37
126
  } })) : field.type === "number" && isPriceField(fieldKey, field) ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(PriceInput, { id: fieldKey, name: controllerField.name, inputRef: controllerField.ref, value: controllerField.value, asCents: isCentsField(fieldKey, field) || /^pricing$/i.test(groupLabel), readOnly: field.readOnly, onBlur: controllerField.onBlur, onChange: controllerField.onChange })) })) : field.type === "number" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx("input", { id: fieldKey, type: "number", readOnly: field.readOnly, value: typeof controllerField.value === "number" &&
@@ -40,20 +129,45 @@ export function ConfigField({ fieldKey, field, groupLabel, variant, schema, watc
40
129
  : "", onBlur: controllerField.onBlur, onChange: (event) => {
41
130
  const next = event.target.value;
42
131
  controllerField.onChange(next === "" ? undefined : Number(next));
43
- }, name: controllerField.name, ref: controllerField.ref })) })) : field.type === "datetime" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(DatetimeInput, { id: fieldKey, name: controllerField.name, inputRef: controllerField.ref, value: controllerField.value, readOnly: field.readOnly, onBlur: controllerField.onBlur, onChange: controllerField.onChange })) })) : field.type === "enum" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(EnumInput, { id: fieldKey, value: controllerField.value, field: field, readOnly: field.readOnly, onChange: controllerField.onChange })) })) : field.type === "enum[]" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(EnumListInput, { id: fieldKey, value: controllerField.value, field: field, readOnly: field.readOnly, onChange: controllerField.onChange })) })) : field.type === "font" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(FontPickerField, { id: fieldKey, value: controllerField.value, label: field.label, description: field.description, readOnly: field.readOnly, fonts: fonts ?? [], loading: fontsLoading, onChange: controllerField.onChange, onRefreshFonts: onRefreshFonts })) })) : field.type === "reference" && client ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(ReferenceInput, { id: fieldKey, value: controllerField.value, field: field, client: client, readOnly: field.readOnly, onChange: controllerField.onChange })) })) : isMultilineField(fieldKey, field) ? (_jsx("textarea", { id: fieldKey, rows: 5, readOnly: field.readOnly, ...form.register(fieldKey) })) : (_jsx("input", { id: fieldKey, type: "text", readOnly: field.readOnly, ...form.register(fieldKey) })), !hideUpload && uploads?.[fieldKey] && onUpload && !field.readOnly ? (_jsx(ImageUploadControl, { meta: uploads[fieldKey], multiple: field.type === "file[]", onUpload: async (files) => {
44
- const uploaded = await onUpload(fieldKey, files);
45
- const urls = uploaded
46
- .map((item) => item.url)
47
- .filter(Boolean);
48
- if (!urls.length)
49
- return;
50
- if (field.type === "file[]") {
51
- form.setValue(fieldKey, [...urlsFromValue(form.getValues(fieldKey)), ...urls], { shouldDirty: true, shouldTouch: true });
52
- return;
53
- }
54
- form.setValue(fieldKey, urls[0], {
132
+ }, name: controllerField.name, ref: controllerField.ref })) })) : field.type === "datetime" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(DatetimeInput, { id: fieldKey, name: controllerField.name, inputRef: controllerField.ref, value: controllerField.value, readOnly: field.readOnly, onBlur: controllerField.onBlur, onChange: controllerField.onChange })) })) : field.type === "enum" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(EnumInput, { id: fieldKey, value: controllerField.value, field: field, readOnly: field.readOnly, onChange: controllerField.onChange })) })) : field.type === "enum[]" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(EnumListInput, { id: fieldKey, value: controllerField.value, field: field, readOnly: field.readOnly, onChange: controllerField.onChange })) })) : field.type === "font" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(FontPickerField, { id: fieldKey, value: controllerField.value, label: field.label, description: field.description, readOnly: field.readOnly, fonts: fonts ?? [], loading: fontsLoading, onChange: controllerField.onChange, onRefreshFonts: onRefreshFonts })) })) : field.type === "reference" && client ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(ReferenceInput, { id: fieldKey, value: controllerField.value, field: field, client: client, readOnly: field.readOnly, onChange: controllerField.onChange })) })) : isMultilineField(fieldKey, field) ? (_jsx("textarea", { id: fieldKey, rows: 5, readOnly: field.readOnly, ...form.register(fieldKey) })) : (_jsx("input", { id: fieldKey, type: "text", readOnly: field.readOnly, ...form.register(fieldKey) })), !hideUpload &&
133
+ !documentField &&
134
+ (canLocalUpload || canPickFiles) ? (_jsxs("div", { className: "vertebr-field-media-actions", children: [canPickFiles ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", onClick: () => setPickerOpen(true), children: "Choose from Files" })) : null, canLocalUpload ? (_jsx(ImageUploadControl, { meta: uploads[fieldKey], multiple: field.type === "file[]", showOptimizeHint: true, onUpload: async (files) => {
135
+ const uploaded = await onUpload(fieldKey, files);
136
+ const urls = uploaded
137
+ .map((item) => item.url)
138
+ .filter(Boolean);
139
+ if (!urls.length)
140
+ return;
141
+ if (field.type === "file[]") {
142
+ form.setValue(fieldKey, [...urlsFromValue(form.getValues(fieldKey)), ...urls], { shouldDirty: true, shouldTouch: true });
143
+ return;
144
+ }
145
+ form.setValue(fieldKey, urls[0], {
146
+ shouldDirty: true,
147
+ shouldTouch: true,
148
+ });
149
+ } })) : null] })) : null, pickerOpen && canPickFiles ? (_jsx(ImagePickerModal, { title: documentField
150
+ ? "Choose PDF"
151
+ : field.type === "file[]"
152
+ ? "Add product photos"
153
+ : "Choose image", confirmLabel: "Use", defaultTab: "gallery", assetKind: documentField ? "document" : "image", recordImages: documentField ? [] : collectRecordImages(schema, watched), galleryUpload: galleryUpload, onListGallery: onListGallery, onUploadGallery: onUploadGallery, onClose: () => setPickerOpen(false), onInsert: applyPickedImages })) : null, docModalOpen &&
154
+ (markdownBodyKey ||
155
+ field.type === "richtext" ||
156
+ isDescriptionField(fieldKey, field)) ? (_jsx(MarkdownDocumentModal, { title: markdownBodyKey && markdownBodyField
157
+ ? displayLabel(markdownBodyKey, markdownBodyField, groupLabel)
158
+ : label, value: markdownBodyKey
159
+ ? typeof form.getValues(markdownBodyKey) === "string"
160
+ ? form.getValues(markdownBodyKey)
161
+ : ""
162
+ : typeof form.getValues(fieldKey) === "string"
163
+ ? form.getValues(fieldKey)
164
+ : "", readOnly: markdownBodyKey
165
+ ? Boolean(markdownBodyField?.readOnly)
166
+ : field.readOnly, recordImages: collectRecordImages(schema, watched), galleryUpload: galleryUpload, onListGallery: onListGallery, onUploadGallery: onUploadGallery, onChange: (next) => {
167
+ const key = markdownBodyKey ?? fieldKey;
168
+ form.setValue(key, next, {
55
169
  shouldDirty: true,
56
170
  shouldTouch: true,
57
171
  });
58
- } })) : null] }));
172
+ }, onClose: () => setDocModalOpen(false) })) : null] }));
59
173
  }
@@ -1 +1 @@
1
- {"version":3,"file":"RecordConfigForm.d.ts","sourceRoot":"","sources":["../../../../src/components/config/form/RecordConfigForm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAYtF,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE3D,MAAM,MAAM,qBAAqB,GAAG,eAAe,GAAG;IACpD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpC,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC7C,QAAQ,CAAC,EAAE,CACT,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,EAAE,KACV,OAAO,CAAC,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACpD,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,IAAI,CAAC;IAC5D,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,IAAI,GAAG,QAAQ,CAAC;IACvB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,MAAM,EACN,OAAO,EACP,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,aAAa,EACb,aAAa,EACb,eAAe,EACf,MAAM,EACN,QAAQ,EACR,OAAO,EACP,MAAM,EACN,IAAI,EACJ,SAAS,EACT,WAAW,EACX,WAAW,GACZ,EAAE,qBAAqB,+BAuJvB"}
1
+ {"version":3,"file":"RecordConfigForm.d.ts","sourceRoot":"","sources":["../../../../src/components/config/form/RecordConfigForm.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAatF,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE3D,MAAM,MAAM,qBAAqB,GAAG,eAAe,GAAG;IACpD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpC,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC7C,QAAQ,CAAC,EAAE,CACT,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,EAAE,KACV,OAAO,CAAC,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACpD,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,IAAI,CAAC;IAC5D,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,IAAI,GAAG,QAAQ,CAAC;IACvB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,MAAM,EACN,OAAO,EACP,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,aAAa,EACb,aAAa,EACb,eAAe,EACf,MAAM,EACN,QAAQ,EACR,OAAO,EACP,MAAM,EACN,IAAI,EACJ,SAAS,EACT,WAAW,EACX,WAAW,GACZ,EAAE,qBAAqB,+BA4LvB"}
@@ -1,15 +1,29 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { looksLikeUrl } from "../../../lib/images";
2
+ import { useState } from "react";
3
+ import { collectRecordImages, looksLikeUrl } from "../../../lib/images";
3
4
  import { ImageUploadControl } from "../../resources/ImageUploadControl";
5
+ import { ImagePickerModal } from "../ImagePickerModal";
4
6
  import { ConfigField } from "./ConfigField";
5
7
  import { ConfigFormActions } from "./ConfigFormActions";
6
8
  import { VisibilityEyeButton } from "../VisibilityEye";
7
9
  import { findPrimaryImageKey, firstImageUrl, groupVisibilityKey, isStockField, } from "./helpers";
8
10
  export function RecordConfigForm({ schema, groups, watched, form, uploads, onUpload, galleryUpload, onListGallery, onUploadGallery, client, onSubmit, isDirty, status, tone, onDiscard, submitLabel, hideActions, }) {
11
+ const [pickerOpen, setPickerOpen] = useState(false);
9
12
  const imageKey = findPrimaryImageKey(schema, watched);
10
13
  const imageValue = imageKey ? firstImageUrl(watched?.[imageKey]) ?? "" : "";
11
14
  const asideGroups = groups.filter((group) => /^(Media|Display|Status)$/i.test(group.id));
12
15
  const mainGroups = groups.filter((group) => !/^(Media|Display|Status)$/i.test(group.id));
16
+ const canPickPrimary = Boolean(imageKey && onListGallery);
17
+ const canUploadPrimary = Boolean(imageKey && uploads?.[imageKey] && onUpload);
18
+ const applyPrimaryImage = (images) => {
19
+ if (!imageKey)
20
+ return;
21
+ const url = images[0]?.url;
22
+ if (!url)
23
+ return;
24
+ form.setValue(imageKey, url, { shouldDirty: true, shouldTouch: true });
25
+ setPickerOpen(false);
26
+ };
13
27
  const fieldProps = {
14
28
  variant: "record",
15
29
  schema,
@@ -22,15 +36,15 @@ export function RecordConfigForm({ schema, groups, watched, form, uploads, onUpl
22
36
  onUploadGallery,
23
37
  client,
24
38
  };
25
- return (_jsxs("form", { className: "vertebr-form", "data-variant": "record", onSubmit: onSubmit, children: [_jsx("div", { className: "vertebr-form-scroll", children: _jsxs("div", { className: "vertebr-record-layout", children: [_jsxs("aside", { className: "vertebr-record-aside", children: [_jsx("div", { className: "vertebr-record-preview", children: imageValue && looksLikeUrl(imageValue) ? (_jsx("img", { src: imageValue, alt: "" })) : (_jsx("div", { className: "vertebr-record-preview-empty", children: "No image" })) }), imageKey && uploads?.[imageKey] && onUpload ? (_jsx(ImageUploadControl, { meta: uploads[imageKey], label: "Drop a new image or browse", onUpload: async (files) => {
26
- const [first] = await onUpload(imageKey, files);
27
- if (first?.url) {
28
- form.setValue(imageKey, first.url, {
29
- shouldDirty: true,
30
- shouldTouch: true,
31
- });
32
- }
33
- } })) : null, asideGroups.map((group) => (_jsxs("div", { className: "vertebr-record-block", "data-layout": /^inventory$/i.test(group.id) ? "chips" : undefined, children: [_jsx(RecordBlockLabel, { groupId: group.id, label: group.label, schema: schema, form: form }), _jsx("div", { className: "vertebr-form-fields", "data-layout": isStockField(group.fields[0]?.[0] ?? "", group.fields[0]?.[1])
39
+ return (_jsxs("form", { className: "vertebr-form", "data-variant": "record", onSubmit: onSubmit, children: [_jsx("div", { className: "vertebr-form-scroll", children: _jsxs("div", { className: "vertebr-record-layout", children: [_jsxs("aside", { className: "vertebr-record-aside", children: [_jsx("div", { className: "vertebr-record-preview", children: imageValue && looksLikeUrl(imageValue) ? (_jsx("img", { src: imageValue, alt: "" })) : (_jsx("div", { className: "vertebr-record-preview-empty", children: "No image" })) }), canPickPrimary || canUploadPrimary ? (_jsxs("div", { className: "vertebr-field-media-actions", children: [canPickPrimary ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", onClick: () => setPickerOpen(true), children: "Choose from Files" })) : null, canUploadPrimary ? (_jsx(ImageUploadControl, { meta: uploads[imageKey], label: "Or upload a new image", onUpload: async (files) => {
40
+ const [first] = await onUpload(imageKey, files);
41
+ if (first?.url) {
42
+ form.setValue(imageKey, first.url, {
43
+ shouldDirty: true,
44
+ shouldTouch: true,
45
+ });
46
+ }
47
+ } })) : null] })) : null, pickerOpen && canPickPrimary ? (_jsx(ImagePickerModal, { title: "Choose product image", confirmLabel: "Use", defaultTab: "gallery", recordImages: collectRecordImages(schema, watched), galleryUpload: galleryUpload, onListGallery: onListGallery, onUploadGallery: onUploadGallery, onClose: () => setPickerOpen(false), onInsert: applyPrimaryImage })) : null, asideGroups.map((group) => (_jsxs("div", { className: "vertebr-record-block", "data-layout": /^inventory$/i.test(group.id) ? "chips" : undefined, children: [_jsx(RecordBlockLabel, { groupId: group.id, label: group.label, schema: schema, form: form }), _jsx("div", { className: "vertebr-form-fields", "data-layout": isStockField(group.fields[0]?.[0] ?? "", group.fields[0]?.[1])
34
48
  ? "chips"
35
49
  : undefined, children: group.fields.map(([key, field]) => (_jsx(ConfigField, { fieldKey: key, field: field, groupLabel: group.label, visibilityGroup: group.id, hideImagePreview: true, hideUpload: key === imageKey, ...fieldProps }, key))) })] }, group.id)))] }), _jsx("div", { className: "vertebr-record-main", children: mainGroups.map((group) => {
36
50
  const inventory = /^inventory$/i.test(group.id);
@@ -1 +1 @@
1
- {"version":3,"file":"FontLibraryBoard.d.ts","sourceRoot":"","sources":["../../../src/components/fonts/FontLibraryBoard.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,mBAAmB,EACnB,uBAAuB,EAExB,MAAM,iBAAiB,CAAC;AAMzB,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,mBAAmB,CAAC;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,EAAE,CACR,KAAK,EAAE,IAAI,EAAE,EACb,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,KACxC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C,CAAC;AAUF,wBAAgB,gBAAgB,CAAC,EAC/B,KAAsB,EACtB,IAAI,EACJ,OAAO,EACP,SAAS,EACT,QAAQ,EACR,cAAc,EACd,QAAQ,GACT,EAAE,qBAAqB,+BAqIvB"}
1
+ {"version":3,"file":"FontLibraryBoard.d.ts","sourceRoot":"","sources":["../../../src/components/fonts/FontLibraryBoard.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,mBAAmB,EACnB,uBAAuB,EAExB,MAAM,iBAAiB,CAAC;AAMzB,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,mBAAmB,CAAC;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,EAAE,CACR,KAAK,EAAE,IAAI,EAAE,EACb,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,KACxC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C,CAAC;AAUF,wBAAgB,gBAAgB,CAAC,EAC/B,KAAsB,EACtB,IAAI,EACJ,OAAO,EACP,SAAS,EACT,QAAQ,EACR,cAAc,EACd,QAAQ,GACT,EAAE,qBAAqB,+BAsIvB"}
@@ -18,7 +18,7 @@ export function FontLibraryBoard({ label = "Font Library", data, loading, onRefr
18
18
  accept: data.accept,
19
19
  maxBytes: data.maxBytes,
20
20
  };
21
- return (_jsxs("div", { className: "vertebr-font-library", children: [_jsx(FontFaceInjector, { items: data.items }), _jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: label }), _jsxs("div", { className: "vertebr-font-library-head-actions", children: [_jsx("span", { className: "vertebr-count", children: data.items.length }), onRefresh ? (_jsx("button", { type: "button", className: "vertebr-btn", onClick: onRefresh, disabled: loading, children: loading ? "Refreshing…" : "Refresh" })) : null] })] }), _jsxs("div", { className: "vertebr-font-library-body", children: [data.canImportGoogle && onImportGoogle ? (_jsx(GoogleFontsBrowser, { disabled: loading, onImport: onImportGoogle, existingFamilies: data.items.map((item) => item.family.toLowerCase()) })) : null, _jsx(ImageUploadControl, { meta: meta, multiple: true, label: "Drop .woff2 / .woff / .ttf files here or browse to upload", onUpload: async (files) => {
21
+ return (_jsxs("div", { className: "vertebr-font-library", children: [_jsx(FontFaceInjector, { items: data.items }), _jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: label }), _jsxs("div", { className: "vertebr-font-library-head-actions", children: [_jsx("span", { className: "vertebr-count", children: data.items.length }), onRefresh ? (_jsx("button", { type: "button", className: "vertebr-btn", onClick: onRefresh, disabled: loading, children: loading ? "Refreshing…" : "Refresh" })) : null] })] }), _jsxs("div", { className: "vertebr-font-library-body", children: [data.canImportGoogle && onImportGoogle ? (_jsx(GoogleFontsBrowser, { disabled: loading, onImport: onImportGoogle, existingFamilies: data.items.map((item) => item.family.toLowerCase()) })) : null, _jsx(ImageUploadControl, { meta: meta, multiple: true, showOptimizeHint: false, label: "Drop .woff2 / .woff / .ttf files here or browse to upload", onUpload: async (files) => {
22
22
  for (const file of files) {
23
23
  const base = file.name.replace(/\.[^.]+$/, "");
24
24
  const weightMatch = /(?:^|[\s_-])(100|200|300|400|500|600|700|800|900)(?:[\s_-]|$)/i.exec(base);
@@ -1 +1 @@
1
- {"version":3,"file":"GalleryBoard.d.ts","sourceRoot":"","sources":["../../../src/components/gallery/GalleryBoard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,iBAAiB,CAAC;AAGvE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C,CAAC;AAEF,wBAAgB,YAAY,CAAC,EAC3B,KAAiB,EACjB,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,EACP,SAAS,EACT,QAAQ,EACR,QAAQ,GACT,EAAE,iBAAiB,+BAiFnB"}
1
+ {"version":3,"file":"GalleryBoard.d.ts","sourceRoot":"","sources":["../../../src/components/gallery/GalleryBoard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,iBAAiB,CAAC;AAIvE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C,CAAC;AAuBF,wBAAgB,YAAY,CAAC,EAC3B,KAAiB,EACjB,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,EACP,SAAS,EACT,QAAQ,EACR,QAAQ,GACT,EAAE,iBAAiB,+BAgHnB"}
@@ -1,14 +1,46 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { isPdfAsset, pdfFileName } from "../../lib/documents";
2
3
  import { ImageUploadControl } from "../resources/ImageUploadControl";
4
+ function acceptsDocuments(accept) {
5
+ return accept.some((entry) => /pdf/i.test(entry) ||
6
+ entry === "application/*" ||
7
+ entry === "*/*");
8
+ }
9
+ function acceptsImages(accept) {
10
+ return (accept.length === 0 ||
11
+ accept.some((entry) => entry.startsWith("image/") ||
12
+ entry === "image/*" ||
13
+ entry === "*/*"));
14
+ }
3
15
  export function GalleryBoard({ label = "Gallery", items, accept, maxBytes, canDelete, loading, onRefresh, onUpload, onDelete, }) {
4
16
  const meta = {
5
17
  field: "gallery",
6
18
  accept,
7
19
  maxBytes,
8
20
  };
9
- return (_jsxs("div", { className: "vertebr-gallery", children: [_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: label }), _jsxs("div", { className: "vertebr-gallery-head-actions", children: [_jsx("span", { className: "vertebr-count", children: items.length }), onRefresh ? (_jsx("button", { type: "button", className: "vertebr-btn", onClick: onRefresh, disabled: loading, children: loading ? "Refreshing…" : "Refresh" })) : null] })] }), _jsxs("div", { className: "vertebr-gallery-body", children: [_jsx(ImageUploadControl, { meta: meta, multiple: true, label: "Drop images here or browse to add to the gallery", onUpload: onUpload }), items.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: "No images in the gallery yet." })) : (_jsx("div", { className: "vertebr-gallery-grid", children: items.map((item) => (_jsxs("article", { className: "vertebr-gallery-card", children: [_jsx("a", { className: "vertebr-gallery-thumb", href: item.url, target: "_blank", rel: "noreferrer", title: item.name ?? item.url, children: _jsx("img", { src: item.url, alt: item.name ?? "", loading: "lazy" }) }), _jsxs("div", { className: "vertebr-gallery-meta", children: [_jsx("strong", { title: item.name ?? item.id, children: item.name ?? item.id }), _jsx("a", { href: item.url, target: "_blank", rel: "noreferrer", children: item.url.replace(/^https?:\/\//i, "") })] }), canDelete && onDelete ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-btn-danger-quiet", onClick: () => {
10
- if (window.confirm("Delete this image from the gallery?")) {
11
- void onDelete(item.id);
12
- }
13
- }, children: "Delete" })) : null] }, item.id))) }))] })] }));
21
+ const docs = acceptsDocuments(accept);
22
+ const images = acceptsImages(accept);
23
+ const dropLabel = docs && images
24
+ ? "Drop images or PDFs here or browse to add to Files"
25
+ : docs
26
+ ? "Drop PDFs here or browse to add to Files"
27
+ : "Drop images here or browse to add to the gallery";
28
+ const emptyLabel = docs && images
29
+ ? "No files yet."
30
+ : docs
31
+ ? "No documents yet."
32
+ : "No images in the gallery yet.";
33
+ return (_jsxs("div", { className: "vertebr-gallery", children: [_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: label }), _jsxs("div", { className: "vertebr-gallery-head-actions", children: [_jsx("span", { className: "vertebr-count", children: items.length }), onRefresh ? (_jsx("button", { type: "button", className: "vertebr-btn", onClick: onRefresh, disabled: loading, children: loading ? "Refreshing…" : "Refresh" })) : null] })] }), _jsxs("div", { className: "vertebr-gallery-body", children: [_jsx(ImageUploadControl, { meta: meta, multiple: true, showOptimizeHint: images, label: dropLabel, onUpload: onUpload }), items.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: emptyLabel })) : (_jsx("div", { className: "vertebr-gallery-grid", children: items.map((item) => {
34
+ const pdf = isPdfAsset(item);
35
+ return (_jsxs("article", { className: "vertebr-gallery-card", children: [_jsx("a", { className: pdf
36
+ ? "vertebr-gallery-thumb vertebr-gallery-thumb-file"
37
+ : "vertebr-gallery-thumb", href: item.url, target: "_blank", rel: "noreferrer", title: item.name ?? item.url, children: pdf ? (_jsx("span", { className: "vertebr-gallery-file-badge", children: "PDF" })) : (_jsx("img", { src: item.url, alt: item.name ?? "", loading: "lazy" })) }), _jsxs("div", { className: "vertebr-gallery-meta", children: [_jsx("strong", { title: item.name ?? item.id, children: item.name ??
38
+ (pdf ? pdfFileName(item.url) : item.id) }), _jsx("a", { href: item.url, target: "_blank", rel: "noreferrer", children: item.url.replace(/^https?:\/\//i, "") })] }), canDelete && onDelete ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-btn-danger-quiet", onClick: () => {
39
+ if (window.confirm(pdf
40
+ ? "Delete this document from Files?"
41
+ : "Delete this image from the gallery?")) {
42
+ void onDelete(item.id);
43
+ }
44
+ }, children: "Delete" })) : null] }, item.id));
45
+ }) }))] })] }));
14
46
  }
@@ -4,7 +4,9 @@ export type ImageUploadControlProps = {
4
4
  disabled?: boolean;
5
5
  multiple?: boolean;
6
6
  label?: string;
7
+ /** Show storefront optimize hint (image uploads). @default true */
8
+ showOptimizeHint?: boolean;
7
9
  onUpload: (files: File[]) => Promise<void>;
8
10
  };
9
- export declare function ImageUploadControl({ meta, disabled, multiple, label, onUpload, }: ImageUploadControlProps): import("react").JSX.Element;
11
+ export declare function ImageUploadControl({ meta, disabled, multiple, label, showOptimizeHint, onUpload, }: ImageUploadControlProps): import("react").JSX.Element;
10
12
  //# sourceMappingURL=ImageUploadControl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ImageUploadControl.d.ts","sourceRoot":"","sources":["../../../src/components/resources/ImageUploadControl.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1D,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,kBAAkB,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5C,CAAC;AAMF,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,QAAQ,EACR,QAAgB,EAChB,KAAK,EACL,QAAQ,GACT,EAAE,uBAAuB,+BA8EzB"}
1
+ {"version":3,"file":"ImageUploadControl.d.ts","sourceRoot":"","sources":["../../../src/components/resources/ImageUploadControl.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1D,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,kBAAkB,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5C,CAAC;AAMF,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,QAAQ,EACR,QAAgB,EAChB,KAAK,EACL,gBAAuB,EACvB,QAAQ,GACT,EAAE,uBAAuB,+BAqFzB"}
@@ -3,7 +3,7 @@ import { useRef, useState } from "react";
3
3
  function acceptAttr(accept) {
4
4
  return accept.join(",");
5
5
  }
6
- export function ImageUploadControl({ meta, disabled, multiple = false, label, onUpload, }) {
6
+ export function ImageUploadControl({ meta, disabled, multiple = false, label, showOptimizeHint = true, onUpload, }) {
7
7
  const inputRef = useRef(null);
8
8
  const [busy, setBusy] = useState(false);
9
9
  const [error, setError] = useState(null);
@@ -42,9 +42,11 @@ export function ImageUploadControl({ meta, disabled, multiple = false, label, on
42
42
  setDragOver(false);
43
43
  void run(event.dataTransfer.files);
44
44
  }, children: [_jsx("p", { className: "vertebr-upload-copy", children: busy
45
- ? "Uploading…"
45
+ ? showOptimizeHint
46
+ ? "Optimizing…"
47
+ : "Uploading…"
46
48
  : label ??
47
49
  (multiple
48
50
  ? "Drop images here or browse"
49
- : "Drop an image here or browse") }), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: disabled || busy, onClick: () => inputRef.current?.click(), children: multiple ? "Choose images" : "Choose image" }), _jsx("input", { ref: inputRef, type: "file", accept: acceptAttr(meta.accept), multiple: multiple, hidden: true, onChange: (event) => void run(event.target.files) })] }), error ? (_jsx("p", { className: "vertebr-upload-error", role: "alert", children: error })) : null] }));
51
+ : "Drop an image here or browse") }), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: disabled || busy, onClick: () => inputRef.current?.click(), children: multiple ? "Choose images" : "Choose image" }), _jsx("input", { ref: inputRef, type: "file", accept: acceptAttr(meta.accept), multiple: multiple, hidden: true, onChange: (event) => void run(event.target.files) })] }), showOptimizeHint ? (_jsx("p", { className: "vertebr-upload-hint", children: "Images are optimized for the storefront" })) : null, error ? (_jsx("p", { className: "vertebr-upload-error", role: "alert", children: error })) : null] }));
50
52
  }
@@ -0,0 +1,14 @@
1
+ import type { FieldSchema, GalleryItem } from "@vertebr/client";
2
+ import { looksLikeUrl } from "./images";
3
+ /** True when a gallery/file item is a PDF (by type, name, or URL). */
4
+ export declare function isPdfAsset(item: {
5
+ url?: string;
6
+ name?: string | null;
7
+ contentType?: string | null;
8
+ }): boolean;
9
+ /** Config string fields meant for a policy/document PDF URL. */
10
+ export declare function isDocumentField(key: string, field?: Pick<FieldSchema, "label" | "type">): boolean;
11
+ export declare function pdfFileName(url: string, fallback?: string): string;
12
+ export declare function filterGalleryByKind(items: GalleryItem[], kind: "image" | "document" | "any"): GalleryItem[];
13
+ export { looksLikeUrl };
14
+ //# sourceMappingURL=documents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../src/lib/documents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,IAAI,EAAE;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,GAAG,OAAO,CAMV;AAED,gEAAgE;AAChE,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,GAAG,MAAM,CAAC,GAC1C,OAAO,CAUT;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,SAAiB,GAAG,MAAM,CAQ1E;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,WAAW,EAAE,EACpB,IAAI,EAAE,OAAO,GAAG,UAAU,GAAG,KAAK,GACjC,WAAW,EAAE,CAIf;AAED,OAAO,EAAE,YAAY,EAAE,CAAC"}
@@ -0,0 +1,42 @@
1
+ import { looksLikeUrl } from "./images";
2
+ /** True when a gallery/file item is a PDF (by type, name, or URL). */
3
+ export function isPdfAsset(item) {
4
+ if (item.contentType && /pdf/i.test(item.contentType))
5
+ return true;
6
+ const name = item.name ?? "";
7
+ if (/\.pdf$/i.test(name))
8
+ return true;
9
+ const url = item.url ?? "";
10
+ return /\.pdf(?:$|\?)/i.test(url);
11
+ }
12
+ /** Config string fields meant for a policy/document PDF URL. */
13
+ export function isDocumentField(key, field) {
14
+ if (field?.type === "file[]")
15
+ return false;
16
+ if (field?.type && field.type !== "string")
17
+ return false;
18
+ const hay = `${key} ${field?.label ?? ""}`;
19
+ if (/\bpdf\b|pdfUrl|pdf_url|documentUrl|document_url/i.test(hay))
20
+ return true;
21
+ // Policy-ish labels only when they also look like a file/URL attachment.
22
+ return (/(policy|legal|terms|privacy|refund|shipping)/i.test(hay) &&
23
+ /(pdf|url|file|attach|document)/i.test(hay));
24
+ }
25
+ export function pdfFileName(url, fallback = "document.pdf") {
26
+ try {
27
+ const path = new URL(url).pathname;
28
+ const base = path.split("/").pop() || fallback;
29
+ return decodeURIComponent(base);
30
+ }
31
+ catch {
32
+ return fallback;
33
+ }
34
+ }
35
+ export function filterGalleryByKind(items, kind) {
36
+ if (kind === "any")
37
+ return items;
38
+ if (kind === "document")
39
+ return items.filter(isPdfAsset);
40
+ return items.filter((item) => !isPdfAsset(item));
41
+ }
42
+ export { looksLikeUrl };
@@ -1047,6 +1047,21 @@
1047
1047
  background: var(--vb-surface-tint);
1048
1048
  }
1049
1049
 
1050
+ .vertebr-md-doc-link {
1051
+ appearance: none;
1052
+ border: 0;
1053
+ padding: 0;
1054
+ background: transparent;
1055
+ color: var(--vb-accent);
1056
+ font: inherit;
1057
+ letter-spacing: inherit;
1058
+ cursor: pointer;
1059
+ }
1060
+
1061
+ .vertebr-md-doc-link:hover {
1062
+ text-decoration: underline;
1063
+ }
1064
+
1050
1065
  .vertebr-field input:hover,
1051
1066
  .vertebr-field textarea:hover {
1052
1067
  border-color: var(--vb-border-strong);
@@ -1111,6 +1126,12 @@
1111
1126
  color: var(--vb-text-subtle);
1112
1127
  }
1113
1128
 
1129
+ .vertebr-field-media-actions {
1130
+ display: flex;
1131
+ flex-direction: column;
1132
+ gap: 0.55rem;
1133
+ }
1134
+
1114
1135
  .vertebr-field[data-kind="image"] {
1115
1136
  grid-column: 1 / -1;
1116
1137
  }
@@ -1294,6 +1315,13 @@
1294
1315
  line-height: 1.35;
1295
1316
  }
1296
1317
 
1318
+ .vertebr-upload-hint {
1319
+ margin: 0;
1320
+ font-size: 0.72rem;
1321
+ color: var(--vb-text-faint, var(--vb-text-muted));
1322
+ line-height: 1.35;
1323
+ }
1324
+
1297
1325
  .vertebr-upload-error {
1298
1326
  margin: 0;
1299
1327
  font-size: 0.78rem;