Forráskód Böngészése

use visual-json (#135)

* use visual-json

* fix lint
Chris Tate 4 hónapja
szülő
commit
5ccb109c08

+ 122 - 31
apps/web/components/playground.tsx

@@ -16,17 +16,20 @@ import { CopyButton } from "./copy-button";
 import { Toaster } from "./ui/sonner";
 import { Header } from "./header";
 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 { playgroundCatalog } from "@/lib/render/catalog";
 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 MobileView =
   | "json"
   | "nested"
   | "stream"
   | "catalog"
+  | "visual"
   | "preview"
   | "generated-code";
 
@@ -233,6 +236,20 @@ export function Playground() {
     [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
     ? JSON.stringify(currentTree, null, 2)
     : "// waiting...";
@@ -479,31 +496,69 @@ ${jsx}
         ? jsonCode
         : activeTab === "nested"
           ? nestedCode
-          : "";
+          : activeTab === "visual"
+            ? jsonCode
+            : "";
 
   const codePane = (
     <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">
-        {(["json", "nested", "stream", "catalog"] as const).map((tab) => (
-          <button
-            key={tab}
-            onClick={() => setActiveTab(tab)}
-            className={`text-xs font-mono transition-colors ${
-              activeTab === tab
-                ? "text-foreground"
-                : "text-muted-foreground hover:text-foreground"
-            }`}
-          >
-            {tab}
-          </button>
-        ))}
+        {(["json", "visual", "nested", "stream", "catalog"] as const).map(
+          (tab) => (
+            <button
+              key={tab}
+              onClick={() => setActiveTab(tab)}
+              className={`text-xs font-mono transition-colors ${
+                activeTab === tab
+                  ? "text-foreground"
+                  : "text-muted-foreground hover:text-foreground"
+              }`}
+            >
+              {tab}
+            </button>
+          ),
+        )}
         <div className="flex-1" />
-        {activeTab !== "catalog" && (
+        {activeTab !== "catalog" && activeTab !== "visual" && (
           <CopyButton text={copyText} className="text-muted-foreground" />
         )}
       </div>
       <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="flex items-center gap-3 px-3 h-9 border-b border-border">
               {(
@@ -735,19 +790,21 @@ ${jsx}
               : 0}
           </button>
           {/* Code tabs */}
-          {(["json", "nested", "stream", "catalog"] as const).map((tab) => (
-            <button
-              key={tab}
-              onClick={() => setMobileView(tab)}
-              className={`text-xs font-mono transition-colors shrink-0 ${
-                mobileView === tab
-                  ? "text-foreground"
-                  : "text-muted-foreground hover:text-foreground"
-              }`}
-            >
-              {tab}
-            </button>
-          ))}
+          {(["json", "visual", "nested", "stream", "catalog"] as const).map(
+            (tab) => (
+              <button
+                key={tab}
+                onClick={() => setMobileView(tab)}
+                className={`text-xs font-mono transition-colors shrink-0 ${
+                  mobileView === tab
+                    ? "text-foreground"
+                    : "text-muted-foreground hover:text-foreground"
+                }`}
+              >
+                {tab}
+              </button>
+            ),
+          )}
           <div className="flex-1" />
           {/* Preview / code toggle */}
           {[
@@ -770,7 +827,41 @@ ${jsx}
 
         {/* Main content area */}
         <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="flex items-center gap-3 px-3 h-9 border-b border-border">
                 {(

+ 1 - 0
apps/web/package.json

@@ -28,6 +28,7 @@
     "@upstash/redis": "^1.36.1",
     "@vercel/analytics": "^1.6.1",
     "@vercel/speed-insights": "^1.3.1",
+    "@visual-json/react": "0.1.1",
     "ai": "^6.0.33",
     "bash-tool": "1.3.14",
     "class-variance-authority": "^0.7.1",

+ 1 - 1
examples/chat/next-env.d.ts

@@ -1,6 +1,6 @@
 /// <reference types="next" />
 /// <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
 // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

+ 1 - 1
examples/dashboard/next-env.d.ts

@@ -1,6 +1,6 @@
 /// <reference types="next" />
 /// <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
 // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

+ 1 - 1
examples/no-ai/next-env.d.ts

@@ -1,6 +1,6 @@
 /// <reference types="next" />
 /// <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
 // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

+ 1 - 1
examples/react-pdf/next-env.d.ts

@@ -1,6 +1,6 @@
 /// <reference types="next" />
 /// <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
 // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

+ 1 - 1
examples/remotion/next-env.d.ts

@@ -1,6 +1,6 @@
 /// <reference types="next" />
 /// <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
 // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

+ 1 - 1
examples/stripe-app/api/next-env.d.ts

@@ -1,6 +1,6 @@
 /// <reference types="next" />
 /// <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
 // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

+ 18 - 0
pnpm-lock.yaml

@@ -98,6 +98,9 @@ importers:
       '@vercel/speed-insights':
         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)
+      '@visual-json/react':
+        specifier: 0.1.1
+        version: 0.1.1(react@19.2.3)
       ai:
         specifier: ^6.0.33
         version: 6.0.33(zod@4.3.5)
@@ -5009,6 +5012,14 @@ packages:
       vue-router:
         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':
     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)
       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':
     dependencies:
       '@standard-schema/spec': 1.1.0