(
item?.bylines?.map((entry) => ({ bylineId: entry.byline.id, roleLabel: entry.roleLabel })) ??
[],
@@ -254,7 +265,21 @@ export function ContentEditor({
const itemDataString = React.useMemo(() => (item ? JSON.stringify(item.data) : ""), [item?.data]);
React.useEffect(() => {
if (item) {
+ const didItemIdChange = item.id !== previousItemIdRef.current;
+ const hasActiveFieldErrors = Object.values(fieldErrorsRef.current).some(
+ (error) => typeof error === "string" && error.length > 0,
+ );
+
+ if (!didItemIdChange && hasActiveFieldErrors) {
+ return;
+ }
+
setFormData(item.data);
+ if (didItemIdChange) {
+ setFieldErrors({});
+ setJsonEditorEpoch((epoch) => epoch + 1);
+ }
+ previousItemIdRef.current = item.id;
setSlug(item.slug || "");
setSlugTouched(!!item.slug);
setStatus(item.status);
@@ -301,6 +326,16 @@ export function ContentEditor({
[formData, slug, activeBylines],
);
const isDirty = isNew || currentData !== lastSavedData;
+ const hasFieldErrors = React.useMemo(
+ () => Object.values(fieldErrors).some((error) => typeof error === "string" && error.length > 0),
+ [fieldErrors],
+ );
+ const handleFieldValidationChange = React.useCallback((name: string, error: string | null) => {
+ setFieldErrors((prev) => {
+ if ((prev[name] ?? null) === error) return prev;
+ return { ...prev, [name]: error };
+ });
+ }, []);
// Autosave with debounce
// Track pending autosave to cancel on manual save
@@ -316,8 +351,8 @@ export function ContentEditor({
return;
}
- // Don't autosave if not dirty or already saving
- if (!isDirty || isSaving || isAutosaving) {
+ // Don't autosave if not dirty, invalid, or already saving
+ if (!isDirty || hasFieldErrors || isSaving || isAutosaving) {
return;
}
@@ -340,7 +375,17 @@ export function ContentEditor({
clearTimeout(autosaveTimeoutRef.current);
}
};
- }, [currentData, isNew, onAutosave, item?.id, isDirty, isSaving, isAutosaving, activeBylines]);
+ }, [
+ currentData,
+ isNew,
+ onAutosave,
+ item?.id,
+ isDirty,
+ hasFieldErrors,
+ isSaving,
+ isAutosaving,
+ activeBylines,
+ ]);
// Cancel pending autosave on manual save
const handleSubmit = (e: React.FormEvent) => {
@@ -534,7 +579,12 @@ export function ContentEditor({
{hasPendingChanges ? "Preview draft" : "Preview"}
)}
-
+
{!isNew && (
<>
{supportsDrafts && hasPendingChanges && onDiscardDraft && (
@@ -612,7 +662,7 @@ export function ContentEditor({
{Object.entries(fields).map(([name, field]) => (
))}
@@ -922,6 +973,7 @@ interface FieldRendererProps {
field: FieldDescriptor;
value: unknown;
onChange: (name: string, value: unknown) => void;
+ onValidationChange?: (name: string, error: string | null) => void;
/** Callback when a portableText editor is ready */
onEditorReady?: (editor: Editor) => void;
/** Minimal chrome - hides toolbar, fades labels, removes borders (distraction-free mode) */
@@ -936,6 +988,26 @@ interface FieldRendererProps {
manifest?: import("../lib/api/client.js").AdminManifest | null;
}
+function JsonEditorFallback({
+ id,
+ label,
+ labelClass,
+}: Pick) {
+ return (
+
+
+ {label}
+
+
+ Loading editor...
+
+
+ );
+}
+
/**
* Render field based on type
*/
@@ -950,6 +1022,7 @@ function FieldRenderer({
onBlockSidebarOpen,
onBlockSidebarClose,
manifest,
+ onValidationChange,
}: FieldRendererProps) {
const pluginAdmins = usePluginAdmins();
const label = field.label || name.charAt(0).toUpperCase() + name.slice(1);
@@ -957,6 +1030,10 @@ function FieldRenderer({
const labelClass = minimal ? "text-kumo-subtle/50 text-xs font-normal" : undefined;
const handleChange = React.useCallback((v: unknown) => onChange(name, v), [onChange, name]);
+ const handleValidationChange = React.useCallback(
+ (error: string | null) => onValidationChange?.(name, error),
+ [name, onValidationChange],
+ );
// Check for plugin field widget override
if (field.widget) {
@@ -1095,6 +1172,22 @@ function FieldRenderer({
/>
);
+ case "json":
+ return (
+ }
+ >
+
+
+ );
+
case "select": {
const selectItems: Record = {};
for (const opt of field.options ?? []) {
diff --git a/packages/admin/src/components/JsonCodeEditor.tsx b/packages/admin/src/components/JsonCodeEditor.tsx
new file mode 100644
index 00000000..885cd862
--- /dev/null
+++ b/packages/admin/src/components/JsonCodeEditor.tsx
@@ -0,0 +1,124 @@
+import { json } from "@codemirror/lang-json";
+import { defaultHighlightStyle, syntaxHighlighting } from "@codemirror/language";
+import { EditorState } from "@codemirror/state";
+import { EditorView, type ViewUpdate, placeholder } from "@codemirror/view";
+import * as React from "react";
+
+import { cn } from "../lib/utils";
+
+function formatJsonValue(value: unknown): string {
+ if (value == null) return "";
+ if (typeof value === "string") {
+ try {
+ return JSON.stringify(JSON.parse(value), null, 2);
+ } catch {
+ return JSON.stringify(value, null, 2);
+ }
+ }
+ try {
+ return JSON.stringify(value, null, 2);
+ } catch {
+ return "";
+ }
+}
+
+const jsonEditorTheme = EditorView.theme({
+ "&": {
+ fontFamily:
+ 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
+ fontSize: "13px",
+ },
+ ".cm-scroller": { overflow: "auto", maxHeight: "20rem", minHeight: "10rem" },
+ ".cm-content": { padding: "0.625rem 0.75rem", caretColor: "currentColor" },
+ ".cm-line": { padding: "0" },
+ "&.cm-focused": { outline: "none" },
+ ".cm-gutters": { display: "none" },
+});
+
+export interface JsonCodeEditorProps {
+ id: string;
+ label: string;
+ labelClass?: string;
+ value: unknown;
+ onChange: (value: unknown) => void;
+ onValidationChange?: (error: string | null) => void;
+}
+
+export function JsonCodeEditor({
+ id,
+ label,
+ labelClass,
+ value,
+ onChange,
+ onValidationChange,
+}: JsonCodeEditorProps) {
+ const containerRef = React.useRef(null);
+ const viewRef = React.useRef(null);
+ const onChangeRef = React.useRef(onChange);
+ const onValidationRef = React.useRef(onValidationChange);
+ onChangeRef.current = onChange;
+ onValidationRef.current = onValidationChange;
+
+ React.useEffect(() => {
+ if (!containerRef.current) return;
+
+ const updateListener = EditorView.updateListener.of((update: ViewUpdate) => {
+ if (!update.docChanged) return;
+ const content = update.state.doc.toString();
+ if (!content.trim()) {
+ onChangeRef.current(null);
+ onValidationRef.current?.(null);
+ return;
+ }
+ try {
+ onChangeRef.current(JSON.parse(content));
+ onValidationRef.current?.(null);
+ } catch (e) {
+ onValidationRef.current?.(e instanceof Error ? e.message : "Invalid JSON");
+ }
+ });
+
+ const view = new EditorView({
+ state: EditorState.create({
+ doc: formatJsonValue(value),
+ extensions: [
+ json(),
+ syntaxHighlighting(defaultHighlightStyle),
+ EditorView.lineWrapping,
+ EditorView.contentAttributes.of({ "aria-labelledby": `${id}-label` }),
+ placeholder('{\n "key": "value"\n}'),
+ jsonEditorTheme,
+ updateListener,
+ ],
+ }),
+ parent: containerRef.current,
+ });
+ viewRef.current = view;
+
+ return () => {
+ view.destroy();
+ viewRef.current = null;
+ };
+ // Only runs on mount. The component is intentionally uncontrolled.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/packages/admin/tests/components/ContentEditor.test.tsx b/packages/admin/tests/components/ContentEditor.test.tsx
index 57359ca2..75f23faf 100644
--- a/packages/admin/tests/components/ContentEditor.test.tsx
+++ b/packages/admin/tests/components/ContentEditor.test.tsx
@@ -143,6 +143,111 @@ describe("ContentEditor", () => {
const input = screen.getByLabelText("Order");
await expect.element(input).toHaveAttribute("type", "number");
});
+
+ it("loads json fields into a syntax-highlighted editor for existing items", async () => {
+ const metadata = { theme: "dark", nested: { enabled: true } };
+ const screen = await renderEditor({
+ fields: { metadata: { kind: "json", label: "Metadata" } },
+ isNew: false,
+ item: makeItem({ data: { metadata } }),
+ });
+
+ // The CodeMirror editor has role="textbox" and is labelled by the
+ // "Metadata" heading via aria-labelledby.
+ const editor = screen.getByRole("textbox", { name: "Metadata" });
+ await expect.element(editor).toBeInTheDocument();
+ // CodeMirror renders each line in a separate div, so DOM textContent
+ // has no newlines between lines. Check key strings are present.
+ await expect.element(editor).toHaveTextContent('"theme"');
+ await expect.element(editor).toHaveTextContent('"dark"');
+ await expect.element(editor).toHaveTextContent('"nested"');
+ });
+
+ it("loads json string values as valid JSON string scalars", async () => {
+ const screen = await renderEditor({
+ fields: { metadata: { kind: "json", label: "Metadata" } },
+ isNew: false,
+ item: makeItem({ data: { metadata: "dark" } }),
+ });
+
+ const editor = screen.getByRole("textbox", { name: "Metadata" });
+ await expect.element(editor).toHaveTextContent('"dark"');
+ });
+
+ it("keeps in-progress json text when same item refreshes", async () => {
+ const fields = { metadata: { kind: "json", label: "Metadata" } } satisfies Record<
+ string,
+ FieldDescriptor
+ >;
+ const item = makeItem({
+ id: "item-1",
+ data: { metadata: { theme: "dark" } },
+ updatedAt: "2025-01-15T10:30:00Z",
+ });
+ const props: ContentEditorProps = {
+ collection: "posts",
+ collectionLabel: "Post",
+ fields,
+ isNew: false,
+ item,
+ onSave: vi.fn(),
+ };
+
+ const screen = await render();
+ const editor = screen.getByRole("textbox", { name: "Metadata" });
+ await editor.fill('{"theme":"light","draft":true}');
+
+ await screen.rerender(
+ ,
+ );
+
+ await expect
+ .element(screen.getByRole("textbox", { name: "Metadata" }))
+ .toHaveTextContent('"light"');
+ await expect
+ .element(screen.getByRole("textbox", { name: "Metadata" }))
+ .toHaveTextContent('"draft"');
+ });
+
+ it("resets json editor when switching to another item", async () => {
+ const fields = { metadata: { kind: "json", label: "Metadata" } } satisfies Record<
+ string,
+ FieldDescriptor
+ >;
+ const props: ContentEditorProps = {
+ collection: "posts",
+ collectionLabel: "Post",
+ fields,
+ isNew: false,
+ item: makeItem({ id: "item-1", data: { metadata: { theme: "dark" } } }),
+ onSave: vi.fn(),
+ };
+
+ const screen = await render();
+ const editor = screen.getByRole("textbox", { name: "Metadata" });
+ await editor.fill('{"theme":"light"}');
+
+ await screen.rerender(
+ ,
+ );
+
+ await expect
+ .element(screen.getByRole("textbox", { name: "Metadata" }))
+ .toHaveTextContent('"other"');
+ await expect
+ .element(screen.getByRole("textbox", { name: "Metadata" }))
+ .not.toHaveTextContent('"light"');
+ });
});
describe("saving", () => {
@@ -165,6 +270,114 @@ describe("ContentEditor", () => {
);
});
+ it("save form parses json editor content back into structured data", async () => {
+ const onSave = vi.fn();
+ const screen = await renderEditor({
+ isNew: false,
+ onSave,
+ fields: { metadata: { kind: "json", label: "Metadata" } },
+ item: makeItem({ data: { metadata: { enabled: false } } }),
+ });
+
+ const editor = screen.getByRole("textbox", { name: "Metadata" });
+ await editor.fill('{"enabled":true,"count":2}');
+
+ const saveBtn = screen.getByRole("button", { name: "Save" });
+ await saveBtn.click();
+
+ expect(onSave).toHaveBeenCalledWith(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ metadata: { enabled: true, count: 2 },
+ }),
+ }),
+ );
+ });
+
+ it("disables save when json editor content is invalid", async () => {
+ const onSave = vi.fn();
+ const screen = await renderEditor({
+ isNew: false,
+ onSave,
+ fields: { metadata: { kind: "json", label: "Metadata" } },
+ item: makeItem({ data: { metadata: { enabled: false } } }),
+ });
+
+ const editor = screen.getByRole("textbox", { name: "Metadata" });
+ await editor.fill('{"enabled": true');
+
+ const saveBtn = screen.getByRole("button", { name: "Save" });
+ await expect.element(saveBtn).toBeDisabled();
+ expect(onSave).not.toHaveBeenCalled();
+ });
+
+ it("keeps json error state when same item refreshes", async () => {
+ const fields = { metadata: { kind: "json", label: "Metadata" } } satisfies Record<
+ string,
+ FieldDescriptor
+ >;
+ const props: ContentEditorProps = {
+ collection: "posts",
+ collectionLabel: "Post",
+ fields,
+ isNew: false,
+ item: makeItem({
+ id: "item-1",
+ data: { metadata: { theme: "dark" } },
+ updatedAt: "2025-01-15T10:30:00Z",
+ }),
+ onSave: vi.fn(),
+ };
+
+ const screen = await render();
+ const editor = screen.getByRole("textbox", { name: "Metadata" });
+ await editor.fill('{"theme": ');
+
+ let saveBtn = screen.getByRole("button", { name: "Save" });
+ await expect.element(saveBtn).toBeDisabled();
+
+ await screen.rerender(
+ ,
+ );
+
+ saveBtn = screen.getByRole("button", { name: "Save" });
+ await expect.element(saveBtn).toBeDisabled();
+ await expect
+ .element(screen.getByRole("textbox", { name: "Metadata" }))
+ .toHaveTextContent('{"theme":');
+ });
+
+ it("does not autosave while json field has validation errors", async () => {
+ vi.useFakeTimers();
+ try {
+ const onAutosave = vi.fn();
+ const screen = await renderEditor({
+ isNew: false,
+ onAutosave,
+ fields: {
+ title: { kind: "string", label: "Title" },
+ metadata: { kind: "json", label: "Metadata" },
+ },
+ item: makeItem({ data: { title: "Initial", metadata: { theme: "dark" } } }),
+ });
+
+ await screen.getByLabelText("Title").fill("Changed title");
+ await screen.getByRole("textbox", { name: "Metadata" }).fill('{"theme": ');
+
+ await vi.advanceTimersByTimeAsync(2100);
+ expect(onAutosave).not.toHaveBeenCalled();
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+
it("SaveButton shows correct dirty state for new items", async () => {
const screen = await renderEditor({ isNew: true });
// New items are always dirty
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ee3a8a5f..b56fcdef 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -473,6 +473,18 @@ importers:
'@cloudflare/kumo':
specifier: ^1.16.0
version: 1.16.0(@phosphor-icons/react@2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(echarts@6.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6)
+ '@codemirror/lang-json':
+ specifier: ^6.0.1
+ version: 6.0.2
+ '@codemirror/language':
+ specifier: ^6.10.8
+ version: 6.12.3
+ '@codemirror/state':
+ specifier: ^6.5.2
+ version: 6.6.0
+ '@codemirror/view':
+ specifier: ^6.36.3
+ version: 6.41.0
'@dnd-kit/core':
specifier: ^6.3.1
version: 6.3.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -2053,6 +2065,18 @@ packages:
'@cloudflare/workers-types@4.20260305.1':
resolution: {integrity: sha512-835BZaIcgjuYIUqgOWJSpwQxFSJ8g/X1OCZFLO7bmirM6TGmVgIGwiGItBgkjUXXCPrYzJEldsJkuFuK7ePuMw==}
+ '@codemirror/lang-json@6.0.2':
+ resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
+
+ '@codemirror/language@6.12.3':
+ resolution: {integrity: sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==}
+
+ '@codemirror/state@6.6.0':
+ resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==}
+
+ '@codemirror/view@6.41.0':
+ resolution: {integrity: sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA==}
+
'@colors/colors@1.5.0':
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
engines: {node: '>=0.1.90'}
@@ -2675,6 +2699,18 @@ packages:
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
+ '@lezer/common@1.5.1':
+ resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==}
+
+ '@lezer/highlight@1.2.3':
+ resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
+
+ '@lezer/json@1.0.3':
+ resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
+
+ '@lezer/lr@1.4.8':
+ resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==}
+
'@libsql/client@0.8.1':
resolution: {integrity: sha512-xGg0F4iTDFpeBZ0r4pA6icGsYa5rG6RAG+i/iLDnpCAnSuTqEWMDdPlVseiq4Z/91lWI9jvvKKiKpovqJ1kZWA==}
@@ -2740,6 +2776,9 @@ packages:
'@manypkg/get-packages@1.1.3':
resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
+ '@marijn/find-cluster-break@1.0.2':
+ resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
+
'@mdx-js/mdx@3.1.1':
resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
@@ -7514,6 +7553,9 @@ packages:
resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==}
engines: {node: '>=14.16'}
+ style-mod@4.1.3:
+ resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
+
style-to-js@1.1.21:
resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
@@ -8527,7 +8569,7 @@ snapshots:
dependencies:
'@astrojs/internal-helpers': 0.8.0
'@astrojs/underscore-redirects': 1.0.3
- '@cloudflare/vite-plugin': 1.26.1(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))(workerd@1.20260401.1)(wrangler@4.80.0)
+ '@cloudflare/vite-plugin': 1.26.1(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))(workerd@1.20260401.1)(wrangler@4.80.0(@cloudflare/workers-types@4.20260305.1))
astro: 6.0.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.55.2)(tsx@4.21.0)(typescript@6.0.0-beta)(yaml@2.8.2)
piccolore: 0.1.3
tinyglobby: 0.2.15
@@ -8553,7 +8595,7 @@ snapshots:
dependencies:
'@astrojs/internal-helpers': 0.8.0
'@astrojs/underscore-redirects': 1.0.3
- '@cloudflare/vite-plugin': 1.26.1(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))(workerd@1.20260401.1)(wrangler@4.80.0)
+ '@cloudflare/vite-plugin': 1.26.1(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))(workerd@1.20260401.1)(wrangler@4.80.0(@cloudflare/workers-types@4.20260305.1))
astro: 6.1.3(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.55.2)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
piccolore: 0.1.3
tinyglobby: 0.2.15
@@ -9308,7 +9350,7 @@ snapshots:
optionalDependencies:
workerd: 1.20260401.1
- '@cloudflare/vite-plugin@1.26.1(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))(workerd@1.20260401.1)(wrangler@4.80.0)':
+ '@cloudflare/vite-plugin@1.26.1(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))(workerd@1.20260401.1)(wrangler@4.80.0(@cloudflare/workers-types@4.20260305.1))':
dependencies:
'@cloudflare/unenv-preset': 2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260401.1)
miniflare: 4.20260301.1
@@ -9353,6 +9395,31 @@ snapshots:
'@cloudflare/workers-types@4.20260305.1': {}
+ '@codemirror/lang-json@6.0.2':
+ dependencies:
+ '@codemirror/language': 6.12.3
+ '@lezer/json': 1.0.3
+
+ '@codemirror/language@6.12.3':
+ dependencies:
+ '@codemirror/state': 6.6.0
+ '@codemirror/view': 6.41.0
+ '@lezer/common': 1.5.1
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.8
+ style-mod: 4.1.3
+
+ '@codemirror/state@6.6.0':
+ dependencies:
+ '@marijn/find-cluster-break': 1.0.2
+
+ '@codemirror/view@6.41.0':
+ dependencies:
+ '@codemirror/state': 6.6.0
+ crelt: 1.0.6
+ style-mod: 4.1.3
+ w3c-keyname: 2.2.8
+
'@colors/colors@1.5.0':
optional: true
@@ -9791,6 +9858,22 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@lezer/common@1.5.1': {}
+
+ '@lezer/highlight@1.2.3':
+ dependencies:
+ '@lezer/common': 1.5.1
+
+ '@lezer/json@1.0.3':
+ dependencies:
+ '@lezer/common': 1.5.1
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.8
+
+ '@lezer/lr@1.4.8':
+ dependencies:
+ '@lezer/common': 1.5.1
+
'@libsql/client@0.8.1':
dependencies:
'@libsql/core': 0.8.1
@@ -9881,6 +9964,8 @@ snapshots:
globby: 11.1.0
read-yaml-file: 1.1.0
+ '@marijn/find-cluster-break@1.0.2': {}
+
'@mdx-js/mdx@3.1.1':
dependencies:
'@types/estree': 1.0.8
@@ -15589,6 +15674,8 @@ snapshots:
strip-json-comments@5.0.3: {}
+ style-mod@4.1.3: {}
+
style-to-js@1.1.21:
dependencies:
style-to-object: 1.0.14