@joycostudio/studio 0.1.2 → 0.1.4

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 CHANGED
@@ -11,71 +11,188 @@ 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
- ## 1. Define a tool
14
+ ## Quick start: Next.js App Router
15
15
 
16
- Tool files are loaded by the runtime in Node too, so keep them free of DOM/three imports and `@/` aliases.
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
- // src/tools/grading.ts
52
+ // studio.tool.ts
20
53
  import { defineTool } from '@joycostudio/studio'
21
- import { property, select, slider } from '@joycostudio/xyz/debug'
22
- import initial from './grading.json'
23
-
24
- export const grading = defineTool({
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
- exposure: slider(property(config, 'exposure'), { min: 0, max: 3 }),
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
- ## 2. Register tools with the runtime
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
- ```ts
39
- // studio.config.ts (its directory is the project root)
40
- import { defineConfig } from '@joycostudio/studio'
41
- import { grading } from './src/tools/grading'
69
+ The preview consumes the live config:
42
70
 
43
- export default defineConfig({ tools: [grading] })
44
- ```
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'
45
77
 
46
- ```json
47
- { "scripts": { "dev": "studio dev -- next dev" } }
78
+ export default function LightingPage() {
79
+ const config = useToolConfig(studio.tool(lighting))
80
+ return <div style={{ filter: `brightness(${config.intensity})` }}>Your scene</div>
81
+ }
48
82
  ```
49
83
 
50
- `studio dev` starts the WebSocket bridge on `ws://127.0.0.1:5179` and the HTTP MCP endpoint on
51
- `http://127.0.0.1:5179/mcp` (`--port` or `port` in the config changes both). It stops when the wrapped command exits.
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`.
52
87
 
53
- Several projects can run at once. When another project's Studio already holds the port, `studio dev` moves to the next
54
- free one and passes it to the wrapped command as `STUDIO_PORT`. Read that variable instead of hardcoding the port:
88
+ ### Overrides and discovery scope
55
89
 
56
90
  ```ts
57
- // next.config.ts
58
- export default { env: { NEXT_PUBLIC_STUDIO_PORT: process.env.STUDIO_PORT ?? '5179' } }
91
+ export default defineTool({
92
+ id: 'lighting',
93
+ title: 'Scene Lighting',
94
+ config: '@/settings/lighting.json',
95
+ preview: '/scene',
96
+ controls: (config) => ({/* ... */}),
97
+ })
59
98
  ```
60
99
 
61
- ## 3. Use it in the app
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.
106
+
107
+ An optional, **user-owned** `studio.config.ts` controls discovery:
62
108
 
63
109
  ```ts
64
- // src/studio.ts
65
- import { createStudio } from '@joycostudio/studio/client'
110
+ import { defineConfig } from '@joycostudio/studio'
66
111
 
67
- export const studio = createStudio({
68
- tools: [grading],
69
- runtime: process.env.NODE_ENV === 'development' && `ws://127.0.0.1:${process.env.NEXT_PUBLIC_STUDIO_PORT}`,
112
+ export default defineConfig({
113
+ toolsDir: 'src/app/experiments',
114
+ // appUrl: 'http://localhost:3000', // Optional MCP capture URL
70
115
  })
71
116
  ```
72
117
 
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' })`.
124
+
125
+ ### Generated files and startup
126
+
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.
130
+
131
+ ```ts
132
+ // src/studio.ts (scaffolded once)
133
+ import { createStudio } from '@joycostudio/studio/next'
134
+ import { tools } from '../.studio/registry.generated'
135
+
136
+ export const studio = createStudio({ tools })
137
+ ```
138
+
73
139
  ```ts
74
- const tool = studio.tool(grading)
75
- apply(tool.config) // a live object: read it in your render loop…
76
- tool.subscribe(apply) // …or react to every change, local or remote
140
+ // next.config.ts
141
+ import { withStudio } from '@joycostudio/studio/next/config'
142
+
143
+ export default withStudio({/* your existing Next options */})
144
+ ```
145
+
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
+ }
77
185
  ```
78
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
+
79
196
  Live edits also work without `studio dev`, including in production. Studio shares configs between
80
197
  same-origin previews and tabs in the same browser using `BroadcastChannel`. Peers match by tool ID,
81
198
  config file, and runtime URL (or no runtime); a newly opened or reloaded preview receives the current
@@ -83,22 +200,41 @@ config from an open peer. Nothing is persisted: closing every page resets these
83
200
  config. Save, Discard, and MCP still require a runtime. If a configured runtime disconnects, browser
84
201
  sync continues; reconnecting restores the runtime's draft.
85
202
 
86
- Floating debug pane, with the same controls (plus save/discard when the runtime is up):
203
+ Floating debug pane, with the same controls and save/discard actions:
204
+
205
+ ```tsx
206
+ // Mount once in a client component that imports your studio instance.
207
+ import { StudioDebug } from '@joycostudio/studio/react'
208
+
209
+ ;<StudioDebug studio={studio} />
210
+ ```
211
+
212
+ Studio owns lazy pane creation, frame refresh, registration, and cleanup through Xyz's
213
+ DebugManager. `?debug` and Option+D keep their existing behavior. The pane and command menu
214
+ are optional; neither is needed for `/studio`.
215
+
216
+ The Next.js adapter reads the endpoint passed by `studio dev`, including port overrides and
217
+ collision fallback. No `next.config` changes or hand-written environment variables are needed.
218
+ It defaults to browser-only mode without the CLI or in production. Restart the dev server when
219
+ changing runtime settings. Other frameworks can use the explicit client API:
87
220
 
88
221
  ```ts
89
- import { everyFrame } from '@joycostudio/studio/client'
90
- import { DebugManager } from '@joycostudio/xyz/debug'
91
- import { Pane } from 'tweakpane'
222
+ import { createStudio } from '@joycostudio/studio/client'
223
+ import config from '../studio.config'
92
224
 
93
- const debug = new DebugManager({ createPane: () => new Pane(), binding: { registerFrame: everyFrame } })
94
- for (const tool of studio.tools) debug.register(tool)
225
+ export const studio = createStudio(config, { runtime: 'ws://127.0.0.1:5179' })
226
+ // Pass runtime: false for browser-only operation.
95
227
  ```
96
228
 
229
+ Existing `createStudio({ tools, runtime })` calls remain supported. Both entry points accept
230
+ an explicit runtime override, including `false`.
231
+
97
232
  The `/studio` route:
98
233
 
99
234
  ```tsx
100
235
  // app/studio/page.tsx
101
236
  'use client'
237
+ import '@joycostudio/studio/styles.css'
102
238
  import { Studio } from '@joycostudio/studio/react'
103
239
  import { studio } from '@/studio'
104
240
 
@@ -107,6 +243,10 @@ export default function StudioPage() {
107
243
  }
108
244
  ```
109
245
 
246
+ Studio and the command menu render in their own shadow roots and bundle their styles, so neither needs a
247
+ stylesheet import. Host page CSS (global resets, `!important` rules, theme variables such as shadcn's
248
+ `--foreground`, or a resized `<html>` font) can't change them, and their styles never reach the host page.
249
+
110
250
  Use the pin button beside a sidebar tool to keep it at the top. Pinned tools also appear first in the
111
251
  **Command/Ctrl+K** menu. Pins are saved in this browser for the current origin and shared across tabs;
112
252
  unpinning restores registration order. Pinning a tool keeps the current preview selected.
@@ -119,8 +259,10 @@ in place, and syncs like any other edit. Text fields keep their native undo whil
119
259
 
120
260
  In Studio, **Command+B** (Mac) or **Ctrl+B** slides both side panels out of view; press it again to bring them back.
121
261
  This also works from the same-origin preview iframe. The sidebar's collapse/expand button controls its width separately,
122
- and that choice is preserved when hiding and showing the panels. Move the pointer near either screen edge to reveal a small
123
- tab; click it to bring both panels back. The tabs also appear on keyboard focus and stay visible on touch devices.
262
+ and that choice is preserved when hiding and showing the panels. Move the pointer near a hidden panel's screen edge to
263
+ reveal a small tab; click it to bring back only that panel, so you can work with just the tools or just the inspector.
264
+ With one panel open, the shortcut hides it; with both hidden, it brings both back. The tabs also appear on keyboard
265
+ focus and stay visible on touch devices.
124
266
  With reduced motion enabled, panels toggle instantly.
125
267
 
126
268
  Previews use the shared `PreviewCanvas`: Reload and the tool title sit on the left, with Expand and
@@ -129,16 +271,21 @@ pinned; enabling auto-hide overlays the header and hides it after three seconds
129
271
  focus, and open menus keep it visible, and touch devices always show the controls. Contained previews
130
272
  fit between the side panels, with the inspector stacked below on narrow screens.
131
273
 
274
+ Each hidden panel and the expanded canvas are kept in session storage, so the layout survives a refresh
275
+ in the same tab. New tabs start with panels shown and the canvas contained.
276
+
132
277
  ### Shared canvas and panel components
133
278
 
134
- Import these from `@joycostudio/studio/react` with `@joycostudio/studio/styles.css`:
279
+ Import these from `@joycostudio/studio/react`. Inside Studio (for example, in a `previewComponent`) they
280
+ are already styled; to use them on your own pages, such as a custom-layout tool preview, also import
281
+ `@joycostudio/studio/styles.css` there:
135
282
 
136
283
  | Component | Use |
137
284
  | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
138
285
  | `StudioSurface`, `StudioToolbar` | Shared inset card and compact action row, including button states. |
139
286
  | `CanvasToolbar` | Pinned/auto-hide header; `label`, optional `expanded`/`onToggle`, and child controls. |
140
287
  | `PreviewCanvas` | Iframe panel with reload, expansion, and a `toolbar` slot replacing the title. |
141
- | `StudioSelect`, `StudioSelectItem` | Labeled Radix select with Studio field and portaled menu styling. |
288
+ | `StudioSelect`, `StudioSelectItem` | Labeled Base UI select with Studio field and portaled menu styling. |
142
289
  | `StudioInput` | Standard input with logical padding (`ps-*`/`pe-*`) for icon-bearing fields. |
143
290
  | `StudioPanel`, `StudioPanelHeader`, `StudioPanelContent`, `StudioPanelFooter` | Floating panel surface, inset sections, and scrolling body used by Studio's inspector. |
144
291
 
@@ -156,7 +303,9 @@ these imports. Module graphs, tracing, and scene-specific controls remain host-o
156
303
 
157
304
  `Studio` accepts `previewComponent` and `getConfigFileHref`, so integrations no longer need to patch
158
305
  the built package. Keep a custom preview component at module scope and forward its iframe ref and
159
- props; Studio uses them for keyboard shortcuts, comparison, and tool-switch lifecycle:
306
+ props; Studio uses them for keyboard shortcuts, comparison, and tool-switch lifecycle. The component
307
+ renders inside Studio's shadow root, so host stylesheets don't apply to it: build its chrome from the
308
+ shared components below (Studio's Tailwind utilities are available too) or inline styles.
160
309
 
161
310
  ```tsx
162
311
  import { forwardRef, type ComponentPropsWithoutRef } from 'react'
@@ -194,7 +343,6 @@ Mount `StudioCommandMenu` once in the host app's root layout so it stays availab
194
343
  ```tsx
195
344
  // src/command-menu.tsx
196
345
  'use client'
197
- import '@joycostudio/studio/command-menu.css'
198
346
  import { StudioCommandMenu } from '@joycostudio/studio/react'
199
347
  import { studio } from './studio'
200
348
 
@@ -204,7 +352,8 @@ export function CommandMenu() {
204
352
  ```
205
353
 
206
354
  Render `<CommandMenu />` alongside the layout's `{children}`. The host owns mounting it, just like the floating debug pane.
207
- Its stylesheet is scoped to the menu and can be imported globally without applying Studio's page styles to the host app.
355
+ It renders in its own shadow root with its styles bundled, so it needs no stylesheet import and is unaffected by the
356
+ host's CSS. (`@joycostudio/studio/command-menu.css` is now empty and only kept so existing imports resolve.)
208
357
 
209
358
  Press **Command+K** (Mac) or **Ctrl+K** to open or close it, type a tool name or ID to search, use **↑/↓** and **Enter** to
210
359
  select, or **Escape** to dismiss. Clicking a result also opens it. Results link to `/studio#tool-id`; switching tools from