Compare commits

...
Author SHA1 Message Date
Chris Tate eb72020a6c fix lint 2026-02-22 15:39:00 -06:00
Chris Tate c0113b6d32 use visual-json 2026-02-22 15:34:07 -06:00
9 changed files with 147 additions and 37 deletions
+122 -31
View File
@@ -16,17 +16,20 @@ import { CopyButton } from "./copy-button";
import { Toaster } from "./ui/sonner"; import { Toaster } from "./ui/sonner";
import { Header } from "./header"; import { Header } from "./header";
import { Sheet, SheetContent, SheetTitle } from "./ui/sheet"; import { Sheet, SheetContent, SheetTitle } from "./ui/sheet";
import { JsonEditor } from "@visual-json/react";
import type { JsonValue } from "@visual-json/react";
import { PlaygroundRenderer } from "@/lib/render/renderer"; import { PlaygroundRenderer } from "@/lib/render/renderer";
import { playgroundCatalog } from "@/lib/render/catalog"; import { playgroundCatalog } from "@/lib/render/catalog";
import { buildCatalogDisplayData } from "@/lib/render/catalog-display"; import { buildCatalogDisplayData } from "@/lib/render/catalog-display";
type Tab = "json" | "nested" | "stream" | "catalog"; type Tab = "json" | "nested" | "stream" | "catalog" | "visual";
type RenderView = "preview" | "code"; type RenderView = "preview" | "code";
type MobileView = type MobileView =
| "json" | "json"
| "nested" | "nested"
| "stream" | "stream"
| "catalog" | "catalog"
| "visual"
| "preview" | "preview"
| "generated-code"; | "generated-code";
@@ -233,6 +236,20 @@ export function Playground() {
[handleSubmit], [handleSubmit],
); );
const handleVisualChange = useCallback(
(value: JsonValue) => {
if (!selectedVersionId || isStreaming) return;
setVersions((prev) =>
prev.map((v) =>
v.id === selectedVersionId
? { ...v, tree: value as unknown as Spec }
: v,
),
);
},
[selectedVersionId, isStreaming],
);
const jsonCode = currentTree const jsonCode = currentTree
? JSON.stringify(currentTree, null, 2) ? JSON.stringify(currentTree, null, 2)
: "// waiting..."; : "// waiting...";
@@ -479,31 +496,69 @@ ${jsx}
? jsonCode ? jsonCode
: activeTab === "nested" : activeTab === "nested"
? nestedCode ? nestedCode
: ""; : activeTab === "visual"
? jsonCode
: "";
const codePane = ( const codePane = (
<div className="h-full flex flex-col border-t border-border"> <div className="h-full flex flex-col border-t border-border">
<div className="border-b border-border px-3 h-9 flex items-center gap-3"> <div className="border-b border-border px-3 h-9 flex items-center gap-3">
{(["json", "nested", "stream", "catalog"] as const).map((tab) => ( {(["json", "visual", "nested", "stream", "catalog"] as const).map(
<button (tab) => (
key={tab} <button
onClick={() => setActiveTab(tab)} key={tab}
className={`text-xs font-mono transition-colors ${ onClick={() => setActiveTab(tab)}
activeTab === tab className={`text-xs font-mono transition-colors ${
? "text-foreground" activeTab === tab
: "text-muted-foreground hover:text-foreground" ? "text-foreground"
}`} : "text-muted-foreground hover:text-foreground"
> }`}
{tab} >
</button> {tab}
))} </button>
),
)}
<div className="flex-1" /> <div className="flex-1" />
{activeTab !== "catalog" && ( {activeTab !== "catalog" && activeTab !== "visual" && (
<CopyButton text={copyText} className="text-muted-foreground" /> <CopyButton text={copyText} className="text-muted-foreground" />
)} )}
</div> </div>
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
{activeTab === "catalog" ? ( {activeTab === "visual" ? (
currentTree ? (
<JsonEditor
value={currentTree as unknown as JsonValue}
onChange={handleVisualChange}
readOnly={isStreaming}
sidebarOpen={false}
height="100%"
className="h-full"
style={
{
"--vj-bg": "var(--background)",
"--vj-bg-panel": "var(--background)",
"--vj-bg-hover": "var(--muted)",
"--vj-bg-selected": "var(--primary)",
"--vj-bg-selected-muted": "var(--muted)",
"--vj-text": "var(--foreground)",
"--vj-text-selected": "var(--primary-foreground)",
"--vj-text-muted": "var(--muted-foreground)",
"--vj-text-dim": "var(--muted-foreground)",
"--vj-border": "var(--border)",
"--vj-border-subtle": "var(--border)",
"--vj-accent": "var(--primary)",
"--vj-accent-muted": "var(--muted)",
"--vj-input-bg": "var(--secondary)",
"--vj-input-border": "var(--border)",
} as React.CSSProperties
}
/>
) : (
<div className="text-muted-foreground/50 p-3 text-sm font-mono">
{"// generate a spec to edit visually"}
</div>
)
) : activeTab === "catalog" ? (
<div className="h-full flex flex-col text-sm"> <div className="h-full flex flex-col text-sm">
<div className="flex items-center gap-3 px-3 h-9 border-b border-border"> <div className="flex items-center gap-3 px-3 h-9 border-b border-border">
{( {(
@@ -735,19 +790,21 @@ ${jsx}
: 0} : 0}
</button> </button>
{/* Code tabs */} {/* Code tabs */}
{(["json", "nested", "stream", "catalog"] as const).map((tab) => ( {(["json", "visual", "nested", "stream", "catalog"] as const).map(
<button (tab) => (
key={tab} <button
onClick={() => setMobileView(tab)} key={tab}
className={`text-xs font-mono transition-colors shrink-0 ${ onClick={() => setMobileView(tab)}
mobileView === tab className={`text-xs font-mono transition-colors shrink-0 ${
? "text-foreground" mobileView === tab
: "text-muted-foreground hover:text-foreground" ? "text-foreground"
}`} : "text-muted-foreground hover:text-foreground"
> }`}
{tab} >
</button> {tab}
))} </button>
),
)}
<div className="flex-1" /> <div className="flex-1" />
{/* Preview / code toggle */} {/* Preview / code toggle */}
{[ {[
@@ -770,7 +827,41 @@ ${jsx}
{/* Main content area */} {/* Main content area */}
<div className="flex-1 min-h-0 overflow-auto"> <div className="flex-1 min-h-0 overflow-auto">
{mobileView === "catalog" ? ( {mobileView === "visual" ? (
currentTree ? (
<JsonEditor
value={currentTree as unknown as JsonValue}
onChange={handleVisualChange}
readOnly={isStreaming}
sidebarOpen={false}
height="100%"
className="h-full"
style={
{
"--vj-bg": "var(--background)",
"--vj-bg-panel": "var(--background)",
"--vj-bg-hover": "var(--muted)",
"--vj-bg-selected": "var(--primary)",
"--vj-bg-selected-muted": "var(--muted)",
"--vj-text": "var(--foreground)",
"--vj-text-selected": "var(--primary-foreground)",
"--vj-text-muted": "var(--muted-foreground)",
"--vj-text-dim": "var(--muted-foreground)",
"--vj-border": "var(--border)",
"--vj-border-subtle": "var(--border)",
"--vj-accent": "var(--primary)",
"--vj-accent-muted": "var(--muted)",
"--vj-input-bg": "var(--secondary)",
"--vj-input-border": "var(--border)",
} as React.CSSProperties
}
/>
) : (
<div className="text-muted-foreground/50 p-3 text-sm font-mono">
{"// generate a spec to edit visually"}
</div>
)
) : mobileView === "catalog" ? (
<div className="h-full flex flex-col text-sm"> <div className="h-full flex flex-col text-sm">
<div className="flex items-center gap-3 px-3 h-9 border-b border-border"> <div className="flex items-center gap-3 px-3 h-9 border-b border-border">
{( {(
+1
View File
@@ -28,6 +28,7 @@
"@upstash/redis": "^1.36.1", "@upstash/redis": "^1.36.1",
"@vercel/analytics": "^1.6.1", "@vercel/analytics": "^1.6.1",
"@vercel/speed-insights": "^1.3.1", "@vercel/speed-insights": "^1.3.1",
"@visual-json/react": "0.1.1",
"ai": "^6.0.33", "ai": "^6.0.33",
"bash-tool": "1.3.14", "bash-tool": "1.3.14",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+18
View File
@@ -98,6 +98,9 @@ importers:
'@vercel/speed-insights': '@vercel/speed-insights':
specifier: ^1.3.1 specifier: ^1.3.1
version: 1.3.1(next@16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) version: 1.3.1(next@16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)
'@visual-json/react':
specifier: 0.1.1
version: 0.1.1(react@19.2.3)
ai: ai:
specifier: ^6.0.33 specifier: ^6.0.33
version: 6.0.33(zod@4.3.5) version: 6.0.33(zod@4.3.5)
@@ -5009,6 +5012,14 @@ packages:
vue-router: vue-router:
optional: true optional: true
'@visual-json/core@0.1.1':
resolution: {integrity: sha512-VLARpQnjJU1SnnYk1NiXScGfp2FyAwyZS2kA8hIJV5OzkaH2oS1dTHsaB8vOiyi14/L7jsbpA4g/EwzRWbxQmg==}
'@visual-json/react@0.1.1':
resolution: {integrity: sha512-KCpFYmBhH5F+xctfvxxVX7mVZK/iwvDwh/ITNkGPzEKXTIJQ16fau+hrG7m7QoTAOxvsdhHtHoCUiEjNHXboUQ==}
peerDependencies:
react: ^18.0.0 || ^19.0.0
'@vitest/expect@4.0.17': '@vitest/expect@4.0.17':
resolution: {integrity: sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==} resolution: {integrity: sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ==}
@@ -16551,6 +16562,13 @@ snapshots:
next: 16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next: 16.1.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react: 19.2.3 react: 19.2.3
'@visual-json/core@0.1.1': {}
'@visual-json/react@0.1.1(react@19.2.3)':
dependencies:
'@visual-json/core': 0.1.1
react: 19.2.3
'@vitest/expect@4.0.17': '@vitest/expect@4.0.17':
dependencies: dependencies:
'@standard-schema/spec': 1.1.0 '@standard-schema/spec': 1.1.0