vestrycms 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +383 -2
- package/dist/config/fields.d.ts +30 -0
- package/dist/config/fields.js +75 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.js +18 -0
- package/dist/config/types.d.ts +412 -0
- package/dist/config/types.js +10 -0
- package/dist/core/api.d.ts +67 -0
- package/dist/core/api.js +543 -0
- package/dist/core/auth.d.ts +1597 -0
- package/dist/core/auth.js +264 -0
- package/dist/core/cache.d.ts +27 -0
- package/dist/core/cache.js +52 -0
- package/dist/core/entries.d.ts +19 -0
- package/dist/core/entries.js +127 -0
- package/dist/core/index-types.d.ts +20 -0
- package/dist/core/index-types.js +1 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +13 -0
- package/dist/core/mail.d.ts +28 -0
- package/dist/core/mail.js +62 -0
- package/dist/core/paths.d.ts +35 -0
- package/dist/core/paths.js +148 -0
- package/dist/core/roles.d.ts +24 -0
- package/dist/core/roles.js +15 -0
- package/dist/core/service.d.ts +102 -0
- package/dist/core/service.js +478 -0
- package/dist/core/source.d.ts +82 -0
- package/dist/core/source.js +75 -0
- package/dist/core/sources/github-app.d.ts +26 -0
- package/dist/core/sources/github-app.js +161 -0
- package/dist/core/sources/github.d.ts +40 -0
- package/dist/core/sources/github.js +278 -0
- package/dist/core/sources/local.d.ts +16 -0
- package/dist/core/sources/local.js +98 -0
- package/dist/core/values.d.ts +22 -0
- package/dist/core/values.js +123 -0
- package/dist/env.d.ts +19 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +167 -0
- package/dist/integration/build-index.d.ts +8 -0
- package/dist/integration/build-index.js +67 -0
- package/dist/integration/client-deps.d.ts +10 -0
- package/dist/integration/client-deps.js +43 -0
- package/dist/integration/dev-middleware.d.ts +6 -0
- package/dist/integration/dev-middleware.js +48 -0
- package/dist/integration/formatter.d.ts +7 -0
- package/dist/integration/formatter.js +56 -0
- package/dist/mdx/expressions.d.ts +14 -0
- package/dist/mdx/expressions.js +141 -0
- package/dist/mdx/index.d.ts +4 -0
- package/dist/mdx/index.js +4 -0
- package/dist/mdx/parse.d.ts +6 -0
- package/dist/mdx/parse.js +401 -0
- package/dist/mdx/serialize.d.ts +9 -0
- package/dist/mdx/serialize.js +485 -0
- package/dist/mdx/types.d.ts +42 -0
- package/dist/mdx/types.js +6 -0
- package/dist/pages/admin.astro +27 -0
- package/dist/pages/api.d.ts +3 -0
- package/dist/pages/api.js +31 -0
- package/dist/ui/App.d.ts +4 -0
- package/dist/ui/App.js +70 -0
- package/dist/ui/api.d.ts +83 -0
- package/dist/ui/api.js +97 -0
- package/dist/ui/auth-client.d.ts +27 -0
- package/dist/ui/auth-client.js +29 -0
- package/dist/ui/components/AssetPicker.d.ts +27 -0
- package/dist/ui/components/AssetPicker.js +97 -0
- package/dist/ui/components/Icon.d.ts +11 -0
- package/dist/ui/components/Icon.js +46 -0
- package/dist/ui/components/Layout.d.ts +15 -0
- package/dist/ui/components/Layout.js +87 -0
- package/dist/ui/components/editor/EditorContext.d.ts +15 -0
- package/dist/ui/components/editor/EditorContext.js +9 -0
- package/dist/ui/components/editor/NodeViews.d.ts +4 -0
- package/dist/ui/components/editor/NodeViews.js +127 -0
- package/dist/ui/components/editor/RichTextEditor.d.ts +24 -0
- package/dist/ui/components/editor/RichTextEditor.js +230 -0
- package/dist/ui/components/editor/extensions.d.ts +30 -0
- package/dist/ui/components/editor/extensions.js +248 -0
- package/dist/ui/components/editor/schema.d.ts +12 -0
- package/dist/ui/components/editor/schema.js +37 -0
- package/dist/ui/components/fields/CodeInput.d.ts +10 -0
- package/dist/ui/components/fields/CodeInput.js +8 -0
- package/dist/ui/components/fields/CodeMirrorInput.d.ts +2 -0
- package/dist/ui/components/fields/CodeMirrorInput.js +44 -0
- package/dist/ui/components/fields/FieldRenderer.d.ts +41 -0
- package/dist/ui/components/fields/FieldRenderer.js +299 -0
- package/dist/ui/components/fields/ImageTools.d.ts +31 -0
- package/dist/ui/components/fields/ImageTools.js +296 -0
- package/dist/ui/components/fields/MarkdownField.d.ts +7 -0
- package/dist/ui/components/fields/MarkdownField.js +16 -0
- package/dist/ui/components/fields/TreeInput.d.ts +13 -0
- package/dist/ui/components/fields/TreeInput.js +221 -0
- package/dist/ui/components/ui.d.ts +63 -0
- package/dist/ui/components/ui.js +113 -0
- package/dist/ui/context.d.ts +17 -0
- package/dist/ui/context.js +13 -0
- package/dist/ui/lib/format.d.ts +5 -0
- package/dist/ui/lib/format.js +11 -0
- package/dist/ui/lib/utils.d.ts +24 -0
- package/dist/ui/lib/utils.js +255 -0
- package/dist/ui/main.d.ts +1 -0
- package/dist/ui/main.js +11 -0
- package/dist/ui/pages/Assets.d.ts +1 -0
- package/dist/ui/pages/Assets.js +69 -0
- package/dist/ui/pages/CollectionList.d.ts +6 -0
- package/dist/ui/pages/CollectionList.js +115 -0
- package/dist/ui/pages/Dashboard.d.ts +1 -0
- package/dist/ui/pages/Dashboard.js +57 -0
- package/dist/ui/pages/EntryEditor.d.ts +3 -0
- package/dist/ui/pages/EntryEditor.js +313 -0
- package/dist/ui/pages/Login.d.ts +6 -0
- package/dist/ui/pages/Login.js +88 -0
- package/dist/ui/pages/Users.d.ts +1 -0
- package/dist/ui/pages/Users.js +102 -0
- package/dist/ui/styles.css +2554 -0
- package/package.json +123 -3
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface EditorExtensionOptions {
|
|
2
|
+
/** Maps a stored image path to a URL the browser can load. */
|
|
3
|
+
resolveSrc?: (src: string) => string;
|
|
4
|
+
placeholder?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The rich text editor's extensions. Its schema must accept every node type
|
|
8
|
+
* and attribute `parseMdx` produces, or they are lost when a page is saved.
|
|
9
|
+
*/
|
|
10
|
+
export declare function editorExtensions(options?: EditorExtensionOptions): (import("@tiptap/core").Node<any, any> | import("@tiptap/core").Node<{
|
|
11
|
+
resolveSrc: (src: string) => string;
|
|
12
|
+
} & Record<string, unknown>, any> | import("@tiptap/core").Extension<any, any> | import("@tiptap/core").Extension<import("@tiptap/starter-kit").StarterKitOptions, any> | import("@tiptap/core").Mark<import("@tiptap/extension-superscript").SuperscriptExtensionOptions, any> | import("@tiptap/core").Extension<import("@tiptap/extension-table").TableKitOptions, any> | import("@tiptap/core").Extension<import("@tiptap/extensions").PlaceholderOptions, any>)[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { TableKit } from "@tiptap/extension-table";
|
|
2
|
+
import Subscript from "@tiptap/extension-subscript";
|
|
3
|
+
import Superscript from "@tiptap/extension-superscript";
|
|
4
|
+
import { Placeholder } from "@tiptap/extensions";
|
|
5
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
6
|
+
import { FootnoteDefinition, FootnoteReference, ListAttributes, MdxComponent, MdxEsm, MdxExpression, MdxImage, MdxInlineComponent, MdxInlineExpression, RawHtml, RawInlineHtml, } from "./extensions.js";
|
|
7
|
+
/**
|
|
8
|
+
* The rich text editor's extensions. Its schema must accept every node type
|
|
9
|
+
* and attribute `parseMdx` produces, or they are lost when a page is saved.
|
|
10
|
+
*/
|
|
11
|
+
export function editorExtensions(options = {}) {
|
|
12
|
+
return [
|
|
13
|
+
StarterKit.configure({
|
|
14
|
+
heading: { levels: [1, 2, 3, 4] },
|
|
15
|
+
link: { openOnClick: false, autolink: true, defaultProtocol: "https" },
|
|
16
|
+
codeBlock: { languageClassPrefix: "language-" },
|
|
17
|
+
dropcursor: { color: "var(--v-primary)", width: 2 },
|
|
18
|
+
}),
|
|
19
|
+
Superscript,
|
|
20
|
+
Subscript,
|
|
21
|
+
MdxImage.configure({ resolveSrc: options.resolveSrc ?? ((src) => src) }),
|
|
22
|
+
TableKit.configure({ table: { resizable: false } }),
|
|
23
|
+
Placeholder.configure({
|
|
24
|
+
placeholder: options.placeholder ?? "Start writing…",
|
|
25
|
+
}),
|
|
26
|
+
MdxComponent,
|
|
27
|
+
MdxInlineComponent,
|
|
28
|
+
MdxExpression,
|
|
29
|
+
MdxInlineExpression,
|
|
30
|
+
MdxEsm,
|
|
31
|
+
RawHtml,
|
|
32
|
+
RawInlineHtml,
|
|
33
|
+
ListAttributes,
|
|
34
|
+
FootnoteDefinition,
|
|
35
|
+
FootnoteReference,
|
|
36
|
+
];
|
|
37
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface CodeInputProps {
|
|
2
|
+
value: string;
|
|
3
|
+
onChange: (v: string) => void;
|
|
4
|
+
language?: "markdown" | "yaml" | "json" | "javascript" | "text";
|
|
5
|
+
rows?: number;
|
|
6
|
+
readOnly?: boolean;
|
|
7
|
+
minHeight?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Code editor (CodeMirror, loaded on demand) with a textarea fallback while it loads. */
|
|
10
|
+
export declare function CodeInput(props: CodeInputProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense, lazy } from "react";
|
|
3
|
+
const CodeMirrorInput = lazy(() => import("./CodeMirrorInput.js"));
|
|
4
|
+
/** Code editor (CodeMirror, loaded on demand) with a textarea fallback while it loads. */
|
|
5
|
+
export function CodeInput(props) {
|
|
6
|
+
const fallback = (_jsx("textarea", { className: "v-textarea v-input--mono", rows: props.rows ?? 6, value: props.value, readOnly: props.readOnly, onChange: (e) => props.onChange(e.target.value), spellCheck: false }));
|
|
7
|
+
return (_jsx(Suspense, { fallback: fallback, children: _jsx(CodeMirrorInput, { ...props }) }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import CodeMirror from "@uiw/react-codemirror";
|
|
3
|
+
import { markdown } from "@codemirror/lang-markdown";
|
|
4
|
+
import { yaml } from "@codemirror/lang-yaml";
|
|
5
|
+
import { EditorView } from "@codemirror/view";
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
const theme = EditorView.theme({
|
|
8
|
+
"&": {
|
|
9
|
+
fontSize: "13px",
|
|
10
|
+
border: "1px solid var(--v-border-strong)",
|
|
11
|
+
borderRadius: "6px",
|
|
12
|
+
background: "#fff",
|
|
13
|
+
},
|
|
14
|
+
"&.cm-focused": {
|
|
15
|
+
outline: "none",
|
|
16
|
+
borderColor: "var(--v-primary)",
|
|
17
|
+
boxShadow: "0 0 0 3px var(--v-primary-ring)",
|
|
18
|
+
},
|
|
19
|
+
".cm-scroller": { fontFamily: "var(--v-mono)", lineHeight: "1.55" },
|
|
20
|
+
".cm-gutters": {
|
|
21
|
+
background: "#f9fafb",
|
|
22
|
+
borderRight: "1px solid var(--v-border)",
|
|
23
|
+
color: "#9ca3af",
|
|
24
|
+
},
|
|
25
|
+
".cm-activeLineGutter": { background: "#f3f4f6" },
|
|
26
|
+
".cm-activeLine": { background: "rgba(37, 99, 235, 0.04)" },
|
|
27
|
+
".cm-content": { padding: "8px 0" },
|
|
28
|
+
});
|
|
29
|
+
export default function CodeMirrorInput({ value, onChange, language = "text", rows, readOnly, minHeight, }) {
|
|
30
|
+
const extensions = useMemo(() => {
|
|
31
|
+
const ext = [theme, EditorView.lineWrapping];
|
|
32
|
+
if (language === "markdown")
|
|
33
|
+
ext.push(markdown());
|
|
34
|
+
if (language === "yaml")
|
|
35
|
+
ext.push(yaml());
|
|
36
|
+
return ext;
|
|
37
|
+
}, [language]);
|
|
38
|
+
return (_jsx(CodeMirror, { value: value, onChange: onChange, extensions: extensions, readOnly: readOnly, editable: !readOnly, minHeight: minHeight ?? (rows ? `${rows * 1.55 * 13 + 16}px` : "120px"), basicSetup: {
|
|
39
|
+
lineNumbers: language !== "text",
|
|
40
|
+
foldGutter: false,
|
|
41
|
+
highlightActiveLine: true,
|
|
42
|
+
autocompletion: false,
|
|
43
|
+
} }));
|
|
44
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { Field, Schema } from "../../../config/types.js";
|
|
3
|
+
import { type ValidationError } from "../../lib/utils.js";
|
|
4
|
+
import { type PendingUpload } from "../AssetPicker.js";
|
|
5
|
+
export interface FieldContext {
|
|
6
|
+
/** Collects files picked through image/file fields so they're committed with the entry. */
|
|
7
|
+
addUpload?: (upload: PendingUpload) => void;
|
|
8
|
+
pendingUploads?: Record<string, PendingUpload>;
|
|
9
|
+
/** Marks the entry as new so slug fields auto-fill. */
|
|
10
|
+
isNew?: boolean;
|
|
11
|
+
/** Values of sibling fields (for slug `from`). */
|
|
12
|
+
siblings?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
export interface FieldProps<F extends Field = Field> {
|
|
15
|
+
fieldKey: string;
|
|
16
|
+
path: string;
|
|
17
|
+
field: F;
|
|
18
|
+
value: unknown;
|
|
19
|
+
onChange: (value: unknown) => void;
|
|
20
|
+
errors?: ValidationError[];
|
|
21
|
+
ctx?: FieldContext;
|
|
22
|
+
/** Omit the label (e.g. when the surrounding card already names the field). */
|
|
23
|
+
hideLabel?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** Renders a schema as a responsive grid of fields. */
|
|
26
|
+
export declare function FieldSet({ schema, values, onChange, errors, filter, ctx, pathPrefix, hideLabels, }: {
|
|
27
|
+
schema: Schema;
|
|
28
|
+
values: Record<string, unknown>;
|
|
29
|
+
onChange: (values: Record<string, unknown>) => void;
|
|
30
|
+
errors?: ValidationError[];
|
|
31
|
+
filter?: (key: string, field: Field) => boolean;
|
|
32
|
+
ctx?: FieldContext;
|
|
33
|
+
pathPrefix?: string;
|
|
34
|
+
/** Keys whose label should be omitted. */
|
|
35
|
+
hideLabels?: string[];
|
|
36
|
+
}): import("react").JSX.Element | null;
|
|
37
|
+
export declare function FieldRenderer(props: FieldProps): import("react").JSX.Element;
|
|
38
|
+
export declare function FieldGroup({ title, children, }: {
|
|
39
|
+
title: ReactNode;
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { ChevronDown, ChevronRight, ChevronUp, Copy, GripVertical, Image as ImageIcon, Plus, Trash2, X, } from "lucide-react";
|
|
4
|
+
import { useId, useRef, useState } from "react";
|
|
5
|
+
import { createPortal } from "react-dom";
|
|
6
|
+
import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors, } from "@dnd-kit/core";
|
|
7
|
+
import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable";
|
|
8
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
9
|
+
import { restrictToParentElement, restrictToVerticalAxis, } from "@dnd-kit/modifiers";
|
|
10
|
+
import { useVestry } from "../../context.js";
|
|
11
|
+
import { basename, cx, defaultValue, fieldLabel, isImagePath, slugify, summarize, } from "../../lib/utils.js";
|
|
12
|
+
import { AssetPicker } from "../AssetPicker.js";
|
|
13
|
+
import { Button, Toggle } from "../ui.js";
|
|
14
|
+
import { MarkdownField } from "./MarkdownField.js";
|
|
15
|
+
import { CodeInput } from "./CodeInput.js";
|
|
16
|
+
import { CropInput, FocalPointInput } from "./ImageTools.js";
|
|
17
|
+
import { TreeInput } from "./TreeInput.js";
|
|
18
|
+
/** Renders a schema as a responsive grid of fields. */
|
|
19
|
+
export function FieldSet({ schema, values, onChange, errors, filter, ctx, pathPrefix = "", hideLabels, }) {
|
|
20
|
+
const entries = Object.entries(schema).filter(([k, f]) => !f.hidden && (!filter || filter(k, f)));
|
|
21
|
+
if (!entries.length)
|
|
22
|
+
return null;
|
|
23
|
+
return (_jsx("div", { className: "v-fields", children: entries.map(([key, field]) => (_jsx("div", { className: cx("v-field", field.width === "half" && "v-field--half", field.width === "third" && "v-field--third"), children: _jsx(FieldRenderer, { fieldKey: key, path: pathPrefix ? `${pathPrefix}.${key}` : key, field: field, value: values[key], onChange: (v) => onChange({ ...values, [key]: v }), errors: errors, ctx: { ...ctx, siblings: values }, hideLabel: hideLabels?.includes(key) }) }, key))) }));
|
|
24
|
+
}
|
|
25
|
+
export function FieldRenderer(props) {
|
|
26
|
+
const { field, fieldKey, path, errors } = props;
|
|
27
|
+
const id = useId();
|
|
28
|
+
const error = errors?.find((e) => e.path === path)?.message;
|
|
29
|
+
const label = fieldLabel(fieldKey, field);
|
|
30
|
+
const inline = field.type === "boolean";
|
|
31
|
+
return (_jsxs("div", { className: cx(error && "has-error"), children: [!inline && !props.hideLabel && (_jsxs("label", { className: "v-field__label", htmlFor: id, children: [label, field.required && _jsx("span", { className: "v-field__required", children: "*" })] })), _jsx(FieldInput, { ...props, id: id, label: label }), field.type === "date" &&
|
|
32
|
+
field.timeHint &&
|
|
33
|
+
typeof props.value === "string" &&
|
|
34
|
+
props.value.length <= 10 && (_jsxs("div", { className: "v-field__hint", children: ["Time: ", field.timeHint] })), field.description && (_jsx("div", { className: "v-field__hint", children: field.description })), error && _jsx("div", { className: "v-field__error", children: error })] }));
|
|
35
|
+
}
|
|
36
|
+
function FieldInput(props) {
|
|
37
|
+
const { field, value, onChange, id, label, ctx } = props;
|
|
38
|
+
const readOnly = field.readOnly;
|
|
39
|
+
switch (field.type) {
|
|
40
|
+
case "text":
|
|
41
|
+
return (_jsx("input", { id: id, className: "v-input", value: value ?? "", placeholder: field.placeholder, maxLength: field.maxLength, readOnly: readOnly, onChange: (e) => onChange(e.target.value) }));
|
|
42
|
+
case "slug":
|
|
43
|
+
return (_jsx(SlugInput, { id: id, value: value ?? "", onChange: onChange, from: field.from, ctx: ctx, readOnly: readOnly }));
|
|
44
|
+
case "textarea":
|
|
45
|
+
return (_jsx("textarea", { id: id, className: "v-textarea", rows: field.rows ?? 4, value: value ?? "", placeholder: field.placeholder, maxLength: field.maxLength, readOnly: readOnly, onChange: (e) => onChange(e.target.value) }));
|
|
46
|
+
case "number":
|
|
47
|
+
return (_jsx("input", { id: id, type: "number", className: "v-input", value: value === null || value === undefined ? "" : String(value), min: field.min, max: field.max, step: field.step ?? "any", readOnly: readOnly, onChange: (e) => onChange(e.target.value === "" ? null : Number(e.target.value)) }));
|
|
48
|
+
case "boolean":
|
|
49
|
+
return (_jsx(Toggle, { checked: Boolean(value), onChange: onChange, label: label, disabled: readOnly }));
|
|
50
|
+
case "date": {
|
|
51
|
+
const str = typeof value === "string"
|
|
52
|
+
? value
|
|
53
|
+
: value instanceof Date
|
|
54
|
+
? value.toISOString()
|
|
55
|
+
: "";
|
|
56
|
+
const datePart = str.slice(0, 10);
|
|
57
|
+
const timePart = str.length > 10 ? str.slice(11, 16) : "";
|
|
58
|
+
if (!field.time) {
|
|
59
|
+
return (_jsx("input", { id: id, type: "date", className: "v-input", value: datePart, readOnly: readOnly, onChange: (e) => onChange(e.target.value) }));
|
|
60
|
+
}
|
|
61
|
+
return (_jsxs("div", { className: "v-input-group", children: [_jsx("input", { id: id, type: "date", className: "v-input", value: datePart, readOnly: readOnly, onChange: (e) => onChange(timePart && e.target.value
|
|
62
|
+
? `${e.target.value}T${timePart}`
|
|
63
|
+
: e.target.value) }), _jsx("input", { type: "time", className: "v-input", style: { flex: "0 0 130px" }, value: timePart, readOnly: readOnly, title: field.timeHint, onChange: (e) => onChange(e.target.value && datePart
|
|
64
|
+
? `${datePart}T${e.target.value}`
|
|
65
|
+
: datePart) })] }));
|
|
66
|
+
}
|
|
67
|
+
case "datetime": {
|
|
68
|
+
if (field.autoUpdate) {
|
|
69
|
+
return (_jsx("input", { id: id, className: "v-input", readOnly: true, value: value ? new Date(String(value)).toLocaleString() : "Set on save" }));
|
|
70
|
+
}
|
|
71
|
+
return (_jsx(DatetimeInput, { id: id, value: value, storage: field.storage ?? "local", onChange: onChange, readOnly: readOnly }));
|
|
72
|
+
}
|
|
73
|
+
case "select":
|
|
74
|
+
return (_jsxs("div", { className: "v-row", children: [field.options.find((o) => o.value === value)?.color && (_jsx("span", { className: "v-swatch", style: {
|
|
75
|
+
background: field.options.find((o) => o.value === value).color,
|
|
76
|
+
} })), _jsxs("select", { id: id, className: "v-select", value: value ?? "", disabled: readOnly, onChange: (e) => onChange(e.target.value || undefined), children: [(field.clearable || value === undefined || value === "") && (_jsx("option", { value: "", children: "\u2014" })), field.options.map((o) => (_jsx("option", { value: o.value, children: o.label }, o.value)))] })] }));
|
|
77
|
+
case "multiselect": {
|
|
78
|
+
const selected = Array.isArray(value) ? value : [];
|
|
79
|
+
return (_jsx("div", { className: "v-chips", children: field.options.map((o) => {
|
|
80
|
+
const on = selected.includes(o.value);
|
|
81
|
+
return (_jsxs("button", { type: "button", className: cx("v-chip", on && "is-on"), onClick: () => onChange(on
|
|
82
|
+
? selected.filter((v) => v !== o.value)
|
|
83
|
+
: [...selected, o.value]), disabled: readOnly, children: [o.color && (_jsx("span", { className: "v-swatch", style: { background: o.color } })), o.label] }, o.value));
|
|
84
|
+
}) }));
|
|
85
|
+
}
|
|
86
|
+
case "color":
|
|
87
|
+
return (_jsxs("div", { className: "v-input-group", children: [_jsx("input", { type: "color", value: value || "#000000", onChange: (e) => onChange(e.target.value), disabled: readOnly, style: { width: 44, height: 36, padding: 2 } }), _jsx("input", { id: id, className: "v-input v-input--mono", value: value ?? "", onChange: (e) => onChange(e.target.value), readOnly: readOnly })] }));
|
|
88
|
+
case "image":
|
|
89
|
+
case "file":
|
|
90
|
+
return _jsx(AssetInput, { ...props });
|
|
91
|
+
case "reference":
|
|
92
|
+
return (_jsx(ReferenceInput, { id: id, collection: field.collection, value: value ?? "", onChange: onChange, readOnly: readOnly }));
|
|
93
|
+
case "object": {
|
|
94
|
+
const obj = (value && typeof value === "object" ? value : {});
|
|
95
|
+
const body = (_jsx(FieldSet, { schema: field.fields, values: obj, onChange: onChange, errors: props.errors, ctx: ctx, pathPrefix: props.path }));
|
|
96
|
+
return field.inline ? (body) : (_jsx("div", { className: "v-group", children: _jsx("div", { className: "v-group__body", children: body }) }));
|
|
97
|
+
}
|
|
98
|
+
case "array":
|
|
99
|
+
return _jsx(ArrayInput, { ...props, field: field });
|
|
100
|
+
case "markdown":
|
|
101
|
+
return (_jsx(MarkdownField, { value: value ?? "", onChange: onChange, components: field.components, placeholder: field.description }));
|
|
102
|
+
case "code":
|
|
103
|
+
return (_jsx(CodeInput, { value: value ?? "", onChange: onChange, language: field.language ?? "text", rows: field.rows, readOnly: readOnly }));
|
|
104
|
+
case "json":
|
|
105
|
+
return (_jsx(JsonInput, { value: value, onChange: onChange, readOnly: readOnly }));
|
|
106
|
+
case "crop":
|
|
107
|
+
return (_jsx(CropInput, { field: field, value: value, onChange: onChange, siblings: ctx?.siblings, pendingUploads: ctx?.pendingUploads }));
|
|
108
|
+
case "focalPoint":
|
|
109
|
+
return (_jsx(FocalPointInput, { field: field, value: value, onChange: onChange, siblings: ctx?.siblings, pendingUploads: ctx?.pendingUploads }));
|
|
110
|
+
case "tree":
|
|
111
|
+
return (_jsx(TreeInput, { field: field, value: value, onChange: onChange, errors: props.errors, ctx: ctx, path: props.path }));
|
|
112
|
+
default:
|
|
113
|
+
return _jsx("input", { className: "v-input", readOnly: true, value: String(value ?? "") });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function SlugInput({ id, value, onChange, from, ctx, readOnly, }) {
|
|
117
|
+
const source = from ? ctx?.siblings?.[from] : undefined;
|
|
118
|
+
const suggestion = typeof source === "string" ? slugify(source) : "";
|
|
119
|
+
return (_jsxs("div", { className: "v-input-group", children: [_jsx("input", { id: id, className: "v-input v-input--mono", value: value, readOnly: readOnly, onChange: (e) => onChange(e.target.value), placeholder: suggestion || undefined }), suggestion && suggestion !== value && !readOnly && (_jsx(Button, { size: "sm", onClick: () => onChange(suggestion), children: "Generate" }))] }));
|
|
120
|
+
}
|
|
121
|
+
function DatetimeInput({ id, value, storage, onChange, readOnly, }) {
|
|
122
|
+
// Display in local wall-clock; store either as `YYYY-MM-DDTHH:mm` or ISO.
|
|
123
|
+
let display = "";
|
|
124
|
+
if (typeof value === "string" && value) {
|
|
125
|
+
if (storage === "local")
|
|
126
|
+
display = value.slice(0, 16);
|
|
127
|
+
else {
|
|
128
|
+
const d = new Date(value);
|
|
129
|
+
if (!Number.isNaN(d.getTime())) {
|
|
130
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
131
|
+
display = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return (_jsx("input", { id: id, type: "datetime-local", className: "v-input", value: display, readOnly: readOnly, onChange: (e) => {
|
|
136
|
+
const v = e.target.value;
|
|
137
|
+
if (!v)
|
|
138
|
+
return onChange("");
|
|
139
|
+
onChange(storage === "local" ? v : new Date(v).toISOString());
|
|
140
|
+
} }));
|
|
141
|
+
}
|
|
142
|
+
function AssetInput(props) {
|
|
143
|
+
const { api } = useVestry();
|
|
144
|
+
const field = props.field;
|
|
145
|
+
const value = props.value ?? "";
|
|
146
|
+
const [open, setOpen] = useState(false);
|
|
147
|
+
const pending = props.ctx?.pendingUploads?.[value];
|
|
148
|
+
const isImage = field.type === "image" || isImagePath(value);
|
|
149
|
+
const previewUrl = value
|
|
150
|
+
? pending
|
|
151
|
+
? pending.previewUrl
|
|
152
|
+
: /^https?:/.test(value)
|
|
153
|
+
? value
|
|
154
|
+
: api.fileUrl(value)
|
|
155
|
+
: null;
|
|
156
|
+
const accept = field.accept ??
|
|
157
|
+
(field.type === "image"
|
|
158
|
+
? ["jpg", "jpeg", "png", "gif", "webp", "avif", "svg"]
|
|
159
|
+
: undefined);
|
|
160
|
+
return (_jsxs("div", { className: "v-image-field", children: [_jsx("div", { className: "v-image-field__preview", children: previewUrl && isImage ? (_jsx("img", { src: previewUrl, alt: "" })) : (_jsx(ImageIcon, {})) }), _jsxs("div", { className: "v-image-field__info", children: [value ? (_jsxs("div", { className: "v-image-field__path", children: [_jsx("strong", { style: { color: "var(--v-text)" }, children: basename(value) }), _jsx("br", {}), value.slice(0, value.length - basename(value).length)] })) : (_jsx("div", { className: "v-muted", style: { marginBottom: 8 }, children: "No file selected" })), _jsxs("div", { className: "v-image-field__actions", children: [_jsx(Button, { size: "sm", onClick: () => setOpen(true), disabled: field.readOnly, children: value ? "Replace" : "Choose" }), value && (_jsxs(Button, { size: "sm", variant: "ghost", onClick: () => props.onChange(""), disabled: field.readOnly, children: [_jsx(X, {}), " Clear"] }))] })] }), open && (_jsx(AssetPicker, { directory: field.directory, accept: accept, allowUpload: field.upload !== false, title: field.type === "image" ? "Choose an image" : "Choose a file", deferUploads: Boolean(props.ctx?.addUpload), onClose: () => setOpen(false), onPick: (path, upload) => {
|
|
161
|
+
if (upload)
|
|
162
|
+
props.ctx?.addUpload?.(upload);
|
|
163
|
+
props.onChange(path);
|
|
164
|
+
setOpen(false);
|
|
165
|
+
} }))] }));
|
|
166
|
+
}
|
|
167
|
+
function ReferenceInput({ id, collection, value, onChange, readOnly, }) {
|
|
168
|
+
const { api } = useVestry();
|
|
169
|
+
const q = useQuery({
|
|
170
|
+
queryKey: ["entries", collection],
|
|
171
|
+
queryFn: () => api.listEntries(collection),
|
|
172
|
+
staleTime: 60_000,
|
|
173
|
+
});
|
|
174
|
+
const entries = q.data?.entries ?? [];
|
|
175
|
+
return (_jsxs("select", { id: id, className: "v-select", value: value, disabled: readOnly, onChange: (e) => onChange(e.target.value || undefined), children: [_jsx("option", { value: "", children: q.isLoading ? "Loading…" : "—" }), value && !entries.some((e) => e.slug === value) && (_jsx("option", { value: value, children: value })), entries.map((e) => (_jsx("option", { value: e.slug, children: e.title }, e.slug)))] }));
|
|
176
|
+
}
|
|
177
|
+
let nextRowId = 0;
|
|
178
|
+
const newRowId = () => `row-${++nextRowId}`;
|
|
179
|
+
/**
|
|
180
|
+
* A sortable list item. dnd-kit moves the neighbours out of the way with
|
|
181
|
+
* transforms so a gap opens where the row will land; the row itself stays in
|
|
182
|
+
* flow as a dimmed placeholder while a clone floats in the DragOverlay.
|
|
183
|
+
*/
|
|
184
|
+
function SortableRow({ id, className, children, }) {
|
|
185
|
+
const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging, } = useSortable({ id });
|
|
186
|
+
return (_jsx("div", { ref: setNodeRef, className: cx(className, isDragging && "is-placeholder"), style: { transform: CSS.Translate.toString(transform), transition }, children: children({
|
|
187
|
+
ref: setActivatorNodeRef,
|
|
188
|
+
listeners: {
|
|
189
|
+
...listeners,
|
|
190
|
+
// Keep the pointer-down from reaching an enclosing rich text
|
|
191
|
+
// editor, which would otherwise treat it as a selection drag.
|
|
192
|
+
onPointerDown: (e) => {
|
|
193
|
+
e.stopPropagation();
|
|
194
|
+
listeners?.onPointerDown?.(e);
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
attributes: attributes,
|
|
198
|
+
}, isDragging) }));
|
|
199
|
+
}
|
|
200
|
+
function ArrayInput(props) {
|
|
201
|
+
const { field, onChange, ctx } = props;
|
|
202
|
+
const items = Array.isArray(props.value) ? props.value : [];
|
|
203
|
+
const [openIdx, setOpenIdx] = useState(null);
|
|
204
|
+
const [activeId, setActiveId] = useState(null);
|
|
205
|
+
const isObject = field.of.type === "object";
|
|
206
|
+
// Stable ids for dnd-kit, kept parallel to the items array.
|
|
207
|
+
const idsRef = useRef([]);
|
|
208
|
+
if (idsRef.current.length < items.length) {
|
|
209
|
+
idsRef.current = [
|
|
210
|
+
...idsRef.current,
|
|
211
|
+
...Array.from({ length: items.length - idsRef.current.length }, newRowId),
|
|
212
|
+
];
|
|
213
|
+
}
|
|
214
|
+
else if (idsRef.current.length > items.length) {
|
|
215
|
+
idsRef.current = idsRef.current.slice(0, items.length);
|
|
216
|
+
}
|
|
217
|
+
const ids = idsRef.current;
|
|
218
|
+
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 4 } }), useSensor(KeyboardSensor, {
|
|
219
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
220
|
+
}));
|
|
221
|
+
const update = (next, nextIds) => {
|
|
222
|
+
if (nextIds)
|
|
223
|
+
idsRef.current = nextIds;
|
|
224
|
+
onChange(next);
|
|
225
|
+
};
|
|
226
|
+
const move = (from, to) => {
|
|
227
|
+
if (to < 0 || to >= items.length || from === to)
|
|
228
|
+
return;
|
|
229
|
+
update(arrayMove(items, from, to), arrayMove(ids, from, to));
|
|
230
|
+
setOpenIdx((o) => (o === from ? to : o));
|
|
231
|
+
};
|
|
232
|
+
const remove = (i) => update(items.filter((_, j) => j !== i), ids.filter((_, j) => j !== i));
|
|
233
|
+
const duplicate = (i) => update([
|
|
234
|
+
...items.slice(0, i + 1),
|
|
235
|
+
JSON.parse(JSON.stringify(items[i])),
|
|
236
|
+
...items.slice(i + 1),
|
|
237
|
+
], [...ids.slice(0, i + 1), newRowId(), ...ids.slice(i + 1)]);
|
|
238
|
+
const setItem = (i, v) => update(items.map((x, j) => (j === i ? v : x)));
|
|
239
|
+
const moveButtons = (i) => (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "xs", icon: true, title: "Move up", onClick: () => move(i, i - 1), disabled: i === 0, children: _jsx(ChevronUp, {}) }), _jsx(Button, { variant: "ghost", size: "xs", icon: true, title: "Move down", onClick: () => move(i, i + 1), disabled: i === items.length - 1, children: _jsx(ChevronDown, {}) })] }));
|
|
240
|
+
/** Header of an object item; `overlay` renders a static clone for the drag preview. */
|
|
241
|
+
const objectHeader = (i, open, handle) => {
|
|
242
|
+
const item = items[i];
|
|
243
|
+
const title = summarize(item, field) || `${fieldLabel("item", field.of)} ${i + 1}`;
|
|
244
|
+
return (_jsxs("div", { className: "v-list__header", onClick: handle ? () => setOpenIdx(open ? null : i) : undefined, children: [_jsx("span", { className: "v-list__handle", title: "Drag to reorder", ref: handle?.ref, ...(handle?.listeners ?? {}), ...(handle?.attributes ?? {}), children: _jsx(GripVertical, {}) }), open ? _jsx(ChevronDown, { size: 14 }) : _jsx(ChevronRight, { size: 14 }), _jsx("span", { className: "v-list__title", children: title }), handle && (_jsxs("div", { className: "v-list__actions", onClick: (e) => e.stopPropagation(), children: [moveButtons(i), _jsx(Button, { variant: "ghost", size: "xs", icon: true, title: "Duplicate", onClick: () => duplicate(i), children: _jsx(Copy, {}) }), _jsx(Button, { variant: "ghost", size: "xs", icon: true, title: "Remove", onClick: () => remove(i), children: _jsx(Trash2, {}) })] }))] }));
|
|
245
|
+
};
|
|
246
|
+
const scalarRow = (i, handle) => (_jsxs(_Fragment, { children: [_jsx("span", { className: "v-list__handle", title: "Drag to reorder", ref: handle?.ref, ...(handle?.listeners ?? {}), ...(handle?.attributes ?? {}), children: _jsx(GripVertical, {}) }), _jsx("div", { className: "v-grow", children: _jsx(FieldInput, { fieldKey: `${props.fieldKey}.${i}`, path: `${props.path}.${i}`, field: field.of, value: items[i], onChange: (v) => setItem(i, v), errors: props.errors, ctx: ctx, id: handle ? `${props.path}.${i}` : `${props.path}.${i}.overlay`, label: "" }) }), _jsxs("div", { className: "v-list__actions", children: [moveButtons(i), _jsx(Button, { variant: "ghost", size: "xs", icon: true, title: "Remove", onClick: () => remove(i), children: _jsx(Trash2, {}) })] })] }));
|
|
247
|
+
const rowClass = (i) => cx(isObject ? "v-list__item" : "v-list__row", isDisabled(items[i]) && "is-disabled");
|
|
248
|
+
const activeIdx = activeId ? ids.indexOf(activeId) : -1;
|
|
249
|
+
return (_jsxs("div", { children: [_jsxs(DndContext, { sensors: sensors, collisionDetection: closestCenter, modifiers: [restrictToVerticalAxis, restrictToParentElement], onDragStart: (e) => {
|
|
250
|
+
setActiveId(String(e.active.id));
|
|
251
|
+
if (isObject)
|
|
252
|
+
setOpenIdx(null);
|
|
253
|
+
}, onDragEnd: (e) => {
|
|
254
|
+
setActiveId(null);
|
|
255
|
+
const from = ids.indexOf(String(e.active.id));
|
|
256
|
+
const to = e.over ? ids.indexOf(String(e.over.id)) : -1;
|
|
257
|
+
if (from !== -1 && to !== -1)
|
|
258
|
+
move(from, to);
|
|
259
|
+
}, onDragCancel: () => setActiveId(null), children: [_jsx(SortableContext, { items: ids, strategy: verticalListSortingStrategy, children: _jsx("div", { className: "v-list", children: items.map((item, i) => {
|
|
260
|
+
const open = openIdx === i;
|
|
261
|
+
return (_jsx(SortableRow, { id: ids[i], className: rowClass(i), children: (handle) => isObject ? (_jsxs(_Fragment, { children: [objectHeader(i, open, handle), open && (_jsx("div", { className: "v-list__body", children: _jsx(FieldSet, { schema: field.of.fields, values: (item ?? {}), onChange: (v) => setItem(i, v), errors: props.errors, ctx: ctx, pathPrefix: `${props.path}.${i}` }) }))] })) : (scalarRow(i, handle)) }, ids[i]));
|
|
262
|
+
}) }) }), createPortal(_jsx(DragOverlay, { dropAnimation: {
|
|
263
|
+
duration: 180,
|
|
264
|
+
easing: "cubic-bezier(0.2, 0, 0, 1)",
|
|
265
|
+
}, children: activeIdx !== -1 && (_jsx("div", { className: cx(rowClass(activeIdx), "v-list__ghost"), children: isObject
|
|
266
|
+
? objectHeader(activeIdx, false, null)
|
|
267
|
+
: scalarRow(activeIdx, null) })) }), document.body)] }), _jsxs(Button, { size: "sm", className: "v-list__add", disabled: field.max !== undefined && items.length >= field.max, onClick: () => {
|
|
268
|
+
update([...items, defaultValue(field.of)], [...ids, newRowId()]);
|
|
269
|
+
setOpenIdx(items.length);
|
|
270
|
+
}, children: [_jsx(Plus, {}), " ", field.addLabel ?? "Add item"] })] }));
|
|
271
|
+
}
|
|
272
|
+
function JsonInput({ value, onChange, readOnly, }) {
|
|
273
|
+
const [text, setText] = useState(() => value === undefined ? "" : JSON.stringify(value, null, 2));
|
|
274
|
+
const [error, setError] = useState(null);
|
|
275
|
+
return (_jsxs("div", { children: [_jsx(CodeInput, { value: text, language: "json", readOnly: readOnly, onChange: (t) => {
|
|
276
|
+
setText(t);
|
|
277
|
+
if (!t.trim()) {
|
|
278
|
+
setError(null);
|
|
279
|
+
onChange(undefined);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
onChange(JSON.parse(t));
|
|
284
|
+
setError(null);
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
setError(e.message);
|
|
288
|
+
}
|
|
289
|
+
} }), error && _jsx("div", { className: "v-field__error", children: error })] }));
|
|
290
|
+
}
|
|
291
|
+
/** Items with `enabled: false` (a common toggle in page-builder data) are shown dimmed. */
|
|
292
|
+
function isDisabled(item) {
|
|
293
|
+
return (typeof item === "object" &&
|
|
294
|
+
item !== null &&
|
|
295
|
+
item.enabled === false);
|
|
296
|
+
}
|
|
297
|
+
export function FieldGroup({ title, children, }) {
|
|
298
|
+
return (_jsxs("div", { className: "v-card", children: [_jsx("div", { className: "v-card__header", children: _jsx("h3", { className: "v-card__title", children: title }) }), _jsx("div", { className: "v-card__body", children: children })] }));
|
|
299
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CropField, FocalPointField } from "../../../config/types.js";
|
|
2
|
+
import type { PendingUpload } from "../AssetPicker.js";
|
|
3
|
+
export interface CropRect {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
export interface FocalPoint {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
}
|
|
13
|
+
/** Largest rectangle of the given aspect that fits the image, centred. */
|
|
14
|
+
export declare function defaultCrop(natural: {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
}, aspect?: number): CropRect;
|
|
18
|
+
export declare function CropInput({ field, value, onChange, siblings, pendingUploads, }: {
|
|
19
|
+
field: CropField;
|
|
20
|
+
value: unknown;
|
|
21
|
+
onChange: (v: CropRect | undefined) => void;
|
|
22
|
+
siblings?: Record<string, unknown>;
|
|
23
|
+
pendingUploads?: Record<string, PendingUpload>;
|
|
24
|
+
}): import("react").JSX.Element;
|
|
25
|
+
export declare function FocalPointInput({ field, value, onChange, siblings, pendingUploads, }: {
|
|
26
|
+
field: FocalPointField;
|
|
27
|
+
value: unknown;
|
|
28
|
+
onChange: (v: FocalPoint | undefined) => void;
|
|
29
|
+
siblings?: Record<string, unknown>;
|
|
30
|
+
pendingUploads?: Record<string, PendingUpload>;
|
|
31
|
+
}): import("react").JSX.Element;
|