@toriistudio/v0-playground 0.5.5 → 0.6.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/README.md CHANGED
@@ -22,7 +22,7 @@ Perfect for prototyping components, sharing usage examples, or building your own
22
22
  To use `@toriistudio/v0-playground`, you’ll need to install the following peer dependencies:
23
23
 
24
24
  ```bash
25
- yarn add @radix-ui/react-label @radix-ui/react-select @radix-ui/react-slider @radix-ui/react-slot @radix-ui/react-switch class-variance-authority clsx lucide-react tailwind-merge tailwindcss-animate @react-three/drei @react-three/fiber three lodash
25
+ yarn add @radix-ui/react-label @radix-ui/react-select @radix-ui/react-slider @radix-ui/react-slot @radix-ui/react-switch class-variance-authority clsx lucide-react tailwind-merge tailwindcss-animate lodash
26
26
  ```
27
27
 
28
28
  Or automate it with:
package/dist/index.d.mts CHANGED
@@ -118,6 +118,7 @@ type ControlsConfig = {
118
118
  showCopyButtonFn?: (args: CopyButtonFnArgs) => string | null | undefined;
119
119
  mainLabel?: string;
120
120
  showGrid?: boolean;
121
+ showPresentationButton?: boolean;
121
122
  addAdvancedPaletteControl?: ResolvedAdvancedPaletteConfig;
122
123
  };
123
124
  type UseControlsConfig = Omit<ControlsConfig, "addAdvancedPaletteControl"> & {
package/dist/index.d.ts CHANGED
@@ -118,6 +118,7 @@ type ControlsConfig = {
118
118
  showCopyButtonFn?: (args: CopyButtonFnArgs) => string | null | undefined;
119
119
  mainLabel?: string;
120
120
  showGrid?: boolean;
121
+ showPresentationButton?: boolean;
121
122
  addAdvancedPaletteControl?: ResolvedAdvancedPaletteConfig;
122
123
  };
123
124
  type UseControlsConfig = Omit<ControlsConfig, "addAdvancedPaletteControl"> & {
package/dist/index.js CHANGED
@@ -168,6 +168,28 @@ var getUrlParams = () => {
168
168
  return entries;
169
169
  };
170
170
 
171
+ // src/constants/urlParams.ts
172
+ var NO_CONTROLS_PARAM = "nocontrols";
173
+ var PRESENTATION_PARAM = "presentation";
174
+ var CONTROLS_ONLY_PARAM = "controlsonly";
175
+
176
+ // src/utils/getControlsChannelName.ts
177
+ var EXCLUDED_KEYS = /* @__PURE__ */ new Set([
178
+ NO_CONTROLS_PARAM,
179
+ PRESENTATION_PARAM,
180
+ CONTROLS_ONLY_PARAM
181
+ ]);
182
+ var getControlsChannelName = () => {
183
+ if (typeof window === "undefined") return null;
184
+ const params = new URLSearchParams(window.location.search);
185
+ for (const key of EXCLUDED_KEYS) {
186
+ params.delete(key);
187
+ }
188
+ const query = params.toString();
189
+ const base = window.location.pathname || "/";
190
+ return `v0-controls:${base}${query ? `?${query}` : ""}`;
191
+ };
192
+
171
193
  // src/lib/advancedPalette.ts
172
194
  var CHANNEL_KEYS = ["r", "g", "b"];
173
195
  var DEFAULT_CHANNEL_LABELS = {
@@ -429,6 +451,18 @@ var ControlsProvider = ({ children }) => {
429
451
  showCopyButton: true
430
452
  });
431
453
  const [componentName, setComponentName] = (0, import_react2.useState)();
454
+ const [channelName, setChannelName] = (0, import_react2.useState)(null);
455
+ const channelRef = (0, import_react2.useRef)(null);
456
+ const instanceIdRef = (0, import_react2.useRef)(null);
457
+ const skipBroadcastRef = (0, import_react2.useRef)(false);
458
+ const latestValuesRef = (0, import_react2.useRef)(values);
459
+ (0, import_react2.useEffect)(() => {
460
+ latestValuesRef.current = values;
461
+ }, [values]);
462
+ (0, import_react2.useEffect)(() => {
463
+ if (typeof window === "undefined") return;
464
+ setChannelName(getControlsChannelName());
465
+ }, []);
432
466
  const setValue = (key, value) => {
433
467
  setValues((prev) => ({ ...prev, [key]: value }));
434
468
  };
@@ -463,6 +497,66 @@ var ControlsProvider = ({ children }) => {
463
497
  return updated;
464
498
  });
465
499
  };
500
+ (0, import_react2.useEffect)(() => {
501
+ if (!channelName) return;
502
+ if (typeof window === "undefined") return;
503
+ if (typeof window.BroadcastChannel === "undefined") return;
504
+ const instanceId = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : Math.random().toString(36).slice(2);
505
+ instanceIdRef.current = instanceId;
506
+ const channel = new BroadcastChannel(channelName);
507
+ channelRef.current = channel;
508
+ const sendValues = () => {
509
+ if (!instanceIdRef.current) return;
510
+ channel.postMessage({
511
+ type: "controls-sync-values",
512
+ source: instanceIdRef.current,
513
+ values: latestValuesRef.current
514
+ });
515
+ };
516
+ const handleMessage = (event) => {
517
+ const data = event.data;
518
+ if (!data || data.source === instanceIdRef.current) return;
519
+ if (data.type === "controls-sync-request") {
520
+ sendValues();
521
+ return;
522
+ }
523
+ if (data.type === "controls-sync-values" && data.values) {
524
+ const incoming = data.values;
525
+ setValues((prev) => {
526
+ const prevKeys = Object.keys(prev);
527
+ const incomingKeys = Object.keys(incoming);
528
+ const sameLength = prevKeys.length === incomingKeys.length;
529
+ const sameValues = sameLength && incomingKeys.every((key) => prev[key] === incoming[key]);
530
+ if (sameValues) return prev;
531
+ skipBroadcastRef.current = true;
532
+ return { ...incoming };
533
+ });
534
+ }
535
+ };
536
+ channel.addEventListener("message", handleMessage);
537
+ channel.postMessage({
538
+ type: "controls-sync-request",
539
+ source: instanceId
540
+ });
541
+ return () => {
542
+ channel.removeEventListener("message", handleMessage);
543
+ channel.close();
544
+ channelRef.current = null;
545
+ instanceIdRef.current = null;
546
+ };
547
+ }, [channelName]);
548
+ (0, import_react2.useEffect)(() => {
549
+ if (!channelRef.current || !instanceIdRef.current) return;
550
+ if (skipBroadcastRef.current) {
551
+ skipBroadcastRef.current = false;
552
+ return;
553
+ }
554
+ channelRef.current.postMessage({
555
+ type: "controls-sync-values",
556
+ source: instanceIdRef.current,
557
+ values
558
+ });
559
+ }, [values]);
466
560
  const contextValue = (0, import_react2.useMemo)(
467
561
  () => ({
468
562
  schema,
@@ -582,7 +676,7 @@ var usePreviewUrl = (values, basePath = "") => {
582
676
  (0, import_react3.useEffect)(() => {
583
677
  if (typeof window === "undefined") return;
584
678
  const params = new URLSearchParams();
585
- params.set("nocontrols", "true");
679
+ params.set(NO_CONTROLS_PARAM, "true");
586
680
  for (const [key, value] of Object.entries(values)) {
587
681
  if (value !== void 0 && value !== null) {
588
682
  params.set(key, value.toString());
@@ -1004,6 +1098,58 @@ var ControlPanel = () => {
1004
1098
  const { leftPanelWidth, isDesktop, isHydrated } = useResizableLayout();
1005
1099
  const { schema, setValue, values, componentName, config } = useControlsContext();
1006
1100
  const previewUrl = usePreviewUrl(values);
1101
+ const buildUrl = (0, import_react5.useCallback)(
1102
+ (modifier) => {
1103
+ if (!previewUrl) return "";
1104
+ const [path, search = ""] = previewUrl.split("?");
1105
+ const params = new URLSearchParams(search);
1106
+ modifier(params);
1107
+ const query = params.toString();
1108
+ return query ? `${path}?${query}` : path;
1109
+ },
1110
+ [previewUrl]
1111
+ );
1112
+ const presentationUrl = (0, import_react5.useMemo)(() => {
1113
+ if (!previewUrl) return "";
1114
+ return buildUrl((params) => {
1115
+ params.set(PRESENTATION_PARAM, "true");
1116
+ });
1117
+ }, [buildUrl, previewUrl]);
1118
+ const controlsOnlyUrl = (0, import_react5.useMemo)(() => {
1119
+ if (!previewUrl) return "";
1120
+ return buildUrl((params) => {
1121
+ params.delete(NO_CONTROLS_PARAM);
1122
+ params.delete(PRESENTATION_PARAM);
1123
+ params.set(CONTROLS_ONLY_PARAM, "true");
1124
+ });
1125
+ }, [buildUrl, previewUrl]);
1126
+ const handlePresentationClick = (0, import_react5.useCallback)(() => {
1127
+ if (typeof window === "undefined" || !presentationUrl) return;
1128
+ window.open(presentationUrl, "_blank", "noopener,noreferrer");
1129
+ if (controlsOnlyUrl) {
1130
+ const viewportWidth = window.innerWidth || 1200;
1131
+ const viewportHeight = window.innerHeight || 900;
1132
+ const controlsWidth = Math.max(
1133
+ 320,
1134
+ Math.min(
1135
+ 600,
1136
+ Math.round(viewportWidth * leftPanelWidth / 100)
1137
+ )
1138
+ );
1139
+ const controlsHeight = Math.max(600, viewportHeight);
1140
+ const controlsFeatures = [
1141
+ "noopener",
1142
+ "noreferrer",
1143
+ "toolbar=0",
1144
+ "menubar=0",
1145
+ "resizable=yes",
1146
+ "scrollbars=yes",
1147
+ `width=${controlsWidth}`,
1148
+ `height=${controlsHeight}`
1149
+ ].join(",");
1150
+ window.open(controlsOnlyUrl, "v0-controls", controlsFeatures);
1151
+ }
1152
+ }, [controlsOnlyUrl, leftPanelWidth, presentationUrl]);
1007
1153
  const jsx14 = (0, import_react5.useMemo)(() => {
1008
1154
  if (!componentName) return "";
1009
1155
  const props = Object.entries(values).map(([key, val]) => {
@@ -1346,19 +1492,34 @@ var ControlPanel = () => {
1346
1492
  }
1347
1493
  ) }, "control-panel-jsx")
1348
1494
  ] }),
1349
- previewUrl && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Button, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1350
- "a",
1351
- {
1352
- href: previewUrl,
1353
- target: "_blank",
1354
- rel: "noopener noreferrer",
1355
- className: "w-full px-4 py-2 text-sm text-center bg-stone-900 hover:bg-stone-800 text-white rounded-md border border-stone-700",
1356
- children: [
1357
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react3.SquareArrowOutUpRight, {}),
1358
- " Open in a New Tab"
1359
- ]
1360
- }
1361
- ) })
1495
+ previewUrl && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex flex-col gap-2", children: [
1496
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Button, { asChild: true, className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1497
+ "a",
1498
+ {
1499
+ href: previewUrl,
1500
+ target: "_blank",
1501
+ rel: "noopener noreferrer",
1502
+ className: "w-full px-4 py-2 text-sm text-center bg-stone-900 hover:bg-stone-800 text-white rounded-md border border-stone-700",
1503
+ children: [
1504
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react3.SquareArrowOutUpRight, {}),
1505
+ " Open in a New Tab"
1506
+ ]
1507
+ }
1508
+ ) }),
1509
+ config?.showPresentationButton && presentationUrl && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1510
+ Button,
1511
+ {
1512
+ type: "button",
1513
+ onClick: handlePresentationClick,
1514
+ variant: "secondary",
1515
+ className: "w-full bg-stone-800 text-white hover:bg-stone-700 border border-stone-700",
1516
+ children: [
1517
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react3.Presentation, {}),
1518
+ " Presentation Mode"
1519
+ ]
1520
+ }
1521
+ )
1522
+ ] })
1362
1523
  ] })
1363
1524
  }
1364
1525
  );
@@ -1414,25 +1575,42 @@ var PreviewContainer_default = PreviewContainer;
1414
1575
 
1415
1576
  // src/components/Playground/Playground.tsx
1416
1577
  var import_jsx_runtime13 = require("react/jsx-runtime");
1417
- var NO_CONTROLS_PARAM = "nocontrols";
1578
+ var HiddenPreview = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { "aria-hidden": "true", className: "hidden", children });
1418
1579
  function Playground({ children }) {
1419
1580
  const [isHydrated, setIsHydrated] = (0, import_react7.useState)(false);
1420
1581
  const [copied, setCopied] = (0, import_react7.useState)(false);
1421
1582
  (0, import_react7.useEffect)(() => {
1422
1583
  setIsHydrated(true);
1423
1584
  }, []);
1424
- const hideControls = (0, import_react7.useMemo)(() => {
1425
- if (typeof window === "undefined") return false;
1426
- return new URLSearchParams(window.location.search).get(NO_CONTROLS_PARAM) === "true";
1585
+ const { showControls, isPresentationMode, isControlsOnly } = (0, import_react7.useMemo)(() => {
1586
+ if (typeof window === "undefined") {
1587
+ return {
1588
+ showControls: true,
1589
+ isPresentationMode: false,
1590
+ isControlsOnly: false
1591
+ };
1592
+ }
1593
+ const params = new URLSearchParams(window.location.search);
1594
+ const presentation = params.get(PRESENTATION_PARAM) === "true";
1595
+ const controlsOnly = params.get(CONTROLS_ONLY_PARAM) === "true";
1596
+ const noControlsParam = params.get(NO_CONTROLS_PARAM) === "true";
1597
+ const showControlsValue = controlsOnly || !presentation && !noControlsParam;
1598
+ return {
1599
+ showControls: showControlsValue,
1600
+ isPresentationMode: presentation,
1601
+ isControlsOnly: controlsOnly
1602
+ };
1427
1603
  }, []);
1604
+ const shouldShowShareButton = !showControls && !isPresentationMode;
1605
+ const layoutHideControls = !showControls || isControlsOnly;
1428
1606
  const handleCopy = () => {
1429
1607
  navigator.clipboard.writeText(window.location.href);
1430
1608
  setCopied(true);
1431
1609
  setTimeout(() => setCopied(false), 2e3);
1432
1610
  };
1433
1611
  if (!isHydrated) return null;
1434
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ResizableLayout, { hideControls, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(ControlsProvider, { children: [
1435
- hideControls && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1612
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ResizableLayout, { hideControls: layoutHideControls, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(ControlsProvider, { children: [
1613
+ shouldShowShareButton && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1436
1614
  "button",
1437
1615
  {
1438
1616
  onClick: handleCopy,
@@ -1443,8 +1621,8 @@ function Playground({ children }) {
1443
1621
  ]
1444
1622
  }
1445
1623
  ),
1446
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PreviewContainer_default, { hideControls, children }),
1447
- !hideControls && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ControlPanel_default, {})
1624
+ isControlsOnly ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(HiddenPreview, { children }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PreviewContainer_default, { hideControls: layoutHideControls, children }),
1625
+ showControls && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ControlPanel_default, {})
1448
1626
  ] }) });
1449
1627
  }
1450
1628
 
package/dist/index.mjs CHANGED
@@ -131,6 +131,28 @@ var getUrlParams = () => {
131
131
  return entries;
132
132
  };
133
133
 
134
+ // src/constants/urlParams.ts
135
+ var NO_CONTROLS_PARAM = "nocontrols";
136
+ var PRESENTATION_PARAM = "presentation";
137
+ var CONTROLS_ONLY_PARAM = "controlsonly";
138
+
139
+ // src/utils/getControlsChannelName.ts
140
+ var EXCLUDED_KEYS = /* @__PURE__ */ new Set([
141
+ NO_CONTROLS_PARAM,
142
+ PRESENTATION_PARAM,
143
+ CONTROLS_ONLY_PARAM
144
+ ]);
145
+ var getControlsChannelName = () => {
146
+ if (typeof window === "undefined") return null;
147
+ const params = new URLSearchParams(window.location.search);
148
+ for (const key of EXCLUDED_KEYS) {
149
+ params.delete(key);
150
+ }
151
+ const query = params.toString();
152
+ const base = window.location.pathname || "/";
153
+ return `v0-controls:${base}${query ? `?${query}` : ""}`;
154
+ };
155
+
134
156
  // src/lib/advancedPalette.ts
135
157
  var CHANNEL_KEYS = ["r", "g", "b"];
136
158
  var DEFAULT_CHANNEL_LABELS = {
@@ -392,6 +414,18 @@ var ControlsProvider = ({ children }) => {
392
414
  showCopyButton: true
393
415
  });
394
416
  const [componentName, setComponentName] = useState2();
417
+ const [channelName, setChannelName] = useState2(null);
418
+ const channelRef = useRef2(null);
419
+ const instanceIdRef = useRef2(null);
420
+ const skipBroadcastRef = useRef2(false);
421
+ const latestValuesRef = useRef2(values);
422
+ useEffect2(() => {
423
+ latestValuesRef.current = values;
424
+ }, [values]);
425
+ useEffect2(() => {
426
+ if (typeof window === "undefined") return;
427
+ setChannelName(getControlsChannelName());
428
+ }, []);
395
429
  const setValue = (key, value) => {
396
430
  setValues((prev) => ({ ...prev, [key]: value }));
397
431
  };
@@ -426,6 +460,66 @@ var ControlsProvider = ({ children }) => {
426
460
  return updated;
427
461
  });
428
462
  };
463
+ useEffect2(() => {
464
+ if (!channelName) return;
465
+ if (typeof window === "undefined") return;
466
+ if (typeof window.BroadcastChannel === "undefined") return;
467
+ const instanceId = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : Math.random().toString(36).slice(2);
468
+ instanceIdRef.current = instanceId;
469
+ const channel = new BroadcastChannel(channelName);
470
+ channelRef.current = channel;
471
+ const sendValues = () => {
472
+ if (!instanceIdRef.current) return;
473
+ channel.postMessage({
474
+ type: "controls-sync-values",
475
+ source: instanceIdRef.current,
476
+ values: latestValuesRef.current
477
+ });
478
+ };
479
+ const handleMessage = (event) => {
480
+ const data = event.data;
481
+ if (!data || data.source === instanceIdRef.current) return;
482
+ if (data.type === "controls-sync-request") {
483
+ sendValues();
484
+ return;
485
+ }
486
+ if (data.type === "controls-sync-values" && data.values) {
487
+ const incoming = data.values;
488
+ setValues((prev) => {
489
+ const prevKeys = Object.keys(prev);
490
+ const incomingKeys = Object.keys(incoming);
491
+ const sameLength = prevKeys.length === incomingKeys.length;
492
+ const sameValues = sameLength && incomingKeys.every((key) => prev[key] === incoming[key]);
493
+ if (sameValues) return prev;
494
+ skipBroadcastRef.current = true;
495
+ return { ...incoming };
496
+ });
497
+ }
498
+ };
499
+ channel.addEventListener("message", handleMessage);
500
+ channel.postMessage({
501
+ type: "controls-sync-request",
502
+ source: instanceId
503
+ });
504
+ return () => {
505
+ channel.removeEventListener("message", handleMessage);
506
+ channel.close();
507
+ channelRef.current = null;
508
+ instanceIdRef.current = null;
509
+ };
510
+ }, [channelName]);
511
+ useEffect2(() => {
512
+ if (!channelRef.current || !instanceIdRef.current) return;
513
+ if (skipBroadcastRef.current) {
514
+ skipBroadcastRef.current = false;
515
+ return;
516
+ }
517
+ channelRef.current.postMessage({
518
+ type: "controls-sync-values",
519
+ source: instanceIdRef.current,
520
+ values
521
+ });
522
+ }, [values]);
429
523
  const contextValue = useMemo(
430
524
  () => ({
431
525
  schema,
@@ -536,7 +630,13 @@ var useUrlSyncedControls = useControls;
536
630
 
537
631
  // src/components/ControlPanel/ControlPanel.tsx
538
632
  import { useState as useState4, useMemo as useMemo3, useCallback as useCallback3 } from "react";
539
- import { Check as Check2, Copy, SquareArrowOutUpRight, ChevronDown as ChevronDown2 } from "lucide-react";
633
+ import {
634
+ Check as Check2,
635
+ Copy,
636
+ SquareArrowOutUpRight,
637
+ ChevronDown as ChevronDown2,
638
+ Presentation
639
+ } from "lucide-react";
540
640
 
541
641
  // src/hooks/usePreviewUrl.ts
542
642
  import { useEffect as useEffect3, useState as useState3 } from "react";
@@ -545,7 +645,7 @@ var usePreviewUrl = (values, basePath = "") => {
545
645
  useEffect3(() => {
546
646
  if (typeof window === "undefined") return;
547
647
  const params = new URLSearchParams();
548
- params.set("nocontrols", "true");
648
+ params.set(NO_CONTROLS_PARAM, "true");
549
649
  for (const [key, value] of Object.entries(values)) {
550
650
  if (value !== void 0 && value !== null) {
551
651
  params.set(key, value.toString());
@@ -972,6 +1072,58 @@ var ControlPanel = () => {
972
1072
  const { leftPanelWidth, isDesktop, isHydrated } = useResizableLayout();
973
1073
  const { schema, setValue, values, componentName, config } = useControlsContext();
974
1074
  const previewUrl = usePreviewUrl(values);
1075
+ const buildUrl = useCallback3(
1076
+ (modifier) => {
1077
+ if (!previewUrl) return "";
1078
+ const [path, search = ""] = previewUrl.split("?");
1079
+ const params = new URLSearchParams(search);
1080
+ modifier(params);
1081
+ const query = params.toString();
1082
+ return query ? `${path}?${query}` : path;
1083
+ },
1084
+ [previewUrl]
1085
+ );
1086
+ const presentationUrl = useMemo3(() => {
1087
+ if (!previewUrl) return "";
1088
+ return buildUrl((params) => {
1089
+ params.set(PRESENTATION_PARAM, "true");
1090
+ });
1091
+ }, [buildUrl, previewUrl]);
1092
+ const controlsOnlyUrl = useMemo3(() => {
1093
+ if (!previewUrl) return "";
1094
+ return buildUrl((params) => {
1095
+ params.delete(NO_CONTROLS_PARAM);
1096
+ params.delete(PRESENTATION_PARAM);
1097
+ params.set(CONTROLS_ONLY_PARAM, "true");
1098
+ });
1099
+ }, [buildUrl, previewUrl]);
1100
+ const handlePresentationClick = useCallback3(() => {
1101
+ if (typeof window === "undefined" || !presentationUrl) return;
1102
+ window.open(presentationUrl, "_blank", "noopener,noreferrer");
1103
+ if (controlsOnlyUrl) {
1104
+ const viewportWidth = window.innerWidth || 1200;
1105
+ const viewportHeight = window.innerHeight || 900;
1106
+ const controlsWidth = Math.max(
1107
+ 320,
1108
+ Math.min(
1109
+ 600,
1110
+ Math.round(viewportWidth * leftPanelWidth / 100)
1111
+ )
1112
+ );
1113
+ const controlsHeight = Math.max(600, viewportHeight);
1114
+ const controlsFeatures = [
1115
+ "noopener",
1116
+ "noreferrer",
1117
+ "toolbar=0",
1118
+ "menubar=0",
1119
+ "resizable=yes",
1120
+ "scrollbars=yes",
1121
+ `width=${controlsWidth}`,
1122
+ `height=${controlsHeight}`
1123
+ ].join(",");
1124
+ window.open(controlsOnlyUrl, "v0-controls", controlsFeatures);
1125
+ }
1126
+ }, [controlsOnlyUrl, leftPanelWidth, presentationUrl]);
975
1127
  const jsx14 = useMemo3(() => {
976
1128
  if (!componentName) return "";
977
1129
  const props = Object.entries(values).map(([key, val]) => {
@@ -1314,19 +1466,34 @@ var ControlPanel = () => {
1314
1466
  }
1315
1467
  ) }, "control-panel-jsx")
1316
1468
  ] }),
1317
- previewUrl && /* @__PURE__ */ jsx10(Button, { asChild: true, children: /* @__PURE__ */ jsxs5(
1318
- "a",
1319
- {
1320
- href: previewUrl,
1321
- target: "_blank",
1322
- rel: "noopener noreferrer",
1323
- className: "w-full px-4 py-2 text-sm text-center bg-stone-900 hover:bg-stone-800 text-white rounded-md border border-stone-700",
1324
- children: [
1325
- /* @__PURE__ */ jsx10(SquareArrowOutUpRight, {}),
1326
- " Open in a New Tab"
1327
- ]
1328
- }
1329
- ) })
1469
+ previewUrl && /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", children: [
1470
+ /* @__PURE__ */ jsx10(Button, { asChild: true, className: "w-full", children: /* @__PURE__ */ jsxs5(
1471
+ "a",
1472
+ {
1473
+ href: previewUrl,
1474
+ target: "_blank",
1475
+ rel: "noopener noreferrer",
1476
+ className: "w-full px-4 py-2 text-sm text-center bg-stone-900 hover:bg-stone-800 text-white rounded-md border border-stone-700",
1477
+ children: [
1478
+ /* @__PURE__ */ jsx10(SquareArrowOutUpRight, {}),
1479
+ " Open in a New Tab"
1480
+ ]
1481
+ }
1482
+ ) }),
1483
+ config?.showPresentationButton && presentationUrl && /* @__PURE__ */ jsxs5(
1484
+ Button,
1485
+ {
1486
+ type: "button",
1487
+ onClick: handlePresentationClick,
1488
+ variant: "secondary",
1489
+ className: "w-full bg-stone-800 text-white hover:bg-stone-700 border border-stone-700",
1490
+ children: [
1491
+ /* @__PURE__ */ jsx10(Presentation, {}),
1492
+ " Presentation Mode"
1493
+ ]
1494
+ }
1495
+ )
1496
+ ] })
1330
1497
  ] })
1331
1498
  }
1332
1499
  );
@@ -1382,25 +1549,42 @@ var PreviewContainer_default = PreviewContainer;
1382
1549
 
1383
1550
  // src/components/Playground/Playground.tsx
1384
1551
  import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
1385
- var NO_CONTROLS_PARAM = "nocontrols";
1552
+ var HiddenPreview = ({ children }) => /* @__PURE__ */ jsx13("div", { "aria-hidden": "true", className: "hidden", children });
1386
1553
  function Playground({ children }) {
1387
1554
  const [isHydrated, setIsHydrated] = useState5(false);
1388
1555
  const [copied, setCopied] = useState5(false);
1389
1556
  useEffect5(() => {
1390
1557
  setIsHydrated(true);
1391
1558
  }, []);
1392
- const hideControls = useMemo4(() => {
1393
- if (typeof window === "undefined") return false;
1394
- return new URLSearchParams(window.location.search).get(NO_CONTROLS_PARAM) === "true";
1559
+ const { showControls, isPresentationMode, isControlsOnly } = useMemo4(() => {
1560
+ if (typeof window === "undefined") {
1561
+ return {
1562
+ showControls: true,
1563
+ isPresentationMode: false,
1564
+ isControlsOnly: false
1565
+ };
1566
+ }
1567
+ const params = new URLSearchParams(window.location.search);
1568
+ const presentation = params.get(PRESENTATION_PARAM) === "true";
1569
+ const controlsOnly = params.get(CONTROLS_ONLY_PARAM) === "true";
1570
+ const noControlsParam = params.get(NO_CONTROLS_PARAM) === "true";
1571
+ const showControlsValue = controlsOnly || !presentation && !noControlsParam;
1572
+ return {
1573
+ showControls: showControlsValue,
1574
+ isPresentationMode: presentation,
1575
+ isControlsOnly: controlsOnly
1576
+ };
1395
1577
  }, []);
1578
+ const shouldShowShareButton = !showControls && !isPresentationMode;
1579
+ const layoutHideControls = !showControls || isControlsOnly;
1396
1580
  const handleCopy = () => {
1397
1581
  navigator.clipboard.writeText(window.location.href);
1398
1582
  setCopied(true);
1399
1583
  setTimeout(() => setCopied(false), 2e3);
1400
1584
  };
1401
1585
  if (!isHydrated) return null;
1402
- return /* @__PURE__ */ jsx13(ResizableLayout, { hideControls, children: /* @__PURE__ */ jsxs7(ControlsProvider, { children: [
1403
- hideControls && /* @__PURE__ */ jsxs7(
1586
+ return /* @__PURE__ */ jsx13(ResizableLayout, { hideControls: layoutHideControls, children: /* @__PURE__ */ jsxs7(ControlsProvider, { children: [
1587
+ shouldShowShareButton && /* @__PURE__ */ jsxs7(
1404
1588
  "button",
1405
1589
  {
1406
1590
  onClick: handleCopy,
@@ -1411,8 +1595,8 @@ function Playground({ children }) {
1411
1595
  ]
1412
1596
  }
1413
1597
  ),
1414
- /* @__PURE__ */ jsx13(PreviewContainer_default, { hideControls, children }),
1415
- !hideControls && /* @__PURE__ */ jsx13(ControlPanel_default, {})
1598
+ isControlsOnly ? /* @__PURE__ */ jsx13(HiddenPreview, { children }) : /* @__PURE__ */ jsx13(PreviewContainer_default, { hideControls: layoutHideControls, children }),
1599
+ showControls && /* @__PURE__ */ jsx13(ControlPanel_default, {})
1416
1600
  ] }) });
1417
1601
  }
1418
1602
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toriistudio/v0-playground",
3
- "version": "0.5.5",
3
+ "version": "0.6.0",
4
4
  "description": "V0 Playground",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",