@vertebr/react 0.10.0 → 0.12.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 (52) hide show
  1. package/dist/components/admin/AddonEditor.d.ts.map +1 -1
  2. package/dist/components/admin/AddonEditor.js +10 -8
  3. package/dist/components/admin/AdminContent.d.ts.map +1 -1
  4. package/dist/components/admin/AdminContent.js +62 -17
  5. package/dist/components/admin/AdminSaveBar.d.ts.map +1 -1
  6. package/dist/components/admin/AdminSaveBar.js +6 -1
  7. package/dist/components/admin/AdminSyncPage.d.ts.map +1 -1
  8. package/dist/components/admin/AdminSyncPage.js +50 -6
  9. package/dist/components/admin/PagePreviewPane.d.ts +3 -1
  10. package/dist/components/admin/PagePreviewPane.d.ts.map +1 -1
  11. package/dist/components/admin/PagePreviewPane.js +41 -9
  12. package/dist/components/admin/SyncPreviewFrame.d.ts +22 -0
  13. package/dist/components/admin/SyncPreviewFrame.d.ts.map +1 -0
  14. package/dist/components/admin/SyncPreviewFrame.js +135 -0
  15. package/dist/components/admin/VertebrAdmin.d.ts.map +1 -1
  16. package/dist/components/admin/VertebrAdmin.js +6 -4
  17. package/dist/components/config/DatetimeInput.d.ts +15 -0
  18. package/dist/components/config/DatetimeInput.d.ts.map +1 -0
  19. package/dist/components/config/DatetimeInput.js +287 -0
  20. package/dist/components/config/EnumInput.d.ts.map +1 -1
  21. package/dist/components/config/EnumInput.js +4 -4
  22. package/dist/components/config/EnumListInput.d.ts.map +1 -1
  23. package/dist/components/config/EnumListInput.js +13 -3
  24. package/dist/components/config/ImagePickerModal.d.ts +9 -1
  25. package/dist/components/config/ImagePickerModal.d.ts.map +1 -1
  26. package/dist/components/config/ImagePickerModal.js +14 -7
  27. package/dist/components/config/RevisionHistory.d.ts +4 -0
  28. package/dist/components/config/RevisionHistory.d.ts.map +1 -1
  29. package/dist/components/config/RevisionHistory.js +63 -8
  30. package/dist/components/config/form/ConfigField.d.ts.map +1 -1
  31. package/dist/components/config/form/ConfigField.js +41 -16
  32. package/dist/components/config/form/ConfigForm.d.ts +1 -1
  33. package/dist/components/config/form/ConfigForm.d.ts.map +1 -1
  34. package/dist/components/config/form/ConfigForm.js +54 -9
  35. package/dist/components/config/form/RecordConfigForm.d.ts.map +1 -1
  36. package/dist/components/config/form/RecordConfigForm.js +24 -10
  37. package/dist/components/config/form/types.d.ts +9 -0
  38. package/dist/components/config/form/types.d.ts.map +1 -1
  39. package/dist/components/fonts/FontLibraryBoard.d.ts.map +1 -1
  40. package/dist/components/fonts/FontLibraryBoard.js +1 -1
  41. package/dist/components/resources/ImageUploadControl.d.ts +3 -1
  42. package/dist/components/resources/ImageUploadControl.d.ts.map +1 -1
  43. package/dist/components/resources/ImageUploadControl.js +5 -3
  44. package/dist/hooks/useSaveBar.d.ts +6 -0
  45. package/dist/hooks/useSaveBar.d.ts.map +1 -1
  46. package/dist/styles/form.css +465 -17
  47. package/dist/styles/modal.css +2 -1
  48. package/dist/styles/page-preview.css +84 -0
  49. package/dist/styles/select.css +23 -0
  50. package/dist/styles/sync.css +388 -4
  51. package/dist/themes/light.css +27 -0
  52. package/package.json +3 -1
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useState } from "react";
3
3
  import { useToast } from "../../hooks/useToast";
4
4
  function formatWhen(at) {
@@ -12,16 +12,32 @@ function formatWhen(at) {
12
12
  minute: "2-digit",
13
13
  });
14
14
  }
15
+ function formatActor(value) {
16
+ if (!value)
17
+ return null;
18
+ if (value.includes("@"))
19
+ return value;
20
+ if (value.length <= 12)
21
+ return value;
22
+ return `${value.slice(0, 6)}…${value.slice(-4)}`;
23
+ }
24
+ /**
25
+ * Compact History control — opens revision list in a modal so the
26
+ * editor form stays the primary surface.
27
+ */
15
28
  export function RevisionHistory({ client, configId, onPublished, }) {
16
29
  const { push } = useToast();
30
+ const [open, setOpen] = useState(false);
17
31
  const [items, setItems] = useState([]);
18
- const [loading, setLoading] = useState(true);
32
+ const [count, setCount] = useState(null);
33
+ const [loading, setLoading] = useState(false);
19
34
  const [busyId, setBusyId] = useState(null);
20
35
  const refresh = useCallback(async () => {
21
36
  setLoading(true);
22
37
  try {
23
38
  const data = await client.listRevisions(configId, { fresh: true });
24
39
  setItems(data.items);
40
+ setCount(data.items.length);
25
41
  }
26
42
  finally {
27
43
  setLoading(false);
@@ -31,10 +47,30 @@ export function RevisionHistory({ client, configId, onPublished, }) {
31
47
  let cancelled = false;
32
48
  void (async () => {
33
49
  try {
34
- setLoading(true);
35
50
  const data = await client.listRevisions(configId, { fresh: true });
36
51
  if (!cancelled)
52
+ setCount(data.items.length);
53
+ }
54
+ catch {
55
+ /* badge is optional */
56
+ }
57
+ })();
58
+ return () => {
59
+ cancelled = true;
60
+ };
61
+ }, [client, configId]);
62
+ useEffect(() => {
63
+ if (!open)
64
+ return;
65
+ let cancelled = false;
66
+ void (async () => {
67
+ try {
68
+ setLoading(true);
69
+ const data = await client.listRevisions(configId, { fresh: true });
70
+ if (!cancelled) {
37
71
  setItems(data.items);
72
+ setCount(data.items.length);
73
+ }
38
74
  }
39
75
  catch (err) {
40
76
  if (!cancelled) {
@@ -49,7 +85,22 @@ export function RevisionHistory({ client, configId, onPublished, }) {
49
85
  return () => {
50
86
  cancelled = true;
51
87
  };
52
- }, [client, configId, push]);
88
+ }, [open, client, configId, push]);
89
+ useEffect(() => {
90
+ if (!open)
91
+ return;
92
+ const onKeyDown = (event) => {
93
+ if (event.key === "Escape")
94
+ setOpen(false);
95
+ };
96
+ window.addEventListener("keydown", onKeyDown);
97
+ const previous = document.body.style.overflow;
98
+ document.body.style.overflow = "hidden";
99
+ return () => {
100
+ window.removeEventListener("keydown", onKeyDown);
101
+ document.body.style.overflow = previous;
102
+ };
103
+ }, [open]);
53
104
  const publish = async (revId) => {
54
105
  setBusyId(revId);
55
106
  try {
@@ -65,20 +116,24 @@ export function RevisionHistory({ client, configId, onPublished, }) {
65
116
  setBusyId(null);
66
117
  }
67
118
  };
68
- const restore = async (revId) => {
119
+ const useRevision = async (revId) => {
69
120
  setBusyId(revId);
70
121
  try {
71
122
  await client.restoreRevision(configId, revId);
72
- push("Draft restored from revision", "ok");
123
+ push("Now editing this revision", "ok");
73
124
  await refresh();
74
125
  await onPublished?.();
75
126
  }
76
127
  catch (err) {
77
- push(err instanceof Error ? err.message : "Restore failed", "danger");
128
+ push(err instanceof Error ? err.message : "Could not switch revision", "danger");
78
129
  }
79
130
  finally {
80
131
  setBusyId(null);
81
132
  }
82
133
  };
83
- return (_jsxs("div", { className: "vertebr-revisions", children: [_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h3", { children: "Revisions" }), _jsx("span", { className: "vertebr-count", children: loading ? "…" : `${items.length}` })] }), items.length === 0 && !loading ? (_jsx("p", { className: "vertebr-revisions-empty", children: "No revisions yet." })) : (_jsx("ul", { className: "vertebr-revisions-list", children: items.map((rev) => (_jsxs("li", { className: "vertebr-revisions-item", children: [_jsxs("div", { className: "vertebr-revisions-meta", children: [_jsx("strong", { children: rev.label?.trim() || rev.id }), _jsx("span", { "data-status": rev.status, children: rev.status }), _jsxs("span", { className: "vertebr-revisions-when", children: [formatWhen(rev.createdAt), rev.createdBy ? ` · ${rev.createdBy}` : ""] })] }), _jsxs("div", { className: "vertebr-revisions-actions", children: [rev.status !== "published" ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: busyId != null, onClick: () => void publish(rev.id), children: busyId === rev.id ? "…" : "Publish" })) : null, _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: busyId != null, onClick: () => void restore(rev.id), children: "Restore" })] })] }, rev.id))) }))] }));
134
+ return (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: "vertebr-revisions-trigger", "aria-haspopup": "dialog", "aria-expanded": open, onClick: () => setOpen(true), children: ["History", count != null ? (_jsx("span", { className: "vertebr-revisions-trigger-count", children: count })) : null] }), open ? (_jsx("div", { className: "vertebr-modal-backdrop", role: "presentation", onClick: () => setOpen(false), children: _jsxs("div", { className: "vertebr-modal vertebr-modal-sm vertebr-revisions-modal", role: "dialog", "aria-modal": "true", "aria-label": "Revision history", onClick: (event) => event.stopPropagation(), children: [_jsxs("header", { className: "vertebr-modal-head", children: [_jsxs("div", { className: "vertebr-modal-heading", children: [_jsx("p", { className: "vertebr-modal-eyebrow", children: "Change safety" }), _jsx("h2", { children: "Revisions" })] }), _jsxs("div", { className: "vertebr-modal-actions", children: [_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: loading || busyId != null, onClick: () => void refresh(), children: loading ? "…" : "Refresh" }), _jsx("button", { type: "button", className: "vertebr-btn", onClick: () => setOpen(false), children: "Close" })] })] }), _jsx("div", { className: "vertebr-modal-body", children: items.length === 0 && !loading ? (_jsx("p", { className: "vertebr-revisions-empty", children: "No revisions yet. Save to create a draft." })) : (_jsx("ul", { className: "vertebr-revisions-list", children: items.map((rev) => {
135
+ const actor = formatActor(rev.createdBy);
136
+ const isCurrent = Boolean(rev.active);
137
+ return (_jsxs("li", { className: "vertebr-revisions-item", "data-active": isCurrent ? "true" : undefined, children: [_jsxs("div", { className: "vertebr-revisions-meta", children: [_jsx("strong", { children: rev.label?.trim() || rev.id }), _jsx("span", { "data-status": rev.status, children: rev.status }), isCurrent ? (_jsx("span", { "data-status": "current", children: "Current" })) : null, _jsxs("span", { className: "vertebr-revisions-when", children: [formatWhen(rev.createdAt), actor ? ` · ${actor}` : ""] })] }), _jsxs("div", { className: "vertebr-revisions-actions", children: [rev.status !== "published" ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: busyId != null, onClick: () => void publish(rev.id), children: busyId === rev.id ? "…" : "Publish" })) : null, isCurrent ? null : (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: busyId != null, onClick: () => void useRevision(rev.id), children: "Use" }))] })] }, rev.id));
138
+ }) })) })] }) })) : null] }));
84
139
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ConfigField.d.ts","sourceRoot":"","sources":["../../../../src/components/config/form/ConfigField.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAIjE,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAsBtF,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAEvF,MAAM,MAAM,gBAAgB,GAAG,eAAe,GAC5C,mBAAmB,GAAG;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpC,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,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB,CAAC;AAEF,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,KAAK,EACL,UAAU,EACV,OAAO,EACP,MAAM,EACN,OAAO,EACP,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,aAAa,EACb,aAAa,EACb,eAAe,EACf,KAAK,EACL,YAAY,EACZ,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,eAAe,EACf,WAAW,EACX,MAAM,GACP,EAAE,gBAAgB,+BAkTlB"}
1
+ {"version":3,"file":"ConfigField.d.ts","sourceRoot":"","sources":["../../../../src/components/config/form/ConfigField.tsx"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAIjE,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAwBtF,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAEvF,MAAM,MAAM,gBAAgB,GAAG,eAAe,GAC5C,mBAAmB,GAAG;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpC,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,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB,CAAC;AAEF,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,KAAK,EACL,UAAU,EACV,OAAO,EACP,MAAM,EACN,OAAO,EACP,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,aAAa,EACb,aAAa,EACb,eAAe,EACf,KAAK,EACL,YAAY,EACZ,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,eAAe,EACf,WAAW,EACX,MAAM,GACP,EAAE,gBAAgB,+BAyXlB"}
@@ -1,4 +1,5 @@
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";
@@ -9,12 +10,15 @@ import { isCentsField, isPriceField } from "../../../lib/price";
9
10
  import { FontPickerField } from "../../fonts/FontPickerField";
10
11
  import { ImageUploadControl } from "../../resources/ImageUploadControl";
11
12
  import { DescriptionInput } from "../DescriptionInput";
13
+ import { DatetimeInput } from "../DatetimeInput";
12
14
  import { EnumInput } from "../EnumInput";
13
15
  import { EnumListInput } from "../EnumListInput";
16
+ import { ImagePickerModal } from "../ImagePickerModal";
14
17
  import { PriceInput } from "../PriceInput";
15
18
  import { ReferenceInput } from "../ReferenceInput";
16
19
  import { displayLabel, firstImageUrl, isMultilineField, fieldVisibilityKey, spanFor, } from "./helpers";
17
20
  export function ConfigField({ fieldKey, field, groupLabel, variant, schema, watched, form, uploads, onUpload, galleryUpload, onListGallery, onUploadGallery, fonts, fontsLoading, onListFonts, onRefreshFonts, hideImagePreview, hideUpload, visibilityGroup, settingsRow, client, }) {
21
+ const [pickerOpen, setPickerOpen] = useState(false);
18
22
  const label = displayLabel(fieldKey, field, groupLabel);
19
23
  const visibilityKey = fieldVisibilityKey(fieldKey, schema, visibilityGroup ?? groupLabel);
20
24
  const visibilityOn = !visibilityKey || watched?.[visibilityKey] !== false;
@@ -22,6 +26,27 @@ export function ConfigField({ fieldKey, field, groupLabel, variant, schema, watc
22
26
  field.type !== "file[]" &&
23
27
  isImageField(fieldKey, field) &&
24
28
  Boolean(firstImageUrl(watched?.[fieldKey]));
29
+ const canPickFiles = !field.readOnly &&
30
+ Boolean(onListGallery) &&
31
+ (field.type === "file[]" || isImageField(fieldKey, field));
32
+ const canLocalUpload = !hideUpload &&
33
+ Boolean(uploads?.[fieldKey] && onUpload) &&
34
+ !field.readOnly;
35
+ const applyPickedImages = (images) => {
36
+ const urls = images.map((image) => image.url).filter(Boolean);
37
+ if (!urls.length)
38
+ return;
39
+ if (field.type === "file[]") {
40
+ form.setValue(fieldKey, [...urlsFromValue(form.getValues(fieldKey)), ...urls], { shouldDirty: true, shouldTouch: true });
41
+ }
42
+ else {
43
+ form.setValue(fieldKey, urls[0], {
44
+ shouldDirty: true,
45
+ shouldTouch: true,
46
+ });
47
+ }
48
+ setPickerOpen(false);
49
+ };
25
50
  if (field.type === "boolean") {
26
51
  if (settingsRow) {
27
52
  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" }) })] })) }));
@@ -39,20 +64,20 @@ export function ConfigField({ fieldKey, field, groupLabel, variant, schema, watc
39
64
  : "", onBlur: controllerField.onBlur, onChange: (event) => {
40
65
  const next = event.target.value;
41
66
  controllerField.onChange(next === "" ? undefined : Number(next));
42
- }, name: controllerField.name, ref: controllerField.ref })) })) : 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) => {
43
- const uploaded = await onUpload(fieldKey, files);
44
- const urls = uploaded
45
- .map((item) => item.url)
46
- .filter(Boolean);
47
- if (!urls.length)
48
- return;
49
- if (field.type === "file[]") {
50
- form.setValue(fieldKey, [...urlsFromValue(form.getValues(fieldKey)), ...urls], { shouldDirty: true, shouldTouch: true });
51
- return;
52
- }
53
- form.setValue(fieldKey, urls[0], {
54
- shouldDirty: true,
55
- shouldTouch: true,
56
- });
57
- } })) : null] }));
67
+ }, 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 && (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[]", onUpload: async (files) => {
68
+ const uploaded = await onUpload(fieldKey, files);
69
+ const urls = uploaded
70
+ .map((item) => item.url)
71
+ .filter(Boolean);
72
+ if (!urls.length)
73
+ return;
74
+ if (field.type === "file[]") {
75
+ form.setValue(fieldKey, [...urlsFromValue(form.getValues(fieldKey)), ...urls], { shouldDirty: true, shouldTouch: true });
76
+ return;
77
+ }
78
+ form.setValue(fieldKey, urls[0], {
79
+ shouldDirty: true,
80
+ shouldTouch: true,
81
+ });
82
+ } })) : null] })) : null, pickerOpen && canPickFiles ? (_jsx(ImagePickerModal, { title: field.type === "file[]" ? "Add product photos" : "Choose image", confirmLabel: "Use", defaultTab: "gallery", recordImages: collectRecordImages(schema, watched), galleryUpload: galleryUpload, onListGallery: onListGallery, onUploadGallery: onUploadGallery, onClose: () => setPickerOpen(false), onInsert: applyPickedImages })) : null] }));
58
83
  }
@@ -1,4 +1,4 @@
1
1
  import type { ConfigFormProps } from "./types";
2
2
  export type { ConfigFormProps, ConfigFormVariant } from "./types";
3
- export declare function ConfigForm({ schema, value, onSave, submitLabel, variant, uploads, onUpload, galleryUpload, onListGallery, onUploadGallery, fonts, fontsLoading, onListFonts, onRefreshFonts, onChange, hideActions, client, canWrite, }: ConfigFormProps): import("react").JSX.Element;
3
+ export declare function ConfigForm({ schema, value, onSave, submitLabel, variant, uploads, onUpload, galleryUpload, onListGallery, onUploadGallery, fonts, fontsLoading, onListFonts, onRefreshFonts, onChange, hideActions, client, canWrite, revisionsEnabled, onPublish, canPublish, }: ConfigFormProps): import("react").JSX.Element;
4
4
  //# sourceMappingURL=ConfigForm.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ConfigForm.d.ts","sourceRoot":"","sources":["../../../../src/components/config/form/ConfigForm.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAElE,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,KAAK,EACL,MAAM,EACN,WAAW,EACX,OAAkB,EAClB,OAAO,EACP,QAAQ,EACR,aAAa,EACb,aAAa,EACb,eAAe,EACf,KAAK,EACL,YAAY,EACZ,WAAW,EACX,cAAc,EACd,QAAQ,EACR,WAAW,EACX,MAAM,EACN,QAAe,GAChB,EAAE,eAAe,+BA2IjB"}
1
+ {"version":3,"file":"ConfigForm.d.ts","sourceRoot":"","sources":["../../../../src/components/config/form/ConfigForm.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAElE,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,KAAK,EACL,MAAM,EACN,WAAW,EACX,OAAkB,EAClB,OAAO,EACP,QAAQ,EACR,aAAa,EACb,aAAa,EACb,eAAe,EACf,KAAK,EACL,YAAY,EACZ,WAAW,EACX,cAAc,EACd,QAAQ,EACR,WAAW,EACX,MAAM,EACN,QAAe,EACf,gBAAwB,EACxB,SAAS,EACT,UAAkB,GACnB,EAAE,eAAe,+BA2LjB"}
@@ -7,7 +7,7 @@ import { useToast } from "../../../hooks/useToast";
7
7
  import { buildZodSchema, groupFields, withSpecVisibility, withSpecVisibilityValues, } from "./helpers";
8
8
  import { RecordConfigForm } from "./RecordConfigForm";
9
9
  import { SettingsConfigForm } from "./SettingsConfigForm";
10
- export function ConfigForm({ schema, value, onSave, submitLabel, variant = "config", uploads, onUpload, galleryUpload, onListGallery, onUploadGallery, fonts, fontsLoading, onListFonts, onRefreshFonts, onChange, hideActions, client, canWrite = true, }) {
10
+ export function ConfigForm({ schema, value, onSave, submitLabel, variant = "config", uploads, onUpload, galleryUpload, onListGallery, onUploadGallery, fonts, fontsLoading, onListFonts, onRefreshFonts, onChange, hideActions, client, canWrite = true, revisionsEnabled = false, onPublish, canPublish = false, }) {
11
11
  const { register, clear } = useSaveBar();
12
12
  const { push: pushToast } = useToast();
13
13
  const editorSchema = useMemo(() => (variant === "record" ? withSpecVisibility(schema) : schema), [schema, variant]);
@@ -18,14 +18,21 @@ export function ConfigForm({ schema, value, onSave, submitLabel, variant = "conf
18
18
  const groups = useMemo(() => groupFields(editorSchema, variant), [editorSchema, variant]);
19
19
  const [status, setStatus] = useState(null);
20
20
  const [tone, setTone] = useState();
21
+ const [publishing, setPublishing] = useState(false);
21
22
  const form = useForm({
22
23
  resolver: zodResolver(zodSchema),
23
24
  defaultValues: editorValue,
24
25
  });
25
26
  const watched = useWatch({ control: form.control });
26
27
  useEffect(() => {
27
- onChange?.((watched ?? {}));
28
- }, [watched, onChange]);
28
+ if (!onChange)
29
+ return;
30
+ const subscription = form.watch((values) => {
31
+ onChange((values ?? {}));
32
+ });
33
+ onChange((form.getValues() ?? {}));
34
+ return () => subscription.unsubscribe();
35
+ }, [form, onChange]);
29
36
  useEffect(() => {
30
37
  form.reset(editorValue);
31
38
  }, [editorValue, form]);
@@ -34,18 +41,24 @@ export function ConfigForm({ schema, value, onSave, submitLabel, variant = "conf
34
41
  setStatus(null);
35
42
  setTone(undefined);
36
43
  };
44
+ const saveLabel = submitLabel ?? (revisionsEnabled ? "Save draft" : "Save");
37
45
  const onSubmit = form.handleSubmit(async (data) => {
38
46
  if (!canWrite)
39
47
  return;
40
- setStatus("Saving…");
48
+ setStatus(revisionsEnabled ? "Saving draft…" : "Saving…");
41
49
  setTone(undefined);
42
50
  register({ saving: true, isDirty: true, visible: true });
43
51
  try {
44
52
  await onSave(data);
45
- setStatus("Saved");
53
+ const nextStatus = revisionsEnabled
54
+ ? "Draft saved — not live until you publish"
55
+ : "Saved";
56
+ setStatus(nextStatus);
46
57
  setTone("ok");
47
58
  form.reset(data);
48
- pushToast("Changes saved", "ok");
59
+ pushToast(revisionsEnabled
60
+ ? "Draft saved. Publish to update the live storefront."
61
+ : "Changes saved", "ok");
49
62
  }
50
63
  catch (error) {
51
64
  const message = error instanceof Error ? error.message : "Save failed";
@@ -57,22 +70,51 @@ export function ConfigForm({ schema, value, onSave, submitLabel, variant = "conf
57
70
  register({ saving: false });
58
71
  }
59
72
  });
73
+ const handlePublish = async () => {
74
+ if (!onPublish || !canPublish)
75
+ return;
76
+ setPublishing(true);
77
+ setStatus("Publishing…");
78
+ setTone(undefined);
79
+ register({ publishing: true, visible: true });
80
+ try {
81
+ await onPublish();
82
+ setStatus("Published");
83
+ setTone("ok");
84
+ pushToast("Published to live storefront", "ok");
85
+ }
86
+ catch (error) {
87
+ const message = error instanceof Error ? error.message : "Publish failed";
88
+ setStatus(message);
89
+ setTone("danger");
90
+ pushToast(message, "danger");
91
+ }
92
+ finally {
93
+ setPublishing(false);
94
+ register({ publishing: false });
95
+ }
96
+ };
60
97
  const isDirty = form.formState.isDirty;
98
+ const showPublish = Boolean(revisionsEnabled && canPublish && onPublish);
61
99
  useEffect(() => {
62
100
  if (hideActions) {
63
101
  clear();
64
102
  return;
65
103
  }
66
104
  register({
67
- visible: isDirty,
105
+ visible: isDirty || Boolean(status) || showPublish,
68
106
  isDirty,
69
- saving: status === "Saving…",
107
+ saving: status === "Saving…" || status === "Saving draft…",
108
+ publishing,
70
109
  message: !canWrite && isDirty
71
110
  ? "Missing config.write capability"
72
111
  : (status ?? undefined),
73
112
  tone: !canWrite && isDirty ? "danger" : tone,
113
+ saveLabel,
74
114
  onSave: canWrite ? () => void onSubmit() : undefined,
75
115
  onDiscard,
116
+ publishLabel: "Publish",
117
+ onPublish: showPublish ? () => void handlePublish() : undefined,
76
118
  });
77
119
  return () => clear();
78
120
  }, [
@@ -85,6 +127,9 @@ export function ConfigForm({ schema, value, onSave, submitLabel, variant = "conf
85
127
  register,
86
128
  clear,
87
129
  canWrite,
130
+ saveLabel,
131
+ showPublish,
132
+ publishing,
88
133
  ]);
89
134
  useEffect(() => {
90
135
  const onKeyDown = (event) => {
@@ -117,7 +162,7 @@ export function ConfigForm({ schema, value, onSave, submitLabel, variant = "conf
117
162
  status,
118
163
  tone,
119
164
  onDiscard,
120
- submitLabel,
165
+ submitLabel: saveLabel,
121
166
  hideActions: true,
122
167
  };
123
168
  if (variant === "record") {
@@ -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);
@@ -30,6 +30,15 @@ export type ConfigFormProps = {
30
30
  client?: VertebrClient;
31
31
  /** When false, save actions are disabled (missing write capability). */
32
32
  canWrite?: boolean;
33
+ /**
34
+ * When true, Save creates a draft (not live). Save bar labels and
35
+ * optional Publish action reflect that.
36
+ */
37
+ revisionsEnabled?: boolean;
38
+ /** Publish the current draft revision to live. */
39
+ onPublish?: () => Promise<void>;
40
+ /** Show Publish when a draft revision is available. */
41
+ canPublish?: boolean;
33
42
  };
34
43
  export type FieldEntry = [string, FieldSchema];
35
44
  export type FieldGroup = {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/components/config/form/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,WAAW,EACX,kBAAkB,EAClB,aAAa,EACd,MAAM,iBAAiB,CAAC;AAEzB,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEpD,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC7C,sDAAsD;IACtD,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,aAAa,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,UAAU,CAAC,CAAC;IAChE,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC7C,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC5D,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IAC/C,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACpD,gDAAgD;IAChD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uDAAuD;IACvD,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAE/C,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,IAAI,CAChC,eAAe,EACf,eAAe,GAAG,eAAe,GAAG,iBAAiB,CACtD,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,eAAe,EACf,OAAO,GAAG,cAAc,GAAG,aAAa,GAAG,gBAAgB,CAC5D,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/components/config/form/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,WAAW,EACX,kBAAkB,EAClB,aAAa,EACd,MAAM,iBAAiB,CAAC;AAEzB,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEpD,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC7C,sDAAsD;IACtD,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,aAAa,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,UAAU,CAAC,CAAC;IAChE,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC7C,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC5D,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IAC/C,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACpD,gDAAgD;IAChD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uDAAuD;IACvD,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,uDAAuD;IACvD,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAE/C,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,IAAI,CAChC,eAAe,EACf,eAAe,GAAG,eAAe,GAAG,iBAAiB,CACtD,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,eAAe,EACf,OAAO,GAAG,cAAc,GAAG,aAAa,GAAG,gBAAgB,CAC5D,CAAC"}
@@ -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);
@@ -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
  }
@@ -5,8 +5,14 @@ export type SaveBarState = {
5
5
  tone?: "ok" | "danger";
6
6
  isDirty: boolean;
7
7
  saving: boolean;
8
+ /** Primary save button label. @default "Save" */
9
+ saveLabel?: string;
8
10
  onSave?: () => void | Promise<void>;
9
11
  onDiscard?: () => void;
12
+ /** Optional publish action when revisions are enabled. */
13
+ publishLabel?: string;
14
+ onPublish?: () => void | Promise<void>;
15
+ publishing?: boolean;
10
16
  };
11
17
  type SaveBarContextValue = {
12
18
  state: SaveBarState;
@@ -1 +1 @@
1
- {"version":3,"file":"useSaveBar.d.ts","sourceRoot":"","sources":["../../src/hooks/useSaveBar.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,IAAI,GAAG,QAAQ,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB,CAAC;AAEF,KAAK,mBAAmB,GAAG;IACzB,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC;IAChD,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAUF,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAuBpE;AAED,wBAAgB,UAAU,wBAMzB"}
1
+ {"version":3,"file":"useSaveBar.d.ts","sourceRoot":"","sources":["../../src/hooks/useSaveBar.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,IAAI,GAAG,QAAQ,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,KAAK,mBAAmB,GAAG;IACzB,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC;IAChD,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAUF,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAuBpE;AAED,wBAAgB,UAAU,wBAMzB"}