@joycostudio/studio 0.1.2 → 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/dist/react.js CHANGED
@@ -11,10 +11,10 @@ import {
11
11
  PlugZap,
12
12
  Unplug
13
13
  } from "lucide-react";
14
- import { useEffect as useEffect4, useRef as useRef3, useState as useState7 } from "react";
14
+ import { useEffect as useEffect5, useRef as useRef3, useState as useState8 } from "react";
15
15
 
16
16
  // package.json
17
- var version = "0.1.2";
17
+ var version = "0.1.3";
18
18
 
19
19
  // src/client/studio.ts
20
20
  import {
@@ -507,9 +507,7 @@ function usePinnedTools(studio) {
507
507
  };
508
508
  }, []);
509
509
  const pinned = new Set(pins);
510
- const tools = [...studio.tools].sort(
511
- (a, b) => Number(pinned.has(b.definition.id)) - Number(pinned.has(a.definition.id))
512
- );
510
+ const tools = [...studio.definitions].sort((a, b) => Number(pinned.has(b.id)) - Number(pinned.has(a.id)));
513
511
  const togglePin = (id) => {
514
512
  const next = pinned.has(id) ? pins.filter((pin) => pin !== id) : [...pins, id];
515
513
  try {
@@ -612,6 +610,25 @@ var PreviewCanvas = forwardRef3(function PreviewCanvas2({ title = "Preview", exp
612
610
  );
613
611
  });
614
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
+
615
632
  // src/react/ui/sidebar.tsx
616
633
  import * as React2 from "react";
617
634
  import { cva as cva2 } from "class-variance-authority";
@@ -1072,13 +1089,20 @@ function forwardShortcuts(frame) {
1072
1089
  function Studio({ studio, previewComponent, getConfigFileHref }) {
1073
1090
  useStudio(studio);
1074
1091
  const { tools, pinned, togglePin } = usePinnedTools(studio);
1075
- const [mounted, setMounted] = useState7(false);
1076
- const [panelsHidden, setPanelsHidden] = useState7(false);
1077
- const [selected, setSelected] = useState7(studio.tools[0]?.definition.id);
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);
1078
1097
  const preview = useRef3(null);
1098
+ const definition = studio.definitions.find((tool) => tool.id === selected);
1079
1099
  const handle = studio.tools.find((tool) => tool.definition.id === selected);
1080
- const Preview = previewComponent ?? (handle?.definition.previewLayout === "custom" ? "iframe" : PreviewCanvas);
1081
- useEffect4(() => {
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(() => {
1082
1106
  const onKey = (event) => {
1083
1107
  if (event.defaultPrevented || event.isComposing || event.altKey || !(event.metaKey || event.ctrlKey)) return;
1084
1108
  if (event.key.toLowerCase() !== "b") return;
@@ -1089,7 +1113,9 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
1089
1113
  active.blur();
1090
1114
  preview.current?.focus();
1091
1115
  }
1092
- setPanelsHidden((hidden) => !hidden);
1116
+ const hide = !layout.current.left || !layout.current.right;
1117
+ setSidebarHidden(hide);
1118
+ setInspectorHidden(hide);
1093
1119
  };
1094
1120
  window.addEventListener("keydown", onKey);
1095
1121
  return () => window.removeEventListener("keydown", onKey);
@@ -1099,10 +1125,10 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
1099
1125
  handle.compare(showSaved);
1100
1126
  postCompare(preview.current?.contentWindow, handle.definition.id, showSaved);
1101
1127
  };
1102
- useEffect4(() => () => handle?.compare(false), [handle]);
1128
+ useEffect5(() => () => handle?.compare(false), [handle]);
1103
1129
  const current = useRef3({ handle, compare });
1104
1130
  current.current = { handle, compare };
1105
- useEffect4(() => {
1131
+ useEffect5(() => {
1106
1132
  const onKey = (event) => {
1107
1133
  if (event.key !== COMPARE_KEY || editable(event.target) || event.repeat) return;
1108
1134
  event.preventDefault();
@@ -1120,7 +1146,7 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
1120
1146
  removeEventListener("blur", release);
1121
1147
  };
1122
1148
  }, []);
1123
- useEffect4(() => {
1149
+ useEffect5(() => {
1124
1150
  const onKey = (event) => {
1125
1151
  const action = event.defaultPrevented ? null : historyAction(event);
1126
1152
  if (!action) return;
@@ -1130,10 +1156,10 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
1130
1156
  addEventListener("keydown", onKey);
1131
1157
  return () => removeEventListener("keydown", onKey);
1132
1158
  }, []);
1133
- useEffect4(() => {
1159
+ useEffect5(() => {
1134
1160
  const fromHash = () => {
1135
1161
  const id = decodeURIComponent(location.hash.slice(1));
1136
- if (studio.tools.some((handle2) => handle2.definition.id === id)) setSelected(id);
1162
+ if (studio.definitions.some((tool) => tool.id === id)) setSelected(id);
1137
1163
  };
1138
1164
  setMounted(true);
1139
1165
  fromHash();
@@ -1146,22 +1172,25 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
1146
1172
  SidebarProvider,
1147
1173
  {
1148
1174
  defaultOpen: sidebarOpen,
1149
- "data-preview-layout": previewComponent ? "canvas" : handle?.definition.previewLayout ?? "canvas",
1175
+ "data-preview-layout": previewComponent ? "canvas" : definition?.previewLayout ?? "canvas",
1176
+ "data-sidebar-panel": hidden.left ? "hidden" : "shown",
1150
1177
  className: "studio-workspace relative h-svh min-h-0 overflow-hidden text-xs",
1151
1178
  children: [
1152
- handle?.definition.preview ? /* @__PURE__ */ jsx13(
1179
+ definition?.preview ? /* @__PURE__ */ jsx13(
1153
1180
  Preview,
1154
1181
  {
1155
1182
  "data-slot": "studio-preview",
1156
1183
  ref: preview,
1157
- src: handle.definition.preview,
1158
- title: handle.definition.title ?? handle.definition.id,
1159
- 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 }
1160
1188
  },
1161
- handle.definition.id
1162
- ) : /* @__PURE__ */ jsx13("div", { className: "absolute inset-0 grid place-items-center text-faint", children: handle ? "This tool has no preview" : "No tools registered" }),
1163
- panelsHidden && ["left", "right"].map((side) => {
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) => {
1164
1192
  const Chevron = side === "left" ? ChevronRight : ChevronLeft;
1193
+ const label = side === "left" ? "Show tools" : "Show inspector";
1165
1194
  return /* @__PURE__ */ jsx13(
1166
1195
  "div",
1167
1196
  {
@@ -1175,8 +1204,9 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
1175
1204
  sideOffset: 8,
1176
1205
  className: "flex items-center gap-2",
1177
1206
  content: /* @__PURE__ */ jsxs7(Fragment2, { children: [
1178
- "Show Studio panels",
1207
+ label,
1179
1208
  " ",
1209
+ /* @__PURE__ */ jsx13("span", { className: "text-faint", children: "\xB7 both" }),
1180
1210
  /* @__PURE__ */ jsxs7(KbdGroup, { children: [
1181
1211
  /* @__PURE__ */ jsx13(Kbd, { children: "\u2318/Ctrl" }),
1182
1212
  /* @__PURE__ */ jsx13(Kbd, { children: "B" })
@@ -1188,8 +1218,8 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
1188
1218
  "data-slot": "studio-panel-reveal",
1189
1219
  variant: "ghost",
1190
1220
  size: "icon",
1191
- "aria-label": "Show Studio panels",
1192
- onClick: () => setPanelsHidden(false),
1221
+ "aria-label": label,
1222
+ onClick: show[side],
1193
1223
  className: cn(
1194
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",
1195
1225
  side === "left" ? "left-1 -translate-x-[calc(100%+0.25rem)]" : "right-1 translate-x-[calc(100%+0.25rem)]"
@@ -1208,11 +1238,11 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
1208
1238
  {
1209
1239
  collapsible: "icon",
1210
1240
  variant: "floating",
1211
- inert: panelsHidden,
1212
- "aria-hidden": panelsHidden,
1241
+ inert: hidden.left,
1242
+ "aria-hidden": hidden.left,
1213
1243
  className: cn(
1214
1244
  "transition-[left,right,width,translate] duration-200 ease-out motion-reduce:transition-none",
1215
- panelsHidden && "-translate-x-[calc(100%+1rem)]"
1245
+ hidden.left && "-translate-x-[calc(100%+1rem)]"
1216
1246
  ),
1217
1247
  children: [
1218
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: [
@@ -1223,20 +1253,21 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
1223
1253
  version
1224
1254
  ] })
1225
1255
  ] }) }),
1226
- /* @__PURE__ */ jsx13(SidebarContent, { children: /* @__PURE__ */ jsx13(SidebarGroup, { children: /* @__PURE__ */ jsx13(SidebarMenu, { children: tools.map(({ definition, state, dirty }) => {
1227
- const title = definition.title ?? definition.id;
1228
- const isPinned = pinned.has(definition.id);
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) ?? {};
1229
1260
  const pinLabel = `${isPinned ? "Unpin" : "Pin"} ${title}`;
1230
1261
  return /* @__PURE__ */ jsxs7(SidebarMenuItem, { children: [
1231
1262
  /* @__PURE__ */ jsxs7(
1232
1263
  SidebarMenuButton,
1233
1264
  {
1234
- isActive: definition.id === selected,
1265
+ isActive: tool.id === selected,
1235
1266
  tooltip: title,
1236
- onClick: () => location.hash = definition.id,
1267
+ onClick: () => location.hash = tool.id,
1237
1268
  children: [
1238
1269
  /* @__PURE__ */ jsxs7("span", { className: "relative", children: [
1239
- /* @__PURE__ */ jsx13(ToolIcon, { icon: definition.icon, className: "size-4" }),
1270
+ /* @__PURE__ */ jsx13(ToolIcon, { icon: tool.icon, className: "size-4" }),
1240
1271
  dirty && /* @__PURE__ */ jsx13(
1241
1272
  "span",
1242
1273
  {
@@ -1259,7 +1290,7 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
1259
1290
  "aria-label": pinLabel,
1260
1291
  "aria-pressed": isPinned,
1261
1292
  showOnHover: !isPinned,
1262
- onClick: () => togglePin(definition.id),
1293
+ onClick: () => togglePin(tool.id),
1263
1294
  className: cn(
1264
1295
  "cursor-pointer [&>svg]:size-3 [@media(hover:none)]:opacity-100",
1265
1296
  isPinned && "text-sidebar-primary"
@@ -1267,7 +1298,7 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
1267
1298
  children: /* @__PURE__ */ jsx13(Pin2, { "aria-hidden": true, fill: isPinned ? "currentColor" : "none" })
1268
1299
  }
1269
1300
  ) })
1270
- ] }, definition.id);
1301
+ ] }, tool.id);
1271
1302
  }) }) }) }),
1272
1303
  /* @__PURE__ */ jsx13(StudioFooter, { connection: studio.connection })
1273
1304
  ]
@@ -1277,11 +1308,11 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
1277
1308
  "aside",
1278
1309
  {
1279
1310
  "data-slot": "studio-inspector",
1280
- inert: panelsHidden,
1281
- "aria-hidden": panelsHidden,
1311
+ inert: hidden.right,
1312
+ "aria-hidden": hidden.right,
1282
1313
  className: cn(
1283
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",
1284
- panelsHidden && "translate-x-[calc(100%+1rem)]"
1315
+ hidden.right && "translate-x-[calc(100%+1rem)]"
1285
1316
  ),
1286
1317
  children: /* @__PURE__ */ jsx13(
1287
1318
  Inspector,
@@ -1303,7 +1334,7 @@ function Studio({ studio, previewComponent, getConfigFileHref }) {
1303
1334
  // src/react/command-menu.tsx
1304
1335
  import { Pin as Pin3 } from "lucide-react";
1305
1336
  import { Dialog } from "radix-ui";
1306
- import { useEffect as useEffect5, useRef as useRef4, useState as useState8 } from "react";
1337
+ import { useEffect as useEffect6, useRef as useRef4, useState as useState9 } from "react";
1307
1338
 
1308
1339
  // src/react/ui/command.tsx
1309
1340
  import * as React3 from "react";
@@ -1371,10 +1402,10 @@ function CommandItem({ className, ...props }) {
1371
1402
  import { jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
1372
1403
  function StudioCommandMenu({ studio, studioPath = "/studio", navigate }) {
1373
1404
  const { tools, pinned } = usePinnedTools(studio);
1374
- const [open, setOpen] = useState8(false);
1405
+ const [open, setOpen] = useState9(false);
1375
1406
  const opener = useRef4(null);
1376
1407
  const input = useRef4(null);
1377
- useEffect5(() => {
1408
+ useEffect6(() => {
1378
1409
  if (window.frameElement?.getAttribute("data-slot") === "studio-preview") return;
1379
1410
  const onKey = (event) => {
1380
1411
  if (event.defaultPrevented || event.isComposing || event.altKey || !(event.metaKey || event.ctrlKey)) return;
@@ -1425,8 +1456,8 @@ function StudioCommandMenu({ studio, studioPath = "/studio", navigate }) {
1425
1456
  /* @__PURE__ */ jsxs9(Command, { label: "Studio tools", loop: true, vimBindings: false, children: [
1426
1457
  /* @__PURE__ */ jsx15(CommandInput, { ref: input, placeholder: "Search tools\u2026" }),
1427
1458
  /* @__PURE__ */ jsxs9(CommandList, { children: [
1428
- /* @__PURE__ */ jsx15(CommandEmpty, { children: studio.tools.length ? "No tools found." : "No tools registered yet." }),
1429
- tools.map(({ definition: tool }) => /* @__PURE__ */ jsxs9(
1459
+ /* @__PURE__ */ jsx15(CommandEmpty, { children: studio.definitions.length ? "No tools found." : "No tools registered yet." }),
1460
+ tools.map((tool) => /* @__PURE__ */ jsxs9(
1430
1461
  CommandItem,
1431
1462
  {
1432
1463
  value: tool.id,