@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 +1 -1
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +200 -22
- package/dist/index.mjs +207 -23
- package/package.json +1 -1
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
|
|
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(
|
|
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.
|
|
1350
|
-
"
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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
|
|
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
|
|
1425
|
-
if (typeof window === "undefined")
|
|
1426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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__ */
|
|
1318
|
-
"
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
|
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
|
|
1393
|
-
if (typeof window === "undefined")
|
|
1394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|