diff --git a/packages/docs-site/src/components/MiniPlaygroundModal.tsx b/packages/docs-site/src/components/MiniPlaygroundModal.tsx
new file mode 100644
index 00000000..4812f951
--- /dev/null
+++ b/packages/docs-site/src/components/MiniPlaygroundModal.tsx
@@ -0,0 +1,21 @@
+import {
+ DialogPlayground,
+ type DialogPlaygroundProps,
+} from "@stackables/bridge-playground";
+// Import playground styles - the docs-site custom.css overrides the base layer
+// to prevent it from breaking Starlight layout
+import "@stackables/bridge-playground/style.css";
+
+/**
+ * A button that opens a modal with MiniPlayground.
+ * Used in documentation to provide interactive examples.
+ */
+export default function MiniPlaygroundModal(props: DialogPlaygroundProps) {
+ // not-content prevents Starlight's .sl-markdown-content styles (e.g. button
+ // height overrides, typography resets) from affecting the playground.
+ return (
+
+
+
+ );
+}
diff --git a/packages/docs-site/src/components/PlaygroundWrapper.tsx b/packages/docs-site/src/components/PlaygroundWrapper.tsx
index a3b6a022..a244bdcf 100644
--- a/packages/docs-site/src/components/PlaygroundWrapper.tsx
+++ b/packages/docs-site/src/components/PlaygroundWrapper.tsx
@@ -1,6 +1,6 @@
-import { Playground } from "@stackables/bridge-playground";
+import { App } from "@stackables/bridge-playground";
import "@stackables/bridge-playground/style.css";
export default function PlaygroundWrapper() {
- return ;
+ return ;
}
diff --git a/packages/docs-site/src/content/docs/reference/30-wiring-routing.mdx b/packages/docs-site/src/content/docs/reference/30-wiring-routing.mdx
index ac1c0dd2..ad8c3309 100644
--- a/packages/docs-site/src/content/docs/reference/30-wiring-routing.mdx
+++ b/packages/docs-site/src/content/docs/reference/30-wiring-routing.mdx
@@ -4,6 +4,7 @@ description: How to move data from sources to targets, build nested payloads, an
---
import { Aside } from "@astrojs/starlight/components";
+import MiniPlaygroundModal from "../../../components/MiniPlaygroundModal.tsx";
Wires are the fundamental building blocks of The Bridge. They define the exact paths data takes as it flows through your graph.
@@ -32,6 +33,49 @@ o.country = "Germany"
```
+
+
## 2. Path Scoping Blocks (Nested Objects)
When you are wiring deeply nested JSON objects (like a complex REST API request body), repeating long path prefixes over and over becomes tedious and difficult to read.
diff --git a/packages/docs-site/src/styles/custom.css b/packages/docs-site/src/styles/custom.css
index a19b1eee..cfd190ce 100644
--- a/packages/docs-site/src/styles/custom.css
+++ b/packages/docs-site/src/styles/custom.css
@@ -1,3 +1,28 @@
+@layer theme, base, starlight, catppuccin, components, utilities;
+@import "tailwindcss";
+/* Source playground components for Tailwind to detect their classes */
+/* @source "../../../playground/src"; */
+@source "../components";
+
+@theme {
+ /* Monospace font stack for code editors - same as playground */
+ --font-mono:
+ "JetBrains Mono", "Fira Code", "Cascadia Code", Consolas, monospace;
+
+ /* Custom accent colors from playground */
+ --color-sky-badge: #164e63;
+ --color-sky-trace: #1e3a5f;
+}
+
+/* Override Tailwind's base layer to prevent it breaking Starlight's layout */
+@layer base {
+ html,
+ body {
+ /* Reset to auto to let Starlight control layout */
+ height: auto;
+ }
+}
+
/* Widen the default content column (Starlight default: 45rem / 720px) */
/* :root {
--sl-content-width: 55rem;
diff --git a/packages/docs-site/worker-configuration.d.ts b/packages/docs-site/worker-configuration.d.ts
index 134c788d..2113d00f 100644
--- a/packages/docs-site/worker-configuration.d.ts
+++ b/packages/docs-site/worker-configuration.d.ts
@@ -1,6 +1,6 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types` (hash: e41227086db6ad8bb19b68d77b165868)
-// Runtime types generated with workerd@1.20260305.0 2026-02-24 global_fetch_strictly_public,nodejs_compat
+// Runtime types generated with workerd@1.20260301.1 2026-02-24 global_fetch_strictly_public,nodejs_compat
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import("./dist/_worker.js/index");
@@ -3328,6 +3328,12 @@ declare abstract class Performance {
get timeOrigin(): number;
/* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancenow) */
now(): number;
+ /**
+ * The **`toJSON()`** method of the Performance interface is a Serialization; it returns a JSON representation of the Performance object.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Performance/toJSON)
+ */
+ toJSON(): object;
}
// AI Search V2 API Error Interfaces
interface AiSearchInternalError extends Error {
diff --git a/packages/playground/package.json b/packages/playground/package.json
index f454f9ab..44c202b2 100644
--- a/packages/playground/package.json
+++ b/packages/playground/package.json
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"exports": {
- ".": "./src/Playground.tsx",
+ ".": "./src/index.ts",
"./style.css": "./src/index.css"
},
"scripts": {
diff --git a/packages/playground/src/App.tsx b/packages/playground/src/App.tsx
index 2587d2e5..4adcb4a0 100644
--- a/packages/playground/src/App.tsx
+++ b/packages/playground/src/App.tsx
@@ -1,28 +1,7 @@
-import { useState, useCallback, useEffect, useMemo, useRef } from "react";
-import {
- Panel,
- Group,
- Separator,
- useDefaultLayout,
-} from "react-resizable-panels";
-import { Editor } from "./components/Editor";
-import { ResultView } from "./components/ResultView";
-import { StandaloneQueryPanel } from "./components/StandaloneQueryPanel";
+import { useEffect } from "react";
+import { Playground } from "./Playground";
import { examples } from "./examples";
-import {
- runBridge,
- runBridgeStandalone,
- getDiagnostics,
- extractBridgeOperations,
- extractOutputFields,
- extractInputSkeleton,
- mergeInputSkeleton,
- clearHttpCache,
- formatBridge,
-} from "./engine";
-import type { RunResult } from "./engine";
-import { buildSchema, type GraphQLSchema } from "graphql";
-import { Button } from "@/components/ui/button";
+import { usePlaygroundState } from "./usePlaygroundState";
import {
Select,
SelectContent,
@@ -30,330 +9,22 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
-import { cn } from "@/lib/utils";
import { ShareDialog } from "./components/ShareDialog";
-import {
- getShareIdFromUrl,
- loadShare,
- clearShareIdFromUrl,
- type PlaygroundMode,
-} from "./share";
+import { getShareIdFromUrl, loadShare, clearShareIdFromUrl } from "./share";
import { ChevronLeftIcon } from "lucide-react";
-// ── resize handle — transparent hit area, no visual indicator ────────────────
-function ResizeHandle({ direction }: { direction: "horizontal" | "vertical" }) {
- return (
-
- );
-}
-
-// ── query tab type ────────────────────────────────────────────────────────────
-type QueryTab = {
- id: string;
- name: string;
- /** Whether the name was explicitly set by the user (disables auto-rename). */
- nameManual?: boolean;
- /** GraphQL query text (graphql mode). */
- query: string;
- /** Standalone mode fields — parallel to the graphql `query` field. */
- operation?: string;
- outputFields?: string;
- inputJson?: string;
-};
-
-// ── extract GraphQL operation name from query text ───────────────────────────
-function extractOperationName(query: string): string | null {
- // Named operation: query/mutation/subscription OpName
- const named =
- /^\s*(?:query|mutation|subscription)\s+([A-Za-z_][A-Za-z0-9_]*)/m.exec(
- query,
- );
- if (named) return named[1]!;
- // Anonymous shorthand { fieldName ... } — use first root field
- const anon = /^\s*\{\s*([A-Za-z_][A-Za-z0-9_]*)/m.exec(query);
- if (anon) return anon[1]!;
- return null;
-}
-
-// ── query tab bar ─────────────────────────────────────────────────────────────
-type QueryTabBarProps = {
- queries: QueryTab[];
- activeTabId: string;
- onSelectTab: (id: string) => void;
- onAddQuery: () => void;
- onRemoveQuery: (id: string) => void;
- onRenameQuery: (id: string, name: string) => void;
- onRun: () => void;
- runDisabled: boolean;
- running: boolean;
- showRunButton?: boolean;
-};
-function QueryTabBar({
- queries,
- activeTabId,
- onSelectTab,
- onAddQuery,
- onRemoveQuery,
- onRenameQuery,
- onRun,
- runDisabled,
- running,
- showRunButton = true,
-}: QueryTabBarProps) {
- const isQueryTab = activeTabId !== "context";
- const canRemove = queries.length > 1;
- const [editingId, setEditingId] = useState(null);
- const editRef = useRef(null);
-
- const commitRename = useCallback(
- (id: string) => {
- const val = editRef.current?.value.trim();
- if (val) onRenameQuery(id, val);
- setEditingId(null);
- },
- [onRenameQuery],
- );
-
- return (
-
- {/* Context tab — always first */}
-
-
- {/* One tab per query */}
- {queries.map((q) => (
-