@vertebr/react 0.5.0 → 0.6.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 (75) hide show
  1. package/dist/components/admin/AdminContent.d.ts +11 -2
  2. package/dist/components/admin/AdminContent.d.ts.map +1 -1
  3. package/dist/components/admin/AdminContent.js +35 -10
  4. package/dist/components/admin/AdminHeader.d.ts +3 -1
  5. package/dist/components/admin/AdminHeader.d.ts.map +1 -1
  6. package/dist/components/admin/AdminHeader.js +12 -3
  7. package/dist/components/admin/PagePreviewPane.d.ts +14 -0
  8. package/dist/components/admin/PagePreviewPane.d.ts.map +1 -0
  9. package/dist/components/admin/PagePreviewPane.js +121 -0
  10. package/dist/components/admin/VertebrAdmin.d.ts +1 -1
  11. package/dist/components/admin/VertebrAdmin.d.ts.map +1 -1
  12. package/dist/components/admin/VertebrAdmin.js +123 -41
  13. package/dist/components/admin/routes.d.ts +21 -0
  14. package/dist/components/admin/routes.d.ts.map +1 -0
  15. package/dist/components/admin/routes.js +170 -0
  16. package/dist/components/admin/types.d.ts +7 -0
  17. package/dist/components/admin/types.d.ts.map +1 -1
  18. package/dist/components/config/VisibilityEye.d.ts +12 -0
  19. package/dist/components/config/VisibilityEye.d.ts.map +1 -1
  20. package/dist/components/config/VisibilityEye.js +12 -4
  21. package/dist/components/config/form/ConfigField.d.ts +5 -3
  22. package/dist/components/config/form/ConfigField.d.ts.map +1 -1
  23. package/dist/components/config/form/ConfigField.js +7 -3
  24. package/dist/components/config/form/ConfigForm.d.ts +1 -1
  25. package/dist/components/config/form/ConfigForm.d.ts.map +1 -1
  26. package/dist/components/config/form/ConfigForm.js +5 -1
  27. package/dist/components/config/form/RecordConfigForm.d.ts +1 -1
  28. package/dist/components/config/form/RecordConfigForm.d.ts.map +1 -1
  29. package/dist/components/config/form/RecordConfigForm.js +2 -2
  30. package/dist/components/config/form/SettingsConfigForm.d.ts +3 -3
  31. package/dist/components/config/form/SettingsConfigForm.d.ts.map +1 -1
  32. package/dist/components/config/form/SettingsConfigForm.js +7 -2
  33. package/dist/components/config/form/helpers/fields.d.ts +2 -1
  34. package/dist/components/config/form/helpers/fields.d.ts.map +1 -1
  35. package/dist/components/config/form/helpers/fields.js +4 -0
  36. package/dist/components/config/form/types.d.ts +6 -1
  37. package/dist/components/config/form/types.d.ts.map +1 -1
  38. package/dist/components/fonts/FontFaceInjector.d.ts +9 -0
  39. package/dist/components/fonts/FontFaceInjector.d.ts.map +1 -0
  40. package/dist/components/fonts/FontFaceInjector.js +28 -0
  41. package/dist/components/fonts/FontLibraryBoard.d.ts +15 -0
  42. package/dist/components/fonts/FontLibraryBoard.d.ts.map +1 -0
  43. package/dist/components/fonts/FontLibraryBoard.js +41 -0
  44. package/dist/components/fonts/FontPickerField.d.ts +16 -0
  45. package/dist/components/fonts/FontPickerField.d.ts.map +1 -0
  46. package/dist/components/fonts/FontPickerField.js +49 -0
  47. package/dist/components/fonts/FontPreviewModal.d.ts +18 -0
  48. package/dist/components/fonts/FontPreviewModal.d.ts.map +1 -0
  49. package/dist/components/fonts/FontPreviewModal.js +57 -0
  50. package/dist/components/fonts/FontPreviewSample.d.ts +8 -0
  51. package/dist/components/fonts/FontPreviewSample.d.ts.map +1 -0
  52. package/dist/components/fonts/FontPreviewSample.js +13 -0
  53. package/dist/components/fonts/GoogleFontsBrowser.d.ts +8 -0
  54. package/dist/components/fonts/GoogleFontsBrowser.d.ts.map +1 -0
  55. package/dist/components/fonts/GoogleFontsBrowser.js +49 -0
  56. package/dist/components/resources/ResourceCell.js +3 -2
  57. package/dist/components/resources/ResourceTable.d.ts +1 -1
  58. package/dist/components/resources/ResourceTable.d.ts.map +1 -1
  59. package/dist/components/resources/ResourceTable.js +123 -5
  60. package/dist/hooks/useVertebrControls.d.ts +10 -1
  61. package/dist/hooks/useVertebrControls.d.ts.map +1 -1
  62. package/dist/hooks/useVertebrControls.js +32 -3
  63. package/dist/index.d.ts +3 -1
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +3 -2
  66. package/dist/styles/base.css +3 -3
  67. package/dist/styles/font-library.css +401 -0
  68. package/dist/styles/form.css +89 -3
  69. package/dist/styles/index.css +3 -1
  70. package/dist/styles/main.css +1 -1
  71. package/dist/styles/page-preview.css +208 -0
  72. package/dist/styles/panel.css +7 -2
  73. package/dist/styles/states.css +6 -3
  74. package/dist/styles/table.css +18 -0
  75. package/package.json +1 -1
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { ImageUploadControl } from "../resources/ImageUploadControl";
4
+ import { FontPreviewModal } from "./FontPreviewModal";
5
+ import { FontFaceInjector } from "./FontFaceInjector";
6
+ import { GoogleFontsBrowser } from "./GoogleFontsBrowser";
7
+ function previewStyle(item) {
8
+ const primary = item.files.find((file) => file.weight >= 600) ?? item.files[0];
9
+ return {
10
+ fontFamily: `"${item.family}", ui-sans-serif, sans-serif`,
11
+ fontWeight: primary?.weight ?? 400,
12
+ };
13
+ }
14
+ export function FontLibraryBoard({ label = "Font Library", data, loading, onRefresh, onUpload, onImportGoogle, onDelete, }) {
15
+ const [previewItem, setPreviewItem] = useState(null);
16
+ const meta = {
17
+ field: "fonts",
18
+ accept: data.accept,
19
+ maxBytes: data.maxBytes,
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) => {
22
+ for (const file of files) {
23
+ const base = file.name.replace(/\.[^.]+$/, "");
24
+ const weightMatch = /(?:^|[\s_-])(100|200|300|400|500|600|700|800|900)(?:[\s_-]|$)/i.exec(base);
25
+ const family = base
26
+ .replace(/(?:^|[\s_-])(100|200|300|400|500|600|700|800|900)(?:[\s_-]|$)/gi, " ")
27
+ .replace(/[\s_-]+/g, " ")
28
+ .trim();
29
+ await onUpload([file], {
30
+ family: family || undefined,
31
+ weight: weightMatch ? Number(weightMatch[1]) : 400,
32
+ });
33
+ }
34
+ } }), _jsx("p", { className: "vertebr-font-library-note", children: "Upload only fonts you have the right to use. Click a font card to preview before using it in Brand typography." }), data.items.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: "No fonts in the library yet." })) : (_jsx("div", { className: "vertebr-font-library-grid", children: data.items.map((item) => (_jsxs("article", { className: "vertebr-font-library-card", children: [_jsxs("button", { type: "button", className: "vertebr-font-library-preview-btn", onClick: () => setPreviewItem(item), title: `Preview ${item.label}`, children: [_jsxs("div", { className: "vertebr-font-library-preview", style: previewStyle(item), children: [_jsx("span", { className: "vertebr-font-library-ag", children: "Ag" }), _jsx("p", { children: "The quick brown fox jumps over the lazy dog." })] }), _jsx("span", { className: "vertebr-font-library-preview-hint", children: "Click to preview" })] }), _jsxs("div", { className: "vertebr-font-library-meta", children: [_jsx("strong", { title: item.label, children: item.label }), _jsxs("span", { children: [item.source === "google" ? "Google Fonts" : "Upload", " \u00B7", " ", item.files.length, " file", item.files.length === 1 ? "" : "s"] }), _jsx("code", { children: item.id })] }), data.canDelete && onDelete ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-btn-danger-quiet", onClick: () => {
35
+ if (window.confirm(`Delete "${item.label}" from the font library?`)) {
36
+ void onDelete(item.id);
37
+ }
38
+ }, children: "Delete" })) : null] }, item.id))) }))] }), _jsx(FontPreviewModal, { open: previewItem != null, title: previewItem?.label ?? "Font preview", subtitle: previewItem
39
+ ? `${previewItem.source === "google" ? "Google Fonts" : "Upload"} · ${previewItem.family}`
40
+ : undefined, family: previewItem?.family ?? "", files: previewItem?.files, onClose: () => setPreviewItem(null) })] }));
41
+ }
@@ -0,0 +1,16 @@
1
+ import type { FontLibraryItem } from "@vertebr/client";
2
+ export type FontPickerFieldProps = {
3
+ id: string;
4
+ value: unknown;
5
+ label?: string;
6
+ description?: string;
7
+ readOnly?: boolean;
8
+ fonts: FontLibraryItem[];
9
+ loading?: boolean;
10
+ onChange: (value: string) => void;
11
+ onRefreshFonts?: () => Promise<void>;
12
+ /** Same-origin API base for font file proxy URLs. */
13
+ fontApiBase?: string;
14
+ };
15
+ export declare function FontPickerField({ id, value, label, description, readOnly, fonts, loading, onChange, onRefreshFonts, fontApiBase, }: FontPickerFieldProps): import("react").JSX.Element;
16
+ //# sourceMappingURL=FontPickerField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FontPickerField.d.ts","sourceRoot":"","sources":["../../../src/components/fonts/FontPickerField.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAKvD,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAUF,wBAAgB,eAAe,CAAC,EAC9B,EAAE,EACF,KAAK,EACL,KAAK,EACL,WAAW,EACX,QAAQ,EACR,KAAK,EACL,OAAO,EACP,QAAQ,EACR,cAAc,EACd,WAA0B,GAC3B,EAAE,oBAAoB,+BA0LtB"}
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { FontPreviewSample } from "./FontPreviewSample";
4
+ import { FontFaceInjector } from "./FontFaceInjector";
5
+ function weightForItem(item, kind) {
6
+ if (!item)
7
+ return kind === "heading" ? 700 : 400;
8
+ if (kind === "heading") {
9
+ return item.files.find((file) => file.weight >= 600)?.weight ?? 700;
10
+ }
11
+ return item.files.find((file) => file.weight >= 400 && file.weight < 600)?.weight ?? 400;
12
+ }
13
+ export function FontPickerField({ id, value, label, description, readOnly, fonts, loading, onChange, onRefreshFonts, fontApiBase = "/admin/api", }) {
14
+ const [open, setOpen] = useState(false);
15
+ const [previewId, setPreviewId] = useState("");
16
+ const initialIdRef = useRef("");
17
+ const selectedId = typeof value === "string" ? value.trim() : "";
18
+ useEffect(() => {
19
+ if (open && onRefreshFonts) {
20
+ void onRefreshFonts();
21
+ }
22
+ }, [open, onRefreshFonts]);
23
+ useEffect(() => {
24
+ if (open) {
25
+ initialIdRef.current = selectedId;
26
+ setPreviewId(selectedId);
27
+ }
28
+ }, [open, selectedId]);
29
+ const closeModal = (revert = false) => {
30
+ if (revert && previewId !== initialIdRef.current) {
31
+ onChange(initialIdRef.current);
32
+ }
33
+ setOpen(false);
34
+ };
35
+ const previewFont = (nextId) => {
36
+ setPreviewId(nextId);
37
+ onChange(nextId);
38
+ };
39
+ const selected = useMemo(() => fonts.find((item) => item.id === selectedId) ?? null, [fonts, selectedId]);
40
+ const previewItem = useMemo(() => {
41
+ if (previewId === "")
42
+ return null;
43
+ return fonts.find((item) => item.id === previewId) ?? null;
44
+ }, [fonts, previewId]);
45
+ return (_jsxs("div", { className: "vertebr-font-picker", children: [_jsx(FontFaceInjector, { items: fonts, apiBase: fontApiBase }), _jsxs("div", { className: "vertebr-font-picker-current", children: [_jsxs("div", { children: [_jsx("strong", { children: selected?.label ?? "Default system fonts" }), selected ? (_jsx("span", { children: selected.family })) : (_jsx("span", { children: "Manrope / Syne until a library font is chosen" }))] }), !readOnly ? (_jsx("button", { id: id, type: "button", className: "vertebr-btn", onClick: () => setOpen(true), children: "Choose font" })) : (_jsx("span", { id: id, className: "vertebr-font-picker-ro-anchor" }))] }), selected ? (_jsx("div", { className: "vertebr-font-picker-preview", children: _jsx(FontPreviewSample, { family: selected.family, bodyWeight: weightForItem(selected, "body"), headingWeight: weightForItem(selected, "heading"), compact: true }) })) : null, open ? (_jsx("div", { className: "vertebr-font-picker-modal-backdrop", onClick: () => closeModal(true), children: _jsxs("div", { className: "vertebr-font-picker-modal", role: "dialog", "aria-labelledby": `${id}-font-picker-title`, onClick: (event) => event.stopPropagation(), children: [_jsxs("div", { className: "vertebr-font-picker-modal-head", children: [_jsxs("div", { children: [_jsx("h3", { id: `${id}-font-picker-title`, children: label ?? "Choose a font" }), description ? _jsx("p", { children: description }) : null] }), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", onClick: () => closeModal(true), children: "Close" })] }), loading ? (_jsx("div", { className: "vertebr-empty", children: "Loading fonts\u2026" })) : fonts.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: "No fonts in the library yet. Open Font Library to upload or add Google Fonts." })) : (_jsxs("div", { className: "vertebr-font-picker-modal-body", children: [_jsx("div", { className: "vertebr-font-picker-grid-scroll", children: _jsxs("div", { className: "vertebr-font-picker-grid", children: [_jsxs("button", { type: "button", className: "vertebr-font-picker-option", "data-active": previewId === "" || undefined, onClick: () => previewFont(""), children: [_jsx("strong", { children: "Default" }), _jsx("span", { children: "Manrope / Syne" })] }), fonts.map((item) => (_jsxs("button", { type: "button", className: "vertebr-font-picker-option", "data-active": item.id === previewId || undefined, style: {
46
+ fontFamily: `"${item.family}", ui-sans-serif, sans-serif`,
47
+ fontWeight: weightForItem(item, "heading"),
48
+ }, onClick: () => previewFont(item.id), children: [_jsx("strong", { children: item.label }), _jsx("span", { children: item.family }), _jsx("em", { children: "The quick brown fox" })] }, item.id)))] }) }), _jsxs("aside", { className: "vertebr-font-picker-detail", children: [_jsx("p", { className: "vertebr-font-picker-detail-label", children: "Preview" }), previewId === "" ? (_jsx(FontPreviewSample, { family: "Manrope", bodyWeight: 400, headingWeight: 700 })) : previewItem ? (_jsx(FontPreviewSample, { family: previewItem.family, bodyWeight: weightForItem(previewItem, "body"), headingWeight: weightForItem(previewItem, "heading") })) : (_jsx("div", { className: "vertebr-empty", children: "Select a font to preview." }))] })] })), _jsxs("div", { className: "vertebr-font-picker-modal-actions", children: [_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", onClick: () => closeModal(true), children: "Cancel" }), _jsx("button", { type: "button", className: "vertebr-btn", disabled: loading, onClick: () => closeModal(false), children: "Done" })] })] }) })) : null] }));
49
+ }
@@ -0,0 +1,18 @@
1
+ import { type FontFaceFile } from "@vertebr/client";
2
+ export type FontPreviewModalProps = {
3
+ open: boolean;
4
+ title: string;
5
+ subtitle?: string;
6
+ family: string;
7
+ /** Self-hosted face files — injected as @font-face rules. */
8
+ files?: FontFaceFile[];
9
+ /** Load a Google Font family from the CDN for preview only. */
10
+ googleFamily?: string;
11
+ bodyWeight?: number;
12
+ headingWeight?: number;
13
+ onClose: () => void;
14
+ onConfirm?: () => void;
15
+ confirmLabel?: string;
16
+ };
17
+ export declare function FontPreviewModal({ open, title, subtitle, family, files, googleFamily, bodyWeight, headingWeight, onClose, onConfirm, confirmLabel, }: FontPreviewModalProps): import("react").JSX.Element | null;
18
+ //# sourceMappingURL=FontPreviewModal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FontPreviewModal.d.ts","sourceRoot":"","sources":["../../../src/components/fonts/FontPreviewModal.tsx"],"names":[],"mappings":"AACA,OAAO,EAAoB,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGtE,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;IACvB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AASF,wBAAgB,gBAAgB,CAAC,EAC/B,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,MAAM,EACN,KAAK,EACL,YAAY,EACZ,UAAU,EACV,aAAa,EACb,OAAO,EACP,SAAS,EACT,YAA8B,GAC/B,EAAE,qBAAqB,sCA0GvB"}
@@ -0,0 +1,57 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useId, useMemo } from "react";
3
+ import { buildFontFaceCss } from "@vertebr/client";
4
+ import { FontPreviewSample } from "./FontPreviewSample";
5
+ function googleCssUrl(family) {
6
+ return ("https://fonts.googleapis.com/css2?" +
7
+ `family=${encodeURIComponent(family)}:wght@400;600;700&display=swap`);
8
+ }
9
+ export function FontPreviewModal({ open, title, subtitle, family, files, googleFamily, bodyWeight, headingWeight, onClose, onConfirm, confirmLabel = "Use this font", }) {
10
+ const styleId = useId().replace(/:/g, "");
11
+ const faceCss = useMemo(() => {
12
+ if (!files?.length)
13
+ return "";
14
+ return buildFontFaceCss({
15
+ id: "preview",
16
+ label: title,
17
+ family,
18
+ source: "upload",
19
+ files,
20
+ });
21
+ }, [files, family, title]);
22
+ useEffect(() => {
23
+ if (!open)
24
+ return;
25
+ let link = null;
26
+ if (googleFamily && !files?.length) {
27
+ link = document.createElement("link");
28
+ link.rel = "stylesheet";
29
+ link.href = googleCssUrl(googleFamily);
30
+ document.head.appendChild(link);
31
+ }
32
+ return () => {
33
+ link?.remove();
34
+ };
35
+ }, [open, googleFamily, files?.length]);
36
+ useEffect(() => {
37
+ if (!open)
38
+ return;
39
+ const onKeyDown = (event) => {
40
+ if (event.key === "Escape")
41
+ onClose();
42
+ };
43
+ window.addEventListener("keydown", onKeyDown);
44
+ return () => window.removeEventListener("keydown", onKeyDown);
45
+ }, [open, onClose]);
46
+ if (!open)
47
+ return null;
48
+ const resolvedBody = bodyWeight ??
49
+ files?.find((file) => file.weight >= 400 && file.weight < 600)?.weight ??
50
+ files?.[0]?.weight ??
51
+ 400;
52
+ const resolvedHeading = headingWeight ??
53
+ files?.find((file) => file.weight >= 600)?.weight ??
54
+ files?.[files.length - 1]?.weight ??
55
+ 700;
56
+ return (_jsxs("div", { className: "vertebr-font-preview-modal-backdrop", onClick: onClose, children: [faceCss ? (_jsx("style", { id: styleId, dangerouslySetInnerHTML: { __html: faceCss } })) : null, _jsxs("div", { className: "vertebr-font-preview-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": `${styleId}-title`, onClick: (event) => event.stopPropagation(), children: [_jsxs("div", { className: "vertebr-font-preview-modal-head", children: [_jsxs("div", { children: [_jsx("h3", { id: `${styleId}-title`, children: title }), subtitle ? _jsx("p", { children: subtitle }) : null] }), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", onClick: onClose, children: "Close" })] }), _jsx("div", { className: "vertebr-font-preview-modal-body", children: _jsx(FontPreviewSample, { family: family, bodyWeight: resolvedBody, headingWeight: resolvedHeading }) }), onConfirm ? (_jsxs("div", { className: "vertebr-font-preview-modal-actions", children: [_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", onClick: onClose, children: "Cancel" }), _jsx("button", { type: "button", className: "vertebr-btn", onClick: onConfirm, children: confirmLabel })] })) : null] })] }));
57
+ }
@@ -0,0 +1,8 @@
1
+ export type FontPreviewSampleProps = {
2
+ family: string;
3
+ bodyWeight?: number;
4
+ headingWeight?: number;
5
+ compact?: boolean;
6
+ };
7
+ export declare function FontPreviewSample({ family, bodyWeight, headingWeight, compact, }: FontPreviewSampleProps): import("react").JSX.Element;
8
+ //# sourceMappingURL=FontPreviewSample.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FontPreviewSample.d.ts","sourceRoot":"","sources":["../../../src/components/fonts/FontPreviewSample.tsx"],"names":[],"mappings":"AAEA,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,UAAgB,EAChB,aAAmB,EACnB,OAAe,GAChB,EAAE,sBAAsB,+BA4CxB"}
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ export function FontPreviewSample({ family, bodyWeight = 400, headingWeight = 700, compact = false, }) {
3
+ const stack = `"${family.replace(/"/g, "")}", ui-sans-serif, sans-serif`;
4
+ const headingStyle = {
5
+ fontFamily: stack,
6
+ fontWeight: headingWeight,
7
+ };
8
+ const bodyStyle = {
9
+ fontFamily: stack,
10
+ fontWeight: bodyWeight,
11
+ };
12
+ return (_jsxs("div", { className: "vertebr-font-preview-sample", "data-compact": compact ? "true" : undefined, children: [_jsx("span", { className: "vertebr-font-preview-eyebrow", style: headingStyle, children: "Eyebrow label" }), _jsx("strong", { className: "vertebr-font-preview-heading", style: headingStyle, children: "The quick brown fox" }), _jsx("p", { className: "vertebr-font-preview-body", style: bodyStyle, children: "Jumps over the lazy dog. Maxer Mentality essentials for people who hold the standard." }), !compact ? (_jsxs(_Fragment, { children: [_jsxs("p", { className: "vertebr-font-preview-alphabet", style: bodyStyle, children: ["ABCDEFGHIJKLMNOPQRSTUVWXYZ", _jsx("br", {}), "abcdefghijklmnopqrstuvwxyz", _jsx("br", {}), "0123456789"] }), _jsxs("div", { className: "vertebr-font-preview-weights", style: bodyStyle, children: [_jsx("span", { style: { fontWeight: 400 }, children: "Regular 400" }), _jsx("span", { style: { fontWeight: 600 }, children: "Semibold 600" }), _jsx("span", { style: { fontWeight: 700 }, children: "Bold 700" })] })] })) : null] }));
13
+ }
@@ -0,0 +1,8 @@
1
+ import { type GoogleFontImportRequest } from "@vertebr/client";
2
+ export type GoogleFontsBrowserProps = {
3
+ disabled?: boolean;
4
+ existingFamilies: string[];
5
+ onImport: (payload: GoogleFontImportRequest) => Promise<void>;
6
+ };
7
+ export declare function GoogleFontsBrowser({ disabled, existingFamilies, onImport, }: GoogleFontsBrowserProps): import("react").JSX.Element;
8
+ //# sourceMappingURL=GoogleFontsBrowser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GoogleFontsBrowser.d.ts","sourceRoot":"","sources":["../../../src/components/fonts/GoogleFontsBrowser.tsx"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,uBAAuB,EAC7B,MAAM,iBAAiB,CAAC;AAGzB,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,QAAQ,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,gBAAgB,EAChB,QAAQ,GACT,EAAE,uBAAuB,+BAkHzB"}
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { GOOGLE_FONTS_CATALOG, } from "@vertebr/client";
4
+ import { FontPreviewModal } from "./FontPreviewModal";
5
+ export function GoogleFontsBrowser({ disabled, existingFamilies, onImport, }) {
6
+ const [query, setQuery] = useState("");
7
+ const [busyFamily, setBusyFamily] = useState(null);
8
+ const [error, setError] = useState(null);
9
+ const [previewEntry, setPreviewEntry] = useState(null);
10
+ const items = useMemo(() => {
11
+ const needle = query.trim().toLowerCase();
12
+ if (!needle)
13
+ return GOOGLE_FONTS_CATALOG;
14
+ return GOOGLE_FONTS_CATALOG.filter((entry) => entry.family.toLowerCase().includes(needle) ||
15
+ entry.category.includes(needle));
16
+ }, [query]);
17
+ const importEntry = async (entry) => {
18
+ if (disabled || busyFamily)
19
+ return;
20
+ setBusyFamily(entry.family);
21
+ setError(null);
22
+ try {
23
+ await onImport({ family: entry.family, weights: entry.weights });
24
+ setPreviewEntry(null);
25
+ }
26
+ catch (err) {
27
+ setError(err instanceof Error ? err.message : "Import failed");
28
+ }
29
+ finally {
30
+ setBusyFamily(null);
31
+ }
32
+ };
33
+ return (_jsxs("section", { className: "vertebr-google-fonts", children: [_jsxs("div", { className: "vertebr-google-fonts-head", children: [_jsxs("div", { children: [_jsx("h3", { children: "Google Fonts" }), _jsx("p", { children: "Browse and add open-source fonts to your library." })] }), _jsx("input", { type: "search", className: "vertebr-google-fonts-search", placeholder: "Search fonts\u2026", value: query, onChange: (event) => setQuery(event.target.value) })] }), error ? _jsx("div", { className: "vertebr-error-inline", children: error }) : null, _jsx("div", { className: "vertebr-google-fonts-grid-scroll", children: _jsx("div", { className: "vertebr-google-fonts-grid", children: items.map((entry) => {
34
+ const exists = existingFamilies.includes(entry.family.toLowerCase());
35
+ return (_jsxs("article", { className: "vertebr-google-fonts-card", children: [_jsxs("button", { type: "button", className: "vertebr-google-fonts-preview-btn", onClick: () => setPreviewEntry(entry), title: `Preview ${entry.family}`, children: [_jsxs("div", { className: "vertebr-google-fonts-card-copy", children: [_jsx("strong", { children: entry.family }), _jsx("span", { children: entry.category.replace("-", " ") }), _jsx("span", { children: entry.weights.join(", ") })] }), _jsx("span", { className: "vertebr-font-library-preview-hint", children: "Click to preview" })] }), _jsx("button", { type: "button", className: "vertebr-btn", disabled: disabled || exists || busyFamily === entry.family, onClick: () => void importEntry(entry), children: exists
36
+ ? "In library"
37
+ : busyFamily === entry.family
38
+ ? "Adding…"
39
+ : "Add to library" })] }, entry.family));
40
+ }) }) }), _jsx(FontPreviewModal, { open: previewEntry != null, title: previewEntry?.family ?? "Font preview", subtitle: previewEntry
41
+ ? `${previewEntry.category.replace("-", " ")} · weights ${previewEntry.weights.join(", ")}`
42
+ : undefined, family: previewEntry?.family ?? "", googleFamily: previewEntry?.family, onClose: () => setPreviewEntry(null), onConfirm: previewEntry &&
43
+ !existingFamilies.includes(previewEntry.family.toLowerCase())
44
+ ? () => void importEntry(previewEntry)
45
+ : undefined, confirmLabel: previewEntry &&
46
+ existingFamilies.includes(previewEntry.family.toLowerCase())
47
+ ? "Already in library"
48
+ : "Add to library" })] }));
49
+ }
@@ -102,8 +102,9 @@ function booleanLabel(fieldKey, field, on) {
102
102
  if (/^show[A-Z]/.test(fieldKey) || /\bshow\b/i.test(field?.label ?? "")) {
103
103
  return on ? "Shown" : "Hidden";
104
104
  }
105
- if (/\bactive\b/i.test(hay))
106
- return on ? "Active" : "Inactive";
105
+ if (/\bactive\b/i.test(hay) || /\bvisible\b/i.test(hay)) {
106
+ return on ? "Visible" : "Hidden";
107
+ }
107
108
  return on ? "On" : "Off";
108
109
  }
109
110
  function ImageThumbs({ urls, detail, }) {
@@ -21,7 +21,7 @@ type ResourcePanelProps = {
21
21
  onActionDone?: () => Promise<void> | void;
22
22
  onPreviewMail?: (rowId: string, actionId: string, input?: Record<string, unknown>) => Promise<MailPreviewResponse>;
23
23
  collectionActions?: Record<string, ResourceActionMeta>;
24
- onCollectionAction?: (actionId: string, input?: Record<string, unknown>) => Promise<void>;
24
+ onCollectionAction?: (actionId: string, input?: Record<string, unknown>) => Promise<unknown>;
25
25
  onUpload?: (field: string, files: File[]) => Promise<Array<{
26
26
  url: string;
27
27
  name?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"ResourceTable.d.ts","sourceRoot":"","sources":["../../../src/components/resources/ResourceTable.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ResourceTable.d.ts","sourceRoot":"","sources":["../../../src/components/resources/ResourceTable.tsx"],"names":[],"mappings":"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"}
@@ -9,6 +9,7 @@ import { ResourceCreateModal } from "./ResourceCreateModal";
9
9
  import { actionInputSchema, ResourceActionModal, } from "./ResourceActionModal";
10
10
  import { ResourceMailModal } from "./ResourceMailModal";
11
11
  import { ResourceViewModal } from "./ResourceViewModal";
12
+ import { VisibilityEyeToggle } from "../config/VisibilityEye";
12
13
  function actionHasInput(meta) {
13
14
  return Boolean(meta?.input && Object.keys(meta.input).length > 0);
14
15
  }
@@ -75,6 +76,17 @@ function isDefaultHiddenColumn(key, field, schema) {
75
76
  }
76
77
  return false;
77
78
  }
79
+ /** Boolean fields owners toggle in the list (storefront visibility). */
80
+ function isListVisibilityField(key, field) {
81
+ if (field?.type !== "boolean" || field.readOnly)
82
+ return false;
83
+ if (/^(active|visible|published)$/i.test(key))
84
+ return true;
85
+ return /\b(active|visible|published)\b/i.test(field.label ?? "");
86
+ }
87
+ function visibilityValue(row, fieldKey) {
88
+ return row[fieldKey] !== false;
89
+ }
78
90
  function defaultVisibility(schema) {
79
91
  const next = {};
80
92
  for (const [key, field] of Object.entries(schema)) {
@@ -134,6 +146,9 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, label,
134
146
  canRight: false,
135
147
  });
136
148
  const [columnVisibility, setColumnVisibility] = useState(() => readVisibility(storageKey, schema));
149
+ /** Pending list visibility toggles keyed by row id → field → value. */
150
+ const [visibilityDrafts, setVisibilityDrafts] = useState({});
151
+ const [savingVisibility, setSavingVisibility] = useState(false);
137
152
  const canUpdate = Boolean(permissions?.update && onUpdate);
138
153
  const canCreate = Boolean(permissions?.create && onCreate);
139
154
  const canDelete = Boolean(permissions?.delete && onDelete);
@@ -142,7 +157,11 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, label,
142
157
  const actionEntries = Object.entries(actions);
143
158
  const collectionActionEntries = Object.entries(collectionActions);
144
159
  const fieldKeys = Object.keys(schema);
160
+ const visibilityFields = useMemo(() => fieldKeys.filter((key) => isListVisibilityField(key, schema[key])), [fieldKeys, schema]);
145
161
  const selectable = Boolean(onAction && actionEntries.length > 0);
162
+ useEffect(() => {
163
+ setVisibilityDrafts({});
164
+ }, [items]);
146
165
  useEffect(() => {
147
166
  const valid = new Set(items
148
167
  .map((row) => resolveRowId(row, rowIdKeys))
@@ -152,6 +171,70 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, label,
152
171
  return next.size === current.size ? current : next;
153
172
  });
154
173
  }, [items, rowIdKeys]);
174
+ const dirtyVisibilityCount = useMemo(() => {
175
+ let count = 0;
176
+ for (const [rowId, fields] of Object.entries(visibilityDrafts)) {
177
+ const row = items.find((item) => resolveRowId(item, rowIdKeys) === rowId);
178
+ if (!row) {
179
+ count += Object.keys(fields).length;
180
+ continue;
181
+ }
182
+ for (const [fieldKey, value] of Object.entries(fields)) {
183
+ if (visibilityValue(row, fieldKey) !== value)
184
+ count += 1;
185
+ }
186
+ }
187
+ return count;
188
+ }, [visibilityDrafts, items, rowIdKeys]);
189
+ const setVisibilityDraft = useCallback((rowId, fieldKey, value) => {
190
+ setVisibilityDrafts((current) => {
191
+ const row = items.find((item) => resolveRowId(item, rowIdKeys) === rowId);
192
+ const saved = row ? visibilityValue(row, fieldKey) : null;
193
+ const nextRow = { ...(current[rowId] ?? {}) };
194
+ if (saved === value)
195
+ delete nextRow[fieldKey];
196
+ else
197
+ nextRow[fieldKey] = value;
198
+ const next = { ...current };
199
+ if (Object.keys(nextRow).length === 0)
200
+ delete next[rowId];
201
+ else
202
+ next[rowId] = nextRow;
203
+ return next;
204
+ });
205
+ }, [items, rowIdKeys]);
206
+ const saveVisibility = useCallback(async () => {
207
+ if (!onUpdate || dirtyVisibilityCount === 0)
208
+ return;
209
+ setSavingVisibility(true);
210
+ const errors = [];
211
+ try {
212
+ for (const [rowId, fields] of Object.entries(visibilityDrafts)) {
213
+ const row = items.find((item) => resolveRowId(item, rowIdKeys) === rowId);
214
+ if (!row)
215
+ continue;
216
+ try {
217
+ await onUpdate(rowId, { ...row, ...fields });
218
+ }
219
+ catch (error) {
220
+ errors.push(`${rowId}: ${error instanceof Error ? error.message : "Failed"}`);
221
+ }
222
+ }
223
+ setVisibilityDrafts({});
224
+ if (errors.length > 0) {
225
+ window.alert(`Saved with ${errors.length} error(s).\n\n${errors.slice(0, 8).join("\n")}`);
226
+ }
227
+ }
228
+ finally {
229
+ setSavingVisibility(false);
230
+ }
231
+ }, [
232
+ onUpdate,
233
+ dirtyVisibilityCount,
234
+ visibilityDrafts,
235
+ items,
236
+ rowIdKeys,
237
+ ]);
155
238
  const runActions = useCallback(async (actionId, rowIds, input) => {
156
239
  if (!onAction || rowIds.length === 0)
157
240
  return;
@@ -194,8 +277,23 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, label,
194
277
  return;
195
278
  setBusyAction(actionId);
196
279
  try {
197
- await onCollectionAction(actionId, input);
280
+ const result = await onCollectionAction(actionId, input);
198
281
  await onActionDone?.();
282
+ if (result &&
283
+ typeof result === "object" &&
284
+ "message" in result &&
285
+ typeof result.message === "string") {
286
+ window.alert(result.message);
287
+ }
288
+ else if (result &&
289
+ typeof result === "object" &&
290
+ "result" in result &&
291
+ result.result &&
292
+ typeof result.result === "object" &&
293
+ "message" in result.result &&
294
+ typeof result.result.message === "string") {
295
+ window.alert(result.result.message);
296
+ }
199
297
  }
200
298
  catch (error) {
201
299
  window.alert(error instanceof Error ? error.message : "Action failed");
@@ -212,7 +310,10 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, label,
212
310
  setPendingCollectionAction(actionId);
213
311
  return;
214
312
  }
215
- if (!window.confirm(`${meta.label}?`))
313
+ const detail = meta.confirm
314
+ ? `${meta.label}?\n\n${meta.confirm}`
315
+ : `${meta.label}?`;
316
+ if (!window.confirm(detail))
216
317
  return;
217
318
  void runCollectionAction(actionId);
218
319
  }, [collectionActions, onCollectionAction, runCollectionAction]);
@@ -305,13 +406,28 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, label,
305
406
  const field = schema[key];
306
407
  const isImage = /(image|img|photo|avatar|thumbnail|thumb|cover)/i.test(key) ||
307
408
  /(image|img|photo)/i.test(field?.label ?? "");
409
+ const listVisibility = canUpdate && isListVisibilityField(key, field);
308
410
  return {
309
411
  id: key,
310
412
  accessorKey: key,
311
413
  header: field?.label ?? humanizeKey(key),
312
414
  meta: { fieldKey: key, field },
313
- cell: ({ getValue, }) => (_jsx(ResourceCell, { fieldKey: key, field: field, value: getValue() })),
314
- size: isImage ? 220 : undefined,
415
+ cell: ({ row, getValue, }) => {
416
+ if (listVisibility) {
417
+ const rowId = resolveRowId(row.original, rowIdKeys);
418
+ if (!rowId) {
419
+ return (_jsx(ResourceCell, { fieldKey: key, field: field, value: getValue() }));
420
+ }
421
+ const checked = visibilityDrafts[rowId]?.[key] ??
422
+ visibilityValue(row.original, key);
423
+ const dirty = visibilityDrafts[rowId]?.[key] !== undefined &&
424
+ visibilityDrafts[rowId]?.[key] !==
425
+ visibilityValue(row.original, key);
426
+ return (_jsx("div", { className: "vertebr-visibility-check", "data-dirty": dirty ? "true" : undefined, onClick: (event) => event.stopPropagation(), children: _jsx(VisibilityEyeToggle, { visible: checked, dirty: dirty, label: field?.label ?? humanizeKey(key), disabled: savingVisibility, onChange: (next) => setVisibilityDraft(rowId, key, next) }) }));
427
+ }
428
+ return (_jsx(ResourceCell, { fieldKey: key, field: field, value: getValue() }));
429
+ },
430
+ size: listVisibility ? 88 : isImage ? 220 : undefined,
315
431
  };
316
432
  })));
317
433
  defs.push({
@@ -343,6 +459,8 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, label,
343
459
  queueAction,
344
460
  onDelete,
345
461
  rowIdKeys,
462
+ visibilityDrafts,
463
+ setVisibilityDraft,
346
464
  ]);
347
465
  const table = useReactTable({
348
466
  data: items,
@@ -377,7 +495,7 @@ export function ResourcePanel({ schema, items, permissions, actions = {}, label,
377
495
  setEditing(null);
378
496
  } })] }));
379
497
  }
380
- const toolbar = (_jsxs("div", { className: "vertebr-table-toolbar", children: [_jsxs("div", { className: "vertebr-table-toolbar-start", children: [columnOptions.length > 0 ? (_jsx(ColumnVisibilityMenu, { columns: columnOptions, visibility: columnVisibility, onChange: setColumnVisibility })) : null, canUpload ? (_jsxs("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-btn-icon", onClick: () => setBulkOpen(true), children: [_jsx(ImageUploadIcon, {}), "Bulk upload"] })) : null, collectionActionEntries.map(([actionId, meta]) => (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: busyAction != null, onClick: () => queueCollectionAction(actionId), children: meta.label }, actionId))), selectable && selected.size > 0 ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "vertebr-count", children: [selected.size, " selected"] }), actionEntries.map(([actionId, meta]) => (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: busyAction != null, onClick: () => queueAction(actionId, [...selected]), children: meta.label }, actionId))), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", onClick: () => setSelected(new Set()), children: "Clear" })] })) : null] }), tableOverflow.overflow ? (_jsxs("div", { className: "vertebr-table-scroll-controls", role: "group", "aria-label": "Scroll columns", children: [_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-btn-icon vertebr-table-scroll-btn", disabled: !tableOverflow.canLeft, onClick: () => scrollTable("left"), "aria-label": "Scroll columns left", children: _jsx(ChevronLeftIcon, {}) }), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-btn-icon vertebr-table-scroll-btn", disabled: !tableOverflow.canRight, onClick: () => scrollTable("right"), "aria-label": "Scroll columns right", children: _jsx(ChevronRightIcon, {}) })] })) : null] }));
498
+ const toolbar = (_jsxs("div", { className: "vertebr-table-toolbar", children: [_jsxs("div", { className: "vertebr-table-toolbar-start", children: [columnOptions.length > 0 ? (_jsx(ColumnVisibilityMenu, { columns: columnOptions, visibility: columnVisibility, onChange: setColumnVisibility })) : null, canUpload ? (_jsxs("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-btn-icon", onClick: () => setBulkOpen(true), children: [_jsx(ImageUploadIcon, {}), "Bulk upload"] })) : null, visibilityFields.length > 0 && dirtyVisibilityCount > 0 ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "vertebr-count", children: [dirtyVisibilityCount, " visibility change", dirtyVisibilityCount === 1 ? "" : "s"] }), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-primary", disabled: savingVisibility, onClick: () => void saveVisibility(), children: savingVisibility ? "Saving…" : "Save visibility" }), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: savingVisibility, onClick: () => setVisibilityDrafts({}), children: "Discard" })] })) : null, collectionActionEntries.map(([actionId, meta]) => (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: busyAction != null, onClick: () => queueCollectionAction(actionId), children: meta.label }, actionId))), selectable && selected.size > 0 ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "vertebr-count", children: [selected.size, " selected"] }), actionEntries.map(([actionId, meta]) => (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: busyAction != null, onClick: () => queueAction(actionId, [...selected]), children: meta.label }, actionId))), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", onClick: () => setSelected(new Set()), children: "Clear" })] })) : null] }), tableOverflow.overflow ? (_jsxs("div", { className: "vertebr-table-scroll-controls", role: "group", "aria-label": "Scroll columns", children: [_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-btn-icon vertebr-table-scroll-btn", disabled: !tableOverflow.canLeft, onClick: () => scrollTable("left"), "aria-label": "Scroll columns left", children: _jsx(ChevronLeftIcon, {}) }), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-btn-icon vertebr-table-scroll-btn", disabled: !tableOverflow.canRight, onClick: () => scrollTable("right"), "aria-label": "Scroll columns right", children: _jsx(ChevronRightIcon, {}) })] })) : null] }));
381
499
  const bulkModal = bulkOpen && uploads && onUpload ? (_jsx(BulkImageUploadModal, { uploads: uploads, canCreate: canCreate, fieldLabels: Object.fromEntries(Object.keys(uploads).map((key) => [
382
500
  key,
383
501
  schema[key]?.label ?? humanizeKey(key),
@@ -6,19 +6,28 @@ type ControlsContextValue = {
6
6
  loading: boolean;
7
7
  error: Error | null;
8
8
  refresh: () => Promise<void>;
9
+ /** True when live admin draft preview is active. */
10
+ preview: boolean;
9
11
  };
10
12
  export type VertebrControlsProviderProps = {
11
13
  /** Vertebr API base, e.g. `/admin/api`. */
12
14
  apiBase?: string;
13
15
  /** Active app environment (qual / production). */
14
16
  environment: string;
17
+ /**
18
+ * Listen for admin draft posts.
19
+ * Defaults to on when the URL has `?vertebrPreview=1`.
20
+ */
21
+ enablePreview?: boolean;
15
22
  children: ReactNode;
16
23
  };
17
24
  export type { VertebrControlsMap };
18
25
  /**
19
26
  * Loads all public Vertebr controls for the storefront once and shares them.
27
+ * When the URL has `?vertebrPreview=1`, also merges live drafts from the
28
+ * admin iframe parent (host still renders).
20
29
  */
21
- export declare function VertebrControlsProvider({ apiBase, environment, children, }: VertebrControlsProviderProps): import("react").JSX.Element;
30
+ export declare function VertebrControlsProvider({ apiBase, environment, enablePreview, children, }: VertebrControlsProviderProps): import("react").JSX.Element;
22
31
  /**
23
32
  * Read a public Vertebr control by id.
24
33
  * Falls back to `fallback` while loading or if the control is missing.
@@ -1 +1 @@
1
- {"version":3,"file":"useVertebrControls.d.ts","sourceRoot":"","sources":["../../src/hooks/useVertebrControls.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAAuB,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE/E,KAAK,oBAAoB,GAAG;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAIF,MAAM,MAAM,4BAA4B,GAAG;IACzC,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF,YAAY,EAAE,kBAAkB,EAAE,CAAC;AAEnC;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,OAAsB,EACtB,WAAW,EACX,QAAQ,GACT,EAAE,4BAA4B,+BAyC9B;AAYD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChE,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,CAAC,GACV;IACD,IAAI,EAAE,CAAC,CAAC;IACR,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAMA;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,IAAI,oBAAoB,CAEzD"}
1
+ {"version":3,"file":"useVertebrControls.d.ts","sourceRoot":"","sources":["../../src/hooks/useVertebrControls.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAKL,KAAK,kBAAkB,EACxB,MAAM,iBAAiB,CAAC;AAEzB,KAAK,oBAAoB,GAAG;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,oDAAoD;IACpD,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAIF,MAAM,MAAM,4BAA4B,GAAG;IACzC,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF,YAAY,EAAE,kBAAkB,EAAE,CAAC;AAEnC;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,OAAsB,EACtB,WAAW,EACX,aAAa,EACb,QAAQ,GACT,EAAE,4BAA4B,+BA2E9B;AAYD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChE,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,CAAC,GACV;IACD,IAAI,EAAE,CAAC,CAAC;IACR,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAMA;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,IAAI,oBAAoB,CAEzD"}