Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@ The visual JSON editor. Schema-aware, embeddable, extensible.
<td><a href="packages/@visual-json/vue"><code>@visual-json/vue</code></a></td>
<td>Vue UI components (TreeView, FormView, DiffView, and more)</td>
</tr>
<tr>
<td><a href="packages/@visual-json/yaml"><code>@visual-json/yaml</code></a></td>
<td>YAML support — parse, serialize, and schema-detect YAML files</td>
</tr>
<tr>
<td><a href="apps/vscode"><code>@visual-json/vscode</code></a></td>
<td>VS Code extension — visual JSON editor with tree sidebar, schema support, and JSONC</td>
<td>VS Code extension — visual editor with tree sidebar, schema support, JSONC, and YAML</td>
</tr>
</tbody>
</table>
Expand Down
7 changes: 4 additions & 3 deletions apps/vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ VS Code extension for [visual-json](https://github.com/vercel-labs/visual-json)

## Features

- **Visual editor** for `.json` and `.jsonc` files via "Open With..." > "visual-json"
- **Visual editor** for `.json`, `.jsonc`, `.yaml`, and `.yml` files via "Open With..." > "visual-json"
- **YAML support** — open and edit YAML files with the same tree and form views used for JSON
- **Tree sidebar** with expand/collapse, drag-and-drop, keyboard navigation, and search
- **Form editor** with inline editing, schema-aware inputs, and breadcrumb navigation
- **Schema support** — auto-detects schemas from `$schema` or known filenames (`package.json`, `tsconfig.json`, etc.)
- **Schema support** — auto-detects schemas from `$schema` or known filenames (`package.json`, `tsconfig.json`, `docker-compose.yml`, GitHub Actions workflows, etc.)
- **Theme integration** — adapts to your VS Code color theme automatically
- **Explorer panel** — sidebar view that syncs with the active JSON file
- **Explorer panel** — sidebar view that syncs with the active JSON or YAML file

## Development

Expand Down
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@upstash/redis": "^1.36.2",
"@visual-json/core": "workspace:*",
"@visual-json/react": "workspace:*",
"@visual-json/yaml": "workspace:*",
"ai": "^6.0.94",
"bash-tool": "^1.3.15",
"class-variance-authority": "^0.7.1",
Expand Down
9 changes: 9 additions & 0 deletions apps/web/src/app/docs/changelog/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ export { metadata } from "./metadata";

# Changelog

## 0.4.0

### Features

- **YAML support** — new `@visual-json/yaml` package for parsing, serializing, and round-tripping YAML through the visual-json tree model. The VS Code extension now opens `.yaml` and `.yml` files with the same tree and form views used for JSON.
- **YAML schema detection** — `resolveSchema` in `@visual-json/core` now recognizes well-known YAML filenames (`docker-compose.yml`, `.gitlab-ci.yml`, `pnpm-workspace.yaml`, etc.) and glob patterns (`.github/workflows/*.yml`, `.github/actions/*/action.yml`) for automatic schema-aware editing.

---

## 0.3.0

### Features
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/app/docs/core/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,16 @@ console.log(history.canUndo, history.canRedo);
## Schema

Resolve JSON Schemas from well-known filenames (e.g. `package.json`, `tsconfig.json`) and
navigate schema properties by path.
navigate schema properties by path. YAML config files are supported too — `resolveSchema`
recognizes filenames like `docker-compose.yml`, `.gitlab-ci.yml`, `pnpm-workspace.yaml`,
and glob patterns like `.github/workflows/*.yml`.

```ts
import { resolveSchema, getPropertySchema, clearSchemaCache } from "@visual-json/core";

const schema = await resolveSchema(json, "package.json");
// Also works with YAML filenames:
// const schema = await resolveSchema(value, "docker-compose.yml");

if (schema) {
const propSchema = getPropertySchema(schema, "$.scripts.build");
Expand Down
12 changes: 8 additions & 4 deletions apps/web/src/app/docs/getting-started/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ visual-json is split into focused packages:
<tr><td><code>@visual-json/core</code></td><td>Headless tree model, operations, schema resolution, diffing, search, and validation</td></tr>
<tr><td><code>@visual-json/react</code></td><td>React UI components — TreeView, FormView, DiffView, and more</td></tr>
<tr><td><code>@visual-json/vue</code></td><td>Vue 3 UI components — TreeView, FormView, DiffView, and more</td></tr>
<tr><td><code>@visual-json/vscode</code></td><td>VS Code extension — open any JSON/JSONC file with a visual editor</td></tr>
<tr><td><code>@visual-json/yaml</code></td><td>YAML support — parse, serialize, and schema-detect YAML files</td></tr>
<tr><td><code>@visual-json/vscode</code></td><td>VS Code extension — open any JSON, JSONC, or YAML file with a visual editor</td></tr>
</tbody>
</table>

Expand Down Expand Up @@ -92,12 +93,15 @@ visual-json supports multiple ways to view and edit your data:

## VS Code Extension

visual-json is also available as a VS Code extension. Open any `.json` or `.jsonc` file,
visual-json is also available as a VS Code extension. Open any `.json`, `.jsonc`, `.yaml`, or `.yml` file,
right-click the tab, and choose **"Open With..." > "visual-json"** to use the visual editor.

The extension includes a tree sidebar, form editor, schema support (auto-detected from
`$schema` or known filenames like `package.json` and `tsconfig.json`), and adapts to your
VS Code theme colors automatically.
`$schema` or known filenames like `package.json`, `tsconfig.json`, `docker-compose.yml`,
GitHub Actions workflows, and more), and adapts to your VS Code theme colors automatically.

YAML files are parsed and serialized transparently — you edit them with the same tree and
form views used for JSON, and changes are saved back as YAML.

## Next Steps

Expand Down
98 changes: 63 additions & 35 deletions apps/web/src/app/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
import { useState, useCallback, useEffect, useRef } from "react";
import type { JsonValue, JsonSchema } from "@visual-json/core";
import { resolveSchema } from "@visual-json/core";
import {
isYamlFile,
parseYamlContent,
stringifyYamlContent,
} from "@visual-json/yaml";
import { JsonEditor } from "@visual-json/react";
import { Button } from "@/components/ui/button";
import {
Expand Down Expand Up @@ -34,6 +39,18 @@ import {
X,
} from "lucide-react";

function serializeValue(value: JsonValue, fname: string): string {
return isYamlFile(fname)
? stringifyYamlContent(value)
: JSON.stringify(value, null, 2);
}

function parseText(text: string, fname: string): JsonValue {
return isYamlFile(fname)
? parseYamlContent(text)
: (JSON.parse(text) as JsonValue);
}

type ViewMode = "tree" | "raw";

const VIEW_MODES: { id: ViewMode; label: string }[] = [
Expand Down Expand Up @@ -70,7 +87,7 @@ const samples: { name: string; filename: string; data: JsonValue }[] = [
},
{
name: "OpenAPI spec",
filename: "openapi.json",
filename: "openapi.yaml",
data: {
openapi: "3.1.0",
info: {
Expand Down Expand Up @@ -481,7 +498,7 @@ export function Editor({
const [editorShowDescriptions, setEditorShowDescriptions] = useState(false);
const [editorShowCounts, setEditorShowCounts] = useState(false);
const [rawText, setRawText] = useState(
JSON.stringify(samples[0].data, null, 2),
serializeValue(samples[0].data, samples[0].filename),
);
const [rawError, setRawError] = useState<string | null>(null);
const [parseError, setParseError] = useState<string | null>(null);
Expand All @@ -506,20 +523,20 @@ export function Editor({
skipRawSync.current = false;
return;
}
setRawText(JSON.stringify(jsonValue, null, 2));
}, [jsonValue]);
setRawText(serializeValue(jsonValue, filename));
}, [jsonValue, filename]);

const loadJson = useCallback((text: string, fname: string) => {
const loadContent = useCallback((text: string, fname: string) => {
try {
const parsed = JSON.parse(text);
const parsed = parseText(text, fname);
setJsonValue(parsed);
setFilename(fname);
setActiveSample(fname);
setSchema(null);
setRawError(null);
setParseError(null);
} catch {
setParseError("Invalid JSON");
setParseError(isYamlFile(fname) ? "Invalid YAML" : "Invalid JSON");
}
}, []);

Expand All @@ -537,24 +554,26 @@ export function Editor({
const handlePaste = useCallback(async () => {
try {
const text = await navigator.clipboard.readText();
loadJson(text, "pasted.json");
loadContent(text, "pasted.json");
} catch {
setPasteText("");
setPasteDialogOpen(true);
}
}, [loadJson]);
}, [loadContent]);

const handlePasteSubmit = useCallback(() => {
if (pasteText.trim()) {
loadJson(pasteText, "pasted.json");
loadContent(pasteText, "pasted.json");
}
setPasteDialogOpen(false);
setPasteText("");
}, [pasteText, loadJson]);
}, [pasteText, loadContent]);

const handleDownload = useCallback(() => {
const text = JSON.stringify(jsonValue, null, 2);
const blob = new Blob([text], { type: "application/json" });
const yaml = isYamlFile(filename);
const text = serializeValue(jsonValue, filename);
const mime = yaml ? "text/yaml" : "application/json";
const blob = new Blob([text], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
Expand All @@ -565,23 +584,32 @@ export function Editor({

const handleCopyJson = useCallback(async () => {
try {
await navigator.clipboard.writeText(JSON.stringify(jsonValue, null, 2));
await navigator.clipboard.writeText(serializeValue(jsonValue, filename));
} catch {
// clipboard access may be denied
}
}, [jsonValue]);
}, [jsonValue, filename]);

const handleRawChange = useCallback((newText: string) => {
setRawText(newText);
try {
const parsed = JSON.parse(newText);
setRawError(null);
skipRawSync.current = true;
setJsonValue(parsed);
} catch (e) {
setRawError(e instanceof Error ? e.message : "Invalid JSON");
}
}, []);
const handleRawChange = useCallback(
(newText: string) => {
setRawText(newText);
try {
const parsed = parseText(newText, filename);
setRawError(null);
skipRawSync.current = true;
setJsonValue(parsed);
} catch (e) {
setRawError(
e instanceof Error
? e.message
: isYamlFile(filename)
? "Invalid YAML"
: "Invalid JSON",
);
}
},
[filename],
);

useEffect(() => {
const el = dropRef.current;
Expand All @@ -606,7 +634,7 @@ export function Editor({
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result === "string")
loadJson(reader.result, file.name);
loadContent(reader.result, file.name);
};
reader.readAsText(file);
}
Expand All @@ -619,7 +647,7 @@ export function Editor({
el.removeEventListener("dragleave", handleDragLeave);
el.removeEventListener("drop", handleDrop);
};
}, [loadJson]);
}, [loadContent]);

return (
<div
Expand All @@ -630,7 +658,7 @@ export function Editor({
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/60 pointer-events-none">
<div className="border-2 border-dashed border-primary rounded-lg p-8">
<span className="text-foreground text-lg font-mono">
Drop JSON file here
Drop JSON or YAML file here
</span>
</div>
</div>
Expand Down Expand Up @@ -681,14 +709,14 @@ export function Editor({
<input
ref={fileInputRef}
type="file"
accept=".json,.jsonc,.json5"
accept=".json,.jsonc,.json5,.yaml,.yml"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result === "string")
loadJson(reader.result, file.name);
loadContent(reader.result, file.name);
};
reader.readAsText(file);
}
Expand All @@ -712,7 +740,7 @@ export function Editor({
size="icon"
className="h-7 w-7"
onClick={handlePaste}
title="Paste JSON"
title="Paste"
>
<ClipboardPaste className="h-3.5 w-3.5" />
</Button>
Expand All @@ -730,7 +758,7 @@ export function Editor({
size="icon"
className="h-7 w-7"
onClick={handleCopyJson}
title="Copy JSON"
title="Copy"
>
<Copy className="h-3.5 w-3.5" />
</Button>
Expand Down Expand Up @@ -863,12 +891,12 @@ export function Editor({
<Dialog open={pasteDialogOpen} onOpenChange={setPasteDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Paste JSON</DialogTitle>
<DialogTitle>Paste JSON or YAML</DialogTitle>
</DialogHeader>
<textarea
value={pasteText}
onChange={(e) => setPasteText(e.target.value)}
placeholder="Paste your JSON here..."
placeholder="Paste your JSON or YAML here..."
spellCheck={false}
className="min-h-[200px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm font-mono resize-none outline-none"
/>
Expand Down
Loading
Loading