@joycostudio/studio 0.1.1 → 0.1.3
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 +44 -8
- package/dist/{chunk-X4YLM5HA.js → chunk-OIMLF2CL.js} +21 -1
- package/dist/chunk-OIMLF2CL.js.map +1 -0
- package/dist/cli.js +59 -14
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +21 -3
- package/dist/client.js +145 -15
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -12
- package/dist/index.js.map +1 -1
- package/dist/{protocol-BrHDtNkL.d.ts → protocol-Dgj_7ibg.d.ts} +29 -13
- package/dist/react.d.ts +42 -11
- package/dist/react.js +127 -68
- package/dist/react.js.map +1 -1
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +10 -5
- package/dist/runtime.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +3 -3
- package/dist/chunk-X4YLM5HA.js.map +0 -1
package/dist/react.d.ts
CHANGED
|
@@ -20,18 +20,24 @@ type Result<T> = Success<T> | Failure;
|
|
|
20
20
|
* Lucide's core package (`import { Palette } from 'lucide'`) exports exactly this shape.
|
|
21
21
|
*/
|
|
22
22
|
type ToolIcon = readonly (readonly [tag: string, attrs: Record<string, string | number | undefined>])[];
|
|
23
|
+
type ToolBase = {
|
|
24
|
+
/** Unique, URL-safe identifier. */
|
|
25
|
+
id: string;
|
|
26
|
+
title?: string;
|
|
27
|
+
/** Shown next to the tool in /studio. Data, not a component, so contracts stay loadable in Node. */
|
|
28
|
+
icon?: ToolIcon;
|
|
29
|
+
/** App route that renders this tool's result, shown next to the controls in /studio. */
|
|
30
|
+
preview?: string;
|
|
31
|
+
/** Use a bare iframe for tools that already own their canvas layout and headers. */
|
|
32
|
+
previewLayout?: 'canvas' | 'custom';
|
|
33
|
+
};
|
|
23
34
|
/**
|
|
24
35
|
* A tool's contract. Isomorphic on purpose: the runtime loads it in Node to
|
|
25
36
|
* validate and persist, the app loads it in the browser to apply and debug.
|
|
26
37
|
*
|
|
27
38
|
* `TConfig` is the one type that is read, edited, applied, and persisted.
|
|
28
39
|
*/
|
|
29
|
-
type ToolDefinition<TConfig extends object = object> = {
|
|
30
|
-
/** Unique, URL-safe identifier. */
|
|
31
|
-
id: string;
|
|
32
|
-
title?: string;
|
|
33
|
-
/** Shown next to the tool in /studio. Data, not a component, so contracts stay loadable in Node. */
|
|
34
|
-
icon?: ToolIcon;
|
|
40
|
+
type ToolDefinition<TConfig extends object = object> = ToolBase & {
|
|
35
41
|
/** Repo-relative JSON file the config persists to. Import that same file as `initial`. */
|
|
36
42
|
file: string;
|
|
37
43
|
/** Config used before the runtime answers (and in production). */
|
|
@@ -41,11 +47,18 @@ type ToolDefinition<TConfig extends object = object> = {
|
|
|
41
47
|
* Doubles as the runtime validator: every control's bounds are enforced on write.
|
|
42
48
|
*/
|
|
43
49
|
controls: (config: TConfig) => DebugSchema;
|
|
44
|
-
/** App route that renders this tool's result, shown next to the controls in /studio. */
|
|
45
|
-
preview?: string;
|
|
46
|
-
/** Use a bare iframe for tools that already own their canvas layout and headers. */
|
|
47
|
-
previewLayout?: 'canvas' | 'custom';
|
|
48
50
|
};
|
|
51
|
+
/** A tool with nothing to tune: a sidebar entry for its preview, without an inspector or config file. */
|
|
52
|
+
type PreviewToolDefinition = ToolBase & {
|
|
53
|
+
preview: string;
|
|
54
|
+
file?: never;
|
|
55
|
+
initial?: never;
|
|
56
|
+
controls?: never;
|
|
57
|
+
};
|
|
58
|
+
/** A tool with a config. */
|
|
59
|
+
type AnyTool = ToolDefinition<any>;
|
|
60
|
+
/** Anything `defineTool` returns: the kind follows from whether it declares a config. */
|
|
61
|
+
type StudioTool = AnyTool | PreviewToolDefinition;
|
|
49
62
|
|
|
50
63
|
/** A tool's live state as owned by the runtime. */
|
|
51
64
|
type ToolState<TConfig = unknown> = {
|
|
@@ -89,19 +102,37 @@ type ToolHandle<TConfig extends object = any> = {
|
|
|
89
102
|
controls(): DebugSchema;
|
|
90
103
|
/** Xyz `Debuggable`: `debugManager.register(handle)` mounts this tool in the app's floating pane. */
|
|
91
104
|
debug(): DebugFolder;
|
|
105
|
+
/** The config on disk: the runtime's saved value or, until one answers, the bundled `initial`. */
|
|
106
|
+
readonly saved: TConfig;
|
|
107
|
+
/** Dotted paths where the draft differs from `saved`. Tracked locally, so it works without a runtime too. */
|
|
108
|
+
readonly changes: string[];
|
|
109
|
+
/** The draft differs from `saved`. */
|
|
110
|
+
readonly dirty: boolean;
|
|
92
111
|
/** True while `compare(true)` shows the saved config in place of the draft. */
|
|
93
112
|
readonly comparing: boolean;
|
|
94
113
|
/**
|
|
95
|
-
* Temporarily show
|
|
114
|
+
* Temporarily show `saved` (true) or go back to the draft (false). Local only: nothing
|
|
96
115
|
* reaches the runtime, and edits made while comparing are dropped when it ends.
|
|
97
116
|
*/
|
|
98
117
|
compare(showSaved: boolean): void;
|
|
118
|
+
/** True when `undo()` has an edit made on this page to step back through. */
|
|
119
|
+
readonly canUndo: boolean;
|
|
120
|
+
readonly canRedo: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Step back through edits made on this page (a slider drag counts as one), then sync the result like any
|
|
123
|
+
* other edit. Only the leaves each edit changed are restored. Returns false with nothing to undo or while comparing.
|
|
124
|
+
*/
|
|
125
|
+
undo(): boolean;
|
|
126
|
+
redo(): boolean;
|
|
99
127
|
save(options?: {
|
|
100
128
|
force?: boolean;
|
|
101
129
|
}): Promise<Result<ToolState<TConfig>>>;
|
|
102
130
|
discard(): Promise<Result<ToolState<TConfig>>>;
|
|
103
131
|
};
|
|
104
132
|
type Studio$1 = {
|
|
133
|
+
/** Every registered tool in order, including preview-only ones. */
|
|
134
|
+
readonly definitions: readonly StudioTool[];
|
|
135
|
+
/** A handle per tool with a config. Preview-only tools have none. */
|
|
105
136
|
readonly tools: readonly ToolHandle[];
|
|
106
137
|
tool<TConfig extends object>(definition: ToolDefinition<TConfig>): ToolHandle<TConfig>;
|
|
107
138
|
readonly connection: Connection;
|
package/dist/react.js
CHANGED
|
@@ -11,18 +11,38 @@ import {
|
|
|
11
11
|
PlugZap,
|
|
12
12
|
Unplug
|
|
13
13
|
} from "lucide-react";
|
|
14
|
-
import { useEffect as
|
|
14
|
+
import { useEffect as useEffect5, useRef as useRef3, useState as useState8 } from "react";
|
|
15
15
|
|
|
16
16
|
// package.json
|
|
17
|
-
var version = "0.1.
|
|
17
|
+
var version = "0.1.3";
|
|
18
18
|
|
|
19
19
|
// src/client/studio.ts
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
button,
|
|
22
|
+
schemaValues
|
|
23
|
+
} from "@joycostudio/xyz/debug";
|
|
21
24
|
var COMPARE_MESSAGE = "joyco-studio:compare";
|
|
22
25
|
function postCompare(target, tool, showSaved) {
|
|
23
26
|
const message = { type: COMPARE_MESSAGE, tool, showSaved };
|
|
24
27
|
target?.postMessage(message, location.origin);
|
|
25
28
|
}
|
|
29
|
+
function resetSlidersOnDoubleClick(handle) {
|
|
30
|
+
return (folder, { fields }) => {
|
|
31
|
+
const sliders = fields().filter(({ field }) => field.kind === "slider");
|
|
32
|
+
const onDoubleClick = (event) => {
|
|
33
|
+
if (handle.comparing || event.target.closest("input")) return;
|
|
34
|
+
const slider = sliders.find(({ blade }) => blade.element.contains(event.target));
|
|
35
|
+
if (slider?.field.kind !== "slider") return;
|
|
36
|
+
const saved = slider.path.reduce(
|
|
37
|
+
(values, key) => values?.[key],
|
|
38
|
+
schemaValues(handle.definition.controls(structuredClone(handle.saved)))
|
|
39
|
+
);
|
|
40
|
+
if (typeof saved === "number") slider.field.write(saved);
|
|
41
|
+
};
|
|
42
|
+
folder.element.addEventListener("dblclick", onDoubleClick);
|
|
43
|
+
return () => folder.element.removeEventListener("dblclick", onDoubleClick);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
26
46
|
var instances = globalThis.__joycoStudio ??= /* @__PURE__ */ new Map();
|
|
27
47
|
|
|
28
48
|
// src/react/hooks.ts
|
|
@@ -49,16 +69,6 @@ import { bindDebugSchema } from "@joycostudio/xyz/debug";
|
|
|
49
69
|
import { SquareSplitHorizontal } from "lucide-react";
|
|
50
70
|
import { useEffect, useRef, useState } from "react";
|
|
51
71
|
|
|
52
|
-
// src/core/diff.ts
|
|
53
|
-
function changedPaths(before, after, path = "") {
|
|
54
|
-
const nested = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
55
|
-
if (nested(before) && nested(after)) {
|
|
56
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
57
|
-
return [...keys].flatMap((key) => changedPaths(before[key], after[key], path ? `${path}.${key}` : key));
|
|
58
|
-
}
|
|
59
|
-
return JSON.stringify(before) === JSON.stringify(after) ? [] : [path];
|
|
60
|
-
}
|
|
61
|
-
|
|
62
72
|
// src/react/theme.ts
|
|
63
73
|
var tweakpaneTheme = {
|
|
64
74
|
"--tp-base-background-color": "transparent",
|
|
@@ -111,9 +121,9 @@ function ToolIcon({ icon = SlidersHorizontal, ...props }) {
|
|
|
111
121
|
"aria-hidden": true,
|
|
112
122
|
...props,
|
|
113
123
|
children: icon.map(([Tag, attrs], index) => {
|
|
114
|
-
const
|
|
124
|
+
const Element = Tag;
|
|
115
125
|
return /* @__PURE__ */ jsx(
|
|
116
|
-
|
|
126
|
+
Element,
|
|
117
127
|
{
|
|
118
128
|
...Object.fromEntries(Object.entries(attrs).map(([name, value]) => [camel(name), value]))
|
|
119
129
|
},
|
|
@@ -310,9 +320,9 @@ function PanelFooter({ className, ...props }) {
|
|
|
310
320
|
import { Fragment, jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
311
321
|
function describe(studio, handle) {
|
|
312
322
|
const { state } = handle;
|
|
323
|
+
if (handle.comparing) return { tone: "text-sky-400", text: "Showing saved" };
|
|
313
324
|
if (studio.connection.phase !== "connected") return { tone: "text-faint", text: "Not persisting" };
|
|
314
325
|
if (!state) return { tone: "text-destructive", text: "Restart studio dev" };
|
|
315
|
-
if (handle.comparing) return { tone: "text-sky-400", text: "Showing saved" };
|
|
316
326
|
if (state.conflict) return { tone: "text-destructive", text: "Changed on disk" };
|
|
317
327
|
if (state.dirty) return { tone: "text-warning", text: "Unsaved" };
|
|
318
328
|
return { tone: "text-success", text: state.saved.hash ? "Saved" : "Not on disk yet" };
|
|
@@ -337,7 +347,10 @@ function Inspector({
|
|
|
337
347
|
if (disposed || !container.current) return;
|
|
338
348
|
const instance = new Pane({ container: container.current });
|
|
339
349
|
pane.current = instance;
|
|
340
|
-
const bound = bindDebugSchema(instance, {
|
|
350
|
+
const bound = bindDebugSchema(instance, {
|
|
351
|
+
schema: () => handle.controls(),
|
|
352
|
+
extend: resetSlidersOnDoubleClick(handle)
|
|
353
|
+
});
|
|
341
354
|
const unsubscribe = handle.subscribe(() => bound.refresh());
|
|
342
355
|
teardown = () => {
|
|
343
356
|
unsubscribe();
|
|
@@ -367,7 +380,7 @@ function Inspector({
|
|
|
367
380
|
const ConfigFile = fileHref ? "a" : "span";
|
|
368
381
|
const status = describe(studio, handle);
|
|
369
382
|
const editable2 = studio.connection.phase === "connected" && !!state?.dirty && !handle.comparing;
|
|
370
|
-
const changes =
|
|
383
|
+
const changes = handle.changes.map(
|
|
371
384
|
(path) => path.split(".").map((key) => key.charAt(0).toUpperCase() + key.slice(1)).join(" \u203A ")
|
|
372
385
|
);
|
|
373
386
|
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
@@ -424,7 +437,7 @@ function Inspector({
|
|
|
424
437
|
size: "icon-sm",
|
|
425
438
|
"aria-label": "Hold to compare with saved",
|
|
426
439
|
"aria-pressed": handle.comparing,
|
|
427
|
-
disabled: !
|
|
440
|
+
disabled: !handle.dirty && !handle.comparing,
|
|
428
441
|
className: "shrink-0 aria-pressed:bg-primary aria-pressed:text-primary-foreground",
|
|
429
442
|
onPointerDown: (event) => {
|
|
430
443
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
@@ -494,9 +507,7 @@ function usePinnedTools(studio) {
|
|
|
494
507
|
};
|
|
495
508
|
}, []);
|
|
496
509
|
const pinned = new Set(pins);
|
|
497
|
-
const tools = [...studio.
|
|
498
|
-
(a, b) => Number(pinned.has(b.definition.id)) - Number(pinned.has(a.definition.id))
|
|
499
|
-
);
|
|
510
|
+
const tools = [...studio.definitions].sort((a, b) => Number(pinned.has(b.id)) - Number(pinned.has(a.id)));
|
|
500
511
|
const togglePin = (id) => {
|
|
501
512
|
const next = pinned.has(id) ? pins.filter((pin) => pin !== id) : [...pins, id];
|
|
502
513
|
try {
|
|
@@ -599,6 +610,25 @@ var PreviewCanvas = forwardRef3(function PreviewCanvas2({ title = "Preview", exp
|
|
|
599
610
|
);
|
|
600
611
|
});
|
|
601
612
|
|
|
613
|
+
// src/react/session-flag.ts
|
|
614
|
+
import { useEffect as useEffect3, useState as useState5 } from "react";
|
|
615
|
+
function useSessionFlag(key) {
|
|
616
|
+
const [value, setValue] = useState5(() => {
|
|
617
|
+
try {
|
|
618
|
+
return sessionStorage.getItem(key) === "true";
|
|
619
|
+
} catch {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
useEffect3(() => {
|
|
624
|
+
try {
|
|
625
|
+
sessionStorage.setItem(key, String(value));
|
|
626
|
+
} catch {
|
|
627
|
+
}
|
|
628
|
+
}, [key, value]);
|
|
629
|
+
return [value, setValue];
|
|
630
|
+
}
|
|
631
|
+
|
|
602
632
|
// src/react/ui/sidebar.tsx
|
|
603
633
|
import * as React2 from "react";
|
|
604
634
|
import { cva as cva2 } from "class-variance-authority";
|
|
@@ -1037,8 +1067,13 @@ function StudioFooter({ connection }) {
|
|
|
1037
1067
|
}
|
|
1038
1068
|
var SHORTCUTS = /* @__PURE__ */ new Set(["b", "s", "k"]);
|
|
1039
1069
|
var COMPARE_KEY = "\\";
|
|
1040
|
-
var editable = (target) => target
|
|
1041
|
-
var
|
|
1070
|
+
var editable = (target) => !!target?.matches?.("input, textarea, select, [contenteditable]");
|
|
1071
|
+
var historyAction = (event) => {
|
|
1072
|
+
if (!(event.metaKey || event.ctrlKey) || event.altKey || editable(event.target)) return null;
|
|
1073
|
+
const key = event.key.toLowerCase();
|
|
1074
|
+
return key === "z" ? event.shiftKey ? "redo" : "undo" : key === "y" ? "redo" : null;
|
|
1075
|
+
};
|
|
1076
|
+
var isShortcut = (event) => (event.metaKey || event.ctrlKey) && SHORTCUTS.has(event.key.toLowerCase()) || event.key === COMPARE_KEY && !editable(event.target) || !!historyAction(event);
|
|
1042
1077
|
function forwardShortcuts(frame) {
|
|
1043
1078
|
const forward = (event) => {
|
|
1044
1079
|
if (event.defaultPrevented || !isShortcut(event)) return;
|
|
@@ -1054,13 +1089,20 @@ function forwardShortcuts(frame) {
|
|
|
1054
1089
|
function Studio({ studio, previewComponent, getConfigFileHref }) {
|
|
1055
1090
|
useStudio(studio);
|
|
1056
1091
|
const { tools, pinned, togglePin } = usePinnedTools(studio);
|
|
1057
|
-
const [mounted, setMounted] =
|
|
1058
|
-
const [
|
|
1059
|
-
const [
|
|
1092
|
+
const [mounted, setMounted] = useState8(false);
|
|
1093
|
+
const [sidebarHidden, setSidebarHidden] = useSessionFlag("joyco-studio:sidebar-hidden");
|
|
1094
|
+
const [inspectorHidden, setInspectorHidden] = useSessionFlag("joyco-studio:inspector-hidden");
|
|
1095
|
+
const [canvasExpanded, setCanvasExpanded] = useSessionFlag("joyco-studio:canvas-expanded");
|
|
1096
|
+
const [selected, setSelected] = useState8(studio.definitions[0]?.id);
|
|
1060
1097
|
const preview = useRef3(null);
|
|
1098
|
+
const definition = studio.definitions.find((tool) => tool.id === selected);
|
|
1061
1099
|
const handle = studio.tools.find((tool) => tool.definition.id === selected);
|
|
1062
|
-
const Preview = previewComponent ?? (
|
|
1063
|
-
|
|
1100
|
+
const Preview = previewComponent ?? (definition?.previewLayout === "custom" ? "iframe" : PreviewCanvas);
|
|
1101
|
+
const hidden = { left: sidebarHidden, right: inspectorHidden || !handle };
|
|
1102
|
+
const show = { left: () => setSidebarHidden(false), right: () => setInspectorHidden(false) };
|
|
1103
|
+
const layout = useRef3(hidden);
|
|
1104
|
+
layout.current = hidden;
|
|
1105
|
+
useEffect5(() => {
|
|
1064
1106
|
const onKey = (event) => {
|
|
1065
1107
|
if (event.defaultPrevented || event.isComposing || event.altKey || !(event.metaKey || event.ctrlKey)) return;
|
|
1066
1108
|
if (event.key.toLowerCase() !== "b") return;
|
|
@@ -1071,7 +1113,9 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
|
|
|
1071
1113
|
active.blur();
|
|
1072
1114
|
preview.current?.focus();
|
|
1073
1115
|
}
|
|
1074
|
-
|
|
1116
|
+
const hide = !layout.current.left || !layout.current.right;
|
|
1117
|
+
setSidebarHidden(hide);
|
|
1118
|
+
setInspectorHidden(hide);
|
|
1075
1119
|
};
|
|
1076
1120
|
window.addEventListener("keydown", onKey);
|
|
1077
1121
|
return () => window.removeEventListener("keydown", onKey);
|
|
@@ -1081,15 +1125,15 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
|
|
|
1081
1125
|
handle.compare(showSaved);
|
|
1082
1126
|
postCompare(preview.current?.contentWindow, handle.definition.id, showSaved);
|
|
1083
1127
|
};
|
|
1084
|
-
|
|
1128
|
+
useEffect5(() => () => handle?.compare(false), [handle]);
|
|
1085
1129
|
const current = useRef3({ handle, compare });
|
|
1086
1130
|
current.current = { handle, compare };
|
|
1087
|
-
|
|
1131
|
+
useEffect5(() => {
|
|
1088
1132
|
const onKey = (event) => {
|
|
1089
1133
|
if (event.key !== COMPARE_KEY || editable(event.target) || event.repeat) return;
|
|
1090
1134
|
event.preventDefault();
|
|
1091
1135
|
const { handle: handle2, compare: compare2 } = current.current;
|
|
1092
|
-
compare2(event.type === "keydown" && !!handle2?.
|
|
1136
|
+
compare2(event.type === "keydown" && !!handle2?.dirty);
|
|
1093
1137
|
};
|
|
1094
1138
|
const release = () => current.current.compare(false);
|
|
1095
1139
|
addEventListener("keydown", onKey);
|
|
@@ -1102,10 +1146,20 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
|
|
|
1102
1146
|
removeEventListener("blur", release);
|
|
1103
1147
|
};
|
|
1104
1148
|
}, []);
|
|
1105
|
-
|
|
1149
|
+
useEffect5(() => {
|
|
1150
|
+
const onKey = (event) => {
|
|
1151
|
+
const action = event.defaultPrevented ? null : historyAction(event);
|
|
1152
|
+
if (!action) return;
|
|
1153
|
+
event.preventDefault();
|
|
1154
|
+
current.current.handle?.[action]();
|
|
1155
|
+
};
|
|
1156
|
+
addEventListener("keydown", onKey);
|
|
1157
|
+
return () => removeEventListener("keydown", onKey);
|
|
1158
|
+
}, []);
|
|
1159
|
+
useEffect5(() => {
|
|
1106
1160
|
const fromHash = () => {
|
|
1107
1161
|
const id = decodeURIComponent(location.hash.slice(1));
|
|
1108
|
-
if (studio.
|
|
1162
|
+
if (studio.definitions.some((tool) => tool.id === id)) setSelected(id);
|
|
1109
1163
|
};
|
|
1110
1164
|
setMounted(true);
|
|
1111
1165
|
fromHash();
|
|
@@ -1118,22 +1172,25 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
|
|
|
1118
1172
|
SidebarProvider,
|
|
1119
1173
|
{
|
|
1120
1174
|
defaultOpen: sidebarOpen,
|
|
1121
|
-
"data-preview-layout": previewComponent ? "canvas" :
|
|
1175
|
+
"data-preview-layout": previewComponent ? "canvas" : definition?.previewLayout ?? "canvas",
|
|
1176
|
+
"data-sidebar-panel": hidden.left ? "hidden" : "shown",
|
|
1122
1177
|
className: "studio-workspace relative h-svh min-h-0 overflow-hidden text-xs",
|
|
1123
1178
|
children: [
|
|
1124
|
-
|
|
1179
|
+
definition?.preview ? /* @__PURE__ */ jsx13(
|
|
1125
1180
|
Preview,
|
|
1126
1181
|
{
|
|
1127
1182
|
"data-slot": "studio-preview",
|
|
1128
1183
|
ref: preview,
|
|
1129
|
-
src:
|
|
1130
|
-
title:
|
|
1131
|
-
onLoad: (event) => forwardShortcuts(event.currentTarget)
|
|
1184
|
+
src: definition.preview,
|
|
1185
|
+
title: definition.title ?? definition.id,
|
|
1186
|
+
onLoad: (event) => forwardShortcuts(event.currentTarget),
|
|
1187
|
+
...Preview === PreviewCanvas && { expanded: canvasExpanded, onExpandedChange: setCanvasExpanded }
|
|
1132
1188
|
},
|
|
1133
|
-
|
|
1134
|
-
) : /* @__PURE__ */ jsx13("div", { className: "absolute inset-0 grid place-items-center text-faint", children:
|
|
1135
|
-
|
|
1189
|
+
definition.id
|
|
1190
|
+
) : /* @__PURE__ */ jsx13("div", { className: "absolute inset-0 grid place-items-center text-faint", children: definition ? "This tool has no preview" : "No tools registered" }),
|
|
1191
|
+
["left", "right"].filter((side) => hidden[side] && (side === "left" || handle)).map((side) => {
|
|
1136
1192
|
const Chevron = side === "left" ? ChevronRight : ChevronLeft;
|
|
1193
|
+
const label = side === "left" ? "Show tools" : "Show inspector";
|
|
1137
1194
|
return /* @__PURE__ */ jsx13(
|
|
1138
1195
|
"div",
|
|
1139
1196
|
{
|
|
@@ -1147,8 +1204,9 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
|
|
|
1147
1204
|
sideOffset: 8,
|
|
1148
1205
|
className: "flex items-center gap-2",
|
|
1149
1206
|
content: /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
1150
|
-
|
|
1207
|
+
label,
|
|
1151
1208
|
" ",
|
|
1209
|
+
/* @__PURE__ */ jsx13("span", { className: "text-faint", children: "\xB7 both" }),
|
|
1152
1210
|
/* @__PURE__ */ jsxs7(KbdGroup, { children: [
|
|
1153
1211
|
/* @__PURE__ */ jsx13(Kbd, { children: "\u2318/Ctrl" }),
|
|
1154
1212
|
/* @__PURE__ */ jsx13(Kbd, { children: "B" })
|
|
@@ -1160,8 +1218,8 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
|
|
|
1160
1218
|
"data-slot": "studio-panel-reveal",
|
|
1161
1219
|
variant: "ghost",
|
|
1162
1220
|
size: "icon",
|
|
1163
|
-
"aria-label":
|
|
1164
|
-
onClick:
|
|
1221
|
+
"aria-label": label,
|
|
1222
|
+
onClick: show[side],
|
|
1165
1223
|
className: cn(
|
|
1166
1224
|
"absolute top-1/2 h-16 w-6 -translate-y-1/2 cursor-pointer px-0 text-sidebar-foreground opacity-0 transition-[translate,opacity] duration-200 ease-out hover:bg-sidebar/95 hover:text-foreground focus:translate-x-0 focus:opacity-100 group-hover/edge:translate-x-0 group-hover/edge:opacity-100 motion-reduce:transition-none [@media(hover:none)]:translate-x-0 [@media(hover:none)]:opacity-100",
|
|
1167
1225
|
side === "left" ? "left-1 -translate-x-[calc(100%+0.25rem)]" : "right-1 translate-x-[calc(100%+0.25rem)]"
|
|
@@ -1180,11 +1238,11 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
|
|
|
1180
1238
|
{
|
|
1181
1239
|
collapsible: "icon",
|
|
1182
1240
|
variant: "floating",
|
|
1183
|
-
inert:
|
|
1184
|
-
"aria-hidden":
|
|
1241
|
+
inert: hidden.left,
|
|
1242
|
+
"aria-hidden": hidden.left,
|
|
1185
1243
|
className: cn(
|
|
1186
1244
|
"transition-[left,right,width,translate] duration-200 ease-out motion-reduce:transition-none",
|
|
1187
|
-
|
|
1245
|
+
hidden.left && "-translate-x-[calc(100%+1rem)]"
|
|
1188
1246
|
),
|
|
1189
1247
|
children: [
|
|
1190
1248
|
/* @__PURE__ */ jsx13(SidebarHeader, { className: "h-10 flex-row items-center overflow-hidden py-0 pr-0 pl-4 transition-[padding] duration-200 ease-out group-data-[collapsible=icon]:pl-[calc(1.5rem-0.875rem*352/144/2)] motion-reduce:transition-none", children: /* @__PURE__ */ jsxs7("span", { className: "flex items-center gap-2.5 text-foreground", children: [
|
|
@@ -1195,27 +1253,28 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
|
|
|
1195
1253
|
version
|
|
1196
1254
|
] })
|
|
1197
1255
|
] }) }),
|
|
1198
|
-
/* @__PURE__ */ jsx13(SidebarContent, { children: /* @__PURE__ */ jsx13(SidebarGroup, { children: /* @__PURE__ */ jsx13(SidebarMenu, { children: tools.map((
|
|
1199
|
-
const title =
|
|
1200
|
-
const isPinned = pinned.has(
|
|
1256
|
+
/* @__PURE__ */ jsx13(SidebarContent, { children: /* @__PURE__ */ jsx13(SidebarGroup, { children: /* @__PURE__ */ jsx13(SidebarMenu, { children: tools.map((tool) => {
|
|
1257
|
+
const title = tool.title ?? tool.id;
|
|
1258
|
+
const isPinned = pinned.has(tool.id);
|
|
1259
|
+
const { state, dirty } = studio.tools.find((handle2) => handle2.definition.id === tool.id) ?? {};
|
|
1201
1260
|
const pinLabel = `${isPinned ? "Unpin" : "Pin"} ${title}`;
|
|
1202
1261
|
return /* @__PURE__ */ jsxs7(SidebarMenuItem, { children: [
|
|
1203
1262
|
/* @__PURE__ */ jsxs7(
|
|
1204
1263
|
SidebarMenuButton,
|
|
1205
1264
|
{
|
|
1206
|
-
isActive:
|
|
1265
|
+
isActive: tool.id === selected,
|
|
1207
1266
|
tooltip: title,
|
|
1208
|
-
onClick: () => location.hash =
|
|
1267
|
+
onClick: () => location.hash = tool.id,
|
|
1209
1268
|
children: [
|
|
1210
1269
|
/* @__PURE__ */ jsxs7("span", { className: "relative", children: [
|
|
1211
|
-
/* @__PURE__ */ jsx13(ToolIcon, { icon:
|
|
1212
|
-
|
|
1270
|
+
/* @__PURE__ */ jsx13(ToolIcon, { icon: tool.icon, className: "size-4" }),
|
|
1271
|
+
dirty && /* @__PURE__ */ jsx13(
|
|
1213
1272
|
"span",
|
|
1214
1273
|
{
|
|
1215
|
-
"aria-label": state
|
|
1274
|
+
"aria-label": state?.conflict ? "Changed on disk" : "Unsaved",
|
|
1216
1275
|
className: cn(
|
|
1217
1276
|
"absolute -top-0.5 -right-0.5 size-1.5 rounded-full",
|
|
1218
|
-
state
|
|
1277
|
+
state?.conflict ? "bg-destructive" : "bg-warning"
|
|
1219
1278
|
)
|
|
1220
1279
|
}
|
|
1221
1280
|
)
|
|
@@ -1231,7 +1290,7 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
|
|
|
1231
1290
|
"aria-label": pinLabel,
|
|
1232
1291
|
"aria-pressed": isPinned,
|
|
1233
1292
|
showOnHover: !isPinned,
|
|
1234
|
-
onClick: () => togglePin(
|
|
1293
|
+
onClick: () => togglePin(tool.id),
|
|
1235
1294
|
className: cn(
|
|
1236
1295
|
"cursor-pointer [&>svg]:size-3 [@media(hover:none)]:opacity-100",
|
|
1237
1296
|
isPinned && "text-sidebar-primary"
|
|
@@ -1239,7 +1298,7 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
|
|
|
1239
1298
|
children: /* @__PURE__ */ jsx13(Pin2, { "aria-hidden": true, fill: isPinned ? "currentColor" : "none" })
|
|
1240
1299
|
}
|
|
1241
1300
|
) })
|
|
1242
|
-
] },
|
|
1301
|
+
] }, tool.id);
|
|
1243
1302
|
}) }) }) }),
|
|
1244
1303
|
/* @__PURE__ */ jsx13(StudioFooter, { connection: studio.connection })
|
|
1245
1304
|
]
|
|
@@ -1249,11 +1308,11 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
|
|
|
1249
1308
|
"aside",
|
|
1250
1309
|
{
|
|
1251
1310
|
"data-slot": "studio-inspector",
|
|
1252
|
-
inert:
|
|
1253
|
-
"aria-hidden":
|
|
1311
|
+
inert: hidden.right,
|
|
1312
|
+
"aria-hidden": hidden.right,
|
|
1254
1313
|
className: cn(
|
|
1255
1314
|
"absolute inset-y-2 right-2 z-10 flex w-(--studio-inspector-width) flex-col gap-1 p-1 transition-transform duration-200 ease-out motion-reduce:transition-none",
|
|
1256
|
-
|
|
1315
|
+
hidden.right && "translate-x-[calc(100%+1rem)]"
|
|
1257
1316
|
),
|
|
1258
1317
|
children: /* @__PURE__ */ jsx13(
|
|
1259
1318
|
Inspector,
|
|
@@ -1275,7 +1334,7 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
|
|
|
1275
1334
|
// src/react/command-menu.tsx
|
|
1276
1335
|
import { Pin as Pin3 } from "lucide-react";
|
|
1277
1336
|
import { Dialog } from "radix-ui";
|
|
1278
|
-
import { useEffect as
|
|
1337
|
+
import { useEffect as useEffect6, useRef as useRef4, useState as useState9 } from "react";
|
|
1279
1338
|
|
|
1280
1339
|
// src/react/ui/command.tsx
|
|
1281
1340
|
import * as React3 from "react";
|
|
@@ -1343,10 +1402,10 @@ function CommandItem({ className, ...props }) {
|
|
|
1343
1402
|
import { jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1344
1403
|
function StudioCommandMenu({ studio, studioPath = "/studio", navigate }) {
|
|
1345
1404
|
const { tools, pinned } = usePinnedTools(studio);
|
|
1346
|
-
const [open, setOpen] =
|
|
1405
|
+
const [open, setOpen] = useState9(false);
|
|
1347
1406
|
const opener = useRef4(null);
|
|
1348
1407
|
const input = useRef4(null);
|
|
1349
|
-
|
|
1408
|
+
useEffect6(() => {
|
|
1350
1409
|
if (window.frameElement?.getAttribute("data-slot") === "studio-preview") return;
|
|
1351
1410
|
const onKey = (event) => {
|
|
1352
1411
|
if (event.defaultPrevented || event.isComposing || event.altKey || !(event.metaKey || event.ctrlKey)) return;
|
|
@@ -1397,8 +1456,8 @@ function StudioCommandMenu({ studio, studioPath = "/studio", navigate }) {
|
|
|
1397
1456
|
/* @__PURE__ */ jsxs9(Command, { label: "Studio tools", loop: true, vimBindings: false, children: [
|
|
1398
1457
|
/* @__PURE__ */ jsx15(CommandInput, { ref: input, placeholder: "Search tools\u2026" }),
|
|
1399
1458
|
/* @__PURE__ */ jsxs9(CommandList, { children: [
|
|
1400
|
-
/* @__PURE__ */ jsx15(CommandEmpty, { children: studio.
|
|
1401
|
-
tools.map((
|
|
1459
|
+
/* @__PURE__ */ jsx15(CommandEmpty, { children: studio.definitions.length ? "No tools found." : "No tools registered yet." }),
|
|
1460
|
+
tools.map((tool) => /* @__PURE__ */ jsxs9(
|
|
1402
1461
|
CommandItem,
|
|
1403
1462
|
{
|
|
1404
1463
|
value: tool.id,
|