diff --git a/.changeset/calm-empty-editor.md b/.changeset/calm-empty-editor.md
new file mode 100644
index 0000000..6e7a73c
--- /dev/null
+++ b/.changeset/calm-empty-editor.md
@@ -0,0 +1,11 @@
+---
+"@qorpe/ui": patch
+---
+
+fix(json-editor): an empty document is never a lint error
+
+The JSON linter flagged an empty editor as a parse error — a red dot on line 1 of
+nothing. A blank editor is an invitation, not a mistake; the linter now answers no
+diagnostics for a zero-length document and stays exactly as strict the moment content
+appears. Consumers no longer need to toggle `lint` off for the empty state — which
+also recreated the editor mid-typing, since `lint` participates in the setup effect.
diff --git a/src/json-editor/index.tsx b/src/json-editor/index.tsx
index a34b4a8..6cd1736 100644
--- a/src/json-editor/index.tsx
+++ b/src/json-editor/index.tsx
@@ -51,8 +51,12 @@ const extensions = (readOnly: boolean, onChange: ((v: string) => void) | undefin
bracketMatching(),
keymap.of([...defaultKeymap, ...historyKeymap, ...foldKeymap, indentWithTab]),
json(),
- // Bodies may hold template syntax ({{…}}), so linting is opt-out — no false parse errors.
- ...(lint ? [linter(jsonParseLinter()), lintGutter()] : []),
+ // Bodies may hold template syntax ({{…}}), so linting is opt-out — no false parse
+ // errors. An EMPTY document is never an error either: a blank editor is an invitation,
+ // and a red parse dot on line 1 of nothing teaches only confusion.
+ ...(lint
+ ? [linter((view) => (view.state.doc.length === 0 ? [] : jsonParseLinter()(view))), lintGutter()]
+ : []),
syntaxHighlighting(highlight),
theme,
EditorState.readOnly.of(readOnly),
diff --git a/src/json-editor/json-editor.test.tsx b/src/json-editor/json-editor.test.tsx
index b1ca402..b8bf366 100644
--- a/src/json-editor/json-editor.test.tsx
+++ b/src/json-editor/json-editor.test.tsx
@@ -17,6 +17,17 @@ describe("the json editor subpath (RFC D6) — CodeMirror stays out of the main
expect(container.textContent).not.toContain('"a"');
});
+ it("an EMPTY document is never a lint error — broken JSON still is", async () => {
+ // A blank editor is an invitation, not a mistake: no red dot on line 1 of nothing.
+ const empty = render();
+ const broken = render();
+ // CodeMirror's linter runs on a delay; the broken editor's marker is the clock.
+ await vi.waitFor(() => {
+ expect(broken.container.querySelector(".cm-lint-marker")).toBeTruthy();
+ }, { timeout: 3000 });
+ expect(empty.container.querySelector(".cm-lint-marker")).toBeNull();
+ });
+
it("Beautify pretty-prints valid JSON and leaves templated bodies untouched", async () => {
const onChange = vi.fn();
render();