@joycostudio/studio 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -4
- package/dist/{chunk-DEJYGBPJ.js → chunk-OEJWPWYJ.js} +1 -1
- package/dist/chunk-OEJWPWYJ.js.map +1 -0
- package/dist/{chunk-ZH3NEN4V.js → chunk-ZPKMVZ67.js} +103 -41
- package/dist/chunk-ZPKMVZ67.js.map +1 -0
- package/dist/cli.js +30 -11
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +33 -5
- package/dist/client.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/next.d.ts +3 -1
- package/dist/next.js +2 -2
- package/dist/{protocol-DjrMADPm.d.ts → protocol-BviGBdSG.d.ts} +7 -5
- package/dist/react.d.ts +46 -5
- package/dist/react.js +461 -362
- package/dist/react.js.map +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/ui.d.ts +43 -0
- package/dist/ui.js +80 -0
- package/dist/ui.js.map +1 -0
- package/package.json +13 -4
- package/dist/chunk-DEJYGBPJ.js.map +0 -1
- package/dist/chunk-ZH3NEN4V.js.map +0 -1
package/README.md
CHANGED
|
@@ -5,7 +5,8 @@ Plug project-specific tools into a `/studio` route, the app's floating debug pan
|
|
|
5
5
|
For contributor instructions on stable releases, betas, and commit previews, see the
|
|
6
6
|
[release workflow guide](../../.changeset/README.md).
|
|
7
7
|
|
|
8
|
-
A tool's controls are an [Xyz](https://hub.joyco.studio/toolbox/xyz) debug schema
|
|
8
|
+
A tool's controls are an [Xyz](https://hub.joyco.studio/toolbox/xyz) debug schema, drawn with Tweakpane or DialKit
|
|
9
|
+
(see [Choosing the controls UI](#choosing-the-controls-ui)). That same schema:
|
|
9
10
|
|
|
10
11
|
- renders the inspector in `/studio`,
|
|
11
12
|
- mounts in the app's floating pane (`?debug` / <kbd>Option</kbd>+<kbd>D</kbd>) through Xyz's `DebugManager`,
|
|
@@ -24,7 +25,8 @@ Open `/studio` and select Example. Move the Scale slider and Save to write the a
|
|
|
24
25
|
The initializer supports `app/` and `src/app/`, installs dependencies, wraps your dev command,
|
|
25
26
|
and adds generation to your typecheck script. It wraps the existing Next config with `withStudio`
|
|
26
27
|
so production builds also generate their registry on a clean checkout. The home page stays yours.
|
|
27
|
-
Use `studio init <directory>` to target another app,
|
|
28
|
+
Use `studio init <directory>` to target another app, `--ui dialkit` to draw controls with DialKit instead of
|
|
29
|
+
Tweakpane, or `--no-install` to prepare files without installing.
|
|
28
30
|
Reruns skip identical files; conflicting or edited files stop initialization before writes.
|
|
29
31
|
If generation fails, initialization restores existing files and removes newly created setup files.
|
|
30
32
|
Route checks include route groups and handlers. If Studio is already declared, install a version
|
|
@@ -131,9 +133,10 @@ in the application's TypeScript program. Scaffolded app files remain user-owned
|
|
|
131
133
|
```ts
|
|
132
134
|
// src/studio.ts (scaffolded once)
|
|
133
135
|
import { createStudio } from '@joycostudio/studio/next'
|
|
136
|
+
import { tweakpane } from '@joycostudio/studio/ui'
|
|
134
137
|
import { tools } from '../.studio/registry.generated'
|
|
135
138
|
|
|
136
|
-
export const studio = createStudio({ tools })
|
|
139
|
+
export const studio = createStudio({ tools, ui: tweakpane(() => import('tweakpane')) })
|
|
137
140
|
```
|
|
138
141
|
|
|
139
142
|
```ts
|
|
@@ -191,7 +194,7 @@ Explicit `defineTool({ id, file, initial, controls, preview })` definitions and
|
|
|
191
194
|
do not combine `tools` with `toolsDir`. To migrate, move the declarations into `studio.tool.ts`,
|
|
192
195
|
keep their IDs, replace `file`/`initial` with an optional `config` path, and default-export them.
|
|
193
196
|
Point the client at the generated registry and wrap Next config with `withStudio`.
|
|
194
|
-
For manual setups, install `@joycostudio/studio`, `@joycostudio/xyz`, and `tweakpane` in the app.
|
|
197
|
+
For manual setups, install `@joycostudio/studio`, `@joycostudio/xyz`, and `tweakpane` or `dialkit` in the app.
|
|
195
198
|
|
|
196
199
|
Live edits also work without `studio dev`, including in production. Studio shares configs between
|
|
197
200
|
same-origin previews and tabs in the same browser using `BroadcastChannel`. Peers match by tool ID,
|
|
@@ -214,6 +217,27 @@ Studio owns lazy pane creation, frame refresh, registration, and cleanup through
|
|
|
214
217
|
DebugManager. `?debug` and Option+D keep their existing behavior. The pane and command menu
|
|
215
218
|
are optional; neither is needed for `/studio`.
|
|
216
219
|
|
|
220
|
+
### Choosing the controls UI
|
|
221
|
+
|
|
222
|
+
The `/studio` inspector and the floating pane draw controls with the library the app passes as `ui`:
|
|
223
|
+
[Tweakpane](https://tweakpane.github.io/docs/) or [DialKit](https://github.com/joshpuckett/dialkit).
|
|
224
|
+
Install only the one you pick; it loads lazily from your app.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { tweakpane } from '@joycostudio/studio/ui'
|
|
228
|
+
createStudio({ tools, ui: tweakpane(() => import('tweakpane')) })
|
|
229
|
+
|
|
230
|
+
// or
|
|
231
|
+
import 'dialkit/vanilla/styles.css'
|
|
232
|
+
import { dialkit } from '@joycostudio/studio/ui'
|
|
233
|
+
createStudio({ tools, ui: dialkit(() => import('dialkit/vanilla')) })
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Tool contracts don't change: the same schema validates and draws with either one.
|
|
237
|
+
`dialkit(load, options)` takes DialKit's root options (`position`, `theme`, …) for the floating pane; the inspector
|
|
238
|
+
always draws it inline, in Studio's palette. `<Studio>` and `<StudioDebug>` need `ui`; a studio that only reads
|
|
239
|
+
configs (`useToolConfig`, `studio.tool(...).config`) doesn't.
|
|
240
|
+
|
|
217
241
|
The Next.js adapter reads the endpoint passed by `studio dev`, including port overrides and
|
|
218
242
|
collision fallback. No `next.config` changes or hand-written environment variables are needed.
|
|
219
243
|
It defaults to browser-only mode without the CLI or in production. Restart the dev server when
|
|
@@ -258,6 +282,20 @@ Undo is per tool and per page: it restores only the values you changed, leaving
|
|
|
258
282
|
in place, and syncs like any other edit. Text fields keep their native undo while focused. History is not persisted.
|
|
259
283
|
`tool.undo()`, `tool.redo()`, `tool.canUndo`, and `tool.canRedo` expose the same history to host code.
|
|
260
284
|
|
|
285
|
+
The inspector footer summarizes unsaved changes on one line. Click it to expand the footer into the full list: each changed
|
|
286
|
+
value shows its saved and current value (with a swatch for colors) and a revert button that resets just that value. A revert
|
|
287
|
+
syncs like any edit and can be undone. The list collapses once nothing is left to save. `tool.diff` lists the same
|
|
288
|
+
`{ path, before, after }` changes, and `tool.revert(path)` resets one from host code.
|
|
289
|
+
|
|
290
|
+
To share a config with a teammate, click the link button in the inspector footer (or **Copy link** in the floating debug
|
|
291
|
+
pane). It copies the current page's URL with the tool's draft encoded in a `studio-share` query parameter; links from
|
|
292
|
+
Studio also select the tool. Opening the link applies that draft as an unsaved edit on top of whatever the recipient has,
|
|
293
|
+
with or without a runtime: it syncs to their previews, other tabs, and runtime like any edit, can be undone, and still needs
|
|
294
|
+
Save to reach the JSON file. The parameter is removed from the address bar once applied, so reloading doesn't reapply it.
|
|
295
|
+
A draft that no longer matches the tool's controls (for example, from another branch) is ignored with a console warning.
|
|
296
|
+
If a configured runtime is unreachable when the link opens, the draft applies locally and the runtime's draft wins once it
|
|
297
|
+
connects. `tool.shareUrl(base?)` builds the same link from host code.
|
|
298
|
+
|
|
261
299
|
In Studio, **Command+B** (Mac) or **Ctrl+B** slides both side panels out of view; press it again to bring them back.
|
|
262
300
|
This also works from the same-origin preview iframe. The sidebar's collapse/expand button controls its width separately,
|
|
263
301
|
and that choice is preserved when hiding and showing the panels. Move the pointer near a hidden panel's screen edge to
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/tool.ts","../src/core/result.ts","../src/core/validate.ts"],"sourcesContent":["import type { DebugHost, DebugSchema } from '@joycostudio/xyz/debug'\n\n/** Controls as an Xyz schema. Drawn with whichever UI the app picks (`createStudio({ ui })`), so `extend` gets an unknown folder. */\nexport type ToolControls = DebugSchema<DebugHost>\n\n/**\n * An SVG icon as data: `[tag, attributes]` children of a 24×24 stroke icon.\n * Lucide's core package (`import { Palette } from 'lucide'`) exports exactly this shape.\n */\nexport type ToolIcon = readonly (readonly [tag: string, attrs: Record<string, string | number | undefined>])[]\n\ntype ToolBase = {\n /** Unique, URL-safe identifier. */\n id: string\n title?: string\n /** Shown next to the tool in /studio. Data, not a component, so contracts stay loadable in Node. */\n icon?: ToolIcon\n /** App route that renders this tool's result, shown next to the controls in /studio. */\n preview?: string\n /** Use a bare iframe for tools that already own their canvas layout and headers. */\n previewLayout?: 'canvas' | 'custom'\n}\n\n/**\n * A tool's contract. Isomorphic on purpose: the runtime loads it in Node to\n * validate and persist, the app loads it in the browser to apply and debug.\n *\n * `TConfig` is the one type that is read, edited, applied, and persisted.\n */\nexport type ToolDefinition<TConfig extends object = object> = ToolBase & {\n /** Repo-relative JSON file the config persists to. Import that same file as `initial`. */\n file: string\n /** Config used before the runtime answers (and in production). */\n initial: TConfig\n /**\n * What can be tweaked, as an Xyz schema bound to `config`.\n * Doubles as the runtime validator: every control's bounds are enforced on write.\n */\n controls: (config: TConfig) => ToolControls\n}\n\n/** A tool with nothing to tune: a sidebar entry for its preview, without an inspector or config file. */\nexport type PreviewToolDefinition = ToolBase & {\n preview: string\n file?: never\n initial?: never\n controls?: never\n}\n\n/** A tool with a config. */\nexport type AnyTool = ToolDefinition<any>\n\n/** Anything `defineTool` returns: the kind follows from whether it declares a config. */\nexport type StudioTool = AnyTool | PreviewToolDefinition\n\nexport const hasConfig = (tool: StudioTool): tool is AnyTool => tool.controls !== undefined\n\n/** Filled by .studio/types.generated.ts in each consuming project. */\nexport interface DiscoveredToolConfigs {}\n\n/** A discovered declaration is resolved into a full definition by the generated registry. */\nexport type DiscoveredToolDefinition<TConfig extends object = object> = ToolBase & {\n /** JSON source relative to this tool file, or a tsconfig path alias. Defaults to ./config.json. */\n config?: string\n controls: (config: TConfig) => ToolControls\n file?: never\n initial?: never\n}\nexport type ToolReference<TConfig extends object> = Pick<ToolDefinition<TConfig>, 'id' | 'controls'>\nexport type ConfigOf<TTool> = TTool extends ToolReference<infer TConfig> ? TConfig : never\n\nconst ID = /^[a-z0-9][a-z0-9-]*$/\n\n/** Discovered tools infer their config from the generated ID-to-JSON type map. */\nexport function defineTool<K extends keyof DiscoveredToolConfigs & string>(\n tool: Omit<DiscoveredToolDefinition<Extract<DiscoveredToolConfigs[K], object>>, 'id'> & { id: K },\n): DiscoveredToolDefinition<Extract<DiscoveredToolConfigs[K], object>>\n/** Existing explicitly registered tools remain supported. */\nexport function defineTool<TConfig extends object>(tool: ToolDefinition<TConfig>): ToolDefinition<TConfig>\nexport function defineTool(tool: PreviewToolDefinition): PreviewToolDefinition\n/** A discovered preview-only tool may infer its preview from the adjacent page. */\nexport function defineTool(\n tool: ToolBase & { controls?: never; config?: never; file?: never; initial?: never },\n): ToolBase\nexport function defineTool(tool: StudioTool | DiscoveredToolDefinition<any> | ToolBase) {\n if (!ID.test(tool.id)) throw new Error(`[studio] tool id \"${tool.id}\" must be kebab-case`)\n const value = tool as Partial<AnyTool>\n if (value.file !== undefined || value.initial !== undefined) {\n if (value.file === undefined || value.initial === undefined || value.controls === undefined)\n throw new Error(`[studio] tool \"${tool.id}\" needs all of file, initial, and controls, or none of them`)\n }\n return tool\n}\n\nexport type StudioConfig = {\n /** Explicit registry; omit to enable convention-based discovery. */\n tools?: readonly StudioTool[]\n /**\n * Directory to scan and watch for `studio.tool.ts` files, relative to\n * the project root.\n *\n * Defaults to the active Next.js app directory: `app/` or `src/app/`.\n * Set a narrower directory to reduce scanning and discovery triggers.\n * This does not change preview URLs.\n *\n * @example 'src/app/experiments'\n */\n toolsDir?: string\n /** Runtime WebSocket and HTTP MCP port. Defaults to 5179. */\n port?: number\n /** Browser origins allowed on the bridge and MCP endpoint. Defaults to localhost origins. */\n origins?: readonly string[]\n /** Running app URL (e.g. http://localhost:3000), used for MCP preview captures. */\n appUrl?: string\n}\n\nexport const DEFAULT_PORT = 5179\n\nexport function defineConfig<T extends StudioConfig>(config: T): T {\n return config\n}\n","export type Success<T> = { ok: true; data: T }\n\nexport type Failure = {\n ok: false\n /** snake_case wire code, e.g. `invalid_config`. */\n error: string\n message?: string\n}\n\nexport type Result<T> = Success<T> | Failure\n\nexport const success = <T>(data: T): Success<T> => ({ ok: true, data })\n\nexport const error = (code: string, message?: string): Failure => ({\n ok: false,\n error: code,\n ...(message === undefined ? {} : { message }),\n})\n","import { error, success, type Result } from './result'\nimport type { ToolControls, ToolDefinition } from './tool'\n\n/**\n * Accept `candidate` as a config for `tool` only if it has the exact shape of\n * `tool.initial` and every control in `tool.controls` accepts its value.\n * Returns a detached copy, never the input.\n */\nexport function validateConfig<TConfig extends object>(\n tool: ToolDefinition<TConfig>,\n candidate: unknown,\n): Result<TConfig> {\n const mismatch = shapeMismatch(tool.initial, candidate, 'config')\n if (mismatch) return error('invalid_config', mismatch)\n const config = structuredClone(candidate) as TConfig\n const invalid = invalidControl(tool.controls(config))\n return invalid ? error('invalid_config', invalid) : success(config)\n}\n\nfunction kind(value: unknown) {\n if (value === null) return 'null'\n if (Array.isArray(value)) return 'array'\n return typeof value\n}\n\nfunction shapeMismatch(reference: unknown, value: unknown, path: string): string | null {\n const expected = kind(reference)\n if (kind(value) !== expected) return `${path}: expected ${expected}, got ${kind(value)}`\n if (expected === 'number' && !Number.isFinite(value)) return `${path}: expected a finite number`\n if (expected === 'array') {\n const [first] = reference as unknown[]\n if (first === undefined) return null\n for (const [index, item] of (value as unknown[]).entries()) {\n const nested = shapeMismatch(first, item, `${path}[${index}]`)\n if (nested) return nested\n }\n }\n if (expected === 'object') {\n const ref = reference as Record<string, unknown>\n const obj = value as Record<string, unknown>\n for (const key of Object.keys(obj)) if (!(key in ref)) return `${path}.${key}: unknown key`\n for (const key of Object.keys(ref)) {\n if (!(key in obj)) return `${path}.${key}: missing`\n const nested = shapeMismatch(ref[key], obj[key], `${path}.${key}`)\n if (nested) return nested\n }\n }\n return null\n}\n\nconst isRgb = (value: unknown) =>\n typeof value === 'object' &&\n value !== null &&\n ['r', 'g', 'b'].every((channel) => Number.isFinite((value as Record<string, unknown>)[channel]))\n\nfunction invalidControl(schema: ToolControls, path = ''): string | null {\n for (const [key, field] of Object.entries(schema)) {\n if (field.kind === 'button') continue\n const name = `${path}${field.label ?? key}`\n if (field.kind === 'folder') {\n const nested = invalidControl(field.definition.schema(), `${name}/`)\n if (nested) return nested\n continue\n }\n const value: unknown = field.read()\n if (field.kind === 'slider') {\n if (typeof value !== 'number' || value < field.min || value > field.max)\n return `${name}: expected a number in [${field.min}, ${field.max}], got ${JSON.stringify(value)}`\n } else if (field.kind === 'bool') {\n if (typeof value !== 'boolean') return `${name}: expected a boolean`\n } else if (field.kind === 'select') {\n const options = Object.values(field.options)\n if (!options.includes(value as string | number))\n return `${name}: expected one of ${JSON.stringify(options)}, got ${JSON.stringify(value)}`\n } else if (field.kind === 'color-string') {\n if (typeof value !== 'string') return `${name}: expected a color string`\n } else if (field.kind === 'color') {\n if (!isRgb(value)) return `${name}: expected an { r, g, b } color`\n }\n }\n return null\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,13 +2,10 @@ import {
|
|
|
2
2
|
error,
|
|
3
3
|
hasConfig,
|
|
4
4
|
validateConfig
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-OEJWPWYJ.js";
|
|
6
6
|
|
|
7
7
|
// src/client/studio.ts
|
|
8
|
-
import {
|
|
9
|
-
button,
|
|
10
|
-
schemaValues
|
|
11
|
-
} from "@joycostudio/xyz/debug";
|
|
8
|
+
import { button } from "@joycostudio/xyz/debug";
|
|
12
9
|
|
|
13
10
|
// src/core/diff.ts
|
|
14
11
|
function diff(before, after, path = []) {
|
|
@@ -19,9 +16,6 @@ function diff(before, after, path = []) {
|
|
|
19
16
|
}
|
|
20
17
|
return JSON.stringify(before) === JSON.stringify(after) ? [] : [{ path, before, after }];
|
|
21
18
|
}
|
|
22
|
-
function changedPaths(before, after) {
|
|
23
|
-
return diff(before, after).map(({ path }) => path.join("."));
|
|
24
|
-
}
|
|
25
19
|
|
|
26
20
|
// src/client/bridge.ts
|
|
27
21
|
function connectBridge(url, handlers) {
|
|
@@ -105,7 +99,8 @@ function connectBrowserSync(definition, runtime, handlers) {
|
|
|
105
99
|
return {
|
|
106
100
|
publish(value) {
|
|
107
101
|
if (closed) return;
|
|
108
|
-
|
|
102
|
+
clock = Math.max(clock + 1, Date.now());
|
|
103
|
+
latest = { type: "config", clock, sender, value: structuredClone(value) };
|
|
109
104
|
channel.postMessage(latest);
|
|
110
105
|
},
|
|
111
106
|
close() {
|
|
@@ -209,6 +204,47 @@ function assignDeep(target, source) {
|
|
|
209
204
|
}
|
|
210
205
|
}
|
|
211
206
|
|
|
207
|
+
// src/client/share.ts
|
|
208
|
+
var PARAM = "studio-share";
|
|
209
|
+
var toBase64Url = (text) => {
|
|
210
|
+
let binary = "";
|
|
211
|
+
for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte);
|
|
212
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
213
|
+
};
|
|
214
|
+
var fromBase64Url = (data) => new TextDecoder().decode(
|
|
215
|
+
Uint8Array.from(atob(data.replace(/-/g, "+").replace(/_/g, "/")), (char) => char.charCodeAt(0))
|
|
216
|
+
);
|
|
217
|
+
function shareUrl(id, config, base = location.href) {
|
|
218
|
+
const url = new URL(base);
|
|
219
|
+
url.searchParams.set(PARAM, `${id}.${toBase64Url(JSON.stringify(config))}`);
|
|
220
|
+
return url.href;
|
|
221
|
+
}
|
|
222
|
+
function readShared(id) {
|
|
223
|
+
if (typeof window === "undefined" || !window.location?.search) return null;
|
|
224
|
+
const param = new URLSearchParams(location.search).get(PARAM);
|
|
225
|
+
if (!param?.startsWith(`${id}.`)) return null;
|
|
226
|
+
try {
|
|
227
|
+
return { value: JSON.parse(fromBase64Url(param.slice(id.length + 1))) };
|
|
228
|
+
} catch {
|
|
229
|
+
console.warn(`[studio] ${id}: ignoring a malformed share link`);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function clearShared() {
|
|
234
|
+
const url = new URL(location.href);
|
|
235
|
+
if (!url.searchParams.has(PARAM)) return;
|
|
236
|
+
url.searchParams.delete(PARAM);
|
|
237
|
+
history.replaceState(history.state, "", url);
|
|
238
|
+
}
|
|
239
|
+
async function copyLink(url) {
|
|
240
|
+
if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(url);
|
|
241
|
+
window.prompt("Copy this link", url);
|
|
242
|
+
}
|
|
243
|
+
function afterLoad(apply) {
|
|
244
|
+
if (document.readyState === "complete") setTimeout(apply);
|
|
245
|
+
else addEventListener("load", () => setTimeout(apply), { once: true });
|
|
246
|
+
}
|
|
247
|
+
|
|
212
248
|
// src/client/studio.ts
|
|
213
249
|
var COMPARE_MESSAGE = "joyco-studio:compare";
|
|
214
250
|
function postCompare(target, tool, showSaved) {
|
|
@@ -228,23 +264,6 @@ function liveSchema(schema) {
|
|
|
228
264
|
})
|
|
229
265
|
);
|
|
230
266
|
}
|
|
231
|
-
function resetSlidersOnDoubleClick(handle) {
|
|
232
|
-
return (folder, { fields }) => {
|
|
233
|
-
const sliders = fields().filter(({ field }) => field.kind === "slider");
|
|
234
|
-
const onDoubleClick = (event) => {
|
|
235
|
-
if (handle.comparing || event.target.closest("input")) return;
|
|
236
|
-
const slider = sliders.find(({ blade }) => blade.element.contains(event.target));
|
|
237
|
-
if (slider?.field.kind !== "slider") return;
|
|
238
|
-
const saved = slider.path.reduce(
|
|
239
|
-
(values, key) => values?.[key],
|
|
240
|
-
schemaValues(handle.definition.controls(structuredClone(handle.saved)))
|
|
241
|
-
);
|
|
242
|
-
if (typeof saved === "number") slider.field.write(saved);
|
|
243
|
-
};
|
|
244
|
-
folder.element.addEventListener("dblclick", onDoubleClick);
|
|
245
|
-
return () => folder.element.removeEventListener("dblclick", onDoubleClick);
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
267
|
function everyFrame(refresh) {
|
|
249
268
|
let frame = requestAnimationFrame(function tick() {
|
|
250
269
|
refresh();
|
|
@@ -273,9 +292,11 @@ function createStudio(config, options = {}) {
|
|
|
273
292
|
let scheduled = false;
|
|
274
293
|
let saved = definition.initial;
|
|
275
294
|
let stashed = null;
|
|
276
|
-
const
|
|
295
|
+
const history2 = createHistory();
|
|
277
296
|
let snapshot = structuredClone(raw);
|
|
278
297
|
const draft = () => stashed ?? raw;
|
|
298
|
+
let shared = readShared(definition.id);
|
|
299
|
+
let closed = false;
|
|
279
300
|
const changed = () => {
|
|
280
301
|
snapshot = structuredClone(raw);
|
|
281
302
|
handle.version++;
|
|
@@ -325,8 +346,28 @@ function createStudio(config, options = {}) {
|
|
|
325
346
|
};
|
|
326
347
|
const edited = () => {
|
|
327
348
|
if (stashed) return changed();
|
|
328
|
-
|
|
349
|
+
history2.record(snapshot, raw);
|
|
350
|
+
changed();
|
|
351
|
+
sync();
|
|
352
|
+
};
|
|
353
|
+
const hydrate = () => {
|
|
354
|
+
if (!shared) return;
|
|
355
|
+
const { value } = shared;
|
|
356
|
+
shared = null;
|
|
357
|
+
afterLoad(() => {
|
|
358
|
+
if (closed) return;
|
|
359
|
+
clearShared();
|
|
360
|
+
const result = validateConfig(definition, value);
|
|
361
|
+
if (!result.ok) return console.warn(`[studio] ${definition.id}: ignoring shared config: ${result.message}`);
|
|
362
|
+
handle.compare(false);
|
|
363
|
+
commit(() => assignDeep(raw, result.data));
|
|
364
|
+
});
|
|
365
|
+
};
|
|
366
|
+
const commit = (apply) => {
|
|
367
|
+
const before = snapshot;
|
|
368
|
+
apply();
|
|
329
369
|
changed();
|
|
370
|
+
history2.record(before, raw, { merge: false });
|
|
330
371
|
sync();
|
|
331
372
|
};
|
|
332
373
|
const restore = (patch) => {
|
|
@@ -344,8 +385,11 @@ function createStudio(config, options = {}) {
|
|
|
344
385
|
get saved() {
|
|
345
386
|
return saved;
|
|
346
387
|
},
|
|
388
|
+
get diff() {
|
|
389
|
+
return diff(saved, draft());
|
|
390
|
+
},
|
|
347
391
|
get changes() {
|
|
348
|
-
return
|
|
392
|
+
return handle.diff.map(({ path }) => path.join("."));
|
|
349
393
|
},
|
|
350
394
|
get dirty() {
|
|
351
395
|
return handle.changes.length > 0;
|
|
@@ -361,13 +405,13 @@ function createStudio(config, options = {}) {
|
|
|
361
405
|
changed();
|
|
362
406
|
},
|
|
363
407
|
get canUndo() {
|
|
364
|
-
return
|
|
408
|
+
return history2.canUndo;
|
|
365
409
|
},
|
|
366
410
|
get canRedo() {
|
|
367
|
-
return
|
|
411
|
+
return history2.canRedo;
|
|
368
412
|
},
|
|
369
|
-
undo: () => !stashed && restore(
|
|
370
|
-
redo: () => !stashed && restore(
|
|
413
|
+
undo: () => !stashed && restore(history2.undo()),
|
|
414
|
+
redo: () => !stashed && restore(history2.redo()),
|
|
371
415
|
subscribe(listener) {
|
|
372
416
|
listeners.add(listener);
|
|
373
417
|
return () => void listeners.delete(listener);
|
|
@@ -376,12 +420,12 @@ function createStudio(config, options = {}) {
|
|
|
376
420
|
debug() {
|
|
377
421
|
const actions = {
|
|
378
422
|
...runtime ? { save: button("Save", () => handle.save().then(fail), "Saved \u2713") } : {},
|
|
379
|
-
discard: button("Discard", () => void handle.discard(), "Discarded")
|
|
423
|
+
discard: button("Discard", () => void handle.discard(), "Discarded"),
|
|
424
|
+
share: button("Copy link", () => copyLink(handle.shareUrl()), "Copied \u2713")
|
|
380
425
|
};
|
|
381
426
|
return {
|
|
382
427
|
title: definition.title ?? definition.id,
|
|
383
|
-
schema: () => ({ ...liveSchema(handle.controls()), ...actions })
|
|
384
|
-
extend: resetSlidersOnDoubleClick(handle)
|
|
428
|
+
schema: () => ({ ...liveSchema(handle.controls()), ...actions })
|
|
385
429
|
};
|
|
386
430
|
},
|
|
387
431
|
async save({ force = false } = {}) {
|
|
@@ -397,22 +441,39 @@ function createStudio(config, options = {}) {
|
|
|
397
441
|
if (!handle.dirty) return false;
|
|
398
442
|
const before = snapshot;
|
|
399
443
|
replace(saved);
|
|
400
|
-
if (!stashed)
|
|
444
|
+
if (!stashed) history2.record(before, raw, { merge: false });
|
|
401
445
|
sync();
|
|
402
446
|
return true;
|
|
403
447
|
},
|
|
404
|
-
|
|
448
|
+
revert(path) {
|
|
449
|
+
const change = handle.diff.find(
|
|
450
|
+
(change2) => typeof path === "string" ? change2.path.join(".") === path : change2.path.length === path.length && change2.path.every((key, i) => key === path[i])
|
|
451
|
+
);
|
|
452
|
+
if (stashed || !change) return false;
|
|
453
|
+
commit(() => writePath(raw, change.path, change.before));
|
|
454
|
+
return true;
|
|
455
|
+
},
|
|
456
|
+
shareUrl: (base) => shareUrl(definition.id, draft(), base),
|
|
457
|
+
/**
|
|
458
|
+
* Connection changes: the runtime's draft wins on (re)connect; without one there is no runtime state.
|
|
459
|
+
* A share link applies on top of the runtime's draft, or locally once the runtime proves unreachable.
|
|
460
|
+
*/
|
|
405
461
|
connection() {
|
|
406
|
-
if (runtimeOwnsDrafts()) return void resync();
|
|
462
|
+
if (runtimeOwnsDrafts()) return void resync().then(hydrate);
|
|
407
463
|
handle.state = null;
|
|
464
|
+
if (connection.phase === "offline") hydrate();
|
|
408
465
|
},
|
|
409
466
|
receive,
|
|
410
|
-
close
|
|
467
|
+
close() {
|
|
468
|
+
closed = true;
|
|
469
|
+
browserSync?.close();
|
|
470
|
+
}
|
|
411
471
|
};
|
|
412
472
|
const browserSync = connectBrowserSync(definition, runtime, {
|
|
413
473
|
enabled: () => !runtimeOwnsDrafts(),
|
|
414
474
|
receive: replace
|
|
415
475
|
});
|
|
476
|
+
if (!runtime) hydrate();
|
|
416
477
|
return handle;
|
|
417
478
|
};
|
|
418
479
|
const handles = new Map(tools.filter(hasConfig).map((definition) => [definition.id, createHandle(definition)]));
|
|
@@ -434,6 +495,7 @@ function createStudio(config, options = {}) {
|
|
|
434
495
|
}
|
|
435
496
|
const studio = {
|
|
436
497
|
definitions: tools,
|
|
498
|
+
ui: config.ui,
|
|
437
499
|
tools: [...handles.values()],
|
|
438
500
|
tool(definition) {
|
|
439
501
|
const handle = handles.get(definition.id);
|
|
@@ -473,4 +535,4 @@ export {
|
|
|
473
535
|
everyFrame,
|
|
474
536
|
createStudio
|
|
475
537
|
};
|
|
476
|
-
//# sourceMappingURL=chunk-
|
|
538
|
+
//# sourceMappingURL=chunk-ZPKMVZ67.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client/studio.ts","../src/core/diff.ts","../src/client/bridge.ts","../src/client/browser-sync.ts","../src/client/history.ts","../src/client/live.ts","../src/client/share.ts"],"sourcesContent":["import { button, type DebugField, type DebugFolder, type DebugHost } from '@joycostudio/xyz/debug'\nimport { diff, type Change } from '../core/diff'\nimport type { ToolState } from '../core/protocol'\nimport { error, type Result } from '../core/result'\nimport { hasConfig, type StudioTool, type ToolControls, type ToolDefinition, type ToolReference } from '../core/tool'\nimport type { StudioUi } from '../ui'\nimport { validateConfig } from '../core/validate'\nimport { connectBridge, type BridgeClient, type Connection } from './bridge'\nimport { connectBrowserSync } from './browser-sync'\nimport { createHistory, writePath } from './history'\nimport { assignDeep, live } from './live'\nimport { afterLoad, clearShared, copyLink, readShared, shareUrl } from './share'\n\nexport type ToolHandle<TConfig extends object = any> = {\n readonly definition: ToolDefinition<TConfig>\n /** The live config. Writes sync to same-origin pages and, when connected, the runtime. */\n readonly config: TConfig\n /** Runtime-owned state; null while no runtime is connected or until it answers. */\n readonly state: ToolState<TConfig> | null\n /** Bumps on every config change. Handy as a React snapshot. */\n readonly version: number\n /** Called after every config change, local or remote. */\n subscribe(listener: (config: TConfig) => void): () => void\n /** The tool's Xyz schema bound to the live config. */\n controls(): ToolControls\n /** Xyz `Debuggable`: `debugManager.register(handle)` mounts this tool in the app's floating pane. */\n debug(): DebugFolder<DebugHost>\n /** The config on disk: the runtime's last reported saved value or, until one answers, the bundled `initial`. */\n readonly saved: TConfig\n /** Dotted paths where the draft differs from `saved`. Tracked locally, so it works without a runtime too. */\n readonly changes: string[]\n /** Each leaf where the draft differs from `saved`: `before` is the saved value, `after` the draft's. */\n readonly diff: Change[]\n /** The draft differs from `saved`. */\n readonly dirty: boolean\n /** True while `compare(true)` shows the saved config in place of the draft. */\n readonly comparing: boolean\n /**\n * Temporarily show `saved` (true) or go back to the draft (false). Local only: nothing\n * reaches the runtime, and edits made while comparing are dropped when it ends.\n */\n compare(showSaved: boolean): void\n /** True when `undo()` has an edit made on this page to step back through. */\n readonly canUndo: boolean\n readonly canRedo: boolean\n /**\n * Step back through edits made on this page (a slider drag counts as one), then sync the result like any\n * other edit. Only the leaves each edit changed are restored. Returns false with nothing to undo or while comparing.\n */\n undo(): boolean\n redo(): boolean\n /** Persist the draft. Needs a connected runtime. */\n save(options?: { force?: boolean }): Promise<Result<ToolState<TConfig>>>\n /**\n * Reset the draft to `saved`, with or without a runtime, then sync it like any other edit.\n * Undoable as its own step. Returns false when there is nothing to discard.\n */\n discard(): boolean\n /**\n * Reset one changed leaf (a dotted path from `changes`, or its keys, which also address keys containing dots) to its saved value, then sync it like any\n * other edit. Undoable as its own step. Returns false when that leaf is unchanged or while comparing.\n */\n revert(path: string | readonly string[]): boolean\n /**\n * A link to `base` (the current page by default) carrying the draft. Opening it on another machine applies\n * that draft as an unsaved, undoable edit; a draft that no longer fits the tool's controls is ignored.\n */\n shareUrl(base?: string | URL): string\n}\n\nexport type Studio = {\n /** Every registered tool in order, including preview-only ones. */\n readonly definitions: readonly StudioTool[]\n /** A handle per tool with a config. Preview-only tools have none. */\n readonly tools: readonly ToolHandle[]\n tool<TConfig extends object>(definition: ToolReference<TConfig>): ToolHandle<TConfig>\n readonly connection: Connection\n /** The debug UI library tools' controls are drawn with. `<Studio>` and `<StudioDebug>` need one. */\n readonly ui: StudioUi | undefined\n /** Bumps on connection or any tool state change. */\n readonly version: number\n subscribe(listener: () => void): () => void\n close(): void\n}\n\nexport type StudioOptions = {\n tools: readonly StudioTool[]\n /**\n * Runtime bridge URL (e.g. `ws://127.0.0.1:5179`). Omit or pass false for browser-only syncing, as in production.\n * Only browsers connect, so a module-scope `createStudio` is safe to evaluate during SSR.\n */\n runtime?: string | false\n /**\n * The debug UI library to draw controls with, from `@joycostudio/studio/ui`:\n * `tweakpane(() => import('tweakpane'))` or `dialkit(() => import('dialkit/vanilla'))`.\n * Required by `<Studio>` and `<StudioDebug>`; reading configs alone doesn't need one.\n */\n ui?: StudioUi\n}\n\nconst COMPARE_MESSAGE = 'joyco-studio:compare'\ntype CompareMessage = { type: typeof COMPARE_MESSAGE; tool: string; showSaved: boolean }\n\n/** Make the studio running in `target` (e.g. a preview iframe) compare `tool` as well. */\nexport function postCompare(target: Window | null | undefined, tool: string, showSaved: boolean) {\n const message: CompareMessage = { type: COMPARE_MESSAGE, tool, showSaved }\n target?.postMessage(message, location.origin)\n}\n\n/** Mark every value control live so a pane registered with `registerFrame` reflects remote edits. */\nfunction liveSchema(schema: ToolControls): ToolControls {\n return Object.fromEntries(\n Object.entries(schema).map(([key, field]): [string, DebugField<DebugHost>] => {\n if (field.kind === 'button') return [key, field]\n if (field.kind === 'folder')\n return [\n key,\n { ...field, definition: { ...field.definition, schema: () => liveSchema(field.definition.schema()) } },\n ]\n return [key, { ...field, live: true }]\n }),\n )\n}\n\n/** A `registerFrame` for Xyz's `DebugManager` / `bindDebugSchema`: refreshes live controls every animation frame. */\nexport function everyFrame(refresh: () => void) {\n let frame = requestAnimationFrame(function tick() {\n refresh()\n frame = requestAnimationFrame(tick)\n })\n return () => cancelAnimationFrame(frame)\n}\n\n/** One studio per runtime URL (or local tool set) per page; replace HMR instances without leaking transports. */\nconst instances: Map<string, Studio> = ((globalThis as { __joycoStudio?: Map<string, Studio> }).__joycoStudio ??=\n new Map())\n\nexport function createStudio(config: StudioOptions, options: Pick<StudioOptions, 'runtime'> = {}): Studio {\n const { tools } = config\n const runtime = options.runtime ?? config.runtime\n const instanceKey = runtime || JSON.stringify(tools.map(({ id, file }) => [id, file]).sort())\n let bridge: BridgeClient | null = null\n let connection: Connection = { phase: runtime ? 'connecting' : 'local' }\n let version = 0\n const studioListeners = new Set<() => void>()\n const touch = () => {\n version++\n for (const listener of studioListeners) listener()\n }\n\n /**\n * Who owns drafts: the runtime while connected, otherwise same-origin pages share them over BroadcastChannel.\n * The only place a draft change depends on the connection; reconnecting hands ownership back to the runtime.\n */\n const runtimeOwnsDrafts = () => connection.phase === 'connected'\n\n const createHandle = <TConfig extends object>(definition: ToolDefinition<TConfig>) => {\n const raw = structuredClone(definition.initial)\n const listeners = new Set<(config: TConfig) => void>()\n let pushing: Promise<unknown> = Promise.resolve()\n let scheduled = false\n /** The last saved config the runtime reported, kept while it is away; the bundled `initial` until one answers. */\n let saved: TConfig = definition.initial\n /** The draft to restore while comparing; null when showing the draft. */\n let stashed: TConfig | null = null\n const history = createHistory()\n /** A copy of `raw` as of the last change, so the next local edit can be diffed against it. */\n let snapshot = structuredClone(raw)\n /** The draft, even while `raw` shows the saved config for comparing. */\n const draft = () => stashed ?? raw\n /** A draft from this page's share link, applied once the runtime (if any) has had its say. */\n let shared = readShared(definition.id)\n let closed = false\n\n const changed = () => {\n snapshot = structuredClone(raw)\n handle.version++\n for (const listener of listeners) listener(handle.config)\n touch()\n }\n const adopt = (state: ToolState) => {\n handle.state = state as ToolState<TConfig>\n saved = state.saved.value as TConfig\n touch()\n }\n /** Replace the whole draft. While comparing, the shown saved config stays put; `changes` reads the new draft. */\n const replace = (value: TConfig) => {\n if (stashed) {\n stashed = structuredClone(value)\n touch()\n } else {\n assignDeep(raw as Record<string, unknown>, value as Record<string, unknown>)\n changed()\n }\n }\n const push = () => {\n if (scheduled || !bridge) return\n scheduled = true\n pushing = Promise.resolve().then(async () => {\n scheduled = false\n const result = await bridge?.request('tool.update', { tool: definition.id, value: draft() })\n if (!result) return\n if (result.ok) adopt(result.data)\n else if (result.error !== 'runtime_unavailable') {\n console.warn(`[studio] ${definition.id}: ${result.message ?? result.error}`)\n await resync()\n }\n })\n }\n /**\n * Send a local draft change on: to the runtime while it owns drafts. Same-origin pages always get a copy,\n * so one opened after the runtime stops starts from it, but they only read it while no runtime is connected.\n */\n const sync = () => {\n browserSync?.publish(draft())\n if (runtimeOwnsDrafts()) push()\n }\n /** The runtime's state, from a broadcast or a reply. */\n const receive = (state: ToolState) => {\n adopt(state)\n replace(state.draft.value as TConfig)\n browserSync?.publish(draft())\n }\n const resync = async () => {\n const result = await bridge?.request('tool.get', { tool: definition.id })\n if (result?.ok) receive(result.data)\n }\n /** Every local change to the live config. */\n const edited = () => {\n // Edits made while comparing stay on this page and are dropped when it ends.\n if (stashed) return changed()\n history.record(snapshot, raw)\n changed()\n sync()\n }\n /** Apply the share link as a local edit, so it syncs and undoes like one. Runs once. */\n const hydrate = () => {\n if (!shared) return\n const { value } = shared\n shared = null\n afterLoad(() => {\n if (closed) return\n clearShared()\n const result = validateConfig(definition, value)\n if (!result.ok) return console.warn(`[studio] ${definition.id}: ignoring shared config: ${result.message}`)\n handle.compare(false)\n commit(() => assignDeep(raw as Record<string, unknown>, result.data as Record<string, unknown>))\n })\n }\n /** Apply a change to the draft as an undo step of its own, never merged with the edits around it. */\n const commit = (apply: () => void) => {\n const before = snapshot\n apply()\n changed()\n history.record(before, raw, { merge: false })\n sync()\n }\n const restore = (patch: ReturnType<typeof history.undo>) => {\n if (!patch) return false\n for (const { path, value } of patch) writePath(raw as Record<string, unknown>, path, value)\n changed()\n sync()\n return true\n }\n\n const handle = {\n definition,\n config: live(raw, edited),\n state: null as ToolState<TConfig> | null,\n version: 0,\n get saved(): TConfig {\n return saved\n },\n get diff() {\n return diff(saved, draft())\n },\n get changes() {\n return handle.diff.map(({ path }) => path.join('.'))\n },\n get dirty() {\n return handle.changes.length > 0\n },\n comparing: false,\n compare(showSaved: boolean) {\n if (showSaved === handle.comparing) return\n if (showSaved) stashed = structuredClone(raw)\n const next = showSaved ? saved : stashed!\n assignDeep(raw as Record<string, unknown>, next as Record<string, unknown>)\n if (!showSaved) stashed = null\n handle.comparing = showSaved\n changed()\n },\n get canUndo() {\n return history.canUndo\n },\n get canRedo() {\n return history.canRedo\n },\n undo: () => !stashed && restore(history.undo()),\n redo: () => !stashed && restore(history.redo()),\n subscribe(listener: (config: TConfig) => void) {\n listeners.add(listener)\n return () => void listeners.delete(listener)\n },\n controls: () => definition.controls(handle.config),\n debug(): DebugFolder<DebugHost> {\n const actions: ToolControls = {\n ...(runtime ? { save: button('Save', () => handle.save().then(fail), 'Saved ✓') } : {}),\n discard: button('Discard', () => void handle.discard(), 'Discarded'),\n share: button('Copy link', () => copyLink(handle.shareUrl()), 'Copied ✓'),\n }\n return {\n title: definition.title ?? definition.id,\n schema: () => ({ ...liveSchema(handle.controls()), ...actions }),\n }\n },\n async save({ force = false } = {}) {\n if (!bridge) return error('runtime_unavailable')\n await pushing\n const revision = handle.state?.draft.revision\n if (revision === undefined) return error('runtime_unavailable')\n const result = await bridge.request('tool.save', { tool: definition.id, revision, force })\n if (result.ok) adopt(result.data)\n return result as Result<ToolState<TConfig>>\n },\n discard() {\n if (!handle.dirty) return false\n const before = snapshot\n replace(saved)\n // Undoable, as a step of its own.\n if (!stashed) history.record(before, raw, { merge: false })\n sync()\n return true\n },\n revert(path: string | readonly string[]) {\n const change = handle.diff.find((change) =>\n typeof path === 'string'\n ? change.path.join('.') === path\n : change.path.length === path.length && change.path.every((key, i) => key === path[i]),\n )\n if (stashed || !change) return false\n commit(() => writePath(raw as Record<string, unknown>, change.path, change.before))\n return true\n },\n shareUrl: (base?: string | URL) => shareUrl(definition.id, draft(), base),\n /**\n * Connection changes: the runtime's draft wins on (re)connect; without one there is no runtime state.\n * A share link applies on top of the runtime's draft, or locally once the runtime proves unreachable.\n */\n connection() {\n if (runtimeOwnsDrafts()) return void resync().then(hydrate)\n handle.state = null\n if (connection.phase === 'offline') hydrate()\n },\n receive,\n close() {\n closed = true\n browserSync?.close()\n },\n }\n const browserSync = connectBrowserSync(definition, runtime, {\n enabled: () => !runtimeOwnsDrafts(),\n receive: replace,\n })\n if (!runtime) hydrate()\n return handle\n }\n\n const handles = new Map(tools.filter(hasConfig).map((definition) => [definition.id, createHandle(definition)]))\n\n // /studio asks its same-origin preview iframe to compare too (see `postCompare`).\n const onMessage = (event: MessageEvent) => {\n const message = event.data as CompareMessage | undefined\n if (event.origin !== location.origin || message?.type !== COMPARE_MESSAGE) return\n handles.get(message.tool)?.compare(message.showSaved)\n }\n if (typeof window !== 'undefined') window.addEventListener?.('message', onMessage)\n\n if (runtime && typeof window !== 'undefined') {\n bridge = connectBridge(runtime, {\n state: (state) => handles.get(state.tool)?.receive(state),\n connection(next) {\n connection = next\n for (const handle of handles.values()) handle.connection()\n touch()\n },\n })\n }\n\n const studio: Studio = {\n definitions: tools,\n ui: config.ui,\n tools: [...handles.values()],\n tool<TConfig extends object>(definition: ToolDefinition<TConfig>) {\n const handle = handles.get(definition.id)\n if (!handle) throw new Error(`[studio] tool \"${definition.id}\" was not passed to createStudio`)\n return handle as unknown as ToolHandle<TConfig>\n },\n get connection() {\n return connection\n },\n get version() {\n return version\n },\n subscribe(listener) {\n studioListeners.add(listener)\n return () => void studioListeners.delete(listener)\n },\n close() {\n bridge?.close()\n for (const handle of handles.values()) handle.close()\n studioListeners.clear()\n if (typeof window !== 'undefined') window.removeEventListener?.('message', onMessage)\n if (instances.get(instanceKey) === studio) instances.delete(instanceKey)\n },\n }\n if (typeof window !== 'undefined') {\n instances.get(instanceKey)?.close()\n instances.set(instanceKey, studio)\n }\n return studio\n}\n\nfunction fail(result: Result<unknown>) {\n if (!result.ok) throw new Error(result.message ?? result.error)\n}\n","export type Change = { path: string[]; before: unknown; after: unknown }\n\n/** Every leaf that differs between two configs of the same shape. Arrays count as leaves. */\nexport function diff(before: unknown, after: unknown, path: string[] = []): Change[] {\n const nested = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n if (nested(before) && nested(after)) {\n const keys = new Set([...Object.keys(before), ...Object.keys(after)])\n return [...keys].flatMap((key) => diff(before[key], after[key], [...path, key]))\n }\n return JSON.stringify(before) === JSON.stringify(after) ? [] : [{ path, before, after }]\n}\n","import type { Op, Operations, ServerMessage, ToolState } from '../core/protocol'\nimport { error, type Result } from '../core/result'\n\nexport type Connection = { phase: 'local' } | { phase: 'connecting' } | { phase: 'connected' } | { phase: 'offline' }\n\ntype Pending = (result: Result<unknown>) => void\n\n/** A reconnecting WebSocket to the Studio runtime with request/response correlation. */\nexport function connectBridge(\n url: string,\n handlers: { state: (state: ToolState) => void; connection: (connection: Connection) => void },\n) {\n let socket: WebSocket | null = null\n let nextId = 0\n let retry = 0\n let timer: ReturnType<typeof setTimeout> | undefined\n let closed = false\n const pending = new Map<number, Pending>()\n\n const open = () => {\n handlers.connection({ phase: 'connecting' })\n const ws = new WebSocket(url)\n socket = ws\n ws.onopen = () => {\n retry = 0\n handlers.connection({ phase: 'connected' })\n }\n ws.onmessage = (event) => {\n const message = JSON.parse(String(event.data)) as ServerMessage\n if (message.type === 'state') return handlers.state(message.state)\n pending.get(message.id)?.(message.result)\n pending.delete(message.id)\n }\n ws.onclose = () => {\n socket = null\n for (const resolve of pending.values()) resolve(error('runtime_unavailable'))\n pending.clear()\n if (closed) return\n handlers.connection({ phase: 'offline' })\n timer = setTimeout(open, Math.min(500 * 2 ** retry++, 5000))\n }\n }\n open()\n\n return {\n request<TOp extends Op>(op: TOp, params: Operations[TOp]['params']): Promise<Result<Operations[TOp]['result']>> {\n if (socket?.readyState !== WebSocket.OPEN) return Promise.resolve(error('runtime_unavailable'))\n const id = nextId++\n socket.send(JSON.stringify({ id, op, params }))\n return new Promise((resolve) => pending.set(id, resolve as Pending))\n },\n close() {\n closed = true\n clearTimeout(timer)\n socket?.close()\n },\n }\n}\n\nexport type BridgeClient = ReturnType<typeof connectBridge>\n","import type { ToolDefinition } from '../core/tool'\nimport { validateConfig } from '../core/validate'\n\ntype Snapshot = { type: 'config'; clock: number; sender: string; value: unknown }\n\n/** Ephemeral config sharing between same-origin pages, including Studio's preview iframe. */\nexport function connectBrowserSync<T extends object>(\n definition: ToolDefinition<T>,\n runtime: string | false | undefined,\n handlers: { enabled(): boolean; receive(value: T): void },\n) {\n // Also exclude SSR and Node environments that expose their own BroadcastChannel implementation.\n if (typeof window === 'undefined' || !window.location?.origin || !window.BroadcastChannel) return null\n\n let channel: BroadcastChannel\n try {\n channel = new window.BroadcastChannel(\n JSON.stringify(['joyco-studio:config', runtime || null, definition.id, definition.file]),\n )\n } catch {\n return null\n }\n const sender = crypto.getRandomValues(new Uint32Array(4)).join('-')\n let clock = 0\n let latest: Snapshot | undefined\n let closed = false\n\n channel.onmessage = ({ data }) => {\n if (closed || !data || typeof data !== 'object') return\n if (data.type === 'request') {\n if (latest) channel.postMessage(latest)\n return\n }\n if (\n data.type !== 'config' ||\n !Number.isSafeInteger(data.clock) ||\n data.clock < 1 ||\n typeof data.sender !== 'string'\n )\n return\n clock = Math.max(clock, data.clock)\n if (!handlers.enabled()) return\n // A stable tie-break keeps simultaneous edits and multiple snapshot replies from diverging.\n if (latest && (data.clock < latest.clock || (data.clock === latest.clock && data.sender <= latest.sender))) return\n const result = validateConfig(definition, data.value)\n if (!result.ok) return\n latest = { type: 'config', clock: data.clock, sender: data.sender, value: result.data }\n handlers.receive(result.data)\n }\n // A newly mounted/reloaded preview asks existing peers for their current draft.\n channel.postMessage({ type: 'request' })\n\n return {\n publish(value: T) {\n if (closed) return\n // Wall time as a floor (peers share this browser's clock), so an edit made on a page that just opened,\n // such as an applied share link, outranks older drafts it hasn't heard about yet.\n clock = Math.max(clock + 1, Date.now())\n latest = { type: 'config', clock, sender, value: structuredClone(value) }\n channel.postMessage(latest)\n },\n close() {\n closed = true\n channel.onmessage = null\n channel.close()\n },\n }\n}\n","import { diff, type Change } from '../core/diff'\n\ntype Entry = { changes: Change[]; key: string; at: number }\n\nconst LIMIT = 100\n/** Edits to the same leaves this close together (a slider drag) undo as one step. */\nconst MERGE_MS = 500\n\n/**\n * Undo/redo for one page's edits. Entries hold only the leaves an edit changed, so stepping back\n * never reverts what another page, the runtime, or an agent changed in between.\n */\nexport function createHistory() {\n const past: Entry[] = []\n let future: Entry[] = []\n /** The newest entry while later edits to the same leaves may still extend it. */\n let open: Entry | null = null\n\n const step = (from: Entry[], to: Entry[], side: 'before' | 'after') => {\n const entry = from.pop()\n if (!entry) return null\n to.push(entry)\n open = null\n return entry.changes.map((change) => ({ path: change.path, value: change[side] }))\n }\n\n return {\n get canUndo() {\n return past.length > 0\n },\n get canRedo() {\n return future.length > 0\n },\n /** Record the edit between two snapshots. `merge: false` keeps it a step of its own (e.g. Discard). */\n record(before: unknown, after: unknown, { merge = true } = {}) {\n // Cloned so later in-place edits (e.g. to an array leaf) cannot rewrite history.\n const changes = structuredClone(diff(before, after))\n if (!changes.length) return\n future = []\n const key = JSON.stringify(changes.map(({ path }) => path))\n const now = Date.now()\n if (merge && open && open.key === key && now - open.at < MERGE_MS) {\n open.changes = open.changes.map((change, i) => ({ ...change, after: changes[i]!.after }))\n open.at = now\n // Dragged back to where it started: nothing left to undo.\n if (open.changes.every((change) => JSON.stringify(change.before) === JSON.stringify(change.after))) {\n past.pop()\n open = null\n }\n return\n }\n const entry = { changes, key, at: now }\n past.push(entry)\n if (past.length > LIMIT) past.shift()\n open = merge ? entry : null\n },\n /** The leaves to write back, or null when there is nothing to undo. */\n undo: () => step(past, future, 'before'),\n redo: () => step(future, past, 'after'),\n }\n}\n\n/** Write `value` at `path` in place; undefined removes the key (it did not exist before the edit). */\nexport function writePath(target: Record<string, unknown>, path: string[], value: unknown) {\n const key = path.at(-1)!\n let parent = target\n for (const segment of path.slice(0, -1)) {\n const next = parent[segment]\n parent = (typeof next === 'object' && next !== null ? next : (parent[segment] = {})) as Record<string, unknown>\n }\n if (value === undefined) delete parent[key]\n else parent[key] = structuredClone(value)\n}\n","/**\n * A deep proxy that reports every write, so plain `property(config, 'key')`\n * bindings from Xyz feed Studio without the tool author wiring callbacks.\n */\nexport function live<T extends object>(target: T, onWrite: () => void): T {\n const proxies = new WeakMap<object, object>()\n const wrap = <O extends object>(object: O): O => {\n let proxy = proxies.get(object)\n if (!proxy) {\n proxy = new Proxy(object, {\n get(target, key, receiver) {\n const value = Reflect.get(target, key, receiver)\n return typeof value === 'object' && value !== null ? wrap(value) : value\n },\n set(target, key, value, receiver) {\n const changed = Reflect.get(target, key, receiver) !== value\n const ok = Reflect.set(target, key, value, receiver)\n if (changed) onWrite()\n return ok\n },\n })\n proxies.set(object, proxy)\n }\n return proxy as O\n }\n return wrap(target)\n}\n\n/**\n * Make `target` equal `source` in place, keeping nested object identities (and every binding to them) intact.\n * Keys `source` lacks are removed.\n */\nexport function assignDeep(target: Record<string, unknown>, source: Record<string, unknown>) {\n for (const key of Object.keys(target)) if (!(key in source)) delete target[key]\n for (const [key, value] of Object.entries(source)) {\n const current = target[key]\n const nested = (v: unknown) => typeof v === 'object' && v !== null && !Array.isArray(v)\n if (nested(current) && nested(value))\n assignDeep(current as Record<string, unknown>, value as Record<string, unknown>)\n else target[key] = structuredClone(value)\n }\n}\n","/**\n * Share links carry one tool's draft in the URL: `?studio-share=<tool id>.<base64url JSON>`.\n * Opening one applies it as an unsaved edit, so teammates can review, save, or discard it.\n */\nconst PARAM = 'studio-share'\n\nconst toBase64Url = (text: string) => {\n // Byte by byte: spreading a large config into `String.fromCharCode` would exceed the argument limit.\n let binary = ''\n for (const byte of new TextEncoder().encode(text)) binary += String.fromCharCode(byte)\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nconst fromBase64Url = (data: string) =>\n new TextDecoder().decode(\n Uint8Array.from(atob(data.replace(/-/g, '+').replace(/_/g, '/')), (char) => char.charCodeAt(0)),\n )\n\n/** `base` (the current page by default) with `config` for tool `id` encoded into it. */\nexport function shareUrl(id: string, config: unknown, base: string | URL = location.href) {\n const url = new URL(base)\n url.searchParams.set(PARAM, `${id}.${toBase64Url(JSON.stringify(config))}`)\n return url.href\n}\n\n/** The config this page's URL shares for tool `id`, if any. Unvalidated. */\nexport function readShared(id: string): { value: unknown } | null {\n if (typeof window === 'undefined' || !window.location?.search) return null\n const param = new URLSearchParams(location.search).get(PARAM)\n if (!param?.startsWith(`${id}.`)) return null\n try {\n return { value: JSON.parse(fromBase64Url(param.slice(id.length + 1))) }\n } catch {\n console.warn(`[studio] ${id}: ignoring a malformed share link`)\n return null\n }\n}\n\n/** Drop the share from the address bar, so reloading after further edits doesn't reapply it. */\nexport function clearShared() {\n const url = new URL(location.href)\n if (!url.searchParams.has(PARAM)) return\n url.searchParams.delete(PARAM)\n history.replaceState(history.state, '', url)\n}\n\n/** Copy a share link. Plain-HTTP origins other than localhost have no clipboard API, so show it to copy by hand. */\nexport async function copyLink(url: string) {\n if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(url)\n window.prompt('Copy this link', url)\n}\n\n/**\n * Run `apply` once the page has loaded. Routers such as Next.js rewrite the URL while hydrating,\n * which would restore a share parameter cleared any earlier.\n */\nexport function afterLoad(apply: () => void) {\n if (document.readyState === 'complete') setTimeout(apply)\n else addEventListener('load', () => setTimeout(apply), { once: true })\n}\n"],"mappings":"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","names":["target","history","change"]}
|
package/dist/cli.js
CHANGED
|
@@ -970,7 +970,7 @@ import { parseArgs } from "util";
|
|
|
970
970
|
// package.json
|
|
971
971
|
var package_default = {
|
|
972
972
|
name: "@joycostudio/studio",
|
|
973
|
-
version: "0.
|
|
973
|
+
version: "0.2.0",
|
|
974
974
|
description: "Plug project-specific tools into a /studio route, a floating debug pane, and MCP agents.",
|
|
975
975
|
type: "module",
|
|
976
976
|
license: "ISC",
|
|
@@ -1017,6 +1017,10 @@ var package_default = {
|
|
|
1017
1017
|
types: "./dist/controls.d.ts",
|
|
1018
1018
|
default: "./dist/controls.js"
|
|
1019
1019
|
},
|
|
1020
|
+
"./ui": {
|
|
1021
|
+
types: "./dist/ui.d.ts",
|
|
1022
|
+
default: "./dist/ui.js"
|
|
1023
|
+
},
|
|
1020
1024
|
"./next/config": {
|
|
1021
1025
|
types: "./dist/next-config.d.ts",
|
|
1022
1026
|
default: "./dist/next-config.js"
|
|
@@ -1026,7 +1030,7 @@ var package_default = {
|
|
|
1026
1030
|
build: "pnpm build:shadow-css && tsup && tailwindcss -i src/react/styles.css -o dist/styles.css --minify && cp src/react/command-menu-export.css dist/command-menu.css",
|
|
1027
1031
|
dev: 'pnpm build:shadow-css && pnpm run --parallel "/^dev:/"',
|
|
1028
1032
|
test: "vitest run",
|
|
1029
|
-
"test:browser": "STUDIO_TEST_BROWSER=1 vitest run test/browser-sync.test.ts",
|
|
1033
|
+
"test:browser": "STUDIO_TEST_BROWSER=1 vitest run test/browser-sync.test.ts test/ui.test.ts",
|
|
1030
1034
|
"test:capture": "STUDIO_TEST_CAPTURE=1 vitest run test/capture.test.ts",
|
|
1031
1035
|
typecheck: "tsc --noEmit",
|
|
1032
1036
|
"dev:js": "tsup --watch",
|
|
@@ -1053,7 +1057,8 @@ var package_default = {
|
|
|
1053
1057
|
typescript: "^5.9.3"
|
|
1054
1058
|
},
|
|
1055
1059
|
peerDependencies: {
|
|
1056
|
-
"@joycostudio/xyz": "^1.
|
|
1060
|
+
"@joycostudio/xyz": "^1.5.0",
|
|
1061
|
+
dialkit: "^2.2.0",
|
|
1057
1062
|
react: ">=18",
|
|
1058
1063
|
"react-dom": ">=18",
|
|
1059
1064
|
tweakpane: "^4.0.5"
|
|
@@ -1067,10 +1072,13 @@ var package_default = {
|
|
|
1067
1072
|
},
|
|
1068
1073
|
"react-dom": {
|
|
1069
1074
|
optional: true
|
|
1075
|
+
},
|
|
1076
|
+
dialkit: {
|
|
1077
|
+
optional: true
|
|
1070
1078
|
}
|
|
1071
1079
|
},
|
|
1072
1080
|
devDependencies: {
|
|
1073
|
-
"@joycostudio/xyz": "^1.
|
|
1081
|
+
"@joycostudio/xyz": "^1.5.0",
|
|
1074
1082
|
"@modelcontextprotocol/client": "^2.3.1",
|
|
1075
1083
|
"@tailwindcss/cli": "^4.3.3",
|
|
1076
1084
|
"@tweakpane/core": "^2.0.5",
|
|
@@ -1078,6 +1086,7 @@ var package_default = {
|
|
|
1078
1086
|
"@types/react": "^19.3.0",
|
|
1079
1087
|
"@types/react-dom": "^19.3.0",
|
|
1080
1088
|
"@types/ws": "^8.18.2",
|
|
1089
|
+
dialkit: "^2.2.0",
|
|
1081
1090
|
react: "^19.3.0",
|
|
1082
1091
|
"react-dom": "^19.3.0",
|
|
1083
1092
|
tailwindcss: "^4.3.3",
|
|
@@ -1138,9 +1147,11 @@ async function init(argv) {
|
|
|
1138
1147
|
const { positionals, values } = parseArgs({
|
|
1139
1148
|
args: argv,
|
|
1140
1149
|
allowPositionals: true,
|
|
1141
|
-
options: { "no-install": { type: "boolean" } }
|
|
1150
|
+
options: { "no-install": { type: "boolean" }, ui: { type: "string", default: "tweakpane" } }
|
|
1142
1151
|
});
|
|
1143
|
-
if (positionals.length > 1) throw new Error("Usage: studio init [directory] [--no-install]");
|
|
1152
|
+
if (positionals.length > 1) throw new Error("Usage: studio init [directory] [--ui tweakpane|dialkit] [--no-install]");
|
|
1153
|
+
const ui = values.ui;
|
|
1154
|
+
if (ui !== "tweakpane" && ui !== "dialkit") throw new Error(`[studio] --ui must be tweakpane or dialkit, got ${ui}.`);
|
|
1144
1155
|
const root = path6.resolve(positionals[0] ?? ".");
|
|
1145
1156
|
const manifest = path6.join(root, "package.json");
|
|
1146
1157
|
const pkg = JSON.parse(readFileSync3(manifest, "utf8"));
|
|
@@ -1161,10 +1172,17 @@ async function init(argv) {
|
|
|
1161
1172
|
const page = `${app}/studio/page.tsx`;
|
|
1162
1173
|
const preview = `${app}/studio-preview/page.tsx`;
|
|
1163
1174
|
const files = {
|
|
1164
|
-
[client]:
|
|
1175
|
+
[client]: ui === "dialkit" ? `import 'dialkit/vanilla/styles.css'
|
|
1176
|
+
import { createStudio } from '@joycostudio/studio/next'
|
|
1177
|
+
import { dialkit } from '@joycostudio/studio/ui'
|
|
1178
|
+
import { tools } from '${relative(client, ".studio/registry.generated")}'
|
|
1179
|
+
|
|
1180
|
+
export const studio = createStudio({ tools, ui: dialkit(() => import('dialkit/vanilla')) })
|
|
1181
|
+
` : `import { createStudio } from '@joycostudio/studio/next'
|
|
1182
|
+
import { tweakpane } from '@joycostudio/studio/ui'
|
|
1165
1183
|
import { tools } from '${relative(client, ".studio/registry.generated")}'
|
|
1166
1184
|
|
|
1167
|
-
export const studio = createStudio({ tools })
|
|
1185
|
+
export const studio = createStudio({ tools, ui: tweakpane(() => import('tweakpane')) })
|
|
1168
1186
|
`,
|
|
1169
1187
|
[data]: '{ "scale": 1 }\n',
|
|
1170
1188
|
[tool2]: `import { defineTool } from '@joycostudio/studio'
|
|
@@ -1251,7 +1269,8 @@ export default function StudioPreview() {
|
|
|
1251
1269
|
if (pkg.dependencies?.["@joycostudio/studio"] || pkg.devDependencies?.["@joycostudio/studio"]) {
|
|
1252
1270
|
try {
|
|
1253
1271
|
const require2 = createRequire(manifest);
|
|
1254
|
-
for (const entry of ["next", "next/config", "controls", "react"])
|
|
1272
|
+
for (const entry of ["next", "next/config", "controls", "react", "ui"])
|
|
1273
|
+
require2.resolve(`@joycostudio/studio/${entry}`);
|
|
1255
1274
|
} catch {
|
|
1256
1275
|
throw new Error(
|
|
1257
1276
|
"[studio] The declared Studio dependency is missing or lacks the required exports. Install the same version (or preview) as this initializer, then rerun init. No files were changed."
|
|
@@ -1261,7 +1280,7 @@ export default function StudioPreview() {
|
|
|
1261
1280
|
const dependencies = {
|
|
1262
1281
|
"@joycostudio/studio": `^${package_default.version}`,
|
|
1263
1282
|
"@joycostudio/xyz": package_default.peerDependencies["@joycostudio/xyz"],
|
|
1264
|
-
|
|
1283
|
+
[ui]: package_default.peerDependencies[ui]
|
|
1265
1284
|
};
|
|
1266
1285
|
pkg.dependencies ??= {};
|
|
1267
1286
|
for (const [name, version] of Object.entries(dependencies)) {
|
|
@@ -1436,7 +1455,7 @@ function watchDiscovery(initial, refresh) {
|
|
|
1436
1455
|
var HELP = `Usage: studio dev [--config studio.config.ts] [--port ${DEFAULT_PORT}] [--app-url http://localhost:3000] [-- <command...>]
|
|
1437
1456
|
studio generate [--config studio.config.ts]
|
|
1438
1457
|
studio install-browser
|
|
1439
|
-
studio init [directory] [--no-install]
|
|
1458
|
+
studio init [directory] [--ui tweakpane|dialkit] [--no-install]
|
|
1440
1459
|
|
|
1441
1460
|
Starts the Studio runtime with a WebSocket bridge and an HTTP MCP endpoint at /mcp,
|
|
1442
1461
|
moving to the next free port when another project's Studio already uses it.
|