@joycostudio/studio 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.js CHANGED
@@ -7,13 +7,14 @@ import {
7
7
  LoaderCircle,
8
8
  PanelLeftClose,
9
9
  PanelLeftOpen,
10
+ Pin as Pin2,
10
11
  PlugZap,
11
12
  Unplug
12
13
  } from "lucide-react";
13
- import { useEffect as useEffect3, useRef as useRef2, useState as useState4 } from "react";
14
+ import { useEffect as useEffect4, useRef as useRef3, useState as useState7 } from "react";
14
15
 
15
16
  // package.json
16
- var version = "0.0.2";
17
+ var version = "0.1.0";
17
18
 
18
19
  // src/client/studio.ts
19
20
  import { button } from "@joycostudio/xyz/debug";
@@ -241,9 +242,72 @@ function TooltipContent({
241
242
  }
242
243
  ) });
243
244
  }
245
+ function Hint({
246
+ children,
247
+ content,
248
+ ...props
249
+ }) {
250
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
251
+ /* @__PURE__ */ jsx4(TooltipTrigger, { asChild: true, children }),
252
+ /* @__PURE__ */ jsx4(TooltipContent, { ...props, children: content })
253
+ ] });
254
+ }
255
+
256
+ // src/react/ui/panel.tsx
257
+ import { Slot as Slot3 } from "radix-ui";
258
+ import { forwardRef as forwardRef2 } from "react";
259
+
260
+ // src/react/surface.tsx
261
+ import { Slot as Slot2 } from "radix-ui";
262
+ import { forwardRef } from "react";
263
+ import { jsx as jsx5 } from "react/jsx-runtime";
264
+ var StudioSurface = forwardRef(
265
+ function StudioSurface2({ asChild = false, className, ...props }, ref) {
266
+ const Comp = asChild ? Slot2.Root : "div";
267
+ return /* @__PURE__ */ jsx5(Comp, { ref, "data-slot": "studio-surface", className: cn("studio-surface", className), ...props });
268
+ }
269
+ );
270
+ var StudioToolbar = forwardRef(function StudioToolbar2({ className, ...props }, ref) {
271
+ return /* @__PURE__ */ jsx5(StudioSurface, { ref, role: "group", className: cn("studio-toolbar", className), ...props });
272
+ });
273
+
274
+ // src/react/ui/panel.tsx
275
+ import { jsx as jsx6 } from "react/jsx-runtime";
276
+ var Panel = forwardRef2(function Panel2({ className, asChild = false, ...props }, ref) {
277
+ const Comp = asChild ? Slot3.Root : "div";
278
+ return /* @__PURE__ */ jsx6(
279
+ Comp,
280
+ {
281
+ ref,
282
+ "data-slot": "panel",
283
+ className: cn(
284
+ "rounded-lg border border-sidebar-border bg-sidebar/85 shadow-xl shadow-black/40 backdrop-blur-xl",
285
+ className
286
+ ),
287
+ ...props
288
+ }
289
+ );
290
+ });
291
+ function PanelHeader({ className, ...props }) {
292
+ return /* @__PURE__ */ jsx6(StudioSurface, { asChild: true, className: cn("px-3 pt-2.5 pb-2", className), children: /* @__PURE__ */ jsx6("header", { "data-slot": "panel-header", ...props }) });
293
+ }
294
+ var PanelContent = forwardRef2(function PanelContent2({ className, ...props }, ref) {
295
+ return /* @__PURE__ */ jsx6(
296
+ "div",
297
+ {
298
+ ref,
299
+ "data-slot": "panel-content",
300
+ className: cn("min-h-0 flex-1 overflow-auto px-1 py-1.5", className),
301
+ ...props
302
+ }
303
+ );
304
+ });
305
+ function PanelFooter({ className, ...props }) {
306
+ return /* @__PURE__ */ jsx6(StudioSurface, { asChild: true, className: cn("grid gap-2 p-2", className), children: /* @__PURE__ */ jsx6("footer", { "data-slot": "panel-footer", ...props }) });
307
+ }
244
308
 
245
309
  // src/react/inspector.tsx
246
- import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
310
+ import { Fragment, jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
247
311
  function describe(studio, handle) {
248
312
  const { state } = handle;
249
313
  if (studio.connection.phase !== "connected") return { tone: "text-faint", text: "Not persisting" };
@@ -256,7 +320,8 @@ function describe(studio, handle) {
256
320
  function Inspector({
257
321
  studio,
258
322
  handle,
259
- onCompare
323
+ onCompare,
324
+ fileHref
260
325
  }) {
261
326
  useStudio(studio);
262
327
  const container = useRef(null);
@@ -299,74 +364,80 @@ function Inspector({
299
364
  if (pane.current) pane.current.disabled = handle.comparing;
300
365
  });
301
366
  const { definition, state } = handle;
367
+ const ConfigFile = fileHref ? "a" : "span";
302
368
  const status = describe(studio, handle);
303
369
  const editable2 = studio.connection.phase === "connected" && !!state?.dirty && !handle.comparing;
304
370
  const changes = (state?.dirty ? changedPaths(state.saved.value, state.draft.value) : []).map(
305
371
  (path) => path.split(".").map((key) => key.charAt(0).toUpperCase() + key.slice(1)).join(" \u203A ")
306
372
  );
307
- return /* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 flex-1 flex-col gap-1 p-1", children: [
308
- /* @__PURE__ */ jsxs2("header", { className: "rounded-[calc(var(--radius-lg)-0.25rem)] bg-card px-3 pt-2.5 pb-2", children: [
373
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
374
+ /* @__PURE__ */ jsxs2(PanelHeader, { children: [
309
375
  /* @__PURE__ */ jsxs2("strong", { className: "flex min-w-0 items-center gap-2 text-foreground", children: [
310
- /* @__PURE__ */ jsx5(ToolIcon, { icon: definition.icon, className: "size-3.5 shrink-0" }),
311
- /* @__PURE__ */ jsx5("span", { className: "truncate", title: definition.title ?? definition.id, children: definition.title ?? definition.id })
376
+ /* @__PURE__ */ jsx7(ToolIcon, { icon: definition.icon, className: "size-3.5 shrink-0" }),
377
+ /* @__PURE__ */ jsx7("span", { className: "truncate", title: definition.title ?? definition.id, children: definition.title ?? definition.id })
312
378
  ] }),
313
379
  /* @__PURE__ */ jsxs2("div", { className: "mt-1 flex items-center gap-3 pl-5.5 text-[11px]", children: [
314
- /* @__PURE__ */ jsx5("span", { className: "min-w-0 flex-1 truncate text-muted-foreground", title: definition.file, children: definition.file }),
380
+ /* @__PURE__ */ jsx7(
381
+ ConfigFile,
382
+ {
383
+ "data-slot": "studio-config-file",
384
+ className: "min-w-0 flex-1 truncate text-muted-foreground",
385
+ href: fileHref,
386
+ title: fileHref ? `Open ${definition.file}` : definition.file,
387
+ children: definition.file
388
+ }
389
+ ),
315
390
  /* @__PURE__ */ jsxs2(
316
391
  "span",
317
392
  {
318
393
  className: `flex shrink-0 items-center gap-1.5 ${status.tone}`,
319
394
  title: state ? `Draft revision ${state.draft.revision}` : void 0,
320
395
  children: [
321
- /* @__PURE__ */ jsx5("span", { "aria-hidden": "true", className: "size-1.5 rounded-full bg-current" }),
396
+ /* @__PURE__ */ jsx7("span", { "aria-hidden": "true", className: "size-1.5 rounded-full bg-current" }),
322
397
  status.text
323
398
  ]
324
399
  }
325
400
  )
326
401
  ] })
327
402
  ] }),
328
- /* @__PURE__ */ jsx5(
329
- "div",
330
- {
331
- ref: container,
332
- "data-slot": "studio-controls",
333
- className: "min-h-0 flex-1 overflow-auto px-1 py-1.5",
334
- style: tweakpaneTheme
335
- }
336
- ),
337
- /* @__PURE__ */ jsxs2("footer", { className: "grid gap-2 rounded-[calc(var(--radius-lg)-0.25rem)] bg-card p-2", children: [
338
- failure && /* @__PURE__ */ jsx5("div", { className: "text-destructive", children: failure }),
403
+ /* @__PURE__ */ jsx7(PanelContent, { ref: container, "data-slot": "studio-controls", style: tweakpaneTheme }),
404
+ /* @__PURE__ */ jsxs2(PanelFooter, { children: [
405
+ failure && /* @__PURE__ */ jsx7("div", { className: "text-destructive", children: failure }),
339
406
  changes.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "truncate text-muted-foreground", title: changes.join(", "), children: [
340
407
  changes.length,
341
408
  " changed: ",
342
- /* @__PURE__ */ jsx5("span", { className: "text-foreground", children: changes.join(", ") })
409
+ /* @__PURE__ */ jsx7("span", { className: "text-foreground", children: changes.join(", ") })
343
410
  ] }),
344
411
  /* @__PURE__ */ jsxs2("div", { className: "flex gap-2", children: [
345
- /* @__PURE__ */ jsxs2(Tooltip, { children: [
346
- /* @__PURE__ */ jsx5(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx5(
347
- Button,
348
- {
349
- variant: "secondary",
350
- size: "icon-sm",
351
- "aria-label": "Hold to compare with saved",
352
- "aria-pressed": handle.comparing,
353
- disabled: !state?.dirty,
354
- className: "shrink-0 aria-pressed:bg-primary aria-pressed:text-primary-foreground",
355
- onPointerDown: (event) => {
356
- event.currentTarget.setPointerCapture(event.pointerId);
357
- onCompare(true);
358
- },
359
- onPointerUp: () => onCompare(false),
360
- onPointerCancel: () => onCompare(false),
361
- children: /* @__PURE__ */ jsx5(SquareSplitHorizontal, {})
362
- }
363
- ) }),
364
- /* @__PURE__ */ jsxs2(TooltipContent, { className: "flex items-center gap-2", children: [
365
- "Hold to compare with saved ",
366
- /* @__PURE__ */ jsx5(Kbd, { children: "\\" })
367
- ] })
368
- ] }),
369
- /* @__PURE__ */ jsx5(
412
+ /* @__PURE__ */ jsx7(
413
+ Hint,
414
+ {
415
+ className: "flex items-center gap-2",
416
+ content: /* @__PURE__ */ jsxs2(Fragment, { children: [
417
+ "Hold to compare with saved ",
418
+ /* @__PURE__ */ jsx7(Kbd, { children: "\\" })
419
+ ] }),
420
+ children: /* @__PURE__ */ jsx7(
421
+ Button,
422
+ {
423
+ variant: "secondary",
424
+ size: "icon-sm",
425
+ "aria-label": "Hold to compare with saved",
426
+ "aria-pressed": handle.comparing,
427
+ disabled: !state?.dirty,
428
+ className: "shrink-0 aria-pressed:bg-primary aria-pressed:text-primary-foreground",
429
+ onPointerDown: (event) => {
430
+ event.currentTarget.setPointerCapture(event.pointerId);
431
+ onCompare(true);
432
+ },
433
+ onPointerUp: () => onCompare(false),
434
+ onPointerCancel: () => onCompare(false),
435
+ children: /* @__PURE__ */ jsx7(SquareSplitHorizontal, {})
436
+ }
437
+ )
438
+ }
439
+ ),
440
+ /* @__PURE__ */ jsx7(
370
441
  Button,
371
442
  {
372
443
  variant: "secondary",
@@ -377,11 +448,11 @@ function Inspector({
377
448
  children: "Discard"
378
449
  }
379
450
  ),
380
- state?.conflict ? /* @__PURE__ */ jsx5(Button, { variant: "destructive", size: "sm", className: "flex-1", onClick: () => save(true), children: "Overwrite file" }) : /* @__PURE__ */ jsxs2(Button, { size: "sm", className: "flex-1", disabled: !editable2, onClick: () => save(), children: [
451
+ state?.conflict ? /* @__PURE__ */ jsx7(Button, { variant: "destructive", size: "sm", className: "flex-1", onClick: () => save(true), children: "Overwrite file" }) : /* @__PURE__ */ jsxs2(Button, { size: "sm", className: "flex-1", disabled: !editable2, onClick: () => save(), children: [
381
452
  "Save",
382
453
  /* @__PURE__ */ jsxs2(KbdGroup, { children: [
383
- /* @__PURE__ */ jsx5(Kbd, { children: "\u2318" }),
384
- /* @__PURE__ */ jsx5(Kbd, { children: "S" })
454
+ /* @__PURE__ */ jsx7(Kbd, { children: "\u2318" }),
455
+ /* @__PURE__ */ jsx7(Kbd, { children: "S" })
385
456
  ] })
386
457
  ] })
387
458
  ] })
@@ -390,16 +461,148 @@ function Inspector({
390
461
  }
391
462
 
392
463
  // src/react/logo.tsx
393
- import { jsx as jsx6 } from "react/jsx-runtime";
464
+ import { jsx as jsx8 } from "react/jsx-runtime";
394
465
  function Logo(props) {
395
- return /* @__PURE__ */ jsx6("svg", { viewBox: "0 0 352 144", fill: "currentColor", "aria-label": "JOYCO", role: "img", ...props, children: /* @__PURE__ */ jsx6("path", { d: "M328 96L280 144H0V40H176V112H192V24H0V0H352V24H328V96Z" }) });
466
+ return /* @__PURE__ */ jsx8("svg", { viewBox: "0 0 352 144", fill: "currentColor", "aria-label": "JOYCO", role: "img", ...props, children: /* @__PURE__ */ jsx8("path", { d: "M328 96L280 144H0V40H176V112H192V24H0V0H352V24H328V96Z" }) });
396
467
  }
397
468
 
469
+ // src/react/pinned-tools.ts
470
+ import { useEffect as useEffect2, useState as useState2 } from "react";
471
+ var STORAGE_KEY = "joyco-studio:pinned-tools";
472
+ var CHANGE_EVENT = "joyco-studio:pins-changed";
473
+ function readPins() {
474
+ try {
475
+ const value = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "[]");
476
+ return Array.isArray(value) ? value.filter((id) => typeof id === "string") : [];
477
+ } catch {
478
+ return [];
479
+ }
480
+ }
481
+ function usePinnedTools(studio) {
482
+ const [pins, setPins] = useState2([]);
483
+ useEffect2(() => {
484
+ setPins(readPins());
485
+ const onChange = (event) => setPins(event.detail);
486
+ const onStorage = (event) => {
487
+ if (event.key === STORAGE_KEY || event.key === null) setPins(readPins());
488
+ };
489
+ window.addEventListener(CHANGE_EVENT, onChange);
490
+ window.addEventListener("storage", onStorage);
491
+ return () => {
492
+ window.removeEventListener(CHANGE_EVENT, onChange);
493
+ window.removeEventListener("storage", onStorage);
494
+ };
495
+ }, []);
496
+ const pinned = new Set(pins);
497
+ const tools = [...studio.tools].sort(
498
+ (a, b) => Number(pinned.has(b.definition.id)) - Number(pinned.has(a.definition.id))
499
+ );
500
+ const togglePin = (id) => {
501
+ const next = pinned.has(id) ? pins.filter((pin) => pin !== id) : [...pins, id];
502
+ try {
503
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
504
+ } catch {
505
+ }
506
+ window.dispatchEvent(new CustomEvent(CHANGE_EVENT, { detail: next }));
507
+ };
508
+ return { tools, pinned, togglePin };
509
+ }
510
+
511
+ // src/react/preview-canvas.tsx
512
+ import { RotateCw } from "lucide-react";
513
+ import { forwardRef as forwardRef3, useImperativeHandle, useRef as useRef2, useState as useState4 } from "react";
514
+
515
+ // src/react/canvas-toolbar.tsx
516
+ import { Maximize, Minimize, Pin, PinOff } from "lucide-react";
517
+ import { useState as useState3 } from "react";
518
+ import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
519
+ function CanvasToolbar({ label, expanded = false, onToggle, children }) {
520
+ const [ghost, setGhost] = useState3(false);
521
+ const ExpandIcon = expanded ? Minimize : Maximize;
522
+ const PinIcon = ghost ? PinOff : Pin;
523
+ return /* @__PURE__ */ jsx9(
524
+ "div",
525
+ {
526
+ className: "studio-canvas-toolbar",
527
+ "data-slot": "canvas-toolbar",
528
+ "data-expanded": expanded || void 0,
529
+ "data-ghost": !expanded && ghost || void 0,
530
+ role: "group",
531
+ "aria-label": label,
532
+ children: /* @__PURE__ */ jsxs3(StudioToolbar, { className: "studio-canvas-actions", children: [
533
+ children,
534
+ onToggle && /* @__PURE__ */ jsx9("div", { className: "studio-canvas-toggle", children: /* @__PURE__ */ jsxs3("button", { "data-slot": "toolbar-action", type: "button", "aria-pressed": expanded, onClick: onToggle, children: [
535
+ /* @__PURE__ */ jsx9(ExpandIcon, { size: 14, strokeWidth: 1.5, "aria-hidden": true }),
536
+ expanded ? "Contain canvas" : "Expand"
537
+ ] }) }),
538
+ !expanded && /* @__PURE__ */ jsx9(
539
+ "button",
540
+ {
541
+ type: "button",
542
+ "aria-label": `Auto-hide ${label.toLowerCase()} header`,
543
+ "aria-pressed": ghost,
544
+ title: ghost ? "Pin header" : "Auto-hide header",
545
+ onClick: () => setGhost(!ghost),
546
+ children: /* @__PURE__ */ jsx9(PinIcon, { size: 14, strokeWidth: 1.5, "aria-hidden": true })
547
+ }
548
+ )
549
+ ] })
550
+ }
551
+ );
552
+ }
553
+
554
+ // src/react/preview-canvas.tsx
555
+ import { jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
556
+ var PreviewCanvas = forwardRef3(function PreviewCanvas2({ title = "Preview", expanded: controlled, onExpandedChange, onReload, toolbar, ...props }, ref) {
557
+ const [localExpanded, setExpanded] = useState4(false);
558
+ const expanded = controlled ?? localExpanded;
559
+ const frame = useRef2(null);
560
+ useImperativeHandle(ref, () => frame.current, []);
561
+ return /* @__PURE__ */ jsxs4(
562
+ "section",
563
+ {
564
+ className: "studio-preview-canvas",
565
+ "data-slot": "preview-canvas",
566
+ "data-expanded": expanded || void 0,
567
+ "aria-label": `${title} canvas`,
568
+ children: [
569
+ /* @__PURE__ */ jsxs4(
570
+ CanvasToolbar,
571
+ {
572
+ label: "Preview canvas",
573
+ expanded,
574
+ onToggle: () => {
575
+ setExpanded(!expanded);
576
+ onExpandedChange?.(!expanded);
577
+ },
578
+ children: [
579
+ /* @__PURE__ */ jsx10(
580
+ "button",
581
+ {
582
+ type: "button",
583
+ "aria-label": "Reload preview",
584
+ title: "Reload preview",
585
+ onClick: () => {
586
+ onReload?.();
587
+ if (frame.current) frame.current.src = frame.current.src;
588
+ },
589
+ children: /* @__PURE__ */ jsx10(RotateCw, { size: 14, strokeWidth: 1.5, "aria-hidden": true })
590
+ }
591
+ ),
592
+ /* @__PURE__ */ jsx10("div", { className: "studio-preview-source", children: toolbar ?? /* @__PURE__ */ jsx10("span", { className: "studio-preview-title", children: title }) })
593
+ ]
594
+ }
595
+ ),
596
+ /* @__PURE__ */ jsx10("iframe", { ...props, ref: frame, title })
597
+ ]
598
+ }
599
+ );
600
+ });
601
+
398
602
  // src/react/ui/sidebar.tsx
399
603
  import * as React2 from "react";
400
604
  import { cva as cva2 } from "class-variance-authority";
401
- import { PanelLeftIcon } from "lucide-react";
402
- import { Slot as Slot2 } from "radix-ui";
605
+ import { Slot as Slot4 } from "radix-ui";
403
606
 
404
607
  // src/react/ui/hooks/use-mobile.ts
405
608
  import * as React from "react";
@@ -421,15 +624,15 @@ function useIsMobile() {
421
624
  // src/react/ui/sheet.tsx
422
625
  import { XIcon } from "lucide-react";
423
626
  import { Dialog as SheetPrimitive } from "radix-ui";
424
- import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
627
+ import { jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
425
628
  function Sheet({ ...props }) {
426
- return /* @__PURE__ */ jsx7(SheetPrimitive.Root, { "data-slot": "sheet", ...props });
629
+ return /* @__PURE__ */ jsx11(SheetPrimitive.Root, { "data-slot": "sheet", ...props });
427
630
  }
428
631
  function SheetPortal({ ...props }) {
429
- return /* @__PURE__ */ jsx7(SheetPrimitive.Portal, { "data-slot": "sheet-portal", ...props });
632
+ return /* @__PURE__ */ jsx11(SheetPrimitive.Portal, { "data-slot": "sheet-portal", ...props });
430
633
  }
431
634
  function SheetOverlay({ className, ...props }) {
432
- return /* @__PURE__ */ jsx7(
635
+ return /* @__PURE__ */ jsx11(
433
636
  SheetPrimitive.Overlay,
434
637
  {
435
638
  "data-slot": "sheet-overlay",
@@ -448,9 +651,9 @@ function SheetContent({
448
651
  showCloseButton = true,
449
652
  ...props
450
653
  }) {
451
- return /* @__PURE__ */ jsxs3(SheetPortal, { children: [
452
- /* @__PURE__ */ jsx7(SheetOverlay, {}),
453
- /* @__PURE__ */ jsxs3(
654
+ return /* @__PURE__ */ jsxs5(SheetPortal, { children: [
655
+ /* @__PURE__ */ jsx11(SheetOverlay, {}),
656
+ /* @__PURE__ */ jsxs5(
454
657
  SheetPrimitive.Content,
455
658
  {
456
659
  "data-slot": "sheet-content",
@@ -465,9 +668,9 @@ function SheetContent({
465
668
  ...props,
466
669
  children: [
467
670
  children,
468
- showCloseButton && /* @__PURE__ */ jsxs3(SheetPrimitive.Close, { className: "absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary", children: [
469
- /* @__PURE__ */ jsx7(XIcon, { className: "size-4" }),
470
- /* @__PURE__ */ jsx7("span", { className: "sr-only", children: "Close" })
671
+ showCloseButton && /* @__PURE__ */ jsxs5(SheetPrimitive.Close, { className: "absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary", children: [
672
+ /* @__PURE__ */ jsx11(XIcon, { className: "size-4" }),
673
+ /* @__PURE__ */ jsx11("span", { className: "sr-only", children: "Close" })
471
674
  ] })
472
675
  ]
473
676
  }
@@ -475,10 +678,10 @@ function SheetContent({
475
678
  ] });
476
679
  }
477
680
  function SheetHeader({ className, ...props }) {
478
- return /* @__PURE__ */ jsx7("div", { "data-slot": "sheet-header", className: cn("flex flex-col gap-1.5 p-4", className), ...props });
681
+ return /* @__PURE__ */ jsx11("div", { "data-slot": "sheet-header", className: cn("flex flex-col gap-1.5 p-4", className), ...props });
479
682
  }
480
683
  function SheetTitle({ className, ...props }) {
481
- return /* @__PURE__ */ jsx7(
684
+ return /* @__PURE__ */ jsx11(
482
685
  SheetPrimitive.Title,
483
686
  {
484
687
  "data-slot": "sheet-title",
@@ -488,7 +691,7 @@ function SheetTitle({ className, ...props }) {
488
691
  );
489
692
  }
490
693
  function SheetDescription({ className, ...props }) {
491
- return /* @__PURE__ */ jsx7(
694
+ return /* @__PURE__ */ jsx11(
492
695
  SheetPrimitive.Description,
493
696
  {
494
697
  "data-slot": "sheet-description",
@@ -499,7 +702,7 @@ function SheetDescription({ className, ...props }) {
499
702
  }
500
703
 
501
704
  // src/react/ui/sidebar.tsx
502
- import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
705
+ import { jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
503
706
  var SIDEBAR_COOKIE_NAME = "sidebar_state";
504
707
  var SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
505
708
  var SIDEBAR_WIDTH = "16rem";
@@ -554,7 +757,7 @@ function SidebarProvider({
554
757
  }),
555
758
  [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
556
759
  );
557
- return /* @__PURE__ */ jsx8(SidebarContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx8(TooltipProvider, { delayDuration: 0, children: /* @__PURE__ */ jsx8(
760
+ return /* @__PURE__ */ jsx12(SidebarContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx12(TooltipProvider, { delayDuration: 0, children: /* @__PURE__ */ jsx12(
558
761
  "div",
559
762
  {
560
763
  "data-slot": "sidebar-wrapper",
@@ -578,8 +781,9 @@ function Sidebar({
578
781
  ...props
579
782
  }) {
580
783
  const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
784
+ const Surface = variant === "floating" ? Panel : "div";
581
785
  if (collapsible === "none") {
582
- return /* @__PURE__ */ jsx8(
786
+ return /* @__PURE__ */ jsx12(
583
787
  "div",
584
788
  {
585
789
  "data-slot": "sidebar",
@@ -590,7 +794,7 @@ function Sidebar({
590
794
  );
591
795
  }
592
796
  if (isMobile) {
593
- return /* @__PURE__ */ jsx8(Sheet, { open: openMobile, onOpenChange: setOpenMobile, ...props, children: /* @__PURE__ */ jsxs4(
797
+ return /* @__PURE__ */ jsx12(Sheet, { open: openMobile, onOpenChange: setOpenMobile, ...props, children: /* @__PURE__ */ jsxs6(
594
798
  SheetContent,
595
799
  {
596
800
  "data-sidebar": "sidebar",
@@ -602,16 +806,16 @@ function Sidebar({
602
806
  },
603
807
  side,
604
808
  children: [
605
- /* @__PURE__ */ jsxs4(SheetHeader, { className: "sr-only", children: [
606
- /* @__PURE__ */ jsx8(SheetTitle, { children: "Sidebar" }),
607
- /* @__PURE__ */ jsx8(SheetDescription, { children: "Displays the mobile sidebar." })
809
+ /* @__PURE__ */ jsxs6(SheetHeader, { className: "sr-only", children: [
810
+ /* @__PURE__ */ jsx12(SheetTitle, { children: "Sidebar" }),
811
+ /* @__PURE__ */ jsx12(SheetDescription, { children: "Displays the mobile sidebar." })
608
812
  ] }),
609
- /* @__PURE__ */ jsx8("div", { className: "flex h-full w-full flex-col", children })
813
+ /* @__PURE__ */ jsx12("div", { className: "flex h-full w-full flex-col", children })
610
814
  ]
611
815
  }
612
816
  ) });
613
817
  }
614
- return /* @__PURE__ */ jsxs4(
818
+ return /* @__PURE__ */ jsxs6(
615
819
  "div",
616
820
  {
617
821
  className: "group peer hidden text-sidebar-foreground md:block",
@@ -621,7 +825,7 @@ function Sidebar({
621
825
  "data-side": side,
622
826
  "data-slot": "sidebar",
623
827
  children: [
624
- /* @__PURE__ */ jsx8(
828
+ /* @__PURE__ */ jsx12(
625
829
  "div",
626
830
  {
627
831
  "data-slot": "sidebar-gap",
@@ -633,7 +837,7 @@ function Sidebar({
633
837
  )
634
838
  }
635
839
  ),
636
- /* @__PURE__ */ jsx8(
840
+ /* @__PURE__ */ jsx12(
637
841
  "div",
638
842
  {
639
843
  "data-slot": "sidebar-container",
@@ -645,12 +849,12 @@ function Sidebar({
645
849
  className
646
850
  ),
647
851
  ...props,
648
- children: /* @__PURE__ */ jsx8(
649
- "div",
852
+ children: /* @__PURE__ */ jsx12(
853
+ Surface,
650
854
  {
651
855
  "data-sidebar": "sidebar",
652
856
  "data-slot": "sidebar-inner",
653
- className: "flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm",
857
+ className: cn("flex h-full w-full flex-col", variant !== "floating" && "bg-sidebar"),
654
858
  children
655
859
  }
656
860
  )
@@ -661,7 +865,7 @@ function Sidebar({
661
865
  );
662
866
  }
663
867
  function SidebarHeader({ className, ...props }) {
664
- return /* @__PURE__ */ jsx8(
868
+ return /* @__PURE__ */ jsx12(
665
869
  "div",
666
870
  {
667
871
  "data-slot": "sidebar-header",
@@ -672,7 +876,7 @@ function SidebarHeader({ className, ...props }) {
672
876
  );
673
877
  }
674
878
  function SidebarFooter({ className, ...props }) {
675
- return /* @__PURE__ */ jsx8(
879
+ return /* @__PURE__ */ jsx12(
676
880
  "div",
677
881
  {
678
882
  "data-slot": "sidebar-footer",
@@ -683,7 +887,7 @@ function SidebarFooter({ className, ...props }) {
683
887
  );
684
888
  }
685
889
  function SidebarContent({ className, ...props }) {
686
- return /* @__PURE__ */ jsx8(
890
+ return /* @__PURE__ */ jsx12(
687
891
  "div",
688
892
  {
689
893
  "data-slot": "sidebar-content",
@@ -697,7 +901,7 @@ function SidebarContent({ className, ...props }) {
697
901
  );
698
902
  }
699
903
  function SidebarGroup({ className, ...props }) {
700
- return /* @__PURE__ */ jsx8(
904
+ return /* @__PURE__ */ jsx12(
701
905
  "div",
702
906
  {
703
907
  "data-slot": "sidebar-group",
@@ -708,7 +912,7 @@ function SidebarGroup({ className, ...props }) {
708
912
  );
709
913
  }
710
914
  function SidebarMenu({ className, ...props }) {
711
- return /* @__PURE__ */ jsx8(
915
+ return /* @__PURE__ */ jsx12(
712
916
  "ul",
713
917
  {
714
918
  "data-slot": "sidebar-menu",
@@ -719,7 +923,7 @@ function SidebarMenu({ className, ...props }) {
719
923
  );
720
924
  }
721
925
  function SidebarMenuItem({ className, ...props }) {
722
- return /* @__PURE__ */ jsx8(
926
+ return /* @__PURE__ */ jsx12(
723
927
  "li",
724
928
  {
725
929
  "data-slot": "sidebar-menu-item",
@@ -758,9 +962,9 @@ function SidebarMenuButton({
758
962
  className,
759
963
  ...props
760
964
  }) {
761
- const Comp = asChild ? Slot2.Root : "button";
965
+ const Comp = asChild ? Slot4.Root : "button";
762
966
  const { isMobile, state } = useSidebar();
763
- const button2 = /* @__PURE__ */ jsx8(
967
+ const button2 = /* @__PURE__ */ jsx12(
764
968
  Comp,
765
969
  {
766
970
  "data-slot": "sidebar-menu-button",
@@ -771,50 +975,61 @@ function SidebarMenuButton({
771
975
  ...props
772
976
  }
773
977
  );
774
- if (!tooltip) {
775
- return button2;
776
- }
777
- if (typeof tooltip === "string") {
778
- tooltip = {
779
- children: tooltip
780
- };
781
- }
782
- return /* @__PURE__ */ jsxs4(Tooltip, { children: [
783
- /* @__PURE__ */ jsx8(TooltipTrigger, { asChild: true, children: button2 }),
784
- /* @__PURE__ */ jsx8(TooltipContent, { side: "right", align: "center", hidden: state !== "collapsed" || isMobile, ...tooltip })
785
- ] });
978
+ if (!tooltip) return button2;
979
+ const { children, ...hint } = typeof tooltip === "string" ? { children: tooltip } : tooltip;
980
+ return /* @__PURE__ */ jsx12(Hint, { side: "right", align: "center", hidden: state !== "collapsed" || isMobile, ...hint, content: children, children: button2 });
981
+ }
982
+ function SidebarMenuAction({
983
+ className,
984
+ asChild = false,
985
+ showOnHover = false,
986
+ ...props
987
+ }) {
988
+ const Comp = asChild ? Slot4.Root : "button";
989
+ return /* @__PURE__ */ jsx12(
990
+ Comp,
991
+ {
992
+ "data-slot": "sidebar-menu-action",
993
+ "data-sidebar": "menu-action",
994
+ className: cn(
995
+ "absolute top-1/2 right-1 flex aspect-square w-5 -translate-y-1/2 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform peer-hover/menu-button:text-sidebar-accent-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
996
+ // Increases the hit area of the button on mobile.
997
+ "after:absolute after:-inset-2 md:after:hidden",
998
+ "group-data-[collapsible=icon]:hidden",
999
+ showOnHover && "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground data-[state=open]:opacity-100 md:opacity-0",
1000
+ className
1001
+ ),
1002
+ ...props
1003
+ }
1004
+ );
786
1005
  }
787
1006
 
788
1007
  // src/react/studio.tsx
789
- import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
1008
+ import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
790
1009
  var CONNECTION = {
791
1010
  connected: { icon: PlugZap, tone: "text-success", label: "Runtime connected" },
792
1011
  connecting: { icon: LoaderCircle, tone: "text-warning animate-spin", label: "Connecting to runtime" },
793
1012
  offline: { icon: Unplug, tone: "text-destructive", label: "Runtime offline" },
794
1013
  local: { icon: Unplug, tone: "text-faint", label: "No runtime" }
795
1014
  };
796
- var PANEL = "rounded-lg border border-sidebar-border bg-sidebar/85 shadow-xl shadow-black/40 backdrop-blur-xl";
797
- var PANEL_CONTAINER = cn(
798
- "[&>[data-slot=sidebar-inner]]:bg-sidebar/85 [&>[data-slot=sidebar-inner]]:shadow-xl [&>[data-slot=sidebar-inner]]:shadow-black/40 [&>[data-slot=sidebar-inner]]:backdrop-blur-xl"
799
- );
800
1015
  function StudioFooter({ connection }) {
801
1016
  const { state, toggleSidebar } = useSidebar();
802
1017
  const { icon: Status, tone, label } = CONNECTION[connection.phase];
803
1018
  const Toggle = state === "expanded" ? PanelLeftClose : PanelLeftOpen;
804
- return /* @__PURE__ */ jsx9(SidebarFooter, { children: /* @__PURE__ */ jsxs5(SidebarMenu, { children: [
805
- /* @__PURE__ */ jsx9(SidebarMenuItem, { children: /* @__PURE__ */ jsxs5(SidebarMenuButton, { tooltip: label, className: "cursor-default hover:bg-transparent active:bg-transparent", children: [
806
- /* @__PURE__ */ jsx9(Status, { className: tone }),
807
- /* @__PURE__ */ jsx9("span", { className: "text-clip! group-data-[collapsible=icon]:hidden", children: label })
1019
+ return /* @__PURE__ */ jsx13(SidebarFooter, { children: /* @__PURE__ */ jsxs7(SidebarMenu, { children: [
1020
+ /* @__PURE__ */ jsx13(SidebarMenuItem, { children: /* @__PURE__ */ jsxs7(SidebarMenuButton, { tooltip: label, className: "cursor-default hover:bg-transparent active:bg-transparent", children: [
1021
+ /* @__PURE__ */ jsx13(Status, { className: tone }),
1022
+ /* @__PURE__ */ jsx13("span", { className: "text-clip! group-data-[collapsible=icon]:hidden", children: label })
808
1023
  ] }) }),
809
- /* @__PURE__ */ jsx9(SidebarMenuItem, { children: /* @__PURE__ */ jsxs5(
1024
+ /* @__PURE__ */ jsx13(SidebarMenuItem, { children: /* @__PURE__ */ jsxs7(
810
1025
  SidebarMenuButton,
811
1026
  {
812
1027
  "aria-label": `${state === "expanded" ? "Collapse" : "Expand"} sidebar`,
813
1028
  tooltip: `${state === "expanded" ? "Collapse" : "Expand"} sidebar`,
814
1029
  onClick: toggleSidebar,
815
1030
  children: [
816
- /* @__PURE__ */ jsx9(Toggle, {}),
817
- /* @__PURE__ */ jsx9("span", { className: "text-clip! group-data-[collapsible=icon]:hidden", children: "Collapse" })
1031
+ /* @__PURE__ */ jsx13(Toggle, {}),
1032
+ /* @__PURE__ */ jsx13("span", { className: "text-clip! group-data-[collapsible=icon]:hidden", children: "Collapse" })
818
1033
  ]
819
1034
  }
820
1035
  ) })
@@ -836,14 +1051,16 @@ function forwardShortcuts(frame) {
836
1051
  } catch {
837
1052
  }
838
1053
  }
839
- function Studio({ studio }) {
1054
+ function Studio({ studio, previewComponent, getConfigFileHref }) {
840
1055
  useStudio(studio);
841
- const [mounted, setMounted] = useState4(false);
842
- const [panelsHidden, setPanelsHidden] = useState4(false);
843
- const [selected, setSelected] = useState4(studio.tools[0]?.definition.id);
844
- const preview = useRef2(null);
1056
+ const { tools, pinned, togglePin } = usePinnedTools(studio);
1057
+ const [mounted, setMounted] = useState7(false);
1058
+ const [panelsHidden, setPanelsHidden] = useState7(false);
1059
+ const [selected, setSelected] = useState7(studio.tools[0]?.definition.id);
1060
+ const preview = useRef3(null);
845
1061
  const handle = studio.tools.find((tool) => tool.definition.id === selected);
846
- useEffect3(() => {
1062
+ const Preview = previewComponent ?? (handle?.definition.previewLayout === "custom" ? "iframe" : PreviewCanvas);
1063
+ useEffect4(() => {
847
1064
  const onKey = (event) => {
848
1065
  if (event.defaultPrevented || event.isComposing || event.altKey || !(event.metaKey || event.ctrlKey)) return;
849
1066
  if (event.key.toLowerCase() !== "b") return;
@@ -864,10 +1081,10 @@ function Studio({ studio }) {
864
1081
  handle.compare(showSaved);
865
1082
  postCompare(preview.current?.contentWindow, handle.definition.id, showSaved);
866
1083
  };
867
- useEffect3(() => () => handle?.compare(false), [handle]);
868
- const current = useRef2({ handle, compare });
1084
+ useEffect4(() => () => handle?.compare(false), [handle]);
1085
+ const current = useRef3({ handle, compare });
869
1086
  current.current = { handle, compare };
870
- useEffect3(() => {
1087
+ useEffect4(() => {
871
1088
  const onKey = (event) => {
872
1089
  if (event.key !== COMPARE_KEY || editable(event.target) || event.repeat) return;
873
1090
  event.preventDefault();
@@ -885,7 +1102,7 @@ function Studio({ studio }) {
885
1102
  removeEventListener("blur", release);
886
1103
  };
887
1104
  }, []);
888
- useEffect3(() => {
1105
+ useEffect4(() => {
889
1106
  const fromHash = () => {
890
1107
  const id = decodeURIComponent(location.hash.slice(1));
891
1108
  if (studio.tools.some((handle2) => handle2.definition.id === id)) setSelected(id);
@@ -897,143 +1114,176 @@ function Studio({ studio }) {
897
1114
  }, [studio]);
898
1115
  if (!mounted) return null;
899
1116
  const sidebarOpen = !document.cookie.split("; ").includes("sidebar_state=false");
900
- return /* @__PURE__ */ jsxs5(SidebarProvider, { defaultOpen: sidebarOpen, className: "relative h-svh min-h-0 overflow-hidden font-mono text-xs", children: [
901
- handle?.definition.preview ? /* @__PURE__ */ jsx9(
902
- "iframe",
903
- {
904
- "data-slot": "studio-preview",
905
- ref: preview,
906
- src: handle.definition.preview,
907
- onLoad: (event) => forwardShortcuts(event.currentTarget),
908
- className: "absolute inset-0 size-full border-0 bg-black"
909
- },
910
- handle.definition.id
911
- ) : /* @__PURE__ */ jsx9("div", { className: "absolute inset-0 grid place-items-center text-faint", children: handle ? "This tool has no preview" : "No tools registered" }),
912
- panelsHidden && ["left", "right"].map((side) => {
913
- const Chevron = side === "left" ? ChevronRight : ChevronLeft;
914
- return /* @__PURE__ */ jsx9(
915
- "div",
916
- {
917
- "data-slot": "studio-panel-edge",
918
- "data-side": side,
919
- className: cn("group/edge absolute inset-y-0 z-20 w-8", side === "left" ? "left-0" : "right-0"),
920
- children: /* @__PURE__ */ jsxs5(Tooltip, { children: [
921
- /* @__PURE__ */ jsx9(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx9(
922
- Button,
923
- {
924
- "data-slot": "studio-panel-reveal",
925
- variant: "ghost",
926
- size: "icon",
927
- "aria-label": "Show Studio panels",
928
- onClick: () => setPanelsHidden(false),
929
- className: cn(
930
- PANEL,
931
- "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",
932
- side === "left" ? "left-1 -translate-x-[calc(100%+0.25rem)]" : "right-1 translate-x-[calc(100%+0.25rem)]"
933
- ),
934
- children: /* @__PURE__ */ jsx9(Chevron, { "aria-hidden": true })
935
- }
936
- ) }),
937
- /* @__PURE__ */ jsxs5(
938
- TooltipContent,
939
- {
940
- side: side === "left" ? "right" : "left",
941
- sideOffset: 8,
942
- className: "flex items-center gap-2",
943
- children: [
944
- "Show Studio panels",
945
- /* @__PURE__ */ jsxs5(KbdGroup, { children: [
946
- /* @__PURE__ */ jsx9(Kbd, { children: "\u2318/Ctrl" }),
947
- /* @__PURE__ */ jsx9(Kbd, { children: "B" })
948
- ] })
949
- ]
950
- }
951
- )
952
- ] })
953
- },
954
- side
955
- );
956
- }),
957
- /* @__PURE__ */ jsxs5(
958
- Sidebar,
959
- {
960
- collapsible: "icon",
961
- variant: "floating",
962
- inert: panelsHidden,
963
- "aria-hidden": panelsHidden,
964
- className: cn(
965
- PANEL_CONTAINER,
966
- "transition-[left,right,width,translate] duration-200 ease-out motion-reduce:transition-none",
967
- panelsHidden && "-translate-x-[calc(100%+1rem)]"
968
- ),
969
- children: [
970
- /* @__PURE__ */ jsx9(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__ */ jsxs5("span", { className: "flex items-center gap-2.5 text-foreground", children: [
971
- /* @__PURE__ */ jsx9(Logo, { className: "h-3.5 w-auto shrink-0" }),
972
- /* @__PURE__ */ jsx9("strong", { className: "tracking-[0.08em] whitespace-nowrap", children: "STUDIO" }),
973
- /* @__PURE__ */ jsxs5("span", { "data-slot": "studio-version", className: "text-[10px] text-faint group-data-[collapsible=icon]:hidden", children: [
974
- "v",
975
- version
976
- ] })
977
- ] }) }),
978
- /* @__PURE__ */ jsx9(SidebarContent, { children: /* @__PURE__ */ jsx9(SidebarGroup, { children: /* @__PURE__ */ jsx9(SidebarMenu, { children: studio.tools.map(({ definition, state }) => {
979
- const title = definition.title ?? definition.id;
980
- return /* @__PURE__ */ jsx9(SidebarMenuItem, { children: /* @__PURE__ */ jsxs5(
981
- SidebarMenuButton,
982
- {
983
- isActive: definition.id === selected,
984
- tooltip: title,
985
- onClick: () => location.hash = definition.id,
986
- children: [
987
- /* @__PURE__ */ jsxs5("span", { className: "relative", children: [
988
- /* @__PURE__ */ jsx9(ToolIcon, { icon: definition.icon, className: "size-4" }),
989
- state?.dirty && /* @__PURE__ */ jsx9(
990
- "span",
991
- {
992
- "aria-label": state.conflict ? "Changed on disk" : "Unsaved",
993
- className: cn(
994
- "absolute -top-0.5 -right-0.5 size-1.5 rounded-full",
995
- state.conflict ? "bg-destructive" : "bg-warning"
996
- )
997
- }
998
- )
1117
+ return /* @__PURE__ */ jsxs7(
1118
+ SidebarProvider,
1119
+ {
1120
+ defaultOpen: sidebarOpen,
1121
+ "data-preview-layout": previewComponent ? "canvas" : handle?.definition.previewLayout ?? "canvas",
1122
+ className: "studio-workspace relative h-svh min-h-0 overflow-hidden text-xs",
1123
+ children: [
1124
+ handle?.definition.preview ? /* @__PURE__ */ jsx13(
1125
+ Preview,
1126
+ {
1127
+ "data-slot": "studio-preview",
1128
+ ref: preview,
1129
+ src: handle.definition.preview,
1130
+ title: handle.definition.title ?? handle.definition.id,
1131
+ onLoad: (event) => forwardShortcuts(event.currentTarget)
1132
+ },
1133
+ handle.definition.id
1134
+ ) : /* @__PURE__ */ jsx13("div", { className: "absolute inset-0 grid place-items-center text-faint", children: handle ? "This tool has no preview" : "No tools registered" }),
1135
+ panelsHidden && ["left", "right"].map((side) => {
1136
+ const Chevron = side === "left" ? ChevronRight : ChevronLeft;
1137
+ return /* @__PURE__ */ jsx13(
1138
+ "div",
1139
+ {
1140
+ "data-slot": "studio-panel-edge",
1141
+ "data-side": side,
1142
+ className: cn("group/edge absolute inset-y-0 z-20 w-8", side === "left" ? "left-0" : "right-0"),
1143
+ children: /* @__PURE__ */ jsx13(
1144
+ Hint,
1145
+ {
1146
+ side: side === "left" ? "right" : "left",
1147
+ sideOffset: 8,
1148
+ className: "flex items-center gap-2",
1149
+ content: /* @__PURE__ */ jsxs7(Fragment2, { children: [
1150
+ "Show Studio panels",
1151
+ " ",
1152
+ /* @__PURE__ */ jsxs7(KbdGroup, { children: [
1153
+ /* @__PURE__ */ jsx13(Kbd, { children: "\u2318/Ctrl" }),
1154
+ /* @__PURE__ */ jsx13(Kbd, { children: "B" })
1155
+ ] })
999
1156
  ] }),
1000
- /* @__PURE__ */ jsx9("span", { className: "text-clip! group-data-[collapsible=icon]:hidden", children: title })
1001
- ]
1002
- }
1003
- ) }, definition.id);
1004
- }) }) }) }),
1005
- /* @__PURE__ */ jsx9(StudioFooter, { connection: studio.connection })
1006
- ]
1007
- }
1008
- ),
1009
- handle && /* @__PURE__ */ jsx9(
1010
- "aside",
1011
- {
1012
- "data-slot": "studio-inspector",
1013
- inert: panelsHidden,
1014
- "aria-hidden": panelsHidden,
1015
- className: cn(
1016
- PANEL,
1017
- "absolute inset-y-2 right-2 z-10 flex w-80 flex-col transition-transform duration-200 ease-out motion-reduce:transition-none",
1018
- panelsHidden && "translate-x-[calc(100%+1rem)]"
1157
+ children: /* @__PURE__ */ jsx13(Panel, { asChild: true, children: /* @__PURE__ */ jsx13(
1158
+ Button,
1159
+ {
1160
+ "data-slot": "studio-panel-reveal",
1161
+ variant: "ghost",
1162
+ size: "icon",
1163
+ "aria-label": "Show Studio panels",
1164
+ onClick: () => setPanelsHidden(false),
1165
+ className: cn(
1166
+ "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
+ side === "left" ? "left-1 -translate-x-[calc(100%+0.25rem)]" : "right-1 translate-x-[calc(100%+0.25rem)]"
1168
+ ),
1169
+ children: /* @__PURE__ */ jsx13(Chevron, { "aria-hidden": true })
1170
+ }
1171
+ ) })
1172
+ }
1173
+ )
1174
+ },
1175
+ side
1176
+ );
1177
+ }),
1178
+ /* @__PURE__ */ jsxs7(
1179
+ Sidebar,
1180
+ {
1181
+ collapsible: "icon",
1182
+ variant: "floating",
1183
+ inert: panelsHidden,
1184
+ "aria-hidden": panelsHidden,
1185
+ className: cn(
1186
+ "transition-[left,right,width,translate] duration-200 ease-out motion-reduce:transition-none",
1187
+ panelsHidden && "-translate-x-[calc(100%+1rem)]"
1188
+ ),
1189
+ children: [
1190
+ /* @__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: [
1191
+ /* @__PURE__ */ jsx13(Logo, { className: "h-3.5 w-auto shrink-0" }),
1192
+ /* @__PURE__ */ jsx13("strong", { className: "tracking-[0.08em] whitespace-nowrap", children: "STUDIO" }),
1193
+ /* @__PURE__ */ jsxs7("span", { "data-slot": "studio-version", className: "text-[10px] text-faint group-data-[collapsible=icon]:hidden", children: [
1194
+ "v",
1195
+ version
1196
+ ] })
1197
+ ] }) }),
1198
+ /* @__PURE__ */ jsx13(SidebarContent, { children: /* @__PURE__ */ jsx13(SidebarGroup, { children: /* @__PURE__ */ jsx13(SidebarMenu, { children: tools.map(({ definition, state }) => {
1199
+ const title = definition.title ?? definition.id;
1200
+ const isPinned = pinned.has(definition.id);
1201
+ const pinLabel = `${isPinned ? "Unpin" : "Pin"} ${title}`;
1202
+ return /* @__PURE__ */ jsxs7(SidebarMenuItem, { children: [
1203
+ /* @__PURE__ */ jsxs7(
1204
+ SidebarMenuButton,
1205
+ {
1206
+ isActive: definition.id === selected,
1207
+ tooltip: title,
1208
+ onClick: () => location.hash = definition.id,
1209
+ children: [
1210
+ /* @__PURE__ */ jsxs7("span", { className: "relative", children: [
1211
+ /* @__PURE__ */ jsx13(ToolIcon, { icon: definition.icon, className: "size-4" }),
1212
+ state?.dirty && /* @__PURE__ */ jsx13(
1213
+ "span",
1214
+ {
1215
+ "aria-label": state.conflict ? "Changed on disk" : "Unsaved",
1216
+ className: cn(
1217
+ "absolute -top-0.5 -right-0.5 size-1.5 rounded-full",
1218
+ state.conflict ? "bg-destructive" : "bg-warning"
1219
+ )
1220
+ }
1221
+ )
1222
+ ] }),
1223
+ /* @__PURE__ */ jsx13("span", { className: "text-clip! group-data-[collapsible=icon]:hidden", children: title })
1224
+ ]
1225
+ }
1226
+ ),
1227
+ /* @__PURE__ */ jsx13(Hint, { content: pinLabel, side: "right", children: /* @__PURE__ */ jsx13(
1228
+ SidebarMenuAction,
1229
+ {
1230
+ type: "button",
1231
+ "aria-label": pinLabel,
1232
+ "aria-pressed": isPinned,
1233
+ showOnHover: !isPinned,
1234
+ onClick: () => togglePin(definition.id),
1235
+ className: cn(
1236
+ "cursor-pointer [&>svg]:size-3 [@media(hover:none)]:opacity-100",
1237
+ isPinned && "text-sidebar-primary"
1238
+ ),
1239
+ children: /* @__PURE__ */ jsx13(Pin2, { "aria-hidden": true, fill: isPinned ? "currentColor" : "none" })
1240
+ }
1241
+ ) })
1242
+ ] }, definition.id);
1243
+ }) }) }) }),
1244
+ /* @__PURE__ */ jsx13(StudioFooter, { connection: studio.connection })
1245
+ ]
1246
+ }
1019
1247
  ),
1020
- children: /* @__PURE__ */ jsx9(Inspector, { studio, handle, onCompare: compare }, handle.definition.id)
1021
- }
1022
- )
1023
- ] });
1248
+ handle && /* @__PURE__ */ jsx13(Panel, { asChild: true, children: /* @__PURE__ */ jsx13(
1249
+ "aside",
1250
+ {
1251
+ "data-slot": "studio-inspector",
1252
+ inert: panelsHidden,
1253
+ "aria-hidden": panelsHidden,
1254
+ className: cn(
1255
+ "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
+ panelsHidden && "translate-x-[calc(100%+1rem)]"
1257
+ ),
1258
+ children: /* @__PURE__ */ jsx13(
1259
+ Inspector,
1260
+ {
1261
+ studio,
1262
+ handle,
1263
+ onCompare: compare,
1264
+ fileHref: getConfigFileHref?.(handle.definition.file)
1265
+ },
1266
+ handle.definition.id
1267
+ )
1268
+ }
1269
+ ) })
1270
+ ]
1271
+ }
1272
+ );
1024
1273
  }
1025
1274
 
1026
1275
  // src/react/command-menu.tsx
1276
+ import { Pin as Pin3 } from "lucide-react";
1027
1277
  import { Dialog } from "radix-ui";
1028
- import { useEffect as useEffect4, useRef as useRef3, useState as useState5 } from "react";
1278
+ import { useEffect as useEffect5, useRef as useRef4, useState as useState8 } from "react";
1029
1279
 
1030
1280
  // src/react/ui/command.tsx
1031
1281
  import * as React3 from "react";
1032
1282
  import { Command as CommandPrimitive } from "cmdk";
1033
1283
  import { SearchIcon } from "lucide-react";
1034
- import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
1284
+ import { jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
1035
1285
  function Command({ className, ...props }) {
1036
- return /* @__PURE__ */ jsx10(
1286
+ return /* @__PURE__ */ jsx14(
1037
1287
  CommandPrimitive,
1038
1288
  {
1039
1289
  "data-slot": "command",
@@ -1046,9 +1296,9 @@ function Command({ className, ...props }) {
1046
1296
  );
1047
1297
  }
1048
1298
  var CommandInput = React3.forwardRef(function CommandInput2({ className, ...props }, ref) {
1049
- return /* @__PURE__ */ jsxs6("div", { "data-slot": "command-input-wrapper", className: "flex h-9 items-center gap-2 border-b px-3", children: [
1050
- /* @__PURE__ */ jsx10(SearchIcon, { "aria-hidden": true, className: "size-4 shrink-0 opacity-50" }),
1051
- /* @__PURE__ */ jsx10(
1299
+ return /* @__PURE__ */ jsxs8("div", { "data-slot": "command-input-wrapper", className: "flex h-9 items-center gap-2 border-b px-3", children: [
1300
+ /* @__PURE__ */ jsx14(SearchIcon, { "aria-hidden": true, className: "size-4 shrink-0 opacity-50" }),
1301
+ /* @__PURE__ */ jsx14(
1052
1302
  CommandPrimitive.Input,
1053
1303
  {
1054
1304
  ref,
@@ -1063,7 +1313,7 @@ var CommandInput = React3.forwardRef(function CommandInput2({ className, ...prop
1063
1313
  ] });
1064
1314
  });
1065
1315
  function CommandList({ className, ...props }) {
1066
- return /* @__PURE__ */ jsx10(
1316
+ return /* @__PURE__ */ jsx14(
1067
1317
  CommandPrimitive.List,
1068
1318
  {
1069
1319
  "data-slot": "command-list",
@@ -1073,10 +1323,10 @@ function CommandList({ className, ...props }) {
1073
1323
  );
1074
1324
  }
1075
1325
  function CommandEmpty({ ...props }) {
1076
- return /* @__PURE__ */ jsx10(CommandPrimitive.Empty, { "data-slot": "command-empty", className: "py-6 text-center text-sm", ...props });
1326
+ return /* @__PURE__ */ jsx14(CommandPrimitive.Empty, { "data-slot": "command-empty", className: "py-6 text-center text-sm", ...props });
1077
1327
  }
1078
1328
  function CommandItem({ className, ...props }) {
1079
- return /* @__PURE__ */ jsx10(
1329
+ return /* @__PURE__ */ jsx14(
1080
1330
  CommandPrimitive.Item,
1081
1331
  {
1082
1332
  "data-slot": "command-item",
@@ -1090,12 +1340,13 @@ function CommandItem({ className, ...props }) {
1090
1340
  }
1091
1341
 
1092
1342
  // src/react/command-menu.tsx
1093
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
1343
+ import { jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
1094
1344
  function StudioCommandMenu({ studio, studioPath = "/studio", navigate }) {
1095
- const [open, setOpen] = useState5(false);
1096
- const opener = useRef3(null);
1097
- const input = useRef3(null);
1098
- useEffect4(() => {
1345
+ const { tools, pinned } = usePinnedTools(studio);
1346
+ const [open, setOpen] = useState8(false);
1347
+ const opener = useRef4(null);
1348
+ const input = useRef4(null);
1349
+ useEffect5(() => {
1099
1350
  if (window.frameElement?.getAttribute("data-slot") === "studio-preview") return;
1100
1351
  const onKey = (event) => {
1101
1352
  if (event.defaultPrevented || event.isComposing || event.altKey || !(event.metaKey || event.ctrlKey)) return;
@@ -1119,9 +1370,9 @@ function StudioCommandMenu({ studio, studioPath = "/studio", navigate }) {
1119
1370
  window.location.assign(target.href);
1120
1371
  }
1121
1372
  };
1122
- return /* @__PURE__ */ jsx11(Dialog.Root, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs7(Dialog.Portal, { children: [
1123
- /* @__PURE__ */ jsx11(Dialog.Overlay, { "data-slot": "studio-command-overlay" }),
1124
- /* @__PURE__ */ jsxs7(
1373
+ return /* @__PURE__ */ jsx15(Dialog.Root, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs9(Dialog.Portal, { children: [
1374
+ /* @__PURE__ */ jsx15(Dialog.Overlay, { "data-slot": "studio-command-overlay" }),
1375
+ /* @__PURE__ */ jsxs9(
1125
1376
  Dialog.Content,
1126
1377
  {
1127
1378
  "data-slot": "studio-command-menu",
@@ -1135,43 +1386,44 @@ function StudioCommandMenu({ studio, studioPath = "/studio", navigate }) {
1135
1386
  opener.current?.focus();
1136
1387
  },
1137
1388
  children: [
1138
- /* @__PURE__ */ jsxs7("div", { "data-slot": "studio-command-heading", children: [
1139
- /* @__PURE__ */ jsxs7(Dialog.Title, { "aria-label": "Studio tools", children: [
1140
- /* @__PURE__ */ jsx11(Logo, { "aria-hidden": true }),
1141
- /* @__PURE__ */ jsx11("strong", { children: "STUDIO" })
1389
+ /* @__PURE__ */ jsxs9("div", { "data-slot": "studio-command-heading", children: [
1390
+ /* @__PURE__ */ jsxs9(Dialog.Title, { "aria-label": "Studio tools", children: [
1391
+ /* @__PURE__ */ jsx15(Logo, { "aria-hidden": true }),
1392
+ /* @__PURE__ */ jsx15("strong", { children: "STUDIO" })
1142
1393
  ] }),
1143
- /* @__PURE__ */ jsx11(Dialog.Close, { "aria-label": "Close tool menu", children: /* @__PURE__ */ jsx11(Kbd, { children: "esc" }) })
1394
+ /* @__PURE__ */ jsx15(Dialog.Close, { "aria-label": "Close tool menu", children: /* @__PURE__ */ jsx15(Kbd, { children: "esc" }) })
1144
1395
  ] }),
1145
- /* @__PURE__ */ jsx11(Dialog.Description, { "data-slot": "studio-command-description", children: "Open a tool in Studio." }),
1146
- /* @__PURE__ */ jsxs7(Command, { label: "Studio tools", loop: true, vimBindings: false, children: [
1147
- /* @__PURE__ */ jsx11(CommandInput, { ref: input, placeholder: "Search tools\u2026" }),
1148
- /* @__PURE__ */ jsxs7(CommandList, { children: [
1149
- /* @__PURE__ */ jsx11(CommandEmpty, { children: studio.tools.length ? "No tools found." : "No tools registered yet." }),
1150
- studio.tools.map(({ definition: tool }) => /* @__PURE__ */ jsxs7(
1396
+ /* @__PURE__ */ jsx15(Dialog.Description, { "data-slot": "studio-command-description", children: "Open a tool in Studio." }),
1397
+ /* @__PURE__ */ jsxs9(Command, { label: "Studio tools", loop: true, vimBindings: false, children: [
1398
+ /* @__PURE__ */ jsx15(CommandInput, { ref: input, placeholder: "Search tools\u2026" }),
1399
+ /* @__PURE__ */ jsxs9(CommandList, { children: [
1400
+ /* @__PURE__ */ jsx15(CommandEmpty, { children: studio.tools.length ? "No tools found." : "No tools registered yet." }),
1401
+ tools.map(({ definition: tool }) => /* @__PURE__ */ jsxs9(
1151
1402
  CommandItem,
1152
1403
  {
1153
1404
  value: tool.id,
1154
1405
  keywords: [tool.title ?? tool.id],
1155
1406
  onSelect: () => select(tool.id),
1156
1407
  children: [
1157
- /* @__PURE__ */ jsx11(ToolIcon, { icon: tool.icon }),
1158
- /* @__PURE__ */ jsx11("span", { "data-slot": "studio-command-label", children: tool.title ?? tool.id })
1408
+ /* @__PURE__ */ jsx15(ToolIcon, { icon: tool.icon }),
1409
+ /* @__PURE__ */ jsx15("span", { "data-slot": "studio-command-label", children: tool.title ?? tool.id }),
1410
+ pinned.has(tool.id) && /* @__PURE__ */ jsx15(Pin3, { "aria-hidden": true })
1159
1411
  ]
1160
1412
  },
1161
1413
  tool.id
1162
1414
  ))
1163
1415
  ] })
1164
1416
  ] }),
1165
- /* @__PURE__ */ jsxs7("div", { "data-slot": "studio-command-footer", "aria-hidden": true, children: [
1166
- /* @__PURE__ */ jsxs7("span", { children: [
1167
- /* @__PURE__ */ jsxs7(KbdGroup, { children: [
1168
- /* @__PURE__ */ jsx11(Kbd, { children: "\u2191" }),
1169
- /* @__PURE__ */ jsx11(Kbd, { children: "\u2193" })
1417
+ /* @__PURE__ */ jsxs9("div", { "data-slot": "studio-command-footer", "aria-hidden": true, children: [
1418
+ /* @__PURE__ */ jsxs9("span", { children: [
1419
+ /* @__PURE__ */ jsxs9(KbdGroup, { children: [
1420
+ /* @__PURE__ */ jsx15(Kbd, { children: "\u2191" }),
1421
+ /* @__PURE__ */ jsx15(Kbd, { children: "\u2193" })
1170
1422
  ] }),
1171
1423
  "navigate"
1172
1424
  ] }),
1173
- /* @__PURE__ */ jsxs7("span", { children: [
1174
- /* @__PURE__ */ jsx11(Kbd, { children: "\u21B5" }),
1425
+ /* @__PURE__ */ jsxs9("span", { children: [
1426
+ /* @__PURE__ */ jsx15(Kbd, { children: "\u21B5" }),
1175
1427
  " open tool"
1176
1428
  ] })
1177
1429
  ] })
@@ -1180,9 +1432,95 @@ function StudioCommandMenu({ studio, studioPath = "/studio", navigate }) {
1180
1432
  )
1181
1433
  ] }) });
1182
1434
  }
1435
+
1436
+ // src/react/select.tsx
1437
+ import { Check, ChevronDown, ChevronUp } from "lucide-react";
1438
+ import { Select } from "radix-ui";
1439
+ import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
1440
+ function StudioSelect({ label, title, className, children, ...props }) {
1441
+ return /* @__PURE__ */ jsxs10(Select.Root, { ...props, children: [
1442
+ /* @__PURE__ */ jsxs10(
1443
+ Select.Trigger,
1444
+ {
1445
+ "data-slot": "select-trigger",
1446
+ "aria-label": label,
1447
+ title,
1448
+ className: cn(
1449
+ "border-input text-foreground focus-visible:border-ring focus-visible:ring-ring/50 flex h-7 max-w-full shrink-0 items-center justify-between gap-2 rounded-md border bg-(--field) px-2 text-xs whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-(--field-hover) focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span:first-child]:truncate",
1450
+ className
1451
+ ),
1452
+ children: [
1453
+ /* @__PURE__ */ jsx16(Select.Value, {}),
1454
+ /* @__PURE__ */ jsx16(Select.Icon, { children: /* @__PURE__ */ jsx16(ChevronDown, { className: "shrink-0 text-muted-foreground", size: 14, strokeWidth: 1.5, "aria-hidden": true }) })
1455
+ ]
1456
+ }
1457
+ ),
1458
+ /* @__PURE__ */ jsx16(Select.Portal, { children: /* @__PURE__ */ jsxs10(
1459
+ Select.Content,
1460
+ {
1461
+ className: "relative z-50 max-h-(--radix-select-content-available-height) min-w-(--radix-select-trigger-width) overflow-hidden rounded-md border border-border bg-popover font-(family-name:--studio-font) text-xs font-normal text-popover-foreground shadow-md",
1462
+ "data-slot": "select-content",
1463
+ position: "popper",
1464
+ align: "end",
1465
+ sideOffset: 6,
1466
+ children: [
1467
+ /* @__PURE__ */ jsx16(Select.ScrollUpButton, { className: "flex justify-center py-1", children: /* @__PURE__ */ jsx16(ChevronUp, { size: 14, "aria-hidden": true }) }),
1468
+ /* @__PURE__ */ jsx16(Select.Viewport, { className: "p-1", children }),
1469
+ /* @__PURE__ */ jsx16(Select.ScrollDownButton, { className: "flex justify-center py-1", children: /* @__PURE__ */ jsx16(ChevronDown, { size: 14, "aria-hidden": true }) })
1470
+ ]
1471
+ }
1472
+ ) })
1473
+ ] });
1474
+ }
1475
+ function StudioSelectItem({ className, children, ...props }) {
1476
+ return /* @__PURE__ */ jsxs10(
1477
+ Select.Item,
1478
+ {
1479
+ "data-slot": "select-item",
1480
+ className: cn(
1481
+ "relative flex min-h-7 cursor-default items-center rounded-sm py-1.5 pr-8 pl-2 outline-none select-none data-[highlighted]:bg-(--field-hover) data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
1482
+ className
1483
+ ),
1484
+ ...props,
1485
+ children: [
1486
+ /* @__PURE__ */ jsx16(Select.ItemText, { children }),
1487
+ /* @__PURE__ */ jsx16(Select.ItemIndicator, { className: "absolute right-2", children: /* @__PURE__ */ jsx16(Check, { size: 14, strokeWidth: 1.5, "aria-hidden": true }) })
1488
+ ]
1489
+ }
1490
+ );
1491
+ }
1492
+
1493
+ // src/react/ui/input.tsx
1494
+ import { forwardRef as forwardRef5 } from "react";
1495
+ import { jsx as jsx17 } from "react/jsx-runtime";
1496
+ var Input = forwardRef5(function Input2({ className, ...props }, ref) {
1497
+ return /* @__PURE__ */ jsx17(
1498
+ "input",
1499
+ {
1500
+ ref,
1501
+ "data-slot": "input",
1502
+ className: cn(
1503
+ "border-input text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 h-8 w-full min-w-0 rounded-md border bg-transparent py-1 ps-3 pe-3 text-sm transition-[color,box-shadow] outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50",
1504
+ className
1505
+ ),
1506
+ ...props
1507
+ }
1508
+ );
1509
+ });
1183
1510
  export {
1511
+ CanvasToolbar,
1512
+ PreviewCanvas,
1184
1513
  Studio,
1185
1514
  StudioCommandMenu,
1515
+ Input as StudioInput,
1516
+ Panel as StudioPanel,
1517
+ PanelContent as StudioPanelContent,
1518
+ PanelFooter as StudioPanelFooter,
1519
+ PanelHeader as StudioPanelHeader,
1520
+ StudioSelect,
1521
+ StudioSelectItem,
1522
+ StudioSurface,
1523
+ StudioToolbar,
1186
1524
  useStudio,
1187
1525
  useToolConfig
1188
1526
  };