@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.
- package/dist/components/admin/AddonEditor.d.ts.map +1 -1
- package/dist/components/admin/AddonEditor.js +10 -8
- package/dist/components/admin/AdminContent.d.ts.map +1 -1
- package/dist/components/admin/AdminContent.js +62 -17
- package/dist/components/admin/AdminSaveBar.d.ts.map +1 -1
- package/dist/components/admin/AdminSaveBar.js +6 -1
- package/dist/components/admin/AdminSyncPage.d.ts.map +1 -1
- package/dist/components/admin/AdminSyncPage.js +50 -6
- package/dist/components/admin/PagePreviewPane.d.ts +3 -1
- package/dist/components/admin/PagePreviewPane.d.ts.map +1 -1
- package/dist/components/admin/PagePreviewPane.js +41 -9
- package/dist/components/admin/SyncPreviewFrame.d.ts +22 -0
- package/dist/components/admin/SyncPreviewFrame.d.ts.map +1 -0
- package/dist/components/admin/SyncPreviewFrame.js +135 -0
- package/dist/components/admin/VertebrAdmin.d.ts.map +1 -1
- package/dist/components/admin/VertebrAdmin.js +6 -4
- package/dist/components/config/DatetimeInput.d.ts +15 -0
- package/dist/components/config/DatetimeInput.d.ts.map +1 -0
- package/dist/components/config/DatetimeInput.js +287 -0
- package/dist/components/config/EnumInput.d.ts.map +1 -1
- package/dist/components/config/EnumInput.js +4 -4
- package/dist/components/config/EnumListInput.d.ts.map +1 -1
- package/dist/components/config/EnumListInput.js +13 -3
- package/dist/components/config/ImagePickerModal.d.ts +9 -1
- package/dist/components/config/ImagePickerModal.d.ts.map +1 -1
- package/dist/components/config/ImagePickerModal.js +14 -7
- package/dist/components/config/RevisionHistory.d.ts +4 -0
- package/dist/components/config/RevisionHistory.d.ts.map +1 -1
- package/dist/components/config/RevisionHistory.js +63 -8
- package/dist/components/config/form/ConfigField.d.ts.map +1 -1
- package/dist/components/config/form/ConfigField.js +41 -16
- package/dist/components/config/form/ConfigForm.d.ts +1 -1
- package/dist/components/config/form/ConfigForm.d.ts.map +1 -1
- package/dist/components/config/form/ConfigForm.js +54 -9
- package/dist/components/config/form/RecordConfigForm.d.ts.map +1 -1
- package/dist/components/config/form/RecordConfigForm.js +24 -10
- package/dist/components/config/form/types.d.ts +9 -0
- package/dist/components/config/form/types.d.ts.map +1 -1
- package/dist/components/fonts/FontLibraryBoard.d.ts.map +1 -1
- package/dist/components/fonts/FontLibraryBoard.js +1 -1
- package/dist/components/resources/ImageUploadControl.d.ts +3 -1
- package/dist/components/resources/ImageUploadControl.d.ts.map +1 -1
- package/dist/components/resources/ImageUploadControl.js +5 -3
- package/dist/hooks/useSaveBar.d.ts +6 -0
- package/dist/hooks/useSaveBar.d.ts.map +1 -1
- package/dist/styles/form.css +465 -17
- package/dist/styles/modal.css +2 -1
- package/dist/styles/page-preview.css +84 -0
- package/dist/styles/select.css +23 -0
- package/dist/styles/sync.css +388 -4
- package/dist/themes/light.css +27 -0
- package/package.json +3 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { buildVertebrPreviewUrl, postVertebrPreviewDraft, subscribeVertebrPreviewReady, } from "@vertebr/client";
|
|
4
|
+
const FRAME_WIDTH = 390;
|
|
5
|
+
const FRAME_HEIGHT = 720;
|
|
6
|
+
function normalizePreviewPath(path) {
|
|
7
|
+
const trimmed = path.trim() || "/";
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(trimmed, "http://vertebr.local");
|
|
10
|
+
return `${url.pathname}${url.search}${url.hash}` || "/";
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Build related drafts for the iframe: prefer plan payloads (real enabled /
|
|
18
|
+
* content). Synthesize only when a listed add-on is missing from the plan.
|
|
19
|
+
*/
|
|
20
|
+
function resolveRelatedDrafts(controlId, relatedDrafts, addons) {
|
|
21
|
+
const next = {};
|
|
22
|
+
for (const [id, value] of Object.entries(relatedDrafts ?? {})) {
|
|
23
|
+
next[id] = { ...value };
|
|
24
|
+
}
|
|
25
|
+
for (const addon of addons ?? []) {
|
|
26
|
+
const existing = next[addon.id] ?? {
|
|
27
|
+
enabled: addon.envEnabled,
|
|
28
|
+
};
|
|
29
|
+
const patched = { ...existing };
|
|
30
|
+
if (patched.enabled === undefined) {
|
|
31
|
+
patched.enabled = addon.envEnabled;
|
|
32
|
+
}
|
|
33
|
+
if (addon.id === "countdown" && controlId === "comingSoon") {
|
|
34
|
+
patched.target = "comingSoon";
|
|
35
|
+
}
|
|
36
|
+
if (addon.id === "countdown" && controlId === "product") {
|
|
37
|
+
patched.target = "product";
|
|
38
|
+
}
|
|
39
|
+
if (addon.id === "reviews" && controlId === "home") {
|
|
40
|
+
patched.showOnHome = true;
|
|
41
|
+
}
|
|
42
|
+
if (addon.id === "reviews" && controlId === "product") {
|
|
43
|
+
patched.showOnProducts = true;
|
|
44
|
+
}
|
|
45
|
+
next[addon.id] = patched;
|
|
46
|
+
}
|
|
47
|
+
return next;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Compact same-origin preview iframe for Sync visual compare.
|
|
51
|
+
* Fixed mobile-sized frame; posts the given draft after ready.
|
|
52
|
+
*/
|
|
53
|
+
export function SyncPreviewFrame({ label, previewPath, controlId, draft, relatedDrafts, addons, }) {
|
|
54
|
+
const iframeRef = useRef(null);
|
|
55
|
+
const stageRef = useRef(null);
|
|
56
|
+
const draftRef = useRef(draft);
|
|
57
|
+
const [frameReady, setFrameReady] = useState(false);
|
|
58
|
+
const [frameError, setFrameError] = useState(false);
|
|
59
|
+
const [scale, setScale] = useState(1);
|
|
60
|
+
const resolvedRelated = useMemo(() => resolveRelatedDrafts(controlId, relatedDrafts, addons), [controlId, relatedDrafts, addons]);
|
|
61
|
+
const relatedRef = useRef(resolvedRelated);
|
|
62
|
+
draftRef.current = draft;
|
|
63
|
+
relatedRef.current = resolvedRelated;
|
|
64
|
+
const path = normalizePreviewPath(previewPath);
|
|
65
|
+
const src = useMemo(() => buildVertebrPreviewUrl(path, {
|
|
66
|
+
controlId,
|
|
67
|
+
}), [path, controlId]);
|
|
68
|
+
const pushDraft = useCallback(() => {
|
|
69
|
+
const win = iframeRef.current?.contentWindow;
|
|
70
|
+
const value = draftRef.current;
|
|
71
|
+
if (!win || !value)
|
|
72
|
+
return;
|
|
73
|
+
postVertebrPreviewDraft(win, controlId, value);
|
|
74
|
+
for (const [id, relatedValue] of Object.entries(relatedRef.current)) {
|
|
75
|
+
postVertebrPreviewDraft(win, id, relatedValue);
|
|
76
|
+
}
|
|
77
|
+
}, [controlId]);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
setFrameReady(false);
|
|
80
|
+
setFrameError(false);
|
|
81
|
+
}, [src]);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
return subscribeVertebrPreviewReady((source) => {
|
|
84
|
+
const win = iframeRef.current?.contentWindow;
|
|
85
|
+
if (!win || source !== win)
|
|
86
|
+
return;
|
|
87
|
+
setFrameReady(true);
|
|
88
|
+
pushDraft();
|
|
89
|
+
});
|
|
90
|
+
}, [pushDraft, src]);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!frameReady)
|
|
93
|
+
return;
|
|
94
|
+
pushDraft();
|
|
95
|
+
}, [draft, resolvedRelated, frameReady, pushDraft]);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const stage = stageRef.current;
|
|
98
|
+
if (!stage || typeof ResizeObserver === "undefined")
|
|
99
|
+
return;
|
|
100
|
+
const update = () => {
|
|
101
|
+
const style = getComputedStyle(stage);
|
|
102
|
+
const padX = (Number.parseFloat(style.paddingLeft) || 0) +
|
|
103
|
+
(Number.parseFloat(style.paddingRight) || 0);
|
|
104
|
+
const padY = (Number.parseFloat(style.paddingTop) || 0) +
|
|
105
|
+
(Number.parseFloat(style.paddingBottom) || 0);
|
|
106
|
+
const availW = Math.max(0, stage.clientWidth - padX);
|
|
107
|
+
const availH = Math.max(0, stage.clientHeight - padY);
|
|
108
|
+
const next = Math.min(1, availW / FRAME_WIDTH, availH / FRAME_HEIGHT);
|
|
109
|
+
setScale(Number.isFinite(next) && next > 0 ? next : 1);
|
|
110
|
+
};
|
|
111
|
+
update();
|
|
112
|
+
const observer = new ResizeObserver(update);
|
|
113
|
+
observer.observe(stage);
|
|
114
|
+
return () => observer.disconnect();
|
|
115
|
+
}, []);
|
|
116
|
+
const status = frameError
|
|
117
|
+
? "Unavailable"
|
|
118
|
+
: frameReady
|
|
119
|
+
? "Connected"
|
|
120
|
+
: "Loading…";
|
|
121
|
+
return (_jsxs("div", { className: "vertebr-sync-preview-frame", children: [_jsxs("div", { className: "vertebr-sync-preview-frame-meta", children: [_jsx("strong", { children: label }), _jsx("span", { children: path }), _jsx("span", { "data-status": frameError ? "error" : frameReady ? "ok" : "pending", children: status })] }), addons ? (_jsx("div", { className: "vertebr-sync-preview-addons", "aria-label": "Add-ons in preview", children: addons.length === 0 ? (_jsx("span", { className: "vertebr-sync-preview-addons-empty", children: "No add-ons" })) : (addons.map((addon) => (_jsxs("span", { className: "vertebr-sync-preview-addon", "data-enabled": addon.envEnabled ? "true" : "false", title: addon.envEnabled
|
|
122
|
+
? `${addon.label} is enabled in this environment`
|
|
123
|
+
: `${addon.label} is off in this environment`, children: [addon.label, _jsx("em", { children: addon.envEnabled ? "on" : "off" })] }, addon.id)))) })) : null, _jsx("div", { ref: stageRef, className: "vertebr-sync-preview-frame-stage", children: _jsx("div", { className: "vertebr-sync-preview-frame-shell", style: {
|
|
124
|
+
width: FRAME_WIDTH * scale,
|
|
125
|
+
height: FRAME_HEIGHT * scale,
|
|
126
|
+
}, children: _jsx("div", { className: "vertebr-sync-preview-frame-scaler", style: {
|
|
127
|
+
width: FRAME_WIDTH,
|
|
128
|
+
height: FRAME_HEIGHT,
|
|
129
|
+
transform: `scale(${scale})`,
|
|
130
|
+
transformOrigin: "top left",
|
|
131
|
+
}, children: frameError ? (_jsxs("div", { className: "vertebr-sync-preview-frame-empty", children: ["Could not load preview at ", path, "."] })) : (_jsx("iframe", { ref: iframeRef, title: `${label} preview`, src: src, width: FRAME_WIDTH, height: FRAME_HEIGHT, onLoad: () => {
|
|
132
|
+
setFrameReady(true);
|
|
133
|
+
pushDraft();
|
|
134
|
+
}, onError: () => setFrameError(true) })) }) }) })] }));
|
|
135
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VertebrAdmin.d.ts","sourceRoot":"","sources":["../../../src/components/admin/VertebrAdmin.tsx"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAGV,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,wBAAgB,YAAY,CAAC,EAC3B,OAAsB,EACtB,QAAQ,EAAE,YAAY,EACtB,KAAuB,EACvB,aAA6B,EAC7B,eAAiC,EACjC,eAAiC,EACjC,sBAAgD,EAChD,cAAc,EACd,aAAa,EACb,SAAS,GACV,EAAE,iBAAiB,+
|
|
1
|
+
{"version":3,"file":"VertebrAdmin.d.ts","sourceRoot":"","sources":["../../../src/components/admin/VertebrAdmin.tsx"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAGV,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,wBAAgB,YAAY,CAAC,EAC3B,OAAsB,EACtB,QAAQ,EAAE,YAAY,EACtB,KAAuB,EACvB,aAA6B,EAC7B,eAAiC,EACjC,eAAiC,EACjC,sBAAgD,EAChD,cAAc,EACd,aAAa,EACb,SAAS,GACV,EAAE,iBAAiB,+BAkuBnB"}
|
|
@@ -380,11 +380,13 @@ export function VertebrAdmin({ apiBase = "/admin/api", basePath: basePathProp, t
|
|
|
380
380
|
return data.items;
|
|
381
381
|
}, [client]);
|
|
382
382
|
const uploadGallery = useCallback(async (files) => {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
383
|
+
// Upload one file per request — base64 bulk payloads easily exceed the
|
|
384
|
+
// host HTTP body limit when several product photos are selected.
|
|
385
|
+
const items = [];
|
|
386
|
+
for (const file of files) {
|
|
387
|
+
const { item } = await client.uploadGalleryFile(file);
|
|
388
|
+
items.push(item);
|
|
386
389
|
}
|
|
387
|
-
const { items } = await client.uploadGalleryFiles(files);
|
|
388
390
|
return items;
|
|
389
391
|
}, [client]);
|
|
390
392
|
const galleryPicker = catalog?.gallery
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type DatetimeInputProps = {
|
|
2
|
+
id: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
value: unknown;
|
|
5
|
+
readOnly?: boolean;
|
|
6
|
+
onBlur?: () => void;
|
|
7
|
+
onChange: (next: string) => void;
|
|
8
|
+
inputRef?: React.Ref<HTMLButtonElement>;
|
|
9
|
+
};
|
|
10
|
+
/** @deprecated Prefer parts helpers; kept for callers/tests. */
|
|
11
|
+
export declare function toDatetimeLocalValue(value: unknown): string;
|
|
12
|
+
/** @deprecated Prefer parts helpers; kept for callers/tests. */
|
|
13
|
+
export declare function fromDatetimeLocalValue(local: string): string;
|
|
14
|
+
export declare function DatetimeInput({ id, name, value, readOnly, onBlur, onChange, inputRef, }: DatetimeInputProps): import("react").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=DatetimeInput.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DatetimeInput.d.ts","sourceRoot":"","sources":["../../../src/components/config/DatetimeInput.tsx"],"names":[],"mappings":"AAIA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;CACzC,CAAC;AAsMF,gEAAgE;AAChE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAI3D;AAED,gEAAgE;AAChE,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM5D;AAuED,wBAAgB,aAAa,CAAC,EAC5B,EAAE,EACF,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,QAAQ,GACT,EAAE,kBAAkB,+BAgQpB"}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as Popover from "@radix-ui/react-popover";
|
|
3
|
+
import { useEffect, useId, useMemo, useState } from "react";
|
|
4
|
+
import { DayPicker } from "react-day-picker";
|
|
5
|
+
function pad(n) {
|
|
6
|
+
return String(n).padStart(2, "0");
|
|
7
|
+
}
|
|
8
|
+
function browserTimeZone() {
|
|
9
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
10
|
+
}
|
|
11
|
+
const TIME_ZONES = [
|
|
12
|
+
{ id: "America/New_York", label: "Eastern (ET)" },
|
|
13
|
+
{ id: "America/Chicago", label: "Central (CT)" },
|
|
14
|
+
{ id: "America/Denver", label: "Mountain (MT)" },
|
|
15
|
+
{ id: "America/Los_Angeles", label: "Pacific (PT)" },
|
|
16
|
+
{ id: "America/Phoenix", label: "Arizona (MST)" },
|
|
17
|
+
{ id: "America/Anchorage", label: "Alaska (AKT)" },
|
|
18
|
+
{ id: "Pacific/Honolulu", label: "Hawaii (HST)" },
|
|
19
|
+
{ id: "UTC", label: "UTC" },
|
|
20
|
+
{ id: "Europe/London", label: "London (GMT/BST)" },
|
|
21
|
+
{ id: "Europe/Paris", label: "Central Europe (CET)" },
|
|
22
|
+
{ id: "Asia/Tokyo", label: "Tokyo (JST)" },
|
|
23
|
+
{ id: "Australia/Sydney", label: "Sydney (AEST)" },
|
|
24
|
+
];
|
|
25
|
+
function timeZoneOptions(current) {
|
|
26
|
+
const known = new Set(TIME_ZONES.map((zone) => zone.id));
|
|
27
|
+
if (known.has(current))
|
|
28
|
+
return TIME_ZONES;
|
|
29
|
+
return [{ id: current, label: current.replace(/_/g, " ") }, ...TIME_ZONES];
|
|
30
|
+
}
|
|
31
|
+
function zonedParts(instant, timeZone) {
|
|
32
|
+
const bits = Object.fromEntries(new Intl.DateTimeFormat("en-US", {
|
|
33
|
+
timeZone,
|
|
34
|
+
year: "numeric",
|
|
35
|
+
month: "2-digit",
|
|
36
|
+
day: "2-digit",
|
|
37
|
+
hour: "2-digit",
|
|
38
|
+
minute: "2-digit",
|
|
39
|
+
hourCycle: "h23",
|
|
40
|
+
})
|
|
41
|
+
.formatToParts(instant)
|
|
42
|
+
.filter((part) => part.type !== "literal")
|
|
43
|
+
.map((part) => [part.type, part.value]));
|
|
44
|
+
return {
|
|
45
|
+
date: `${bits.year}-${bits.month}-${bits.day}`,
|
|
46
|
+
time: `${bits.hour}:${bits.minute}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Wall-clock date/time in `timeZone` → UTC ISO. */
|
|
50
|
+
function toIsoInZone(parts, timeZone) {
|
|
51
|
+
if (!parts.date)
|
|
52
|
+
return "";
|
|
53
|
+
const time = parts.time || "00:00";
|
|
54
|
+
const dateBits = parts.date.split("-").map(Number);
|
|
55
|
+
const timeBits = time.split(":").map(Number);
|
|
56
|
+
const year = dateBits[0];
|
|
57
|
+
const month = dateBits[1];
|
|
58
|
+
const day = dateBits[2];
|
|
59
|
+
const hour = timeBits[0];
|
|
60
|
+
const minute = timeBits[1];
|
|
61
|
+
if (year === undefined ||
|
|
62
|
+
month === undefined ||
|
|
63
|
+
day === undefined ||
|
|
64
|
+
hour === undefined ||
|
|
65
|
+
minute === undefined ||
|
|
66
|
+
![year, month, day, hour, minute].every(Number.isFinite)) {
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
const desired = Date.UTC(year, month - 1, day, hour, minute, 0);
|
|
70
|
+
let utcMs = desired;
|
|
71
|
+
for (let i = 0; i < 2; i += 1) {
|
|
72
|
+
const shown = zonedParts(new Date(utcMs), timeZone);
|
|
73
|
+
const shownDate = shown.date.split("-").map(Number);
|
|
74
|
+
const shownTime = shown.time.split(":").map(Number);
|
|
75
|
+
const sy = shownDate[0];
|
|
76
|
+
const sm = shownDate[1];
|
|
77
|
+
const sd = shownDate[2];
|
|
78
|
+
const sh = shownTime[0];
|
|
79
|
+
const smin = shownTime[1];
|
|
80
|
+
if (sy === undefined ||
|
|
81
|
+
sm === undefined ||
|
|
82
|
+
sd === undefined ||
|
|
83
|
+
sh === undefined ||
|
|
84
|
+
smin === undefined) {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
const asUtc = Date.UTC(sy, sm - 1, sd, sh, smin, 0);
|
|
88
|
+
utcMs -= asUtc - desired;
|
|
89
|
+
}
|
|
90
|
+
return new Date(utcMs).toISOString();
|
|
91
|
+
}
|
|
92
|
+
function partsFromValue(value, timeZone) {
|
|
93
|
+
if (typeof value !== "string")
|
|
94
|
+
return { date: "", time: "" };
|
|
95
|
+
const trimmed = value.trim();
|
|
96
|
+
if (!trimmed)
|
|
97
|
+
return { date: "", time: "" };
|
|
98
|
+
const ms = Date.parse(trimmed);
|
|
99
|
+
if (Number.isFinite(ms))
|
|
100
|
+
return zonedParts(new Date(ms), timeZone);
|
|
101
|
+
const match = trimmed.match(/^(\d{4}-\d{2}-\d{2})(?:[T\s](\d{2}:\d{2})(?::\d{2})?)?/);
|
|
102
|
+
if (!match)
|
|
103
|
+
return { date: "", time: "" };
|
|
104
|
+
return { date: match[1] ?? "", time: match[2] ?? "00:00" };
|
|
105
|
+
}
|
|
106
|
+
function to24h(hour12, meridiem) {
|
|
107
|
+
const hour = ((hour12 - 1) % 12) + 1;
|
|
108
|
+
if (meridiem === "AM")
|
|
109
|
+
return hour === 12 ? 0 : hour;
|
|
110
|
+
return hour === 12 ? 12 : hour + 12;
|
|
111
|
+
}
|
|
112
|
+
function toClock(time) {
|
|
113
|
+
const [hRaw, mRaw] = (time || "12:00").split(":");
|
|
114
|
+
let hour24 = Number(hRaw);
|
|
115
|
+
const minute = pad(Number.isFinite(Number(mRaw)) ? Number(mRaw) : 0);
|
|
116
|
+
if (!Number.isFinite(hour24))
|
|
117
|
+
hour24 = 12;
|
|
118
|
+
const meridiem = hour24 >= 12 ? "PM" : "AM";
|
|
119
|
+
const hour12 = hour24 % 12 === 0 ? 12 : hour24 % 12;
|
|
120
|
+
return { hour12: String(hour12), minute, meridiem };
|
|
121
|
+
}
|
|
122
|
+
function fromClock(clock) {
|
|
123
|
+
const hour12 = Number(clock.hour12);
|
|
124
|
+
const safeHour = Number.isFinite(hour12) ? hour12 : 12;
|
|
125
|
+
return `${pad(to24h(safeHour, clock.meridiem))}:${clock.minute || "00"}`;
|
|
126
|
+
}
|
|
127
|
+
function dateFromYmd(ymd) {
|
|
128
|
+
if (!ymd)
|
|
129
|
+
return undefined;
|
|
130
|
+
const [year, month, day] = ymd.split("-").map(Number);
|
|
131
|
+
if (!year || !month || !day)
|
|
132
|
+
return undefined;
|
|
133
|
+
return new Date(year, month - 1, day);
|
|
134
|
+
}
|
|
135
|
+
function ymdFromDate(date) {
|
|
136
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
137
|
+
}
|
|
138
|
+
function formatDateLabel(ymd) {
|
|
139
|
+
const date = dateFromYmd(ymd);
|
|
140
|
+
if (!date)
|
|
141
|
+
return "mm/dd/yyyy";
|
|
142
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
143
|
+
month: "short",
|
|
144
|
+
day: "numeric",
|
|
145
|
+
year: "numeric",
|
|
146
|
+
}).format(date);
|
|
147
|
+
}
|
|
148
|
+
function formatTimeLabel(time) {
|
|
149
|
+
if (!time)
|
|
150
|
+
return "--:-- --";
|
|
151
|
+
const clock = toClock(time);
|
|
152
|
+
return `${clock.hour12}:${clock.minute} ${clock.meridiem}`;
|
|
153
|
+
}
|
|
154
|
+
function formatTimeZoneShort(timeZone, instant = new Date()) {
|
|
155
|
+
const part = new Intl.DateTimeFormat("en-US", {
|
|
156
|
+
timeZone,
|
|
157
|
+
timeZoneName: "short",
|
|
158
|
+
})
|
|
159
|
+
.formatToParts(instant)
|
|
160
|
+
.find((item) => item.type === "timeZoneName");
|
|
161
|
+
return part?.value ?? timeZone;
|
|
162
|
+
}
|
|
163
|
+
function formatSummary(parts, timeZone) {
|
|
164
|
+
if (!parts.date)
|
|
165
|
+
return null;
|
|
166
|
+
const iso = toIsoInZone(parts, timeZone);
|
|
167
|
+
if (!iso)
|
|
168
|
+
return null;
|
|
169
|
+
const instant = new Date(iso);
|
|
170
|
+
const when = new Intl.DateTimeFormat(undefined, {
|
|
171
|
+
timeZone,
|
|
172
|
+
weekday: "short",
|
|
173
|
+
month: "short",
|
|
174
|
+
day: "numeric",
|
|
175
|
+
year: "numeric",
|
|
176
|
+
hour: "numeric",
|
|
177
|
+
minute: "2-digit",
|
|
178
|
+
}).format(instant);
|
|
179
|
+
return `${when} ${formatTimeZoneShort(timeZone, instant)}`;
|
|
180
|
+
}
|
|
181
|
+
/** @deprecated Prefer parts helpers; kept for callers/tests. */
|
|
182
|
+
export function toDatetimeLocalValue(value) {
|
|
183
|
+
const parts = partsFromValue(value, browserTimeZone());
|
|
184
|
+
if (!parts.date)
|
|
185
|
+
return "";
|
|
186
|
+
return `${parts.date}T${parts.time || "00:00"}`;
|
|
187
|
+
}
|
|
188
|
+
/** @deprecated Prefer parts helpers; kept for callers/tests. */
|
|
189
|
+
export function fromDatetimeLocalValue(local) {
|
|
190
|
+
const trimmed = local.trim();
|
|
191
|
+
if (!trimmed)
|
|
192
|
+
return "";
|
|
193
|
+
const ms = Date.parse(trimmed);
|
|
194
|
+
if (!Number.isFinite(ms))
|
|
195
|
+
return "";
|
|
196
|
+
return new Date(ms).toISOString();
|
|
197
|
+
}
|
|
198
|
+
const HOURS_12 = Array.from({ length: 12 }, (_, i) => String(i + 1));
|
|
199
|
+
const MINUTES = Array.from({ length: 60 }, (_, i) => pad(i));
|
|
200
|
+
function IconCalendar() {
|
|
201
|
+
return (_jsxs("svg", { className: "vertebr-datetime-icon", width: "16", height: "16", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", children: [_jsx("rect", { x: "3", y: "4.5", width: "14", height: "12", rx: "2", stroke: "currentColor", strokeWidth: "1.5" }), _jsx("path", { d: "M3 8.5h14M7 3v3M13 3v3", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })] }));
|
|
202
|
+
}
|
|
203
|
+
function IconClock() {
|
|
204
|
+
return (_jsxs("svg", { className: "vertebr-datetime-icon", width: "16", height: "16", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", children: [_jsx("circle", { cx: "10", cy: "10", r: "7", stroke: "currentColor", strokeWidth: "1.5" }), _jsx("path", { d: "M10 6.5V10l2.5 1.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })] }));
|
|
205
|
+
}
|
|
206
|
+
function IconChevron({ direction }) {
|
|
207
|
+
return (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", "aria-hidden": "true", children: _jsx("path", { d: direction === "left" ? "M8.5 3.5 5 7l3.5 3.5" : "M5.5 3.5 9 7l-3.5 3.5", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
208
|
+
}
|
|
209
|
+
export function DatetimeInput({ id, name, value, readOnly, onBlur, onChange, inputRef, }) {
|
|
210
|
+
const labelId = useId();
|
|
211
|
+
const [open, setOpen] = useState(false);
|
|
212
|
+
const [timeZone, setTimeZone] = useState(browserTimeZone);
|
|
213
|
+
const [parts, setParts] = useState(() => partsFromValue(value, browserTimeZone()));
|
|
214
|
+
const summary = useMemo(() => formatSummary(parts, timeZone), [parts, timeZone]);
|
|
215
|
+
const selected = useMemo(() => dateFromYmd(parts.date), [parts.date]);
|
|
216
|
+
const clock = useMemo(() => toClock(parts.time || "12:00"), [parts.time]);
|
|
217
|
+
const zones = useMemo(() => timeZoneOptions(timeZone), [timeZone]);
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
setParts(partsFromValue(value, timeZone));
|
|
220
|
+
// Re-parse when the stored ISO changes; timezone switches keep wall-clock
|
|
221
|
+
// and rewrite ISO via onTimeZoneChange instead.
|
|
222
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- intentional
|
|
223
|
+
}, [value]);
|
|
224
|
+
const commit = (next, zone = timeZone) => {
|
|
225
|
+
setParts(next);
|
|
226
|
+
onChange(toIsoInZone(next, zone));
|
|
227
|
+
};
|
|
228
|
+
const setClock = (next) => {
|
|
229
|
+
const time = fromClock(next);
|
|
230
|
+
if (!parts.date) {
|
|
231
|
+
commit({ date: ymdFromDate(new Date()), time });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
commit({ date: parts.date, time });
|
|
235
|
+
};
|
|
236
|
+
const onTimeZoneChange = (nextZone) => {
|
|
237
|
+
setTimeZone(nextZone);
|
|
238
|
+
if (!parts.date)
|
|
239
|
+
return;
|
|
240
|
+
// Keep the wall-clock launch time; reinterpret in the new zone.
|
|
241
|
+
onChange(toIsoInZone(parts, nextZone));
|
|
242
|
+
};
|
|
243
|
+
return (_jsxs("div", { className: "vertebr-datetime", "data-empty": parts.date ? "false" : "true", children: [_jsxs(Popover.Root, { open: readOnly ? false : open, onOpenChange: (next) => {
|
|
244
|
+
if (readOnly)
|
|
245
|
+
return;
|
|
246
|
+
setOpen(next);
|
|
247
|
+
if (!next)
|
|
248
|
+
onBlur?.();
|
|
249
|
+
}, children: [_jsxs("div", { className: "vertebr-datetime-shell", children: [_jsx(Popover.Trigger, { asChild: true, children: _jsxs("button", { id: id, ref: inputRef, type: "button", name: name, className: "vertebr-datetime-cell", disabled: readOnly, "aria-labelledby": `${labelId}-date ${labelId}-time`, children: [_jsxs("span", { className: "vertebr-datetime-cell-inner", children: [_jsx(IconCalendar, {}), _jsxs("span", { className: "vertebr-datetime-cell-copy", children: [_jsx("span", { className: "vertebr-datetime-cell-label", id: `${labelId}-date`, children: "Date" }), _jsx("span", { className: "vertebr-datetime-value", "data-empty": parts.date ? "false" : "true", children: formatDateLabel(parts.date) })] })] }), _jsx("span", { className: "vertebr-datetime-divider", "aria-hidden": "true" }), _jsxs("span", { className: "vertebr-datetime-cell-inner", children: [_jsx(IconClock, {}), _jsxs("span", { className: "vertebr-datetime-cell-copy", children: [_jsx("span", { className: "vertebr-datetime-cell-label", id: `${labelId}-time`, children: "Time" }), _jsx("span", { className: "vertebr-datetime-value", "data-empty": parts.time ? "false" : "true", children: parts.time
|
|
250
|
+
? `${formatTimeLabel(parts.time)} ${formatTimeZoneShort(timeZone)}`
|
|
251
|
+
: "--:-- --" })] })] })] }) }), parts.date && !readOnly ? (_jsx("button", { type: "button", className: "vertebr-datetime-clear", "aria-label": "Clear date and time", onClick: () => {
|
|
252
|
+
commit({ date: "", time: "" });
|
|
253
|
+
setOpen(false);
|
|
254
|
+
}, children: "Clear" })) : null] }), _jsx(Popover.Portal, { children: _jsxs(Popover.Content, { className: "vertebr-datetime-popover", align: "start", sideOffset: 8, collisionPadding: 12, onOpenAutoFocus: (event) => event.preventDefault(), children: [_jsx(DayPicker, { mode: "single", navLayout: "around", selected: selected, defaultMonth: selected ?? new Date(), onSelect: (day) => {
|
|
255
|
+
if (!day)
|
|
256
|
+
return;
|
|
257
|
+
commit({
|
|
258
|
+
date: ymdFromDate(day),
|
|
259
|
+
time: parts.time || fromClock({ hour12: "12", minute: "00", meridiem: "PM" }),
|
|
260
|
+
});
|
|
261
|
+
}, className: "vertebr-day-picker", classNames: {
|
|
262
|
+
root: "vertebr-day-picker",
|
|
263
|
+
months: "vertebr-day-picker-months",
|
|
264
|
+
month: "vertebr-day-picker-month",
|
|
265
|
+
month_caption: "vertebr-day-picker-caption",
|
|
266
|
+
month_grid: "vertebr-day-picker-grid",
|
|
267
|
+
caption_label: "vertebr-day-picker-caption-label",
|
|
268
|
+
nav: "vertebr-day-picker-nav",
|
|
269
|
+
button_previous: "vertebr-day-picker-nav-btn",
|
|
270
|
+
button_next: "vertebr-day-picker-nav-btn",
|
|
271
|
+
weekdays: "vertebr-day-picker-weekdays",
|
|
272
|
+
weekday: "vertebr-day-picker-weekday",
|
|
273
|
+
week: "vertebr-day-picker-week",
|
|
274
|
+
day: "vertebr-day-picker-day",
|
|
275
|
+
day_button: "vertebr-day-picker-day-btn",
|
|
276
|
+
selected: "vertebr-day-picker-selected",
|
|
277
|
+
today: "vertebr-day-picker-today",
|
|
278
|
+
outside: "vertebr-day-picker-outside",
|
|
279
|
+
disabled: "vertebr-day-picker-disabled",
|
|
280
|
+
hidden: "vertebr-day-picker-hidden",
|
|
281
|
+
}, components: {
|
|
282
|
+
Chevron: ({ orientation }) => (_jsx(IconChevron, { direction: orientation === "left" ? "left" : "right" })),
|
|
283
|
+
} }), _jsxs("div", { className: "vertebr-datetime-time-row", children: [_jsxs("label", { className: "vertebr-datetime-time-field", children: [_jsx("span", { children: "Hour" }), _jsx("select", { className: "vertebr-datetime-time-select", value: clock.hour12, onChange: (event) => setClock({ ...clock, hour12: event.target.value }), children: HOURS_12.map((value) => (_jsx("option", { value: value, children: value }, value))) })] }), _jsx("span", { className: "vertebr-datetime-time-sep", "aria-hidden": "true", children: ":" }), _jsxs("label", { className: "vertebr-datetime-time-field", children: [_jsx("span", { children: "Minute" }), _jsx("select", { className: "vertebr-datetime-time-select", value: clock.minute, onChange: (event) => setClock({ ...clock, minute: event.target.value }), children: MINUTES.map((value) => (_jsx("option", { value: value, children: value }, value))) })] }), _jsxs("label", { className: "vertebr-datetime-time-field vertebr-datetime-time-field-meridiem", children: [_jsx("span", { children: "AM/PM" }), _jsxs("select", { className: "vertebr-datetime-time-select", value: clock.meridiem, onChange: (event) => setClock({
|
|
284
|
+
...clock,
|
|
285
|
+
meridiem: event.target.value,
|
|
286
|
+
}), children: [_jsx("option", { value: "AM", children: "AM" }), _jsx("option", { value: "PM", children: "PM" })] })] })] }), _jsxs("label", { className: "vertebr-datetime-time-field vertebr-datetime-timezone", children: [_jsx("span", { children: "Timezone" }), _jsx("select", { className: "vertebr-datetime-time-select", value: timeZone, onChange: (event) => onTimeZoneChange(event.target.value), children: zones.map((zone) => (_jsx("option", { value: zone.id, children: zone.label }, zone.id))) })] })] }) })] }), summary ? (_jsx("p", { className: "vertebr-datetime-summary", children: summary })) : (_jsx("p", { className: "vertebr-datetime-summary vertebr-datetime-summary-empty", children: "Pick a launch date and time" }))] }));
|
|
287
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EnumInput.d.ts","sourceRoot":"","sources":["../../../src/components/config/EnumInput.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGnD,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,CAAC;AAEF,wBAAgB,SAAS,CAAC,EACxB,EAAE,EACF,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,GACT,EAAE,cAAc,+
|
|
1
|
+
{"version":3,"file":"EnumInput.d.ts","sourceRoot":"","sources":["../../../src/components/config/EnumInput.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGnD,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,CAAC;AAEF,wBAAgB,SAAS,CAAC,EACxB,EAAE,EACF,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,GACT,EAAE,cAAc,+BAkDhB"}
|
|
@@ -2,10 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import * as Select from "@radix-ui/react-select";
|
|
3
3
|
import { optionLabel } from "./form/helpers/labels";
|
|
4
4
|
export function EnumInput({ id, value, field, readOnly, onChange, }) {
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
const baseOptions = field.options?.filter(Boolean) ?? [];
|
|
6
|
+
const raw = typeof value === "string" ? value : "";
|
|
7
|
+
const options = raw && !baseOptions.includes(raw) ? [raw, ...baseOptions] : baseOptions;
|
|
8
|
+
const current = raw && options.includes(raw) ? raw : (options[0] ?? "");
|
|
9
9
|
if (options.length === 0) {
|
|
10
10
|
return (_jsx("input", { id: id, type: "text", readOnly: readOnly, value: current, disabled: true }));
|
|
11
11
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EnumListInput.d.ts","sourceRoot":"","sources":["../../../src/components/config/EnumListInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGnD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACrC,CAAC;
|
|
1
|
+
{"version":3,"file":"EnumListInput.d.ts","sourceRoot":"","sources":["../../../src/components/config/EnumListInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGnD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACrC,CAAC;AAyBF,wBAAgB,aAAa,CAAC,EAC5B,EAAE,EACF,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,GACT,EAAE,kBAAkB,+BAyEpB"}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { optionLabel } from "./form/helpers/labels";
|
|
3
3
|
function asStringList(value) {
|
|
4
|
-
if (
|
|
5
|
-
return
|
|
6
|
-
|
|
4
|
+
if (Array.isArray(value)) {
|
|
5
|
+
return value.map((item) => String(item).trim()).filter(Boolean);
|
|
6
|
+
}
|
|
7
|
+
if (typeof value === "string") {
|
|
8
|
+
return value
|
|
9
|
+
.split(",")
|
|
10
|
+
.map((item) => item.trim())
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
return [];
|
|
7
14
|
}
|
|
8
15
|
function orderedRows(options, selected) {
|
|
9
16
|
const known = new Set(options);
|
|
@@ -18,6 +25,9 @@ export function EnumListInput({ id, value, field, readOnly, onChange, }) {
|
|
|
18
25
|
const options = field.options?.filter(Boolean) ?? [];
|
|
19
26
|
const selected = asStringList(value);
|
|
20
27
|
const rows = orderedRows(options, selected);
|
|
28
|
+
if (options.length === 0) {
|
|
29
|
+
return (_jsx("p", { className: "vertebr-field-files-empty", id: id, children: "No products available in this environment." }));
|
|
30
|
+
}
|
|
21
31
|
const commit = (nextRows) => {
|
|
22
32
|
onChange(nextRows.filter((row) => row.on).map((row) => row.id));
|
|
23
33
|
};
|
|
@@ -6,6 +6,7 @@ export type PickerImage = {
|
|
|
6
6
|
name: string;
|
|
7
7
|
source: "record" | "gallery" | "url";
|
|
8
8
|
};
|
|
9
|
+
type Tab = "record" | "gallery";
|
|
9
10
|
export type ImagePickerModalProps = {
|
|
10
11
|
recordImages?: RecordImage[];
|
|
11
12
|
galleryUpload?: Pick<ResourceUploadMeta, "accept" | "maxBytes">;
|
|
@@ -13,6 +14,13 @@ export type ImagePickerModalProps = {
|
|
|
13
14
|
onUploadGallery?: (files: File[]) => Promise<GalleryItem[]>;
|
|
14
15
|
onClose: () => void;
|
|
15
16
|
onInsert: (images: PickerImage[]) => void;
|
|
17
|
+
/** Dialog heading. @default "Choose images" */
|
|
18
|
+
title?: string;
|
|
19
|
+
/** Primary action label prefix. @default "Use" */
|
|
20
|
+
confirmLabel?: string;
|
|
21
|
+
/** Prefer Files tab when both product photos and gallery exist. */
|
|
22
|
+
defaultTab?: Tab;
|
|
16
23
|
};
|
|
17
|
-
export declare function ImagePickerModal({ recordImages, galleryUpload, onListGallery, onUploadGallery, onClose, onInsert, }: ImagePickerModalProps): import("react").
|
|
24
|
+
export declare function ImagePickerModal({ recordImages, galleryUpload, onListGallery, onUploadGallery, onClose, onInsert, title, confirmLabel, defaultTab, }: ImagePickerModalProps): import("react").ReactPortal;
|
|
25
|
+
export {};
|
|
18
26
|
//# sourceMappingURL=ImagePickerModal.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImagePickerModal.d.ts","sourceRoot":"","sources":["../../../src/components/config/ImagePickerModal.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ImagePickerModal.d.ts","sourceRoot":"","sources":["../../../src/components/config/ImagePickerModal.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,KAAK,CAAC;CACtC,CAAC;AAEF,KAAK,GAAG,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEhC,MAAM,MAAM,qBAAqB,GAAG;IAClC,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,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,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC;IAC1C,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,UAAU,CAAC,EAAE,GAAG,CAAC;CAClB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,EAC/B,YAAiB,EACjB,aAAa,EACb,aAAa,EACb,eAAe,EACf,OAAO,EACP,QAAQ,EACR,KAAuB,EACvB,YAAoB,EACpB,UAAU,GACX,EAAE,qBAAqB,+BAiQvB"}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
3
4
|
import { ImageUploadControl } from "../resources/ImageUploadControl";
|
|
4
|
-
export function ImagePickerModal({ recordImages = [], galleryUpload, onListGallery, onUploadGallery, onClose, onInsert, }) {
|
|
5
|
+
export function ImagePickerModal({ recordImages = [], galleryUpload, onListGallery, onUploadGallery, onClose, onInsert, title = "Choose images", confirmLabel = "Use", defaultTab, }) {
|
|
5
6
|
const hasGallery = Boolean(onListGallery);
|
|
6
|
-
const [tab, setTab] = useState(
|
|
7
|
+
const [tab, setTab] = useState(() => {
|
|
8
|
+
if (defaultTab)
|
|
9
|
+
return defaultTab;
|
|
10
|
+
if (recordImages.length)
|
|
11
|
+
return "record";
|
|
12
|
+
return "gallery";
|
|
13
|
+
});
|
|
7
14
|
const [gallery, setGallery] = useState([]);
|
|
8
15
|
const [loading, setLoading] = useState(false);
|
|
9
16
|
const [error, setError] = useState(null);
|
|
@@ -84,11 +91,11 @@ export function ImagePickerModal({ recordImages = [], galleryUpload, onListGalle
|
|
|
84
91
|
setSelected((current) => ({ ...current, [image.id]: image }));
|
|
85
92
|
setUrl("");
|
|
86
93
|
};
|
|
87
|
-
return (_jsx("div", { className: "vertebr-modal-backdrop", role: "presentation", onClick: onClose, children: _jsxs("div", { className: "vertebr-modal", role: "dialog", "aria-modal": "true", "aria-label":
|
|
94
|
+
return createPortal(_jsx("div", { className: "vertebr-modal-backdrop", role: "presentation", onClick: onClose, children: _jsxs("div", { className: "vertebr-modal", role: "dialog", "aria-modal": "true", "aria-label": title, 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: "Media" }), _jsx("h2", { children: title })] }), _jsx("button", { type: "button", className: "vertebr-btn", onClick: onClose, children: "Close" })] }), _jsxs("div", { className: "vertebr-modal-body vertebr-picker-body", children: [recordImages.length > 0 && hasGallery ? (_jsxs("div", { className: "vertebr-picker-tabs", role: "tablist", children: [_jsxs("button", { type: "button", role: "tab", "aria-selected": tab === "record", "data-active": tab === "record" || undefined, onClick: () => setTab("record"), children: ["Product photos", _jsx("span", { children: recordTiles.length })] }), _jsxs("button", { type: "button", role: "tab", "aria-selected": tab === "gallery", "data-active": tab === "gallery" || undefined, onClick: () => setTab("gallery"), children: ["Files", _jsx("span", { children: loading ? "…" : galleryTiles.length })] })] })) : null, tab === "gallery" && canGalleryUpload ? (_jsx(ImageUploadControl, { meta: {
|
|
88
95
|
field: "gallery",
|
|
89
96
|
accept: galleryUpload.accept,
|
|
90
97
|
maxBytes: galleryUpload.maxBytes,
|
|
91
|
-
}, multiple: true, label: "Drop images to add them to
|
|
98
|
+
}, multiple: true, label: "Drop images to add them to Files", onUpload: async (files) => {
|
|
92
99
|
const items = await onUploadGallery(files);
|
|
93
100
|
setGallery((current) => [...items, ...current]);
|
|
94
101
|
setSelected((current) => {
|
|
@@ -112,9 +119,9 @@ export function ImagePickerModal({ recordImages = [], galleryUpload, onListGalle
|
|
|
112
119
|
} }), _jsx("button", { type: "button", className: "vertebr-btn", disabled: !url.trim(), onClick: addUrl, children: "Add" })] })] }), error ? (_jsx("p", { className: "vertebr-status", "data-tone": "danger", children: error })) : null, loading && tab === "gallery" ? (_jsx("div", { className: "vertebr-empty", children: "Loading gallery\u2026" })) : tiles.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: tab === "record"
|
|
113
120
|
? "No product photos on this record yet."
|
|
114
121
|
: hasGallery
|
|
115
|
-
? "No images in
|
|
116
|
-
: "No
|
|
122
|
+
? "No images in Files yet."
|
|
123
|
+
: "No Files library is configured. Paste a URL or add product photos." })) : (_jsx("div", { className: "vertebr-picker-grid", children: tiles.map((image) => {
|
|
117
124
|
const active = Boolean(selected[image.id]);
|
|
118
125
|
return (_jsxs("button", { type: "button", className: "vertebr-picker-tile", "data-selected": active || undefined, onClick: () => toggle(image), children: [_jsx("span", { className: "vertebr-picker-thumb", children: _jsx("img", { src: image.url, alt: "" }) }), _jsx("span", { className: "vertebr-picker-name", children: image.name })] }, image.id));
|
|
119
|
-
}) }))] }), _jsxs("footer", { className: "vertebr-picker-foot", children: [_jsxs("span", { className: "vertebr-picker-count", children: [picked.length, " selected"] }), _jsxs("button", { type: "button", className: "vertebr-btn vertebr-btn-primary", disabled: picked.length === 0, onClick: () => onInsert(picked), children: ["
|
|
126
|
+
}) }))] }), _jsxs("footer", { className: "vertebr-picker-foot", children: [_jsxs("span", { className: "vertebr-picker-count", children: [picked.length, " selected"] }), _jsxs("button", { type: "button", className: "vertebr-btn vertebr-btn-primary", disabled: picked.length === 0, onClick: () => onInsert(picked), children: [confirmLabel, " ", picked.length || "", picked.length === 1 ? " image" : " images"] })] })] }) }), document.body);
|
|
120
127
|
}
|
|
@@ -4,5 +4,9 @@ export type RevisionHistoryProps = {
|
|
|
4
4
|
configId: string;
|
|
5
5
|
onPublished?: () => void | Promise<void>;
|
|
6
6
|
};
|
|
7
|
+
/**
|
|
8
|
+
* Compact History control — opens revision list in a modal so the
|
|
9
|
+
* editor form stays the primary surface.
|
|
10
|
+
*/
|
|
7
11
|
export declare function RevisionHistory({ client, configId, onPublished, }: RevisionHistoryProps): import("react").JSX.Element;
|
|
8
12
|
//# sourceMappingURL=RevisionHistory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RevisionHistory.d.ts","sourceRoot":"","sources":["../../../src/components/config/RevisionHistory.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAgB,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGnE,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C,CAAC;
|
|
1
|
+
{"version":3,"file":"RevisionHistory.d.ts","sourceRoot":"","sources":["../../../src/components/config/RevisionHistory.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAgB,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGnE,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C,CAAC;AAoBF;;;GAGG;AACH,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,QAAQ,EACR,WAAW,GACZ,EAAE,oBAAoB,+BAuNtB"}
|