@joycostudio/studio 0.1.3 → 0.1.5
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 +190 -57
- package/dist/{chunk-OIMLF2CL.js → chunk-DEJYGBPJ.js} +6 -5
- package/dist/chunk-DEJYGBPJ.js.map +1 -0
- package/dist/chunk-HVGJXFXT.js +314 -0
- package/dist/chunk-HVGJXFXT.js.map +1 -0
- package/dist/chunk-ZH3NEN4V.js +476 -0
- package/dist/chunk-ZH3NEN4V.js.map +1 -0
- package/dist/cli.js +869 -52
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +11 -6
- package/dist/client.js +5 -460
- package/dist/client.js.map +1 -1
- package/dist/command-menu.css +4 -1
- package/dist/controls.d.ts +1 -0
- package/dist/controls.js +3 -0
- package/dist/controls.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/next-config.d.ts +4 -0
- package/dist/next-config.js +135 -0
- package/dist/next-config.js.map +1 -0
- package/dist/next.d.ts +8 -0
- package/dist/next.js +15 -0
- package/dist/next.js.map +1 -0
- package/dist/{protocol-Dgj_7ibg.d.ts → protocol-DjrMADPm.d.ts} +40 -5
- package/dist/react.d.ts +22 -10
- package/dist/react.js +368 -252
- package/dist/react.js.map +1 -1
- package/dist/runtime.d.ts +32 -4
- package/dist/runtime.js +97 -83
- package/dist/runtime.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +27 -7
- package/dist/chunk-OIMLF2CL.js.map +0 -1
package/README.md
CHANGED
|
@@ -11,106 +11,231 @@ A tool's controls are an [Xyz](https://hub.joyco.studio/toolbox/xyz) debug schem
|
|
|
11
11
|
- mounts in the app's floating pane (`?debug` / <kbd>Option</kbd>+<kbd>D</kbd>) through Xyz's `DebugManager`,
|
|
12
12
|
- validates every write in the runtime (slider bounds, select options, types, and the shape of `initial`).
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Quick start: Next.js App Router
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
In an existing TypeScript Next.js app:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pnpm dlx @joycostudio/studio init
|
|
20
|
+
pnpm dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Open `/studio` and select Example. Move the Scale slider and Save to write the adjacent JSON.
|
|
24
|
+
The initializer supports `app/` and `src/app/`, installs dependencies, wraps your dev command,
|
|
25
|
+
and adds generation to your typecheck script. It wraps the existing Next config with `withStudio`
|
|
26
|
+
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, or `--no-install` to prepare files without installing.
|
|
28
|
+
Reruns skip identical files; conflicting or edited files stop initialization before writes.
|
|
29
|
+
If generation fails, initialization restores existing files and removes newly created setup files.
|
|
30
|
+
Route checks include route groups and handlers. If Studio is already declared, install a version
|
|
31
|
+
with the new exports before running init; an older or missing installation is rejected before writes.
|
|
32
|
+
Existing Studio integrations and custom shell dev scripts use the manual migration below.
|
|
33
|
+
|
|
34
|
+
## One folder, one tool
|
|
35
|
+
|
|
36
|
+
Write these three files anywhere under your active app directory:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
src/app/experiments/lighting/
|
|
40
|
+
page.tsx # Your normal Next.js preview
|
|
41
|
+
studio.tool.ts # Controls and explicit stable ID
|
|
42
|
+
config.json # Saved values
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`config.json`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{ "intensity": 1 }
|
|
49
|
+
```
|
|
17
50
|
|
|
18
51
|
```ts
|
|
19
|
-
//
|
|
52
|
+
// studio.tool.ts
|
|
20
53
|
import { defineTool } from '@joycostudio/studio'
|
|
21
|
-
import { property,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
id: 'color-grading',
|
|
26
|
-
file: 'src/tools/grading.json', // where saves go, repo-relative
|
|
27
|
-
initial, // the same file, imported: production ships the saved config
|
|
28
|
-
preview: '/', // route shown next to the inspector in /studio
|
|
54
|
+
import { property, slider } from '@joycostudio/studio/controls'
|
|
55
|
+
|
|
56
|
+
export default defineTool({
|
|
57
|
+
id: 'lighting',
|
|
29
58
|
controls: (config) => ({
|
|
30
|
-
|
|
31
|
-
tonemap: select(property(config, 'tonemap'), { ACES: 'aces', Filmic: 'filmic' }),
|
|
59
|
+
intensity: slider(property(config, 'intensity'), { min: 0, max: 5 }),
|
|
32
60
|
}),
|
|
33
61
|
})
|
|
34
62
|
```
|
|
35
63
|
|
|
36
|
-
|
|
64
|
+
Studio discovers `studio.tool.ts` and infers the JSON source, save destination, preview URL,
|
|
65
|
+
and title. `id` is explicit so moving the route does not change its identity. Controls receive
|
|
66
|
+
the JSON's inferred TypeScript shape through a generated module augmentation; imports always
|
|
67
|
+
come from `@joycostudio/studio`, never a per-tool generated helper.
|
|
37
68
|
|
|
38
|
-
|
|
39
|
-
|
|
69
|
+
The preview consumes the live config:
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
// src/app/experiments/lighting/page.tsx
|
|
73
|
+
'use client'
|
|
74
|
+
import { useToolConfig } from '@joycostudio/studio/react'
|
|
75
|
+
import { studio } from '../../../studio'
|
|
76
|
+
import lighting from './studio.tool'
|
|
77
|
+
|
|
78
|
+
export default function LightingPage() {
|
|
79
|
+
const config = useToolConfig(studio.tool(lighting))
|
|
80
|
+
return <div style={{ filter: `brightness(${config.intensity})` }}>Your scene</div>
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For a render loop, read `studio.tool(lighting).config` directly, or subscribe to changes with
|
|
85
|
+
`studio.tool(lighting).subscribe(apply)`. Tool definitions also load in Node: keep DOM, React
|
|
86
|
+
components, and renderer imports in your page or scene, not in `studio.tool.ts`.
|
|
87
|
+
|
|
88
|
+
### Overrides and discovery scope
|
|
40
89
|
|
|
41
90
|
```ts
|
|
42
|
-
export
|
|
91
|
+
export default defineTool({
|
|
92
|
+
id: 'lighting',
|
|
93
|
+
title: 'Scene Lighting',
|
|
94
|
+
config: '@/settings/lighting.json',
|
|
95
|
+
preview: '/scene',
|
|
96
|
+
controls: (config) => ({/* ... */}),
|
|
97
|
+
})
|
|
43
98
|
```
|
|
44
99
|
|
|
45
|
-
`
|
|
46
|
-
|
|
100
|
+
`config` is a single JSON path for both initial values and saves. It defaults to `./config.json`;
|
|
101
|
+
relative paths resolve from the tool file, and aliases resolve through the app's `tsconfig.json`,
|
|
102
|
+
including inherited paths. JSON must exist and remain inside the project. Two tools cannot write
|
|
103
|
+
the same file. For deterministic generation, default-export `defineTool({ ... })` directly:
|
|
104
|
+
`id`, `config`, and explicit `preview` must be string literals, with no spread/computed option keys.
|
|
105
|
+
Control callbacks can contain normal application logic.
|
|
47
106
|
|
|
48
|
-
|
|
107
|
+
An optional, **user-owned** `studio.config.ts` controls discovery:
|
|
49
108
|
|
|
50
109
|
```ts
|
|
51
|
-
// studio.config.ts (its directory is the project root)
|
|
52
110
|
import { defineConfig } from '@joycostudio/studio'
|
|
53
|
-
import { grading } from './src/tools/grading'
|
|
54
111
|
|
|
55
|
-
export default defineConfig({
|
|
112
|
+
export default defineConfig({
|
|
113
|
+
toolsDir: 'src/app/experiments',
|
|
114
|
+
// appUrl: 'http://localhost:3000', // Optional MCP capture URL
|
|
115
|
+
})
|
|
56
116
|
```
|
|
57
117
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
118
|
+
`toolsDir` is relative to the project root and limits scanning and watching. By default Studio uses
|
|
119
|
+
`app/`, or `src/app/` when there is no root `app/`. Narrowing it does not change preview URLs.
|
|
120
|
+
Hidden directories, node_modules, and symlinked directories are excluded. Route groups such as
|
|
121
|
+
`(experiments)` are stripped from inferred URLs. Dynamic, parallel, intercepted, private, or
|
|
122
|
+
outside-app routes need an explicit concrete `preview`; a missing adjacent page also needs one.
|
|
123
|
+
A preview-only tool can omit `controls` and JSON entirely: `defineTool({ id: 'gallery' })`.
|
|
61
124
|
|
|
62
|
-
|
|
63
|
-
`http://127.0.0.1:5179/mcp` (`--port` or `port` in the config changes both). It stops when the wrapped command exits.
|
|
125
|
+
### Generated files and startup
|
|
64
126
|
|
|
65
|
-
|
|
66
|
-
|
|
127
|
+
Studio owns only `.studio/registry.generated.ts` and `.studio/types.generated.ts`; ignore `.studio/`
|
|
128
|
+
in Git. The registry references the type map, so importing the registry includes its augmentation
|
|
129
|
+
in the application's TypeScript program. Scaffolded app files remain user-owned after init.
|
|
67
130
|
|
|
68
131
|
```ts
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
132
|
+
// src/studio.ts (scaffolded once)
|
|
133
|
+
import { createStudio } from '@joycostudio/studio/next'
|
|
134
|
+
import { tools } from '../.studio/registry.generated'
|
|
72
135
|
|
|
73
|
-
|
|
136
|
+
export const studio = createStudio({ tools })
|
|
137
|
+
```
|
|
74
138
|
|
|
75
139
|
```ts
|
|
76
|
-
//
|
|
77
|
-
import {
|
|
140
|
+
// next.config.ts
|
|
141
|
+
import { withStudio } from '@joycostudio/studio/next/config'
|
|
78
142
|
|
|
79
|
-
export
|
|
80
|
-
tools: [grading],
|
|
81
|
-
runtime: process.env.NODE_ENV === 'development' && `ws://127.0.0.1:${process.env.NEXT_PUBLIC_STUDIO_PORT}`,
|
|
82
|
-
})
|
|
143
|
+
export default withStudio({/* your existing Next options */})
|
|
83
144
|
```
|
|
84
145
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
146
|
+
`withStudio` accepts an object or an async/sync Next configuration function and generates before
|
|
147
|
+
`next dev` or `next build`. `studio dev -- next dev` generates first and owns discovery watching
|
|
148
|
+
when the runtime is running. Plain `next dev` also watches, but uses browser-only config syncing.
|
|
149
|
+
For standalone typechecking, run `studio generate && tsc --noEmit` (init configures this script).
|
|
150
|
+
If your TypeScript project excludes the client/registry, explicitly include `.studio/types.generated.ts`.
|
|
151
|
+
Restart the dev server after changing runtime port, origins, or capture URL settings.
|
|
152
|
+
|
|
153
|
+
During `studio dev`, value-only config changes travel through the runtime transport without
|
|
154
|
+
Next Fast Refresh or a preview remount, including saves and external JSON edits. The generated
|
|
155
|
+
registry embeds stable bootstrap values instead of importing the writable JSON at runtime;
|
|
156
|
+
type-only imports still infer controls from the source JSON. New pages receive current values
|
|
157
|
+
when they connect to the runtime. Changes to JSON structure/types regenerate the bootstrap values.
|
|
158
|
+
A new runtime session, plain `next dev`, `studio generate`, and production builds read fresh values.
|
|
159
|
+
Read live values through `studio.tool(...)` or `useToolConfig`; directly importing the JSON into
|
|
160
|
+
your page or tool helpers gives Next a dependency that will still trigger HMR on saves.
|
|
161
|
+
|
|
162
|
+
Adding/removing/renaming tools refreshes the registry. Normal page-content edits stay with Next's
|
|
163
|
+
HMR. Tool definitions and their local imported helpers are tracked, as are referenced JSON files
|
|
164
|
+
outside `toolsDir` and TypeScript alias settings. Output is written only when changed.
|
|
165
|
+
Missing or malformed JSON configs remain watched after a rejected update, so completing them
|
|
166
|
+
retries discovery automatically, including aliases outside `toolsDir`.
|
|
167
|
+
The runtime preserves valid drafts during refresh; removing or moving a dirty tool, or making
|
|
168
|
+
its draft incompatible with new controls, rejects the refresh with a diagnostic. Save/discard
|
|
169
|
+
that draft to retry. Invalid updates do not replace the last accepted registry. If a deleted
|
|
170
|
+
source is still imported by that registry, restore it before resolving the draft; Next may show
|
|
171
|
+
a temporary module-not-found error while a removed tool is being rediscovered.
|
|
172
|
+
|
|
173
|
+
Mount the editor route once (init creates it):
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
// src/app/studio/page.tsx
|
|
177
|
+
'use client'
|
|
178
|
+
import '@joycostudio/studio/styles.css'
|
|
179
|
+
import { Studio } from '@joycostudio/studio/react'
|
|
180
|
+
import { studio } from '../../studio'
|
|
181
|
+
|
|
182
|
+
export default function StudioPage() {
|
|
183
|
+
return <Studio studio={studio} />
|
|
184
|
+
}
|
|
89
185
|
```
|
|
90
186
|
|
|
187
|
+
### Existing manual integrations
|
|
188
|
+
|
|
189
|
+
Explicit `defineTool({ id, file, initial, controls, preview })` definitions and
|
|
190
|
+
`defineConfig({ tools: [...] })` remain supported. An explicit tools list disables discovery;
|
|
191
|
+
do not combine `tools` with `toolsDir`. To migrate, move the declarations into `studio.tool.ts`,
|
|
192
|
+
keep their IDs, replace `file`/`initial` with an optional `config` path, and default-export them.
|
|
193
|
+
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.
|
|
195
|
+
|
|
91
196
|
Live edits also work without `studio dev`, including in production. Studio shares configs between
|
|
92
197
|
same-origin previews and tabs in the same browser using `BroadcastChannel`. Peers match by tool ID,
|
|
93
198
|
config file, and runtime URL (or no runtime); a newly opened or reloaded preview receives the current
|
|
94
199
|
config from an open peer. Nothing is persisted: closing every page resets these edits to the shipped
|
|
95
|
-
config.
|
|
96
|
-
|
|
200
|
+
config. Discard works the same either way: it resets the draft to the saved config (the runtime's last
|
|
201
|
+
reported one, or the shipped config) and syncs like any edit. Save and MCP require a runtime. If a configured
|
|
202
|
+
runtime disconnects, browser sync continues; reconnecting restores the runtime's draft.
|
|
203
|
+
|
|
204
|
+
Floating debug pane, with the same controls and save/discard actions:
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
// Mount once in a client component that imports your studio instance.
|
|
208
|
+
import { StudioDebug } from '@joycostudio/studio/react'
|
|
97
209
|
|
|
98
|
-
|
|
210
|
+
;<StudioDebug studio={studio} />
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Studio owns lazy pane creation, frame refresh, registration, and cleanup through Xyz's
|
|
214
|
+
DebugManager. `?debug` and Option+D keep their existing behavior. The pane and command menu
|
|
215
|
+
are optional; neither is needed for `/studio`.
|
|
216
|
+
|
|
217
|
+
The Next.js adapter reads the endpoint passed by `studio dev`, including port overrides and
|
|
218
|
+
collision fallback. No `next.config` changes or hand-written environment variables are needed.
|
|
219
|
+
It defaults to browser-only mode without the CLI or in production. Restart the dev server when
|
|
220
|
+
changing runtime settings. Other frameworks can use the explicit client API:
|
|
99
221
|
|
|
100
222
|
```ts
|
|
101
|
-
import {
|
|
102
|
-
import
|
|
103
|
-
import { Pane } from 'tweakpane'
|
|
223
|
+
import { createStudio } from '@joycostudio/studio/client'
|
|
224
|
+
import config from '../studio.config'
|
|
104
225
|
|
|
105
|
-
const
|
|
106
|
-
|
|
226
|
+
export const studio = createStudio(config, { runtime: 'ws://127.0.0.1:5179' })
|
|
227
|
+
// Pass runtime: false for browser-only operation.
|
|
107
228
|
```
|
|
108
229
|
|
|
230
|
+
Existing `createStudio({ tools, runtime })` calls remain supported. Both entry points accept
|
|
231
|
+
an explicit runtime override, including `false`.
|
|
232
|
+
|
|
109
233
|
The `/studio` route:
|
|
110
234
|
|
|
111
235
|
```tsx
|
|
112
236
|
// app/studio/page.tsx
|
|
113
237
|
'use client'
|
|
238
|
+
import '@joycostudio/studio/styles.css'
|
|
114
239
|
import { Studio } from '@joycostudio/studio/react'
|
|
115
240
|
import { studio } from '@/studio'
|
|
116
241
|
|
|
@@ -119,6 +244,10 @@ export default function StudioPage() {
|
|
|
119
244
|
}
|
|
120
245
|
```
|
|
121
246
|
|
|
247
|
+
Studio and the command menu render in their own shadow roots and bundle their styles, so neither needs a
|
|
248
|
+
stylesheet import. Host page CSS (global resets, `!important` rules, theme variables such as shadcn's
|
|
249
|
+
`--foreground`, or a resized `<html>` font) can't change them, and their styles never reach the host page.
|
|
250
|
+
|
|
122
251
|
Use the pin button beside a sidebar tool to keep it at the top. Pinned tools also appear first in the
|
|
123
252
|
**Command/Ctrl+K** menu. Pins are saved in this browser for the current origin and shared across tabs;
|
|
124
253
|
unpinning restores registration order. Pinning a tool keeps the current preview selected.
|
|
@@ -148,14 +277,16 @@ in the same tab. New tabs start with panels shown and the canvas contained.
|
|
|
148
277
|
|
|
149
278
|
### Shared canvas and panel components
|
|
150
279
|
|
|
151
|
-
Import these from `@joycostudio/studio/react`
|
|
280
|
+
Import these from `@joycostudio/studio/react`. Inside Studio (for example, in a `previewComponent`) they
|
|
281
|
+
are already styled; to use them on your own pages, such as a custom-layout tool preview, also import
|
|
282
|
+
`@joycostudio/studio/styles.css` there:
|
|
152
283
|
|
|
153
284
|
| Component | Use |
|
|
154
285
|
| ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
155
286
|
| `StudioSurface`, `StudioToolbar` | Shared inset card and compact action row, including button states. |
|
|
156
287
|
| `CanvasToolbar` | Pinned/auto-hide header; `label`, optional `expanded`/`onToggle`, and child controls. |
|
|
157
288
|
| `PreviewCanvas` | Iframe panel with reload, expansion, and a `toolbar` slot replacing the title. |
|
|
158
|
-
| `StudioSelect`, `StudioSelectItem` | Labeled
|
|
289
|
+
| `StudioSelect`, `StudioSelectItem` | Labeled Base UI select with Studio field and portaled menu styling. |
|
|
159
290
|
| `StudioInput` | Standard input with logical padding (`ps-*`/`pe-*`) for icon-bearing fields. |
|
|
160
291
|
| `StudioPanel`, `StudioPanelHeader`, `StudioPanelContent`, `StudioPanelFooter` | Floating panel surface, inset sections, and scrolling body used by Studio's inspector. |
|
|
161
292
|
|
|
@@ -173,7 +304,9 @@ these imports. Module graphs, tracing, and scene-specific controls remain host-o
|
|
|
173
304
|
|
|
174
305
|
`Studio` accepts `previewComponent` and `getConfigFileHref`, so integrations no longer need to patch
|
|
175
306
|
the built package. Keep a custom preview component at module scope and forward its iframe ref and
|
|
176
|
-
props; Studio uses them for keyboard shortcuts, comparison, and tool-switch lifecycle
|
|
307
|
+
props; Studio uses them for keyboard shortcuts, comparison, and tool-switch lifecycle. The component
|
|
308
|
+
renders inside Studio's shadow root, so host stylesheets don't apply to it: build its chrome from the
|
|
309
|
+
shared components below (Studio's Tailwind utilities are available too) or inline styles.
|
|
177
310
|
|
|
178
311
|
```tsx
|
|
179
312
|
import { forwardRef, type ComponentPropsWithoutRef } from 'react'
|
|
@@ -211,7 +344,6 @@ Mount `StudioCommandMenu` once in the host app's root layout so it stays availab
|
|
|
211
344
|
```tsx
|
|
212
345
|
// src/command-menu.tsx
|
|
213
346
|
'use client'
|
|
214
|
-
import '@joycostudio/studio/command-menu.css'
|
|
215
347
|
import { StudioCommandMenu } from '@joycostudio/studio/react'
|
|
216
348
|
import { studio } from './studio'
|
|
217
349
|
|
|
@@ -221,7 +353,8 @@ export function CommandMenu() {
|
|
|
221
353
|
```
|
|
222
354
|
|
|
223
355
|
Render `<CommandMenu />` alongside the layout's `{children}`. The host owns mounting it, just like the floating debug pane.
|
|
224
|
-
|
|
356
|
+
It renders in its own shadow root with its styles bundled, so it needs no stylesheet import and is unaffected by the
|
|
357
|
+
host's CSS. (`@joycostudio/studio/command-menu.css` is now empty and only kept so existing imports resolve.)
|
|
225
358
|
|
|
226
359
|
Press **Command+K** (Mac) or **Ctrl+K** to open or close it, type a tool name or ID to search, use **↑/↓** and **Enter** to
|
|
227
360
|
select, or **Escape** to dismiss. Clicking a result also opens it. Results link to `/studio#tool-id`; switching tools from
|
|
@@ -3,10 +3,11 @@ var hasConfig = (tool) => tool.controls !== void 0;
|
|
|
3
3
|
var ID = /^[a-z0-9][a-z0-9-]*$/;
|
|
4
4
|
function defineTool(tool) {
|
|
5
5
|
if (!ID.test(tool.id)) throw new Error(`[studio] tool id "${tool.id}" must be kebab-case`);
|
|
6
|
-
const
|
|
7
|
-
if (
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const value = tool;
|
|
7
|
+
if (value.file !== void 0 || value.initial !== void 0) {
|
|
8
|
+
if (value.file === void 0 || value.initial === void 0 || value.controls === void 0)
|
|
9
|
+
throw new Error(`[studio] tool "${tool.id}" needs all of file, initial, and controls, or none of them`);
|
|
10
|
+
}
|
|
10
11
|
return tool;
|
|
11
12
|
}
|
|
12
13
|
var DEFAULT_PORT = 5179;
|
|
@@ -97,4 +98,4 @@ export {
|
|
|
97
98
|
error,
|
|
98
99
|
validateConfig
|
|
99
100
|
};
|
|
100
|
-
//# sourceMappingURL=chunk-
|
|
101
|
+
//# sourceMappingURL=chunk-DEJYGBPJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/tool.ts","../src/core/result.ts","../src/core/validate.ts"],"sourcesContent":["import type { DebugSchema } from '@joycostudio/xyz/debug'\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/Tweakpane schema bound to `config`.\n * Doubles as the runtime validator: every control's bounds are enforced on write.\n */\n controls: (config: TConfig) => DebugSchema\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) => DebugSchema\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 type { DebugSchema } from '@joycostudio/xyz/debug'\nimport { error, success, type Result } from './result'\nimport type { 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: DebugSchema, 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":[]}
|