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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +383 -2
  3. package/dist/config/fields.d.ts +30 -0
  4. package/dist/config/fields.js +75 -0
  5. package/dist/config/index.d.ts +9 -0
  6. package/dist/config/index.js +18 -0
  7. package/dist/config/types.d.ts +412 -0
  8. package/dist/config/types.js +10 -0
  9. package/dist/core/api.d.ts +67 -0
  10. package/dist/core/api.js +543 -0
  11. package/dist/core/auth.d.ts +1597 -0
  12. package/dist/core/auth.js +264 -0
  13. package/dist/core/cache.d.ts +27 -0
  14. package/dist/core/cache.js +52 -0
  15. package/dist/core/entries.d.ts +19 -0
  16. package/dist/core/entries.js +127 -0
  17. package/dist/core/index-types.d.ts +20 -0
  18. package/dist/core/index-types.js +1 -0
  19. package/dist/core/index.d.ts +13 -0
  20. package/dist/core/index.js +13 -0
  21. package/dist/core/mail.d.ts +28 -0
  22. package/dist/core/mail.js +62 -0
  23. package/dist/core/paths.d.ts +35 -0
  24. package/dist/core/paths.js +148 -0
  25. package/dist/core/roles.d.ts +24 -0
  26. package/dist/core/roles.js +15 -0
  27. package/dist/core/service.d.ts +102 -0
  28. package/dist/core/service.js +478 -0
  29. package/dist/core/source.d.ts +82 -0
  30. package/dist/core/source.js +75 -0
  31. package/dist/core/sources/github-app.d.ts +26 -0
  32. package/dist/core/sources/github-app.js +161 -0
  33. package/dist/core/sources/github.d.ts +40 -0
  34. package/dist/core/sources/github.js +278 -0
  35. package/dist/core/sources/local.d.ts +16 -0
  36. package/dist/core/sources/local.js +98 -0
  37. package/dist/core/values.d.ts +22 -0
  38. package/dist/core/values.js +123 -0
  39. package/dist/env.d.ts +19 -0
  40. package/dist/index.d.ts +18 -0
  41. package/dist/index.js +167 -0
  42. package/dist/integration/build-index.d.ts +8 -0
  43. package/dist/integration/build-index.js +67 -0
  44. package/dist/integration/client-deps.d.ts +10 -0
  45. package/dist/integration/client-deps.js +43 -0
  46. package/dist/integration/dev-middleware.d.ts +6 -0
  47. package/dist/integration/dev-middleware.js +48 -0
  48. package/dist/integration/formatter.d.ts +7 -0
  49. package/dist/integration/formatter.js +56 -0
  50. package/dist/mdx/expressions.d.ts +14 -0
  51. package/dist/mdx/expressions.js +141 -0
  52. package/dist/mdx/index.d.ts +4 -0
  53. package/dist/mdx/index.js +4 -0
  54. package/dist/mdx/parse.d.ts +6 -0
  55. package/dist/mdx/parse.js +401 -0
  56. package/dist/mdx/serialize.d.ts +9 -0
  57. package/dist/mdx/serialize.js +485 -0
  58. package/dist/mdx/types.d.ts +42 -0
  59. package/dist/mdx/types.js +6 -0
  60. package/dist/pages/admin.astro +27 -0
  61. package/dist/pages/api.d.ts +3 -0
  62. package/dist/pages/api.js +31 -0
  63. package/dist/ui/App.d.ts +4 -0
  64. package/dist/ui/App.js +70 -0
  65. package/dist/ui/api.d.ts +83 -0
  66. package/dist/ui/api.js +97 -0
  67. package/dist/ui/auth-client.d.ts +27 -0
  68. package/dist/ui/auth-client.js +29 -0
  69. package/dist/ui/components/AssetPicker.d.ts +27 -0
  70. package/dist/ui/components/AssetPicker.js +97 -0
  71. package/dist/ui/components/Icon.d.ts +11 -0
  72. package/dist/ui/components/Icon.js +46 -0
  73. package/dist/ui/components/Layout.d.ts +15 -0
  74. package/dist/ui/components/Layout.js +87 -0
  75. package/dist/ui/components/editor/EditorContext.d.ts +15 -0
  76. package/dist/ui/components/editor/EditorContext.js +9 -0
  77. package/dist/ui/components/editor/NodeViews.d.ts +4 -0
  78. package/dist/ui/components/editor/NodeViews.js +127 -0
  79. package/dist/ui/components/editor/RichTextEditor.d.ts +24 -0
  80. package/dist/ui/components/editor/RichTextEditor.js +230 -0
  81. package/dist/ui/components/editor/extensions.d.ts +30 -0
  82. package/dist/ui/components/editor/extensions.js +248 -0
  83. package/dist/ui/components/editor/schema.d.ts +12 -0
  84. package/dist/ui/components/editor/schema.js +37 -0
  85. package/dist/ui/components/fields/CodeInput.d.ts +10 -0
  86. package/dist/ui/components/fields/CodeInput.js +8 -0
  87. package/dist/ui/components/fields/CodeMirrorInput.d.ts +2 -0
  88. package/dist/ui/components/fields/CodeMirrorInput.js +44 -0
  89. package/dist/ui/components/fields/FieldRenderer.d.ts +41 -0
  90. package/dist/ui/components/fields/FieldRenderer.js +299 -0
  91. package/dist/ui/components/fields/ImageTools.d.ts +31 -0
  92. package/dist/ui/components/fields/ImageTools.js +296 -0
  93. package/dist/ui/components/fields/MarkdownField.d.ts +7 -0
  94. package/dist/ui/components/fields/MarkdownField.js +16 -0
  95. package/dist/ui/components/fields/TreeInput.d.ts +13 -0
  96. package/dist/ui/components/fields/TreeInput.js +221 -0
  97. package/dist/ui/components/ui.d.ts +63 -0
  98. package/dist/ui/components/ui.js +113 -0
  99. package/dist/ui/context.d.ts +17 -0
  100. package/dist/ui/context.js +13 -0
  101. package/dist/ui/lib/format.d.ts +5 -0
  102. package/dist/ui/lib/format.js +11 -0
  103. package/dist/ui/lib/utils.d.ts +24 -0
  104. package/dist/ui/lib/utils.js +255 -0
  105. package/dist/ui/main.d.ts +1 -0
  106. package/dist/ui/main.js +11 -0
  107. package/dist/ui/pages/Assets.d.ts +1 -0
  108. package/dist/ui/pages/Assets.js +69 -0
  109. package/dist/ui/pages/CollectionList.d.ts +6 -0
  110. package/dist/ui/pages/CollectionList.js +115 -0
  111. package/dist/ui/pages/Dashboard.d.ts +1 -0
  112. package/dist/ui/pages/Dashboard.js +57 -0
  113. package/dist/ui/pages/EntryEditor.d.ts +3 -0
  114. package/dist/ui/pages/EntryEditor.js +313 -0
  115. package/dist/ui/pages/Login.d.ts +6 -0
  116. package/dist/ui/pages/Login.js +88 -0
  117. package/dist/ui/pages/Users.d.ts +1 -0
  118. package/dist/ui/pages/Users.js +102 -0
  119. package/dist/ui/styles.css +2554 -0
  120. 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,2 @@
1
+ import type { CodeInputProps } from "./CodeInput.js";
2
+ export default function CodeMirrorInput({ value, onChange, language, rows, readOnly, minHeight, }: CodeInputProps): import("react").JSX.Element;
@@ -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;