@vertebr/react 0.5.1 → 0.7.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/AdminContent.d.ts +11 -2
- package/dist/components/admin/AdminContent.d.ts.map +1 -1
- package/dist/components/admin/AdminContent.js +35 -10
- package/dist/components/admin/AdminHeader.d.ts +3 -1
- package/dist/components/admin/AdminHeader.d.ts.map +1 -1
- package/dist/components/admin/AdminHeader.js +12 -3
- package/dist/components/admin/PagePreviewPane.d.ts +14 -0
- package/dist/components/admin/PagePreviewPane.d.ts.map +1 -0
- package/dist/components/admin/PagePreviewPane.js +121 -0
- package/dist/components/admin/VertebrAdmin.d.ts +1 -1
- package/dist/components/admin/VertebrAdmin.d.ts.map +1 -1
- package/dist/components/admin/VertebrAdmin.js +123 -41
- package/dist/components/admin/routes.d.ts +21 -0
- package/dist/components/admin/routes.d.ts.map +1 -0
- package/dist/components/admin/routes.js +170 -0
- package/dist/components/admin/types.d.ts +7 -0
- package/dist/components/admin/types.d.ts.map +1 -1
- package/dist/components/config/VisibilityEye.d.ts +12 -0
- package/dist/components/config/VisibilityEye.d.ts.map +1 -1
- package/dist/components/config/VisibilityEye.js +12 -4
- package/dist/components/config/form/ConfigField.d.ts +5 -3
- package/dist/components/config/form/ConfigField.d.ts.map +1 -1
- package/dist/components/config/form/ConfigField.js +7 -3
- 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 +5 -1
- package/dist/components/config/form/RecordConfigForm.d.ts +1 -1
- package/dist/components/config/form/RecordConfigForm.d.ts.map +1 -1
- package/dist/components/config/form/RecordConfigForm.js +2 -2
- package/dist/components/config/form/SettingsConfigForm.d.ts +3 -3
- package/dist/components/config/form/SettingsConfigForm.d.ts.map +1 -1
- package/dist/components/config/form/SettingsConfigForm.js +7 -2
- package/dist/components/config/form/helpers/fields.d.ts +2 -1
- package/dist/components/config/form/helpers/fields.d.ts.map +1 -1
- package/dist/components/config/form/helpers/fields.js +4 -0
- package/dist/components/config/form/types.d.ts +6 -1
- package/dist/components/config/form/types.d.ts.map +1 -1
- package/dist/components/fonts/FontFaceInjector.d.ts +9 -0
- package/dist/components/fonts/FontFaceInjector.d.ts.map +1 -0
- package/dist/components/fonts/FontFaceInjector.js +28 -0
- package/dist/components/fonts/FontLibraryBoard.d.ts +15 -0
- package/dist/components/fonts/FontLibraryBoard.d.ts.map +1 -0
- package/dist/components/fonts/FontLibraryBoard.js +41 -0
- package/dist/components/fonts/FontPickerField.d.ts +16 -0
- package/dist/components/fonts/FontPickerField.d.ts.map +1 -0
- package/dist/components/fonts/FontPickerField.js +60 -0
- package/dist/components/fonts/FontPreviewModal.d.ts +18 -0
- package/dist/components/fonts/FontPreviewModal.d.ts.map +1 -0
- package/dist/components/fonts/FontPreviewModal.js +57 -0
- package/dist/components/fonts/FontPreviewSample.d.ts +8 -0
- package/dist/components/fonts/FontPreviewSample.d.ts.map +1 -0
- package/dist/components/fonts/FontPreviewSample.js +13 -0
- package/dist/components/fonts/GoogleFontsBrowser.d.ts +8 -0
- package/dist/components/fonts/GoogleFontsBrowser.d.ts.map +1 -0
- package/dist/components/fonts/GoogleFontsBrowser.js +49 -0
- package/dist/components/resources/ResourceCell.js +3 -2
- package/dist/components/resources/ResourceTable.d.ts +1 -1
- package/dist/components/resources/ResourceTable.d.ts.map +1 -1
- package/dist/components/resources/ResourceTable.js +123 -5
- package/dist/hooks/useVertebrControls.d.ts +10 -1
- package/dist/hooks/useVertebrControls.d.ts.map +1 -1
- package/dist/hooks/useVertebrControls.js +32 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/styles/base.css +11 -17
- package/dist/styles/buttons.css +3 -12
- package/dist/styles/font-library.css +401 -0
- package/dist/styles/form.css +89 -3
- package/dist/styles/index.css +3 -1
- package/dist/styles/main.css +1 -1
- package/dist/styles/page-preview.css +208 -0
- package/dist/styles/panel.css +7 -2
- package/dist/styles/states.css +6 -3
- package/dist/styles/table.css +18 -0
- package/dist/themes/dark.css +48 -46
- package/dist/themes/index.css +1 -0
- package/dist/themes/light.css +91 -85
- package/dist/themes/tokens.css +52 -0
- 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,+BAqMtB"}
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
const faceItems = useMemo(() => {
|
|
46
|
+
const items = [];
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
for (const item of [selected, previewItem]) {
|
|
49
|
+
if (!item || seen.has(item.id))
|
|
50
|
+
continue;
|
|
51
|
+
seen.add(item.id);
|
|
52
|
+
items.push(item);
|
|
53
|
+
}
|
|
54
|
+
return items;
|
|
55
|
+
}, [previewItem, selected]);
|
|
56
|
+
return (_jsxs("div", { className: "vertebr-font-picker", children: [_jsx(FontFaceInjector, { items: faceItems, 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: {
|
|
57
|
+
fontFamily: `"${item.family}", ui-sans-serif, sans-serif`,
|
|
58
|
+
fontWeight: weightForItem(item, "heading"),
|
|
59
|
+
}, 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] }));
|
|
60
|
+
}
|
|
@@ -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 ? "
|
|
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<
|
|
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":"AASA,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,iBAAiB,CAAC;
|
|
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
|
-
|
|
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, }) =>
|
|
314
|
-
|
|
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,
|
|
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"}
|