Skip to content

Commit b724fdc

Browse files
Copilotaarne
andcommitted
feat: add Bridge Playground — client-side Vite + React static site
Co-authored-by: aarne <82001+aarne@users.noreply.github.com>
1 parent 9cb0f38 commit b724fdc

11 files changed

Lines changed: 1627 additions & 0 deletions

File tree

packages/playground/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Bridge Playground</title>
7+
</head>
8+
<body>
9+
<div id="root"></div>
10+
<script type="module" src="/src/main.tsx"></script>
11+
</body>
12+
</html>

packages/playground/package.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "@stackables/bridge-playground",
3+
"version": "0.0.1",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc -p tsconfig.json --noEmit && vite build",
9+
"preview": "vite preview"
10+
},
11+
"dependencies": {
12+
"@stackables/bridge": "workspace:*",
13+
"graphql": "^16",
14+
"react": "^19.1.0",
15+
"react-dom": "^19.1.0"
16+
},
17+
"devDependencies": {
18+
"@types/react": "^19.1.6",
19+
"@types/react-dom": "^19.1.5",
20+
"@vitejs/plugin-react": "^4.5.2",
21+
"typescript": "^5.9.3",
22+
"vite": "^6.3.5"
23+
}
24+
}

packages/playground/src/App.tsx

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { useState, useCallback } from "react";
2+
import type { CSSProperties } from "react";
3+
import { Editor } from "./components/Editor";
4+
import { ResultView } from "./components/ResultView";
5+
import { examples } from "./examples";
6+
import { runBridge, getDiagnostics } from "./engine";
7+
import type { RunResult } from "./engine";
8+
9+
const PANEL_STYLE: CSSProperties = {
10+
background: "#1e293b",
11+
borderRadius: 12,
12+
padding: 20,
13+
display: "flex",
14+
flexDirection: "column",
15+
gap: 16,
16+
};
17+
18+
export function App() {
19+
const [exampleIndex, setExampleIndex] = useState(0);
20+
const ex = examples[exampleIndex] ?? examples[0]!;
21+
22+
const [schema, setSchema] = useState(ex.schema);
23+
const [bridge, setBridge] = useState(ex.bridge);
24+
const [query, setQuery] = useState(ex.query);
25+
const [result, setResult] = useState<RunResult | null>(null);
26+
const [loading, setLoading] = useState(false);
27+
28+
const selectExample = useCallback((index: number) => {
29+
const e = examples[index] ?? examples[0]!;
30+
setExampleIndex(index);
31+
setSchema(e.schema);
32+
setBridge(e.bridge);
33+
setQuery(e.query);
34+
setResult(null);
35+
}, []);
36+
37+
const handleRun = useCallback(async () => {
38+
setLoading(true);
39+
setResult(null);
40+
try {
41+
const r = await runBridge(schema, bridge, query);
42+
setResult(r);
43+
} finally {
44+
setLoading(false);
45+
}
46+
}, [schema, bridge, query]);
47+
48+
const diagnostics = getDiagnostics(bridge).diagnostics;
49+
const hasErrors = diagnostics.some((d) => d.severity === "error");
50+
51+
return (
52+
<div style={{
53+
minHeight: "100vh",
54+
background: "#0f172a",
55+
color: "#e2e8f0",
56+
fontFamily: "system-ui, -apple-system, sans-serif",
57+
display: "flex",
58+
flexDirection: "column",
59+
}}>
60+
{/* Header */}
61+
<header style={{
62+
borderBottom: "1px solid #1e293b",
63+
padding: "14px 24px",
64+
display: "flex",
65+
alignItems: "center",
66+
gap: 24,
67+
flexWrap: "wrap",
68+
}}>
69+
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
70+
<span style={{ fontSize: 22, fontWeight: 700, color: "#38bdf8", letterSpacing: "-0.02em" }}>
71+
Bridge
72+
</span>
73+
<span style={{
74+
background: "#164e63",
75+
color: "#38bdf8",
76+
fontSize: 11,
77+
fontWeight: 600,
78+
padding: "2px 8px",
79+
borderRadius: 99,
80+
letterSpacing: "0.05em",
81+
}}>
82+
PLAYGROUND
83+
</span>
84+
</div>
85+
<nav style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
86+
{examples.map((e, i) => (
87+
<button
88+
key={i}
89+
onClick={() => selectExample(i)}
90+
style={{
91+
padding: "5px 14px",
92+
borderRadius: 6,
93+
border: "1px solid",
94+
borderColor: i === exampleIndex ? "#38bdf8" : "#334155",
95+
background: i === exampleIndex ? "#0c4a6e" : "transparent",
96+
color: i === exampleIndex ? "#38bdf8" : "#94a3b8",
97+
fontSize: 13,
98+
cursor: "pointer",
99+
fontWeight: i === exampleIndex ? 600 : 400,
100+
transition: "all 0.15s",
101+
}}
102+
>
103+
{e.name}
104+
</button>
105+
))}
106+
</nav>
107+
<div style={{ marginLeft: "auto", color: "#475569", fontSize: 12 }}>
108+
All code runs in-browser · no server required
109+
</div>
110+
</header>
111+
112+
{/* Description */}
113+
<div style={{ padding: "10px 24px", color: "#64748b", fontSize: 13 }}>
114+
{ex.description}
115+
</div>
116+
117+
{/* Main content */}
118+
<main style={{
119+
flex: 1,
120+
padding: "0 24px 24px",
121+
display: "grid",
122+
gridTemplateColumns: "1fr 1fr",
123+
gridTemplateRows: "auto auto",
124+
gap: 16,
125+
}}>
126+
{/* Left top: Schema */}
127+
<div style={PANEL_STYLE}>
128+
<Editor label="GraphQL Schema" value={schema} onChange={setSchema} height="220px" />
129+
</div>
130+
131+
{/* Right top: Bridge DSL */}
132+
<div style={{ ...PANEL_STYLE, gridRow: "1 / 3" }}>
133+
<Editor label="Bridge DSL" value={bridge} onChange={setBridge} height="400px" />
134+
{diagnostics.length > 0 && (
135+
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
136+
<label style={{ fontSize: 12, fontWeight: 600, color: "#94a3b8", textTransform: "uppercase", letterSpacing: "0.05em" }}>
137+
Diagnostics
138+
</label>
139+
<div style={{
140+
background: "#0f172a",
141+
border: "1px solid #1e293b",
142+
borderRadius: 8,
143+
padding: "8px 12px",
144+
}}>
145+
{diagnostics.map((d, i) => (
146+
<div key={i} style={{
147+
color: d.severity === "error" ? "#fca5a5" : "#fde68a",
148+
fontFamily: "monospace",
149+
fontSize: 12,
150+
display: "flex",
151+
gap: 8,
152+
}}>
153+
<span style={{ opacity: 0.6 }}>
154+
{d.severity === "error" ? "✗" : "⚠"}
155+
</span>
156+
<span>
157+
{d.message}
158+
{" "}(line {d.range.start.line + 1})
159+
</span>
160+
</div>
161+
))}
162+
</div>
163+
</div>
164+
)}
165+
</div>
166+
167+
{/* Left bottom: Query + Run + Result */}
168+
<div style={PANEL_STYLE}>
169+
<Editor label="GraphQL Query" value={query} onChange={setQuery} height="140px" />
170+
<button
171+
onClick={handleRun}
172+
disabled={loading || hasErrors}
173+
style={{
174+
padding: "10px 24px",
175+
background: loading || hasErrors ? "#1e3a4a" : "#0ea5e9",
176+
color: loading || hasErrors ? "#475569" : "#fff",
177+
border: "none",
178+
borderRadius: 8,
179+
fontSize: 14,
180+
fontWeight: 600,
181+
cursor: loading || hasErrors ? "not-allowed" : "pointer",
182+
transition: "background 0.15s",
183+
alignSelf: "flex-start",
184+
}}
185+
>
186+
{loading ? "Running…" : "▶ Run"}
187+
</button>
188+
<ResultView
189+
result={result?.data}
190+
errors={result?.errors}
191+
loading={loading}
192+
/>
193+
</div>
194+
</main>
195+
</div>
196+
);
197+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
type Props = {
3+
label: string;
4+
value: string;
5+
onChange: (value: string) => void;
6+
height?: string;
7+
language?: string;
8+
};
9+
10+
export function Editor({ label, value, onChange, height = "200px" }: Props) {
11+
return (
12+
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
13+
<label style={{ fontSize: 12, fontWeight: 600, color: "#94a3b8", textTransform: "uppercase", letterSpacing: "0.05em" }}>
14+
{label}
15+
</label>
16+
<textarea
17+
value={value}
18+
onChange={(e) => onChange(e.target.value)}
19+
spellCheck={false}
20+
style={{
21+
height,
22+
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace",
23+
fontSize: 13,
24+
lineHeight: 1.6,
25+
padding: "10px 14px",
26+
background: "#0f172a",
27+
color: "#e2e8f0",
28+
border: "1px solid #1e293b",
29+
borderRadius: 8,
30+
resize: "vertical",
31+
outline: "none",
32+
caretColor: "#38bdf8",
33+
boxSizing: "border-box",
34+
width: "100%",
35+
}}
36+
onFocus={(e) => { e.target.style.borderColor = "#38bdf8"; }}
37+
onBlur={(e) => { e.target.style.borderColor = "#1e293b"; }}
38+
/>
39+
</div>
40+
);
41+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
2+
type Props = {
3+
result: unknown | null;
4+
errors: string[] | undefined;
5+
loading: boolean;
6+
};
7+
8+
export function ResultView({ result, errors, loading }: Props) {
9+
if (loading) {
10+
return (
11+
<div style={{ padding: 20, color: "#94a3b8", fontFamily: "monospace", fontSize: 13 }}>
12+
Running…
13+
</div>
14+
);
15+
}
16+
17+
if (!result && !errors) {
18+
return (
19+
<div style={{ padding: 20, color: "#475569", fontFamily: "monospace", fontSize: 13 }}>
20+
Press <strong style={{ color: "#94a3b8" }}>Run</strong> to execute the query.
21+
</div>
22+
);
23+
}
24+
25+
return (
26+
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
27+
{errors && errors.length > 0 && (
28+
<div style={{
29+
background: "#450a0a",
30+
border: "1px solid #7f1d1d",
31+
borderRadius: 8,
32+
padding: "10px 14px",
33+
}}>
34+
<div style={{ fontSize: 12, fontWeight: 600, color: "#fca5a5", marginBottom: 6, textTransform: "uppercase", letterSpacing: "0.05em" }}>
35+
Errors
36+
</div>
37+
{errors.map((err, i) => (
38+
<div key={i} style={{ color: "#fca5a5", fontFamily: "monospace", fontSize: 13 }}>{err}</div>
39+
))}
40+
</div>
41+
)}
42+
{result !== undefined && (
43+
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
44+
<label style={{ fontSize: 12, fontWeight: 600, color: "#94a3b8", textTransform: "uppercase", letterSpacing: "0.05em" }}>
45+
Result
46+
</label>
47+
<pre style={{
48+
background: "#0f172a",
49+
border: "1px solid #1e293b",
50+
borderRadius: 8,
51+
padding: "10px 14px",
52+
margin: 0,
53+
color: "#86efac",
54+
fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, monospace",
55+
fontSize: 13,
56+
lineHeight: 1.6,
57+
overflowX: "auto",
58+
minHeight: 60,
59+
}}>
60+
{JSON.stringify(result, null, 2)}
61+
</pre>
62+
</div>
63+
)}
64+
</div>
65+
);
66+
}

0 commit comments

Comments
 (0)