@vertebr/react 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,8 @@ export type DescriptionInputProps = {
13
13
  onBlur?: () => void;
14
14
  onChange: (next: string) => void;
15
15
  inputRef?: React.Ref<HTMLElement>;
16
+ /** Opens a document-style modal for focused policy editing. */
17
+ onOpenDocumentView?: () => void;
16
18
  };
17
- export declare function DescriptionInput({ id, name, value, readOnly, placeholder, recordImages, galleryUpload, onListGallery, onUploadGallery, onBlur, onChange, inputRef, }: DescriptionInputProps): import("react").JSX.Element;
19
+ export declare function DescriptionInput({ id, name, value, readOnly, placeholder, recordImages, galleryUpload, onListGallery, onUploadGallery, onBlur, onChange, inputRef, onOpenDocumentView, }: DescriptionInputProps): import("react").JSX.Element;
18
20
  //# sourceMappingURL=DescriptionInput.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DescriptionInput.d.ts","sourceRoot":"","sources":["../../../src/components/config/DescriptionInput.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpD,MAAM,MAAM,qBAAqB,GAAG;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,aAAa,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,UAAU,CAAC,CAAC;IAChE,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC7C,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC5D,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;CACnC,CAAC;AAoCF,wBAAgB,gBAAgB,CAAC,EAC/B,EAAE,EACF,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,WAAoC,EACpC,YAAY,EACZ,aAAa,EACb,aAAa,EACb,eAAe,EACf,MAAM,EACN,QAAQ,EACR,QAAQ,GACT,EAAE,qBAAqB,+BA6RvB"}
1
+ {"version":3,"file":"DescriptionInput.d.ts","sourceRoot":"","sources":["../../../src/components/config/DescriptionInput.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpD,MAAM,MAAM,qBAAqB,GAAG;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,aAAa,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,UAAU,CAAC,CAAC;IAChE,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC7C,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC5D,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAClC,+DAA+D;IAC/D,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;CACjC,CAAC;AAoCF,wBAAgB,gBAAgB,CAAC,EAC/B,EAAE,EACF,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,WAAoC,EACpC,YAAY,EACZ,aAAa,EACb,aAAa,EACb,eAAe,EACf,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,kBAAkB,GACnB,EAAE,qBAAqB,+BAqTvB"}
@@ -19,7 +19,7 @@ function assignRef(ref, node) {
19
19
  function ToolbarButton({ active, disabled, title, onClick, children, }) {
20
20
  return (_jsx("button", { type: "button", className: "vertebr-md-btn", "data-active": active || undefined, disabled: disabled, title: title, onMouseDown: (event) => event.preventDefault(), onClick: onClick, children: children }));
21
21
  }
22
- export function DescriptionInput({ id, name, value, readOnly, placeholder = "Write a description…", recordImages, galleryUpload, onListGallery, onUploadGallery, onBlur, onChange, inputRef, }) {
22
+ export function DescriptionInput({ id, name, value, readOnly, placeholder = "Write a description…", recordImages, galleryUpload, onListGallery, onUploadGallery, onBlur, onChange, inputRef, onOpenDocumentView, }) {
23
23
  const text = typeof value === "string" ? value : "";
24
24
  const onChangeRef = useRef(onChange);
25
25
  const onBlurRef = useRef(onBlur);
@@ -148,5 +148,5 @@ export function DescriptionInput({ id, name, value, readOnly, placeholder = "Wri
148
148
  rows: 3,
149
149
  cols: 3,
150
150
  withHeaderRow: true,
151
- }).run(), children: "Table" }), marks?.table ? (_jsxs(_Fragment, { children: [_jsx(ToolbarButton, { disabled: locked, title: "Add column", onClick: () => editor?.chain().focus().addColumnAfter().run(), children: "+Col" }), _jsx(ToolbarButton, { disabled: locked, title: "Add row", onClick: () => editor?.chain().focus().addRowAfter().run(), children: "+Row" }), _jsx(ToolbarButton, { disabled: locked, title: "Delete table", onClick: () => editor?.chain().focus().deleteTable().run(), children: "Del" })] })) : null, _jsx(ToolbarButton, { active: marks?.link, disabled: locked, title: "Link", onClick: setLink, children: "Link" }), _jsx(ToolbarButton, { disabled: locked, title: "Insert images", onClick: () => setPickerOpen(true), children: "Image" })] }), _jsx(EditorContent, { editor: editor }), _jsxs("p", { className: "vertebr-field-prose-meta", children: ["Markdown", _jsx("span", { "aria-hidden": "true", children: " \u00B7 " }), stats.words, " ", stats.words === 1 ? "word" : "words", _jsx("span", { "aria-hidden": "true", children: " \u00B7 " }), stats.characters, " ", stats.characters === 1 ? "character" : "characters"] }), pickerOpen ? (_jsx(ImagePickerModal, { recordImages: recordImages, galleryUpload: galleryUpload, onListGallery: onListGallery, onUploadGallery: onUploadGallery, onClose: () => setPickerOpen(false), onInsert: insertImages })) : null] }));
151
+ }).run(), children: "Table" }), marks?.table ? (_jsxs(_Fragment, { children: [_jsx(ToolbarButton, { disabled: locked, title: "Add column", onClick: () => editor?.chain().focus().addColumnAfter().run(), children: "+Col" }), _jsx(ToolbarButton, { disabled: locked, title: "Add row", onClick: () => editor?.chain().focus().addRowAfter().run(), children: "+Row" }), _jsx(ToolbarButton, { disabled: locked, title: "Delete table", onClick: () => editor?.chain().focus().deleteTable().run(), children: "Del" })] })) : null, _jsx(ToolbarButton, { active: marks?.link, disabled: locked, title: "Link", onClick: setLink, children: "Link" }), _jsx(ToolbarButton, { disabled: locked, title: "Insert images", onClick: () => setPickerOpen(true), children: "Image" }), onOpenDocumentView ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "vertebr-md-sep", "aria-hidden": "true" }), _jsx(ToolbarButton, { disabled: false, title: "Open document view", onClick: onOpenDocumentView, children: "Doc" })] })) : null] }), _jsx(EditorContent, { editor: editor }), _jsxs("p", { className: "vertebr-field-prose-meta", children: ["Markdown", _jsx("span", { "aria-hidden": "true", children: " \u00B7 " }), stats.words, " ", stats.words === 1 ? "word" : "words", _jsx("span", { "aria-hidden": "true", children: " \u00B7 " }), stats.characters, " ", stats.characters === 1 ? "character" : "characters", onOpenDocumentView ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", children: " \u00B7 " }), _jsx("button", { type: "button", className: "vertebr-md-doc-link", onClick: onOpenDocumentView, children: "Open in document view" })] })) : null] }), pickerOpen ? (_jsx(ImagePickerModal, { recordImages: recordImages, galleryUpload: galleryUpload, onListGallery: onListGallery, onUploadGallery: onUploadGallery, onClose: () => setPickerOpen(false), onInsert: insertImages })) : null] }));
152
152
  }
@@ -0,0 +1,17 @@
1
+ export type DocumentAttachmentFieldProps = {
2
+ id: string;
3
+ label: string;
4
+ value: string;
5
+ readOnly?: boolean;
6
+ description?: string;
7
+ /** Sibling markdown body available for document-view editing. */
8
+ markdownBody?: string;
9
+ canPickFiles?: boolean;
10
+ canUpload?: boolean;
11
+ onChangeUrl: (next: string) => void;
12
+ onOpenPicker?: () => void;
13
+ onUploadFiles?: (files: File[]) => Promise<void>;
14
+ onEditMarkdown?: () => void;
15
+ };
16
+ export declare function DocumentAttachmentField({ id, label, value, readOnly, description, markdownBody, canPickFiles, canUpload, onChangeUrl, onOpenPicker, onUploadFiles, onEditMarkdown, }: DocumentAttachmentFieldProps): import("react").JSX.Element;
17
+ //# sourceMappingURL=DocumentAttachmentField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DocumentAttachmentField.d.ts","sourceRoot":"","sources":["../../../src/components/config/DocumentAttachmentField.tsx"],"names":[],"mappings":"AAGA,MAAM,MAAM,4BAA4B,GAAG;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B,CAAC;AAEF,wBAAgB,uBAAuB,CAAC,EACtC,EAAE,EACF,KAAK,EACL,KAAK,EACL,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,WAAW,EACX,YAAY,EACZ,aAAa,EACb,cAAc,GACf,EAAE,4BAA4B,+BAqI9B"}
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useRef, useState } from "react";
3
+ import { pdfFileName } from "../../lib/documents";
4
+ export function DocumentAttachmentField({ id, label, value, readOnly, description, markdownBody, canPickFiles, canUpload, onChangeUrl, onOpenPicker, onUploadFiles, onEditMarkdown, }) {
5
+ const inputRef = useRef(null);
6
+ const [busy, setBusy] = useState(false);
7
+ const [showUrl, setShowUrl] = useState(() => Boolean(value.trim()));
8
+ const url = value.trim();
9
+ const hasPdf = Boolean(url);
10
+ const canEditMd = Boolean(onEditMarkdown && markdownBody !== undefined);
11
+ const handleFiles = async (files) => {
12
+ if (!files?.length || !onUploadFiles || readOnly)
13
+ return;
14
+ setBusy(true);
15
+ try {
16
+ await onUploadFiles([...files]);
17
+ setShowUrl(true);
18
+ }
19
+ finally {
20
+ setBusy(false);
21
+ if (inputRef.current)
22
+ inputRef.current.value = "";
23
+ }
24
+ };
25
+ return (_jsxs("div", { className: "vertebr-doc-attach", children: [_jsxs("div", { className: "vertebr-doc-attach-card", "data-has-file": hasPdf || undefined, children: [_jsxs("div", { className: "vertebr-doc-attach-main", children: [_jsx("div", { className: "vertebr-doc-attach-icon", "aria-hidden": "true", children: "PDF" }), _jsxs("div", { className: "vertebr-doc-attach-copy", children: [_jsx("p", { className: "vertebr-doc-attach-title", children: hasPdf ? pdfFileName(url) : "No PDF attached" }), _jsx("p", { className: "vertebr-doc-attach-hint", children: hasPdf
26
+ ? "Optional download for shoppers. Markdown remains the live policy."
27
+ : description?.trim() ||
28
+ "Optional PDF download. Markdown remains the live policy." })] })] }), _jsxs("div", { className: "vertebr-doc-attach-actions", children: [canEditMd ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-primary", onClick: onEditMarkdown, children: "Edit document" })) : null, canPickFiles && !readOnly ? (_jsx("button", { type: "button", className: "vertebr-btn", onClick: onOpenPicker, disabled: busy, children: "Choose PDF" })) : null, canUpload && !readOnly ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "vertebr-btn", disabled: busy, onClick: () => inputRef.current?.click(), children: busy ? "Uploading…" : "Upload" }), _jsx("input", { ref: inputRef, type: "file", accept: "application/pdf,.pdf", hidden: true, onChange: (event) => void handleFiles(event.target.files) })] })) : null, hasPdf ? (_jsx("a", { className: "vertebr-btn", href: url, target: "_blank", rel: "noreferrer", children: "Open" })) : null, hasPdf && !readOnly ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", onClick: () => {
29
+ onChangeUrl("");
30
+ setShowUrl(false);
31
+ }, children: "Remove" })) : null] })] }), !readOnly ? (showUrl ? (_jsxs("label", { className: "vertebr-doc-attach-url", htmlFor: id, children: [_jsxs("span", { children: [label, " URL"] }), _jsx("input", { id: id, type: "url", placeholder: "https://\u2026/policy.pdf", value: value, onChange: (event) => onChangeUrl(event.target.value) })] })) : (_jsx("button", { type: "button", className: "vertebr-doc-attach-url-toggle", onClick: () => setShowUrl(true), children: "Paste PDF URL" }))) : hasPdf ? (_jsxs("label", { className: "vertebr-doc-attach-url", htmlFor: id, children: [_jsxs("span", { children: [label, " URL"] }), _jsx("input", { id: id, type: "url", readOnly: true, value: value })] })) : null] }));
32
+ }
@@ -7,6 +7,7 @@ export type PickerImage = {
7
7
  source: "record" | "gallery" | "url";
8
8
  };
9
9
  type Tab = "record" | "gallery";
10
+ type AssetKind = "image" | "document" | "any";
10
11
  export type ImagePickerModalProps = {
11
12
  recordImages?: RecordImage[];
12
13
  galleryUpload?: Pick<ResourceUploadMeta, "accept" | "maxBytes">;
@@ -20,7 +21,9 @@ export type ImagePickerModalProps = {
20
21
  confirmLabel?: string;
21
22
  /** Prefer Files tab when both product photos and gallery exist. */
22
23
  defaultTab?: Tab;
24
+ /** Limit Files library to images or PDFs. @default "image" */
25
+ assetKind?: AssetKind;
23
26
  };
24
- export declare function ImagePickerModal({ recordImages, galleryUpload, onListGallery, onUploadGallery, onClose, onInsert, title, confirmLabel, defaultTab, }: ImagePickerModalProps): import("react").ReactPortal;
27
+ export declare function ImagePickerModal({ recordImages, galleryUpload, onListGallery, onUploadGallery, onClose, onInsert, title, confirmLabel, defaultTab, assetKind, }: ImagePickerModalProps): import("react").ReactPortal;
25
28
  export {};
26
29
  //# sourceMappingURL=ImagePickerModal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ImagePickerModal.d.ts","sourceRoot":"","sources":["../../../src/components/config/ImagePickerModal.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,KAAK,CAAC;CACtC,CAAC;AAEF,KAAK,GAAG,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEhC,MAAM,MAAM,qBAAqB,GAAG;IAClC,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,aAAa,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,UAAU,CAAC,CAAC;IAChE,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC7C,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC5D,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC;IAC1C,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,UAAU,CAAC,EAAE,GAAG,CAAC;CAClB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,EAC/B,YAAiB,EACjB,aAAa,EACb,aAAa,EACb,eAAe,EACf,OAAO,EACP,QAAQ,EACR,KAAuB,EACvB,YAAoB,EACpB,UAAU,GACX,EAAE,qBAAqB,+BAiQvB"}
1
+ {"version":3,"file":"ImagePickerModal.d.ts","sourceRoot":"","sources":["../../../src/components/config/ImagePickerModal.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAGvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,KAAK,CAAC;CACtC,CAAC;AAEF,KAAK,GAAG,GAAG,QAAQ,GAAG,SAAS,CAAC;AAChC,KAAK,SAAS,GAAG,OAAO,GAAG,UAAU,GAAG,KAAK,CAAC;AAE9C,MAAM,MAAM,qBAAqB,GAAG;IAClC,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,aAAa,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,UAAU,CAAC,CAAC;IAChE,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC7C,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC5D,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC;IAC1C,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,EAC/B,YAAiB,EACjB,aAAa,EACb,aAAa,EACb,eAAe,EACf,OAAO,EACP,QAAQ,EACR,KAAuB,EACvB,YAAoB,EACpB,UAAU,EACV,SAAmB,GACpB,EAAE,qBAAqB,+BAqSvB"}
@@ -1,13 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useState } from "react";
3
3
  import { createPortal } from "react-dom";
4
+ import { filterGalleryByKind, isPdfAsset } from "../../lib/documents";
4
5
  import { ImageUploadControl } from "../resources/ImageUploadControl";
5
- export function ImagePickerModal({ recordImages = [], galleryUpload, onListGallery, onUploadGallery, onClose, onInsert, title = "Choose images", confirmLabel = "Use", defaultTab, }) {
6
+ export function ImagePickerModal({ recordImages = [], galleryUpload, onListGallery, onUploadGallery, onClose, onInsert, title = "Choose images", confirmLabel = "Use", defaultTab, assetKind = "image", }) {
6
7
  const hasGallery = Boolean(onListGallery);
8
+ const docsOnly = assetKind === "document";
7
9
  const [tab, setTab] = useState(() => {
8
10
  if (defaultTab)
9
11
  return defaultTab;
10
- if (recordImages.length)
12
+ if (!docsOnly && recordImages.length)
11
13
  return "record";
12
14
  return "gallery";
13
15
  });
@@ -38,7 +40,7 @@ export function ImagePickerModal({ recordImages = [], galleryUpload, onListGalle
38
40
  void onListGallery()
39
41
  .then((items) => {
40
42
  if (!cancelled)
41
- setGallery(items);
43
+ setGallery(filterGalleryByKind(items, assetKind));
42
44
  })
43
45
  .catch((err) => {
44
46
  if (!cancelled) {
@@ -52,13 +54,15 @@ export function ImagePickerModal({ recordImages = [], galleryUpload, onListGalle
52
54
  return () => {
53
55
  cancelled = true;
54
56
  };
55
- }, [hasGallery, onListGallery]);
56
- const recordTiles = useMemo(() => recordImages.map((image) => ({
57
- id: `record:${image.id}`,
58
- url: image.url,
59
- name: image.name,
60
- source: "record",
61
- })), [recordImages]);
57
+ }, [hasGallery, onListGallery, assetKind]);
58
+ const recordTiles = useMemo(() => docsOnly
59
+ ? []
60
+ : recordImages.map((image) => ({
61
+ id: `record:${image.id}`,
62
+ url: image.url,
63
+ name: image.name,
64
+ source: "record",
65
+ })), [recordImages, docsOnly]);
62
66
  const galleryTiles = useMemo(() => gallery.map((item) => ({
63
67
  id: `gallery:${item.id}`,
64
68
  url: item.url,
@@ -68,6 +72,8 @@ export function ImagePickerModal({ recordImages = [], galleryUpload, onListGalle
68
72
  const tiles = tab === "record" ? recordTiles : galleryTiles;
69
73
  const picked = Object.values(selected);
70
74
  const canGalleryUpload = Boolean(galleryUpload && onUploadGallery);
75
+ const unit = docsOnly ? "document" : "image";
76
+ const units = docsOnly ? "documents" : "images";
71
77
  const toggle = (image) => {
72
78
  setSelected((current) => {
73
79
  const next = { ...current };
@@ -85,22 +91,33 @@ export function ImagePickerModal({ recordImages = [], galleryUpload, onListGalle
85
91
  const image = {
86
92
  id: `url:${href}`,
87
93
  url: href,
88
- name: "Image",
94
+ name: docsOnly ? "Document" : "Image",
89
95
  source: "url",
90
96
  };
91
97
  setSelected((current) => ({ ...current, [image.id]: image }));
92
98
  setUrl("");
93
99
  };
94
- return createPortal(_jsx("div", { className: "vertebr-modal-backdrop", role: "presentation", onClick: onClose, children: _jsxs("div", { className: "vertebr-modal", role: "dialog", "aria-modal": "true", "aria-label": title, onClick: (event) => event.stopPropagation(), children: [_jsxs("header", { className: "vertebr-modal-head", children: [_jsxs("div", { className: "vertebr-modal-heading", children: [_jsx("p", { className: "vertebr-modal-eyebrow", children: "Media" }), _jsx("h2", { children: title })] }), _jsx("button", { type: "button", className: "vertebr-btn", onClick: onClose, children: "Close" })] }), _jsxs("div", { className: "vertebr-modal-body vertebr-picker-body", children: [recordImages.length > 0 && hasGallery ? (_jsxs("div", { className: "vertebr-picker-tabs", role: "tablist", children: [_jsxs("button", { type: "button", role: "tab", "aria-selected": tab === "record", "data-active": tab === "record" || undefined, onClick: () => setTab("record"), children: ["Product photos", _jsx("span", { children: recordTiles.length })] }), _jsxs("button", { type: "button", role: "tab", "aria-selected": tab === "gallery", "data-active": tab === "gallery" || undefined, onClick: () => setTab("gallery"), children: ["Files", _jsx("span", { children: loading ? "…" : galleryTiles.length })] })] })) : null, tab === "gallery" && canGalleryUpload ? (_jsx(ImageUploadControl, { meta: {
100
+ const galleryAccept = docsOnly && galleryUpload
101
+ ? {
102
+ ...galleryUpload,
103
+ accept: galleryUpload.accept.some((entry) => /pdf/i.test(entry))
104
+ ? galleryUpload.accept
105
+ : [...galleryUpload.accept, "application/pdf"],
106
+ }
107
+ : galleryUpload;
108
+ return createPortal(_jsx("div", { className: "vertebr-modal-backdrop", role: "presentation", onClick: onClose, children: _jsxs("div", { className: "vertebr-modal", role: "dialog", "aria-modal": "true", "aria-label": title, onClick: (event) => event.stopPropagation(), children: [_jsxs("header", { className: "vertebr-modal-head", children: [_jsxs("div", { className: "vertebr-modal-heading", children: [_jsx("p", { className: "vertebr-modal-eyebrow", children: docsOnly ? "Documents" : "Media" }), _jsx("h2", { children: title })] }), _jsx("button", { type: "button", className: "vertebr-btn", onClick: onClose, children: "Close" })] }), _jsxs("div", { className: "vertebr-modal-body vertebr-picker-body", children: [!docsOnly && recordImages.length > 0 && hasGallery ? (_jsxs("div", { className: "vertebr-picker-tabs", role: "tablist", children: [_jsxs("button", { type: "button", role: "tab", "aria-selected": tab === "record", "data-active": tab === "record" || undefined, onClick: () => setTab("record"), children: ["Product photos", _jsx("span", { children: recordTiles.length })] }), _jsxs("button", { type: "button", role: "tab", "aria-selected": tab === "gallery", "data-active": tab === "gallery" || undefined, onClick: () => setTab("gallery"), children: ["Files", _jsx("span", { children: loading ? "…" : galleryTiles.length })] })] })) : null, tab === "gallery" && canGalleryUpload && galleryAccept ? (_jsx(ImageUploadControl, { meta: {
95
109
  field: "gallery",
96
- accept: galleryUpload.accept,
97
- maxBytes: galleryUpload.maxBytes,
98
- }, multiple: true, label: "Drop images to add them to Files", onUpload: async (files) => {
110
+ accept: galleryAccept.accept,
111
+ maxBytes: galleryAccept.maxBytes,
112
+ }, multiple: true, showOptimizeHint: !docsOnly, label: docsOnly
113
+ ? "Drop PDFs to add them to Files"
114
+ : "Drop images to add them to Files", onUpload: async (files) => {
99
115
  const items = await onUploadGallery(files);
100
- setGallery((current) => [...items, ...current]);
116
+ const kept = filterGalleryByKind(items, assetKind);
117
+ setGallery((current) => [...kept, ...current]);
101
118
  setSelected((current) => {
102
119
  const next = { ...current };
103
- for (const item of items) {
120
+ for (const item of kept) {
104
121
  const image = {
105
122
  id: `gallery:${item.id}`,
106
123
  url: item.url,
@@ -111,17 +128,22 @@ export function ImagePickerModal({ recordImages = [], galleryUpload, onListGalle
111
128
  }
112
129
  return next;
113
130
  });
114
- } })) : null, _jsxs("label", { className: "vertebr-field", children: [_jsx("span", { children: "Or paste an image URL" }), _jsxs("div", { className: "vertebr-picker-url", children: [_jsx("input", { type: "url", value: url, placeholder: "https://", onChange: (event) => setUrl(event.target.value), onKeyDown: (event) => {
131
+ } })) : null, _jsxs("label", { className: "vertebr-field", children: [_jsxs("span", { children: ["Or paste a ", docsOnly ? "PDF" : "image", " URL"] }), _jsxs("div", { className: "vertebr-picker-url", children: [_jsx("input", { type: "url", value: url, placeholder: "https://", onChange: (event) => setUrl(event.target.value), onKeyDown: (event) => {
115
132
  if (event.key === "Enter") {
116
133
  event.preventDefault();
117
134
  addUrl();
118
135
  }
119
- } }), _jsx("button", { type: "button", className: "vertebr-btn", disabled: !url.trim(), onClick: addUrl, children: "Add" })] })] }), error ? (_jsx("p", { className: "vertebr-status", "data-tone": "danger", children: error })) : null, loading && tab === "gallery" ? (_jsx("div", { className: "vertebr-empty", children: "Loading gallery\u2026" })) : tiles.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: tab === "record"
136
+ } }), _jsx("button", { type: "button", className: "vertebr-btn", disabled: !url.trim(), onClick: addUrl, children: "Add" })] })] }), error ? (_jsx("p", { className: "vertebr-status", "data-tone": "danger", children: error })) : null, loading && tab === "gallery" ? (_jsx("div", { className: "vertebr-empty", children: "Loading Files\u2026" })) : tiles.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: tab === "record"
120
137
  ? "No product photos on this record yet."
121
138
  : hasGallery
122
- ? "No images in Files yet."
123
- : "No Files library is configured. Paste a URL or add product photos." })) : (_jsx("div", { className: "vertebr-picker-grid", children: tiles.map((image) => {
139
+ ? docsOnly
140
+ ? "No PDFs in Files yet."
141
+ : "No images in Files yet."
142
+ : "No Files library is configured. Paste a URL instead." })) : (_jsx("div", { className: "vertebr-picker-grid", children: tiles.map((image) => {
124
143
  const active = Boolean(selected[image.id]);
125
- return (_jsxs("button", { type: "button", className: "vertebr-picker-tile", "data-selected": active || undefined, onClick: () => toggle(image), children: [_jsx("span", { className: "vertebr-picker-thumb", children: _jsx("img", { src: image.url, alt: "" }) }), _jsx("span", { className: "vertebr-picker-name", children: image.name })] }, image.id));
126
- }) }))] }), _jsxs("footer", { className: "vertebr-picker-foot", children: [_jsxs("span", { className: "vertebr-picker-count", children: [picked.length, " selected"] }), _jsxs("button", { type: "button", className: "vertebr-btn vertebr-btn-primary", disabled: picked.length === 0, onClick: () => onInsert(picked), children: [confirmLabel, " ", picked.length || "", picked.length === 1 ? " image" : " images"] })] })] }) }), document.body);
144
+ const pdf = docsOnly || isPdfAsset(image);
145
+ return (_jsxs("button", { type: "button", className: "vertebr-picker-tile", "data-selected": active || undefined, onClick: () => toggle(image), children: [_jsx("span", { className: pdf
146
+ ? "vertebr-picker-thumb vertebr-gallery-thumb-file"
147
+ : "vertebr-picker-thumb", children: pdf ? (_jsx("span", { className: "vertebr-gallery-file-badge", children: "PDF" })) : (_jsx("img", { src: image.url, alt: "" })) }), _jsx("span", { className: "vertebr-picker-name", children: image.name })] }, image.id));
148
+ }) }))] }), _jsxs("footer", { className: "vertebr-picker-foot", children: [_jsxs("span", { className: "vertebr-picker-count", children: [picked.length, " selected"] }), _jsxs("button", { type: "button", className: "vertebr-btn vertebr-btn-primary", disabled: picked.length === 0, onClick: () => onInsert(picked), children: [confirmLabel, " ", picked.length || "", picked.length === 1 ? ` ${unit}` : ` ${units}`] })] })] }) }), document.body);
127
149
  }
@@ -0,0 +1,19 @@
1
+ import type { GalleryItem, ResourceUploadMeta } from "@vertebr/client";
2
+ import type { RecordImage } from "../../lib/images";
3
+ export type MarkdownDocumentModalProps = {
4
+ title?: string;
5
+ value: string;
6
+ readOnly?: boolean;
7
+ recordImages?: RecordImage[];
8
+ galleryUpload?: Pick<ResourceUploadMeta, "accept" | "maxBytes">;
9
+ onListGallery?: () => Promise<GalleryItem[]>;
10
+ onUploadGallery?: (files: File[]) => Promise<GalleryItem[]>;
11
+ onChange: (next: string) => void;
12
+ onClose: () => void;
13
+ };
14
+ /**
15
+ * Full-page document chrome for editing policy markdown
16
+ * (PDF-like page, not a binary PDF editor).
17
+ */
18
+ export declare function MarkdownDocumentModal({ title, value, readOnly, recordImages, galleryUpload, onListGallery, onUploadGallery, onChange, onClose, }: MarkdownDocumentModalProps): import("react").ReactPortal;
19
+ //# sourceMappingURL=MarkdownDocumentModal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MarkdownDocumentModal.d.ts","sourceRoot":"","sources":["../../../src/components/config/MarkdownDocumentModal.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpD,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,aAAa,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,UAAU,CAAC,CAAC;IAChE,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC7C,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC5D,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,KAAyB,EACzB,KAAK,EACL,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,aAAa,EACb,eAAe,EACf,QAAQ,EACR,OAAO,GACR,EAAE,0BAA0B,+BAkF5B"}
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { descriptionStats } from "../../lib/description";
5
+ import { DescriptionInput } from "./DescriptionInput";
6
+ /**
7
+ * Full-page document chrome for editing policy markdown
8
+ * (PDF-like page, not a binary PDF editor).
9
+ */
10
+ export function MarkdownDocumentModal({ title = "Policy document", value, readOnly, recordImages, galleryUpload, onListGallery, onUploadGallery, onChange, onClose, }) {
11
+ const [draft, setDraft] = useState(value);
12
+ const stats = descriptionStats(draft);
13
+ useEffect(() => {
14
+ setDraft(value);
15
+ }, [value]);
16
+ useEffect(() => {
17
+ const onKeyDown = (event) => {
18
+ if (event.key === "Escape")
19
+ onClose();
20
+ };
21
+ window.addEventListener("keydown", onKeyDown);
22
+ const previous = document.body.style.overflow;
23
+ document.body.style.overflow = "hidden";
24
+ return () => {
25
+ window.removeEventListener("keydown", onKeyDown);
26
+ document.body.style.overflow = previous;
27
+ };
28
+ }, [onClose]);
29
+ const saveAndClose = () => {
30
+ onChange(draft);
31
+ onClose();
32
+ };
33
+ return createPortal(_jsx("div", { className: "vertebr-modal-backdrop vertebr-doc-modal-backdrop", role: "presentation", onClick: onClose, children: _jsxs("div", { className: "vertebr-modal vertebr-doc-modal", role: "dialog", "aria-modal": "true", "aria-label": title, onClick: (event) => event.stopPropagation(), children: [_jsxs("header", { className: "vertebr-modal-head", children: [_jsxs("div", { className: "vertebr-modal-heading", children: [_jsx("p", { className: "vertebr-modal-eyebrow", children: "Document" }), _jsx("h2", { children: title })] }), _jsxs("div", { className: "vertebr-doc-modal-actions", children: [_jsxs("span", { className: "vertebr-doc-modal-stats", children: [stats.words, " words \u00B7 ", stats.characters, " characters"] }), !readOnly ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-primary", onClick: saveAndClose, children: "Done" })) : null, _jsx("button", { type: "button", className: "vertebr-btn", onClick: onClose, children: "Close" })] })] }), _jsx("div", { className: "vertebr-modal-body vertebr-doc-modal-body", children: _jsx("div", { className: "vertebr-doc-page", children: _jsx(DescriptionInput, { id: "vertebr-doc-modal-editor", value: draft, readOnly: readOnly, placeholder: "Write the policy\u2026", recordImages: recordImages, galleryUpload: galleryUpload, onListGallery: onListGallery, onUploadGallery: onUploadGallery, onChange: setDraft }) }) })] }) }), document.body);
34
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"ConfigField.d.ts","sourceRoot":"","sources":["../../../../src/components/config/form/ConfigField.tsx"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAIjE,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAwBtF,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAEvF,MAAM,MAAM,gBAAgB,GAAG,eAAe,GAC5C,mBAAmB,GAAG;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC7C,QAAQ,CAAC,EAAE,CACT,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,EAAE,KACV,OAAO,CAAC,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACpD,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB,CAAC;AAEF,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,KAAK,EACL,UAAU,EACV,OAAO,EACP,MAAM,EACN,OAAO,EACP,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,aAAa,EACb,aAAa,EACb,eAAe,EACf,KAAK,EACL,YAAY,EACZ,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,eAAe,EACf,WAAW,EACX,MAAM,GACP,EAAE,gBAAgB,+BAyXlB"}
1
+ {"version":3,"file":"ConfigField.d.ts","sourceRoot":"","sources":["../../../../src/components/config/form/ConfigField.tsx"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAIjE,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AA2BtF,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAmBvF,MAAM,MAAM,gBAAgB,GAAG,eAAe,GAC5C,mBAAmB,GAAG;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC7C,QAAQ,CAAC,EAAE,CACT,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,EAAE,KACV,OAAO,CAAC,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACpD,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB,CAAC;AAEF,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,KAAK,EACL,UAAU,EACV,OAAO,EACP,MAAM,EACN,OAAO,EACP,IAAI,EACJ,OAAO,EACP,QAAQ,EACR,aAAa,EACb,aAAa,EACb,eAAe,EACf,KAAK,EACL,YAAY,EACZ,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,eAAe,EACf,WAAW,EACX,MAAM,GACP,EAAE,gBAAgB,+BA4elB"}
@@ -6,10 +6,13 @@ import * as Switch from "@radix-ui/react-switch";
6
6
  import { VisibilityEyeButton } from "../VisibilityEye";
7
7
  import { isDescriptionField } from "../../../lib/description";
8
8
  import { collectRecordImages, isImageField, urlsFromValue, } from "../../../lib/images";
9
+ import { isDocumentField } from "../../../lib/documents";
9
10
  import { isCentsField, isPriceField } from "../../../lib/price";
10
11
  import { FontPickerField } from "../../fonts/FontPickerField";
11
12
  import { ImageUploadControl } from "../../resources/ImageUploadControl";
12
13
  import { DescriptionInput } from "../DescriptionInput";
14
+ import { DocumentAttachmentField } from "../DocumentAttachmentField";
15
+ import { MarkdownDocumentModal } from "../MarkdownDocumentModal";
13
16
  import { DatetimeInput } from "../DatetimeInput";
14
17
  import { EnumInput } from "../EnumInput";
15
18
  import { EnumListInput } from "../EnumListInput";
@@ -17,9 +20,39 @@ import { ImagePickerModal } from "../ImagePickerModal";
17
20
  import { PriceInput } from "../PriceInput";
18
21
  import { ReferenceInput } from "../ReferenceInput";
19
22
  import { displayLabel, firstImageUrl, isMultilineField, fieldVisibilityKey, spanFor, } from "./helpers";
23
+ function findMarkdownBodyKey(schema, currentKey) {
24
+ const preferred = ["body", "content", "policyBody", "text"];
25
+ for (const key of preferred) {
26
+ const field = schema[key];
27
+ if (!field || key === currentKey)
28
+ continue;
29
+ if (field.type === "richtext" || isDescriptionField(key, field))
30
+ return key;
31
+ }
32
+ for (const [key, field] of Object.entries(schema)) {
33
+ if (key === currentKey)
34
+ continue;
35
+ if (field.type === "richtext" || isDescriptionField(key, field))
36
+ return key;
37
+ }
38
+ return null;
39
+ }
20
40
  export function ConfigField({ fieldKey, field, groupLabel, variant, schema, watched, form, uploads, onUpload, galleryUpload, onListGallery, onUploadGallery, fonts, fontsLoading, onListFonts, onRefreshFonts, hideImagePreview, hideUpload, visibilityGroup, settingsRow, client, }) {
21
41
  const [pickerOpen, setPickerOpen] = useState(false);
42
+ const [docModalOpen, setDocModalOpen] = useState(false);
22
43
  const label = displayLabel(fieldKey, field, groupLabel);
44
+ const documentField = isDocumentField(fieldKey, field);
45
+ const markdownBodyKey = documentField
46
+ ? findMarkdownBodyKey(schema, fieldKey)
47
+ : null;
48
+ const markdownBodyField = markdownBodyKey
49
+ ? schema[markdownBodyKey]
50
+ : undefined;
51
+ const markdownBodyValue = markdownBodyKey && typeof watched?.[markdownBodyKey] === "string"
52
+ ? watched[markdownBodyKey]
53
+ : markdownBodyKey
54
+ ? ""
55
+ : undefined;
23
56
  const visibilityKey = fieldVisibilityKey(fieldKey, schema, visibilityGroup ?? groupLabel);
24
57
  const visibilityOn = !visibilityKey || watched?.[visibilityKey] !== false;
25
58
  const showInlinePreview = !hideImagePreview &&
@@ -28,10 +61,13 @@ export function ConfigField({ fieldKey, field, groupLabel, variant, schema, watc
28
61
  Boolean(firstImageUrl(watched?.[fieldKey]));
29
62
  const canPickFiles = !field.readOnly &&
30
63
  Boolean(onListGallery) &&
31
- (field.type === "file[]" || isImageField(fieldKey, field));
64
+ (field.type === "file[]" ||
65
+ isImageField(fieldKey, field) ||
66
+ documentField);
32
67
  const canLocalUpload = !hideUpload &&
33
68
  Boolean(uploads?.[fieldKey] && onUpload) &&
34
69
  !field.readOnly;
70
+ const canGalleryUpload = Boolean(onUploadGallery);
35
71
  const applyPickedImages = (images) => {
36
72
  const urls = images.map((image) => image.url).filter(Boolean);
37
73
  if (!urls.length)
@@ -53,9 +89,38 @@ export function ConfigField({ fieldKey, field, groupLabel, variant, schema, watc
53
89
  }
54
90
  return (_jsxs("div", { className: "vertebr-switch-row", children: [_jsxs("div", { className: "vertebr-switch-row-copy", children: [_jsx(Label.Root, { htmlFor: fieldKey, children: label }), field.description ? (_jsx("p", { className: "vertebr-switch-row-desc", children: field.description })) : null] }), _jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(Switch.Root, { className: "vertebr-switch", id: fieldKey, checked: Boolean(controllerField.value), onCheckedChange: controllerField.onChange, children: _jsx(Switch.Thumb, { className: "vertebr-switch-thumb" }) })) })] }));
55
91
  }
56
- return (_jsxs("div", { className: "vertebr-field", "data-span": spanFor(fieldKey, field, variant), "data-kind": isImageField(fieldKey, field) ? "image" : undefined, "data-storefront": visibilityKey ? (visibilityOn ? "on" : "off") : undefined, children: [_jsxs("div", { className: "vertebr-field-head", children: [_jsx(Label.Root, { htmlFor: fieldKey, children: label }), visibilityKey ? (_jsx(VisibilityEyeButton, { name: visibilityKey, label: label, form: form })) : null] }), showInlinePreview ? (_jsxs("div", { className: "vertebr-field-media", children: [_jsx("img", { src: firstImageUrl(watched[fieldKey]) ?? "", alt: "" }), _jsx("input", { id: fieldKey, type: "text", readOnly: field.readOnly, ...form.register(fieldKey) })] })) : field.type === "richtext" || isDescriptionField(fieldKey, field) ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(DescriptionInput, { id: fieldKey, name: controllerField.name, inputRef: controllerField.ref, value: typeof controllerField.value === "string"
92
+ return (_jsxs("div", { className: "vertebr-field", "data-span": spanFor(fieldKey, field, variant), "data-kind": isImageField(fieldKey, field)
93
+ ? "image"
94
+ : documentField
95
+ ? "document"
96
+ : undefined, "data-storefront": visibilityKey ? (visibilityOn ? "on" : "off") : undefined, children: [_jsxs("div", { className: "vertebr-field-head", children: [_jsx(Label.Root, { htmlFor: fieldKey, children: label }), visibilityKey ? (_jsx(VisibilityEyeButton, { name: visibilityKey, label: label, form: form })) : null] }), showInlinePreview ? (_jsxs("div", { className: "vertebr-field-media", children: [_jsx("img", { src: firstImageUrl(watched[fieldKey]) ?? "", alt: "" }), _jsx("input", { id: fieldKey, type: "text", readOnly: field.readOnly, ...form.register(fieldKey) })] })) : documentField ? (_jsx(DocumentAttachmentField, { id: fieldKey, label: label, value: typeof watched?.[fieldKey] === "string" ? watched[fieldKey] : "", readOnly: field.readOnly, description: field.description, markdownBody: markdownBodyValue, canPickFiles: canPickFiles, canUpload: canLocalUpload || canGalleryUpload, onChangeUrl: (next) => form.setValue(fieldKey, next, {
97
+ shouldDirty: true,
98
+ shouldTouch: true,
99
+ }), onOpenPicker: () => setPickerOpen(true), onUploadFiles: async (files) => {
100
+ if (canLocalUpload) {
101
+ const uploaded = await onUpload(fieldKey, files);
102
+ const url = uploaded[0]?.url;
103
+ if (url) {
104
+ form.setValue(fieldKey, url, {
105
+ shouldDirty: true,
106
+ shouldTouch: true,
107
+ });
108
+ }
109
+ return;
110
+ }
111
+ if (!onUploadGallery)
112
+ return;
113
+ const items = await onUploadGallery(files);
114
+ const url = items[0]?.url;
115
+ if (url) {
116
+ form.setValue(fieldKey, url, {
117
+ shouldDirty: true,
118
+ shouldTouch: true,
119
+ });
120
+ }
121
+ }, onEditMarkdown: markdownBodyKey ? () => setDocModalOpen(true) : undefined })) : field.type === "richtext" || isDescriptionField(fieldKey, field) ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(DescriptionInput, { id: fieldKey, name: controllerField.name, inputRef: controllerField.ref, value: typeof controllerField.value === "string"
57
122
  ? controllerField.value
58
- : "", readOnly: field.readOnly, recordImages: collectRecordImages(schema, watched), galleryUpload: galleryUpload, onListGallery: onListGallery, onUploadGallery: onUploadGallery, onBlur: controllerField.onBlur, onChange: controllerField.onChange })) })) : field.type === "file[]" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => {
123
+ : "", readOnly: field.readOnly, recordImages: collectRecordImages(schema, watched), galleryUpload: galleryUpload, onListGallery: onListGallery, onUploadGallery: onUploadGallery, onBlur: controllerField.onBlur, onChange: controllerField.onChange, onOpenDocumentView: () => setDocModalOpen(true) })) })) : field.type === "file[]" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => {
59
124
  const urls = urlsFromValue(controllerField.value);
60
125
  return (_jsx("div", { className: "vertebr-field-files", children: urls.length > 0 ? (_jsx("ul", { className: "vertebr-field-files-list", children: urls.map((url) => (_jsxs("li", { children: [_jsx("img", { src: url, alt: "" }), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", onClick: () => controllerField.onChange(urls.filter((item) => item !== url)), children: "Remove" })] }, url))) })) : (_jsx("p", { className: "vertebr-field-files-empty", children: "No photos yet" })) }));
61
126
  } })) : field.type === "number" && isPriceField(fieldKey, field) ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(PriceInput, { id: fieldKey, name: controllerField.name, inputRef: controllerField.ref, value: controllerField.value, asCents: isCentsField(fieldKey, field) || /^pricing$/i.test(groupLabel), readOnly: field.readOnly, onBlur: controllerField.onBlur, onChange: controllerField.onChange })) })) : field.type === "number" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx("input", { id: fieldKey, type: "number", readOnly: field.readOnly, value: typeof controllerField.value === "number" &&
@@ -64,7 +129,9 @@ export function ConfigField({ fieldKey, field, groupLabel, variant, schema, watc
64
129
  : "", onBlur: controllerField.onBlur, onChange: (event) => {
65
130
  const next = event.target.value;
66
131
  controllerField.onChange(next === "" ? undefined : Number(next));
67
- }, name: controllerField.name, ref: controllerField.ref })) })) : field.type === "datetime" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(DatetimeInput, { id: fieldKey, name: controllerField.name, inputRef: controllerField.ref, value: controllerField.value, readOnly: field.readOnly, onBlur: controllerField.onBlur, onChange: controllerField.onChange })) })) : field.type === "enum" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(EnumInput, { id: fieldKey, value: controllerField.value, field: field, readOnly: field.readOnly, onChange: controllerField.onChange })) })) : field.type === "enum[]" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(EnumListInput, { id: fieldKey, value: controllerField.value, field: field, readOnly: field.readOnly, onChange: controllerField.onChange })) })) : field.type === "font" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(FontPickerField, { id: fieldKey, value: controllerField.value, label: field.label, description: field.description, readOnly: field.readOnly, fonts: fonts ?? [], loading: fontsLoading, onChange: controllerField.onChange, onRefreshFonts: onRefreshFonts })) })) : field.type === "reference" && client ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(ReferenceInput, { id: fieldKey, value: controllerField.value, field: field, client: client, readOnly: field.readOnly, onChange: controllerField.onChange })) })) : isMultilineField(fieldKey, field) ? (_jsx("textarea", { id: fieldKey, rows: 5, readOnly: field.readOnly, ...form.register(fieldKey) })) : (_jsx("input", { id: fieldKey, type: "text", readOnly: field.readOnly, ...form.register(fieldKey) })), !hideUpload && (canLocalUpload || canPickFiles) ? (_jsxs("div", { className: "vertebr-field-media-actions", children: [canPickFiles ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", onClick: () => setPickerOpen(true), children: "Choose from Files" })) : null, canLocalUpload ? (_jsx(ImageUploadControl, { meta: uploads[fieldKey], multiple: field.type === "file[]", onUpload: async (files) => {
132
+ }, name: controllerField.name, ref: controllerField.ref })) })) : field.type === "datetime" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(DatetimeInput, { id: fieldKey, name: controllerField.name, inputRef: controllerField.ref, value: controllerField.value, readOnly: field.readOnly, onBlur: controllerField.onBlur, onChange: controllerField.onChange })) })) : field.type === "enum" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(EnumInput, { id: fieldKey, value: controllerField.value, field: field, readOnly: field.readOnly, onChange: controllerField.onChange })) })) : field.type === "enum[]" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(EnumListInput, { id: fieldKey, value: controllerField.value, field: field, readOnly: field.readOnly, onChange: controllerField.onChange })) })) : field.type === "font" ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(FontPickerField, { id: fieldKey, value: controllerField.value, label: field.label, description: field.description, readOnly: field.readOnly, fonts: fonts ?? [], loading: fontsLoading, onChange: controllerField.onChange, onRefreshFonts: onRefreshFonts })) })) : field.type === "reference" && client ? (_jsx(Controller, { control: form.control, name: fieldKey, render: ({ field: controllerField }) => (_jsx(ReferenceInput, { id: fieldKey, value: controllerField.value, field: field, client: client, readOnly: field.readOnly, onChange: controllerField.onChange })) })) : isMultilineField(fieldKey, field) ? (_jsx("textarea", { id: fieldKey, rows: 5, readOnly: field.readOnly, ...form.register(fieldKey) })) : (_jsx("input", { id: fieldKey, type: "text", readOnly: field.readOnly, ...form.register(fieldKey) })), !hideUpload &&
133
+ !documentField &&
134
+ (canLocalUpload || canPickFiles) ? (_jsxs("div", { className: "vertebr-field-media-actions", children: [canPickFiles ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", onClick: () => setPickerOpen(true), children: "Choose from Files" })) : null, canLocalUpload ? (_jsx(ImageUploadControl, { meta: uploads[fieldKey], multiple: field.type === "file[]", showOptimizeHint: true, onUpload: async (files) => {
68
135
  const uploaded = await onUpload(fieldKey, files);
69
136
  const urls = uploaded
70
137
  .map((item) => item.url)
@@ -79,5 +146,28 @@ export function ConfigField({ fieldKey, field, groupLabel, variant, schema, watc
79
146
  shouldDirty: true,
80
147
  shouldTouch: true,
81
148
  });
82
- } })) : null] })) : null, pickerOpen && canPickFiles ? (_jsx(ImagePickerModal, { title: field.type === "file[]" ? "Add product photos" : "Choose image", confirmLabel: "Use", defaultTab: "gallery", recordImages: collectRecordImages(schema, watched), galleryUpload: galleryUpload, onListGallery: onListGallery, onUploadGallery: onUploadGallery, onClose: () => setPickerOpen(false), onInsert: applyPickedImages })) : null] }));
149
+ } })) : null] })) : null, pickerOpen && canPickFiles ? (_jsx(ImagePickerModal, { title: documentField
150
+ ? "Choose PDF"
151
+ : field.type === "file[]"
152
+ ? "Add product photos"
153
+ : "Choose image", confirmLabel: "Use", defaultTab: "gallery", assetKind: documentField ? "document" : "image", recordImages: documentField ? [] : collectRecordImages(schema, watched), galleryUpload: galleryUpload, onListGallery: onListGallery, onUploadGallery: onUploadGallery, onClose: () => setPickerOpen(false), onInsert: applyPickedImages })) : null, docModalOpen &&
154
+ (markdownBodyKey ||
155
+ field.type === "richtext" ||
156
+ isDescriptionField(fieldKey, field)) ? (_jsx(MarkdownDocumentModal, { title: markdownBodyKey && markdownBodyField
157
+ ? displayLabel(markdownBodyKey, markdownBodyField, groupLabel)
158
+ : label, value: markdownBodyKey
159
+ ? typeof form.getValues(markdownBodyKey) === "string"
160
+ ? form.getValues(markdownBodyKey)
161
+ : ""
162
+ : typeof form.getValues(fieldKey) === "string"
163
+ ? form.getValues(fieldKey)
164
+ : "", readOnly: markdownBodyKey
165
+ ? Boolean(markdownBodyField?.readOnly)
166
+ : field.readOnly, recordImages: collectRecordImages(schema, watched), galleryUpload: galleryUpload, onListGallery: onListGallery, onUploadGallery: onUploadGallery, onChange: (next) => {
167
+ const key = markdownBodyKey ?? fieldKey;
168
+ form.setValue(key, next, {
169
+ shouldDirty: true,
170
+ shouldTouch: true,
171
+ });
172
+ }, onClose: () => setDocModalOpen(false) })) : null] }));
83
173
  }
@@ -1 +1 @@
1
- {"version":3,"file":"GalleryBoard.d.ts","sourceRoot":"","sources":["../../../src/components/gallery/GalleryBoard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,iBAAiB,CAAC;AAGvE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C,CAAC;AAEF,wBAAgB,YAAY,CAAC,EAC3B,KAAiB,EACjB,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,EACP,SAAS,EACT,QAAQ,EACR,QAAQ,GACT,EAAE,iBAAiB,+BAiFnB"}
1
+ {"version":3,"file":"GalleryBoard.d.ts","sourceRoot":"","sources":["../../../src/components/gallery/GalleryBoard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,iBAAiB,CAAC;AAIvE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C,CAAC;AAuBF,wBAAgB,YAAY,CAAC,EAC3B,KAAiB,EACjB,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,EACP,SAAS,EACT,QAAQ,EACR,QAAQ,GACT,EAAE,iBAAiB,+BAgHnB"}
@@ -1,14 +1,46 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { isPdfAsset, pdfFileName } from "../../lib/documents";
2
3
  import { ImageUploadControl } from "../resources/ImageUploadControl";
4
+ function acceptsDocuments(accept) {
5
+ return accept.some((entry) => /pdf/i.test(entry) ||
6
+ entry === "application/*" ||
7
+ entry === "*/*");
8
+ }
9
+ function acceptsImages(accept) {
10
+ return (accept.length === 0 ||
11
+ accept.some((entry) => entry.startsWith("image/") ||
12
+ entry === "image/*" ||
13
+ entry === "*/*"));
14
+ }
3
15
  export function GalleryBoard({ label = "Gallery", items, accept, maxBytes, canDelete, loading, onRefresh, onUpload, onDelete, }) {
4
16
  const meta = {
5
17
  field: "gallery",
6
18
  accept,
7
19
  maxBytes,
8
20
  };
9
- return (_jsxs("div", { className: "vertebr-gallery", children: [_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: label }), _jsxs("div", { className: "vertebr-gallery-head-actions", children: [_jsx("span", { className: "vertebr-count", children: items.length }), onRefresh ? (_jsx("button", { type: "button", className: "vertebr-btn", onClick: onRefresh, disabled: loading, children: loading ? "Refreshing…" : "Refresh" })) : null] })] }), _jsxs("div", { className: "vertebr-gallery-body", children: [_jsx(ImageUploadControl, { meta: meta, multiple: true, label: "Drop images here or browse to add to the gallery", onUpload: onUpload }), items.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: "No images in the gallery yet." })) : (_jsx("div", { className: "vertebr-gallery-grid", children: items.map((item) => (_jsxs("article", { className: "vertebr-gallery-card", children: [_jsx("a", { className: "vertebr-gallery-thumb", href: item.url, target: "_blank", rel: "noreferrer", title: item.name ?? item.url, children: _jsx("img", { src: item.url, alt: item.name ?? "", loading: "lazy" }) }), _jsxs("div", { className: "vertebr-gallery-meta", children: [_jsx("strong", { title: item.name ?? item.id, children: item.name ?? item.id }), _jsx("a", { href: item.url, target: "_blank", rel: "noreferrer", children: item.url.replace(/^https?:\/\//i, "") })] }), canDelete && onDelete ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-btn-danger-quiet", onClick: () => {
10
- if (window.confirm("Delete this image from the gallery?")) {
11
- void onDelete(item.id);
12
- }
13
- }, children: "Delete" })) : null] }, item.id))) }))] })] }));
21
+ const docs = acceptsDocuments(accept);
22
+ const images = acceptsImages(accept);
23
+ const dropLabel = docs && images
24
+ ? "Drop images or PDFs here or browse to add to Files"
25
+ : docs
26
+ ? "Drop PDFs here or browse to add to Files"
27
+ : "Drop images here or browse to add to the gallery";
28
+ const emptyLabel = docs && images
29
+ ? "No files yet."
30
+ : docs
31
+ ? "No documents yet."
32
+ : "No images in the gallery yet.";
33
+ return (_jsxs("div", { className: "vertebr-gallery", children: [_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: label }), _jsxs("div", { className: "vertebr-gallery-head-actions", children: [_jsx("span", { className: "vertebr-count", children: items.length }), onRefresh ? (_jsx("button", { type: "button", className: "vertebr-btn", onClick: onRefresh, disabled: loading, children: loading ? "Refreshing…" : "Refresh" })) : null] })] }), _jsxs("div", { className: "vertebr-gallery-body", children: [_jsx(ImageUploadControl, { meta: meta, multiple: true, showOptimizeHint: images, label: dropLabel, onUpload: onUpload }), items.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: emptyLabel })) : (_jsx("div", { className: "vertebr-gallery-grid", children: items.map((item) => {
34
+ const pdf = isPdfAsset(item);
35
+ return (_jsxs("article", { className: "vertebr-gallery-card", children: [_jsx("a", { className: pdf
36
+ ? "vertebr-gallery-thumb vertebr-gallery-thumb-file"
37
+ : "vertebr-gallery-thumb", href: item.url, target: "_blank", rel: "noreferrer", title: item.name ?? item.url, children: pdf ? (_jsx("span", { className: "vertebr-gallery-file-badge", children: "PDF" })) : (_jsx("img", { src: item.url, alt: item.name ?? "", loading: "lazy" })) }), _jsxs("div", { className: "vertebr-gallery-meta", children: [_jsx("strong", { title: item.name ?? item.id, children: item.name ??
38
+ (pdf ? pdfFileName(item.url) : item.id) }), _jsx("a", { href: item.url, target: "_blank", rel: "noreferrer", children: item.url.replace(/^https?:\/\//i, "") })] }), canDelete && onDelete ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-btn-danger-quiet", onClick: () => {
39
+ if (window.confirm(pdf
40
+ ? "Delete this document from Files?"
41
+ : "Delete this image from the gallery?")) {
42
+ void onDelete(item.id);
43
+ }
44
+ }, children: "Delete" })) : null] }, item.id));
45
+ }) }))] })] }));
14
46
  }
@@ -0,0 +1,14 @@
1
+ import type { FieldSchema, GalleryItem } from "@vertebr/client";
2
+ import { looksLikeUrl } from "./images";
3
+ /** True when a gallery/file item is a PDF (by type, name, or URL). */
4
+ export declare function isPdfAsset(item: {
5
+ url?: string;
6
+ name?: string | null;
7
+ contentType?: string | null;
8
+ }): boolean;
9
+ /** Config string fields meant for a policy/document PDF URL. */
10
+ export declare function isDocumentField(key: string, field?: Pick<FieldSchema, "label" | "type">): boolean;
11
+ export declare function pdfFileName(url: string, fallback?: string): string;
12
+ export declare function filterGalleryByKind(items: GalleryItem[], kind: "image" | "document" | "any"): GalleryItem[];
13
+ export { looksLikeUrl };
14
+ //# sourceMappingURL=documents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../src/lib/documents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,IAAI,EAAE;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,GAAG,OAAO,CAMV;AAED,gEAAgE;AAChE,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,GAAG,MAAM,CAAC,GAC1C,OAAO,CAUT;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,SAAiB,GAAG,MAAM,CAQ1E;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,WAAW,EAAE,EACpB,IAAI,EAAE,OAAO,GAAG,UAAU,GAAG,KAAK,GACjC,WAAW,EAAE,CAIf;AAED,OAAO,EAAE,YAAY,EAAE,CAAC"}
@@ -0,0 +1,42 @@
1
+ import { looksLikeUrl } from "./images";
2
+ /** True when a gallery/file item is a PDF (by type, name, or URL). */
3
+ export function isPdfAsset(item) {
4
+ if (item.contentType && /pdf/i.test(item.contentType))
5
+ return true;
6
+ const name = item.name ?? "";
7
+ if (/\.pdf$/i.test(name))
8
+ return true;
9
+ const url = item.url ?? "";
10
+ return /\.pdf(?:$|\?)/i.test(url);
11
+ }
12
+ /** Config string fields meant for a policy/document PDF URL. */
13
+ export function isDocumentField(key, field) {
14
+ if (field?.type === "file[]")
15
+ return false;
16
+ if (field?.type && field.type !== "string")
17
+ return false;
18
+ const hay = `${key} ${field?.label ?? ""}`;
19
+ if (/\bpdf\b|pdfUrl|pdf_url|documentUrl|document_url/i.test(hay))
20
+ return true;
21
+ // Policy-ish labels only when they also look like a file/URL attachment.
22
+ return (/(policy|legal|terms|privacy|refund|shipping)/i.test(hay) &&
23
+ /(pdf|url|file|attach|document)/i.test(hay));
24
+ }
25
+ export function pdfFileName(url, fallback = "document.pdf") {
26
+ try {
27
+ const path = new URL(url).pathname;
28
+ const base = path.split("/").pop() || fallback;
29
+ return decodeURIComponent(base);
30
+ }
31
+ catch {
32
+ return fallback;
33
+ }
34
+ }
35
+ export function filterGalleryByKind(items, kind) {
36
+ if (kind === "any")
37
+ return items;
38
+ if (kind === "document")
39
+ return items.filter(isPdfAsset);
40
+ return items.filter((item) => !isPdfAsset(item));
41
+ }
42
+ export { looksLikeUrl };
@@ -1047,6 +1047,21 @@
1047
1047
  background: var(--vb-surface-tint);
1048
1048
  }
1049
1049
 
1050
+ .vertebr-md-doc-link {
1051
+ appearance: none;
1052
+ border: 0;
1053
+ padding: 0;
1054
+ background: transparent;
1055
+ color: var(--vb-accent);
1056
+ font: inherit;
1057
+ letter-spacing: inherit;
1058
+ cursor: pointer;
1059
+ }
1060
+
1061
+ .vertebr-md-doc-link:hover {
1062
+ text-decoration: underline;
1063
+ }
1064
+
1050
1065
  .vertebr-field input:hover,
1051
1066
  .vertebr-field textarea:hover {
1052
1067
  border-color: var(--vb-border-strong);
@@ -58,6 +58,243 @@
58
58
  object-fit: cover;
59
59
  }
60
60
 
61
+ .vertebr-gallery-thumb-file {
62
+ display: grid;
63
+ place-items: center;
64
+ background:
65
+ linear-gradient(
66
+ 145deg,
67
+ color-mix(in srgb, var(--vb-accent) 12%, var(--vb-bg-muted)),
68
+ var(--vb-bg-muted)
69
+ );
70
+ }
71
+
72
+ .vertebr-gallery-file-badge {
73
+ display: inline-flex;
74
+ align-items: center;
75
+ justify-content: center;
76
+ min-width: 3.25rem;
77
+ padding: 0.35rem 0.55rem;
78
+ border-radius: 6px;
79
+ border: 1px solid var(--vb-border);
80
+ background: var(--vb-bg-elevated);
81
+ color: var(--vb-text);
82
+ font-size: 0.75rem;
83
+ font-weight: 700;
84
+ letter-spacing: 0.06em;
85
+ }
86
+
87
+ .vertebr-field-document {
88
+ display: flex;
89
+ flex-wrap: wrap;
90
+ align-items: center;
91
+ gap: 0.55rem;
92
+ margin-bottom: 0.55rem;
93
+ padding: 0.65rem 0.75rem;
94
+ border: 1px solid var(--vb-border);
95
+ border-radius: 8px;
96
+ background: var(--vb-bg-muted);
97
+ }
98
+
99
+ .vertebr-field-document-badge {
100
+ display: inline-flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ padding: 0.2rem 0.45rem;
104
+ border-radius: 4px;
105
+ border: 1px solid var(--vb-border);
106
+ background: var(--vb-bg-elevated);
107
+ font-size: 0.68rem;
108
+ font-weight: 700;
109
+ letter-spacing: 0.06em;
110
+ }
111
+
112
+ .vertebr-field-document a {
113
+ color: var(--vb-accent);
114
+ font-size: 0.85rem;
115
+ text-decoration: none;
116
+ overflow: hidden;
117
+ text-overflow: ellipsis;
118
+ white-space: nowrap;
119
+ max-width: 100%;
120
+ }
121
+
122
+ .vertebr-field-document a:hover {
123
+ text-decoration: underline;
124
+ }
125
+
126
+ .vertebr-doc-attach {
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: 0.65rem;
130
+ min-width: 0;
131
+ }
132
+
133
+ .vertebr-doc-attach-card {
134
+ display: flex;
135
+ flex-direction: column;
136
+ gap: 0.85rem;
137
+ min-width: 0;
138
+ padding: 0.9rem 1rem;
139
+ border: 1px solid var(--vb-border);
140
+ border-radius: 12px;
141
+ background: var(--vb-bg-elevated);
142
+ }
143
+
144
+ .vertebr-doc-attach-card[data-has-file="true"] {
145
+ border-color: color-mix(in srgb, var(--vb-accent) 30%, var(--vb-border));
146
+ }
147
+
148
+ .vertebr-doc-attach-main {
149
+ display: flex;
150
+ gap: 0.75rem;
151
+ align-items: flex-start;
152
+ min-width: 0;
153
+ }
154
+
155
+ .vertebr-doc-attach-icon {
156
+ flex: 0 0 auto;
157
+ display: grid;
158
+ place-items: center;
159
+ width: 2.5rem;
160
+ height: 3rem;
161
+ border-radius: 7px;
162
+ border: 1px solid var(--vb-border);
163
+ background: var(--vb-bg-muted);
164
+ color: var(--vb-text);
165
+ font-size: 0.65rem;
166
+ font-weight: 800;
167
+ letter-spacing: 0.06em;
168
+ box-shadow: inset 0 -0.4rem 0 color-mix(in srgb, var(--vb-danger, #b42318) 55%, transparent);
169
+ }
170
+
171
+ .vertebr-doc-attach-copy {
172
+ min-width: 0;
173
+ flex: 1 1 auto;
174
+ }
175
+
176
+ .vertebr-doc-attach-title {
177
+ margin: 0;
178
+ font-size: 0.9rem;
179
+ font-weight: 650;
180
+ color: var(--vb-text);
181
+ overflow-wrap: anywhere;
182
+ }
183
+
184
+ .vertebr-doc-attach-hint {
185
+ margin: 0.3rem 0 0;
186
+ font-size: 0.78rem;
187
+ line-height: 1.4;
188
+ color: var(--vb-text-subtle);
189
+ }
190
+
191
+ .vertebr-doc-attach-actions {
192
+ display: grid;
193
+ grid-template-columns: 1fr 1fr;
194
+ gap: 0.4rem;
195
+ width: 100%;
196
+ }
197
+
198
+ .vertebr-doc-attach-actions .vertebr-btn {
199
+ display: inline-flex;
200
+ align-items: center;
201
+ justify-content: center;
202
+ width: 100%;
203
+ min-width: 0;
204
+ text-align: center;
205
+ text-decoration: none;
206
+ box-sizing: border-box;
207
+ }
208
+
209
+ .vertebr-doc-attach-actions .vertebr-btn-primary {
210
+ grid-column: 1 / -1;
211
+ }
212
+
213
+ .vertebr-doc-attach-url {
214
+ display: flex;
215
+ flex-direction: column;
216
+ gap: 0.35rem;
217
+ font-size: 0.78rem;
218
+ color: var(--vb-text-subtle);
219
+ }
220
+
221
+ .vertebr-doc-attach-url input {
222
+ width: 100%;
223
+ min-width: 0;
224
+ }
225
+
226
+ .vertebr-doc-attach-url-toggle {
227
+ appearance: none;
228
+ align-self: flex-start;
229
+ border: 0;
230
+ padding: 0;
231
+ background: transparent;
232
+ color: var(--vb-text-subtle);
233
+ font: inherit;
234
+ font-size: 0.8rem;
235
+ font-weight: 600;
236
+ cursor: pointer;
237
+ }
238
+
239
+ .vertebr-doc-attach-url-toggle:hover {
240
+ color: var(--vb-accent);
241
+ text-decoration: underline;
242
+ }
243
+
244
+ .vertebr-doc-modal-backdrop {
245
+ align-items: stretch;
246
+ justify-items: center;
247
+ padding: 1.5rem;
248
+ }
249
+
250
+ .vertebr-doc-modal {
251
+ width: min(100%, 920px);
252
+ max-height: min(94vh, 980px);
253
+ }
254
+
255
+ .vertebr-doc-modal-actions {
256
+ display: inline-flex;
257
+ flex-wrap: wrap;
258
+ align-items: center;
259
+ gap: 0.5rem;
260
+ }
261
+
262
+ .vertebr-doc-modal-stats {
263
+ margin-right: 0.35rem;
264
+ font-size: 0.75rem;
265
+ color: var(--vb-text-subtle);
266
+ }
267
+
268
+ .vertebr-doc-modal-body {
269
+ padding: 1.25rem 1.35rem 1.5rem;
270
+ overflow: auto;
271
+ background:
272
+ radial-gradient(
273
+ circle at top,
274
+ color-mix(in srgb, var(--vb-bg-muted) 80%, transparent),
275
+ transparent 55%
276
+ ),
277
+ var(--vb-bg);
278
+ }
279
+
280
+ .vertebr-doc-page {
281
+ margin: 0 auto;
282
+ max-width: 46rem;
283
+ padding: 1.35rem 1.35rem 1.6rem;
284
+ border: 1px solid var(--vb-border);
285
+ border-radius: 4px;
286
+ background: var(--vb-bg-elevated);
287
+ box-shadow:
288
+ 0 1px 0 color-mix(in srgb, #000 6%, transparent),
289
+ 0 18px 40px color-mix(in srgb, #000 12%, transparent);
290
+ }
291
+
292
+ .vertebr-doc-page .vertebr-field-prose {
293
+ border: 0;
294
+ background: transparent;
295
+ box-shadow: none;
296
+ }
297
+
61
298
  .vertebr-gallery-meta {
62
299
  min-width: 0;
63
300
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertebr/react",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Vertebr admin dashboard UI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",