@toriistudio/v0-playground 0.5.5 → 0.7.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/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components/Playground/Playground.tsx
2
- import { useEffect as useEffect5, useMemo as useMemo4, useState as useState5 } from "react";
2
+ import { useEffect as useEffect6, useMemo as useMemo4, useState as useState5 } from "react";
3
3
  import { Check as Check3, Copy as Copy2 } from "lucide-react";
4
4
 
5
5
  // src/context/ResizableLayout.tsx
@@ -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 = {
@@ -389,9 +411,22 @@ var ControlsProvider = ({ children }) => {
389
411
  const [schema, setSchema] = useState2({});
390
412
  const [values, setValues] = useState2({});
391
413
  const [config, setConfig] = useState2({
392
- showCopyButton: true
414
+ showCopyButton: true,
415
+ showCodeSnippet: false
393
416
  });
394
417
  const [componentName, setComponentName] = useState2();
418
+ const [channelName, setChannelName] = useState2(null);
419
+ const channelRef = useRef2(null);
420
+ const instanceIdRef = useRef2(null);
421
+ const skipBroadcastRef = useRef2(false);
422
+ const latestValuesRef = useRef2(values);
423
+ useEffect2(() => {
424
+ latestValuesRef.current = values;
425
+ }, [values]);
426
+ useEffect2(() => {
427
+ if (typeof window === "undefined") return;
428
+ setChannelName(getControlsChannelName());
429
+ }, []);
395
430
  const setValue = (key, value) => {
396
431
  setValues((prev) => ({ ...prev, [key]: value }));
397
432
  };
@@ -426,6 +461,66 @@ var ControlsProvider = ({ children }) => {
426
461
  return updated;
427
462
  });
428
463
  };
464
+ useEffect2(() => {
465
+ if (!channelName) return;
466
+ if (typeof window === "undefined") return;
467
+ if (typeof window.BroadcastChannel === "undefined") return;
468
+ const instanceId = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : Math.random().toString(36).slice(2);
469
+ instanceIdRef.current = instanceId;
470
+ const channel = new BroadcastChannel(channelName);
471
+ channelRef.current = channel;
472
+ const sendValues = () => {
473
+ if (!instanceIdRef.current) return;
474
+ channel.postMessage({
475
+ type: "controls-sync-values",
476
+ source: instanceIdRef.current,
477
+ values: latestValuesRef.current
478
+ });
479
+ };
480
+ const handleMessage = (event) => {
481
+ const data = event.data;
482
+ if (!data || data.source === instanceIdRef.current) return;
483
+ if (data.type === "controls-sync-request") {
484
+ sendValues();
485
+ return;
486
+ }
487
+ if (data.type === "controls-sync-values" && data.values) {
488
+ const incoming = data.values;
489
+ setValues((prev) => {
490
+ const prevKeys = Object.keys(prev);
491
+ const incomingKeys = Object.keys(incoming);
492
+ const sameLength = prevKeys.length === incomingKeys.length;
493
+ const sameValues = sameLength && incomingKeys.every((key) => prev[key] === incoming[key]);
494
+ if (sameValues) return prev;
495
+ skipBroadcastRef.current = true;
496
+ return { ...incoming };
497
+ });
498
+ }
499
+ };
500
+ channel.addEventListener("message", handleMessage);
501
+ channel.postMessage({
502
+ type: "controls-sync-request",
503
+ source: instanceId
504
+ });
505
+ return () => {
506
+ channel.removeEventListener("message", handleMessage);
507
+ channel.close();
508
+ channelRef.current = null;
509
+ instanceIdRef.current = null;
510
+ };
511
+ }, [channelName]);
512
+ useEffect2(() => {
513
+ if (!channelRef.current || !instanceIdRef.current) return;
514
+ if (skipBroadcastRef.current) {
515
+ skipBroadcastRef.current = false;
516
+ return;
517
+ }
518
+ channelRef.current.postMessage({
519
+ type: "controls-sync-values",
520
+ source: instanceIdRef.current,
521
+ values
522
+ });
523
+ }, [values]);
429
524
  const contextValue = useMemo(
430
525
  () => ({
431
526
  schema,
@@ -535,8 +630,14 @@ var useControls = (schema, options) => {
535
630
  var useUrlSyncedControls = useControls;
536
631
 
537
632
  // src/components/ControlPanel/ControlPanel.tsx
538
- 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 { useState as useState4, useMemo as useMemo3, useCallback as useCallback3, useEffect as useEffect5, useRef as useRef4 } from "react";
634
+ import {
635
+ Check as Check2,
636
+ Copy,
637
+ SquareArrowOutUpRight,
638
+ ChevronDown as ChevronDown2,
639
+ Presentation
640
+ } from "lucide-react";
540
641
 
541
642
  // src/hooks/usePreviewUrl.ts
542
643
  import { useEffect as useEffect3, useState as useState3 } from "react";
@@ -545,7 +646,7 @@ var usePreviewUrl = (values, basePath = "") => {
545
646
  useEffect3(() => {
546
647
  if (typeof window === "undefined") return;
547
648
  const params = new URLSearchParams();
548
- params.set("nocontrols", "true");
649
+ params.set(NO_CONTROLS_PARAM, "true");
549
650
  for (const [key, value] of Object.entries(values)) {
550
651
  if (value !== void 0 && value !== null) {
551
652
  params.set(key, value.toString());
@@ -966,12 +1067,280 @@ var AdvancedPaletteControl_default = AdvancedPaletteControl;
966
1067
 
967
1068
  // src/components/ControlPanel/ControlPanel.tsx
968
1069
  import { Fragment, jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
1070
+ var splitPropsString = (input) => {
1071
+ const props = [];
1072
+ let current = "";
1073
+ let curlyDepth = 0;
1074
+ let squareDepth = 0;
1075
+ let parenDepth = 0;
1076
+ let inSingleQuote = false;
1077
+ let inDoubleQuote = false;
1078
+ let inBacktick = false;
1079
+ let escapeNext = false;
1080
+ for (const char of input) {
1081
+ if (escapeNext) {
1082
+ current += char;
1083
+ escapeNext = false;
1084
+ continue;
1085
+ }
1086
+ if (char === "\\") {
1087
+ current += char;
1088
+ escapeNext = true;
1089
+ continue;
1090
+ }
1091
+ if (char === "'" && !inDoubleQuote && !inBacktick) {
1092
+ inSingleQuote = !inSingleQuote;
1093
+ current += char;
1094
+ continue;
1095
+ }
1096
+ if (char === '"' && !inSingleQuote && !inBacktick) {
1097
+ inDoubleQuote = !inDoubleQuote;
1098
+ current += char;
1099
+ continue;
1100
+ }
1101
+ if (char === "`" && !inSingleQuote && !inDoubleQuote) {
1102
+ inBacktick = !inBacktick;
1103
+ current += char;
1104
+ continue;
1105
+ }
1106
+ if (!inSingleQuote && !inDoubleQuote && !inBacktick) {
1107
+ if (char === "{") {
1108
+ curlyDepth += 1;
1109
+ } else if (char === "}") {
1110
+ curlyDepth = Math.max(0, curlyDepth - 1);
1111
+ } else if (char === "[") {
1112
+ squareDepth += 1;
1113
+ } else if (char === "]") {
1114
+ squareDepth = Math.max(0, squareDepth - 1);
1115
+ } else if (char === "(") {
1116
+ parenDepth += 1;
1117
+ } else if (char === ")") {
1118
+ parenDepth = Math.max(0, parenDepth - 1);
1119
+ }
1120
+ }
1121
+ const atTopLevel = !inSingleQuote && !inDoubleQuote && !inBacktick && curlyDepth === 0 && squareDepth === 0 && parenDepth === 0;
1122
+ if (atTopLevel && /\s/.test(char)) {
1123
+ if (current.trim()) {
1124
+ props.push(current.trim());
1125
+ }
1126
+ current = "";
1127
+ continue;
1128
+ }
1129
+ current += char;
1130
+ }
1131
+ if (current.trim()) {
1132
+ props.push(current.trim());
1133
+ }
1134
+ return props;
1135
+ };
1136
+ var formatJsxCodeSnippet = (input) => {
1137
+ const trimmed = input.trim();
1138
+ if (!trimmed) return "";
1139
+ if (trimmed.includes("\n")) {
1140
+ return trimmed;
1141
+ }
1142
+ if (!trimmed.startsWith("<") || !trimmed.endsWith(">")) {
1143
+ return trimmed;
1144
+ }
1145
+ if (!trimmed.endsWith("/>")) {
1146
+ return trimmed;
1147
+ }
1148
+ const inner = trimmed.slice(1, -2).trim();
1149
+ const firstSpaceIndex = inner.indexOf(" ");
1150
+ if (firstSpaceIndex === -1) {
1151
+ return `<${inner} />`;
1152
+ }
1153
+ const componentName = inner.slice(0, firstSpaceIndex);
1154
+ const propsString = inner.slice(firstSpaceIndex + 1).trim();
1155
+ if (!propsString) {
1156
+ return `<${componentName} />`;
1157
+ }
1158
+ const propsList = splitPropsString(propsString);
1159
+ if (propsList.length === 0) {
1160
+ return `<${componentName} ${propsString} />`;
1161
+ }
1162
+ const formattedProps = propsList.map((prop) => ` ${prop}`).join("\n");
1163
+ return `<${componentName}
1164
+ ${formattedProps}
1165
+ />`;
1166
+ };
1167
+ var isWhitespace = (char) => /\s/.test(char);
1168
+ var isAttrNameChar = (char) => /[A-Za-z0-9_$\-.:]/.test(char);
1169
+ var isAlphaStart = (char) => /[A-Za-z_$]/.test(char);
1170
+ var tokenizeJsx = (input) => {
1171
+ const tokens = [];
1172
+ let i = 0;
1173
+ while (i < input.length) {
1174
+ const char = input[i];
1175
+ if (char === "<") {
1176
+ tokens.push({ type: "punctuation", value: "<" });
1177
+ i += 1;
1178
+ if (input[i] === "/") {
1179
+ tokens.push({ type: "punctuation", value: "/" });
1180
+ i += 1;
1181
+ }
1182
+ const start = i;
1183
+ while (i < input.length && isAttrNameChar(input[i])) {
1184
+ i += 1;
1185
+ }
1186
+ if (i > start) {
1187
+ tokens.push({ type: "tag", value: input.slice(start, i) });
1188
+ }
1189
+ continue;
1190
+ }
1191
+ if (char === "/" && input[i + 1] === ">") {
1192
+ tokens.push({ type: "punctuation", value: "/>" });
1193
+ i += 2;
1194
+ continue;
1195
+ }
1196
+ if (char === ">") {
1197
+ tokens.push({ type: "punctuation", value: ">" });
1198
+ i += 1;
1199
+ continue;
1200
+ }
1201
+ if (char === "=") {
1202
+ tokens.push({ type: "punctuation", value: "=" });
1203
+ i += 1;
1204
+ continue;
1205
+ }
1206
+ if (char === '"' || char === "'" || char === "`") {
1207
+ const quote = char;
1208
+ let j = i + 1;
1209
+ let value = quote;
1210
+ while (j < input.length) {
1211
+ const current = input[j];
1212
+ value += current;
1213
+ if (current === quote && input[j - 1] !== "\\") {
1214
+ break;
1215
+ }
1216
+ j += 1;
1217
+ }
1218
+ tokens.push({ type: "string", value });
1219
+ i = j + 1;
1220
+ continue;
1221
+ }
1222
+ if (char === "{") {
1223
+ let depth = 1;
1224
+ let j = i + 1;
1225
+ while (j < input.length && depth > 0) {
1226
+ if (input[j] === "{") {
1227
+ depth += 1;
1228
+ } else if (input[j] === "}") {
1229
+ depth -= 1;
1230
+ }
1231
+ j += 1;
1232
+ }
1233
+ const expression = input.slice(i, j);
1234
+ tokens.push({ type: "expression", value: expression });
1235
+ i = j;
1236
+ continue;
1237
+ }
1238
+ if (isAlphaStart(char)) {
1239
+ const start = i;
1240
+ i += 1;
1241
+ while (i < input.length && isAttrNameChar(input[i])) {
1242
+ i += 1;
1243
+ }
1244
+ const word = input.slice(start, i);
1245
+ let k = i;
1246
+ while (k < input.length && isWhitespace(input[k])) {
1247
+ k += 1;
1248
+ }
1249
+ if (input[k] === "=") {
1250
+ tokens.push({ type: "attrName", value: word });
1251
+ } else {
1252
+ tokens.push({ type: "plain", value: word });
1253
+ }
1254
+ continue;
1255
+ }
1256
+ tokens.push({ type: "plain", value: char });
1257
+ i += 1;
1258
+ }
1259
+ return tokens;
1260
+ };
1261
+ var TOKEN_CLASS_MAP = {
1262
+ tag: "text-sky-300",
1263
+ attrName: "text-amber-200",
1264
+ string: "text-emerald-300",
1265
+ expression: "text-purple-300",
1266
+ punctuation: "text-stone-400"
1267
+ };
1268
+ var highlightJsx = (input) => {
1269
+ const tokens = tokenizeJsx(input);
1270
+ const nodes = [];
1271
+ tokens.forEach((token, index) => {
1272
+ if (token.type === "plain") {
1273
+ nodes.push(token.value);
1274
+ } else {
1275
+ nodes.push(
1276
+ /* @__PURE__ */ jsx10("span", { className: TOKEN_CLASS_MAP[token.type], children: token.value }, `token-${index}`)
1277
+ );
1278
+ }
1279
+ });
1280
+ return nodes;
1281
+ };
969
1282
  var ControlPanel = () => {
970
1283
  const [copied, setCopied] = useState4(false);
1284
+ const [codeCopied, setCodeCopied] = useState4(false);
1285
+ const [isCodeVisible, setIsCodeVisible] = useState4(false);
971
1286
  const [folderStates, setFolderStates] = useState4({});
1287
+ const codeCopyTimeoutRef = useRef4(null);
972
1288
  const { leftPanelWidth, isDesktop, isHydrated } = useResizableLayout();
973
1289
  const { schema, setValue, values, componentName, config } = useControlsContext();
1290
+ const isControlsOnlyView = typeof window !== "undefined" && new URLSearchParams(window.location.search).get(CONTROLS_ONLY_PARAM) === "true";
974
1291
  const previewUrl = usePreviewUrl(values);
1292
+ const buildUrl = useCallback3(
1293
+ (modifier) => {
1294
+ if (!previewUrl) return "";
1295
+ const [path, search = ""] = previewUrl.split("?");
1296
+ const params = new URLSearchParams(search);
1297
+ modifier(params);
1298
+ const query = params.toString();
1299
+ return query ? `${path}?${query}` : path;
1300
+ },
1301
+ [previewUrl]
1302
+ );
1303
+ const presentationUrl = useMemo3(() => {
1304
+ if (!previewUrl) return "";
1305
+ return buildUrl((params) => {
1306
+ params.set(PRESENTATION_PARAM, "true");
1307
+ });
1308
+ }, [buildUrl, previewUrl]);
1309
+ const controlsOnlyUrl = useMemo3(() => {
1310
+ if (!previewUrl) return "";
1311
+ return buildUrl((params) => {
1312
+ params.delete(NO_CONTROLS_PARAM);
1313
+ params.delete(PRESENTATION_PARAM);
1314
+ params.set(CONTROLS_ONLY_PARAM, "true");
1315
+ });
1316
+ }, [buildUrl, previewUrl]);
1317
+ const handlePresentationClick = useCallback3(() => {
1318
+ if (typeof window === "undefined" || !presentationUrl) return;
1319
+ window.open(presentationUrl, "_blank", "noopener,noreferrer");
1320
+ if (controlsOnlyUrl) {
1321
+ const viewportWidth = window.innerWidth || 1200;
1322
+ const viewportHeight = window.innerHeight || 900;
1323
+ const controlsWidth = Math.max(
1324
+ 320,
1325
+ Math.min(
1326
+ 600,
1327
+ Math.round(viewportWidth * leftPanelWidth / 100)
1328
+ )
1329
+ );
1330
+ const controlsHeight = Math.max(600, viewportHeight);
1331
+ const controlsFeatures = [
1332
+ "noopener",
1333
+ "noreferrer",
1334
+ "toolbar=0",
1335
+ "menubar=0",
1336
+ "resizable=yes",
1337
+ "scrollbars=yes",
1338
+ `width=${controlsWidth}`,
1339
+ `height=${controlsHeight}`
1340
+ ].join(",");
1341
+ window.open(controlsOnlyUrl, "v0-controls", controlsFeatures);
1342
+ }
1343
+ }, [controlsOnlyUrl, leftPanelWidth, presentationUrl]);
975
1344
  const jsx14 = useMemo3(() => {
976
1345
  if (!componentName) return "";
977
1346
  const props = Object.entries(values).map(([key, val]) => {
@@ -1092,6 +1461,65 @@ var ControlPanel = () => {
1092
1461
  jsonToComponentString
1093
1462
  }) ?? jsx14;
1094
1463
  const shouldShowCopyButton = config?.showCopyButton !== false && Boolean(copyText);
1464
+ const baseSnippet = copyText || jsx14;
1465
+ const formattedCode = useMemo3(
1466
+ () => formatJsxCodeSnippet(baseSnippet),
1467
+ [baseSnippet]
1468
+ );
1469
+ const hasCodeSnippet = Boolean(config?.showCodeSnippet && formattedCode);
1470
+ const highlightedCode = useMemo3(
1471
+ () => formattedCode ? highlightJsx(formattedCode) : null,
1472
+ [formattedCode]
1473
+ );
1474
+ useEffect5(() => {
1475
+ if (!hasCodeSnippet) {
1476
+ setIsCodeVisible(false);
1477
+ }
1478
+ }, [hasCodeSnippet]);
1479
+ useEffect5(() => {
1480
+ setCodeCopied(false);
1481
+ if (codeCopyTimeoutRef.current) {
1482
+ clearTimeout(codeCopyTimeoutRef.current);
1483
+ codeCopyTimeoutRef.current = null;
1484
+ }
1485
+ }, [formattedCode]);
1486
+ useEffect5(() => {
1487
+ return () => {
1488
+ if (codeCopyTimeoutRef.current) {
1489
+ clearTimeout(codeCopyTimeoutRef.current);
1490
+ }
1491
+ };
1492
+ }, []);
1493
+ const handleToggleCodeVisibility = useCallback3(() => {
1494
+ setIsCodeVisible((prev) => {
1495
+ const next = !prev;
1496
+ if (!next) {
1497
+ setCodeCopied(false);
1498
+ if (codeCopyTimeoutRef.current) {
1499
+ clearTimeout(codeCopyTimeoutRef.current);
1500
+ codeCopyTimeoutRef.current = null;
1501
+ }
1502
+ }
1503
+ return next;
1504
+ });
1505
+ }, []);
1506
+ const handleCodeCopy = useCallback3(() => {
1507
+ if (!formattedCode) return;
1508
+ if (typeof navigator === "undefined" || !navigator.clipboard || typeof navigator.clipboard.writeText !== "function") {
1509
+ return;
1510
+ }
1511
+ navigator.clipboard.writeText(formattedCode).then(() => {
1512
+ setCodeCopied(true);
1513
+ if (codeCopyTimeoutRef.current) {
1514
+ clearTimeout(codeCopyTimeoutRef.current);
1515
+ }
1516
+ codeCopyTimeoutRef.current = setTimeout(() => {
1517
+ setCodeCopied(false);
1518
+ codeCopyTimeoutRef.current = null;
1519
+ }, 3e3);
1520
+ }).catch(() => {
1521
+ });
1522
+ }, [formattedCode]);
1095
1523
  const labelize = (key) => key.replace(/([A-Z])/g, " $1").replace(/[\-_]/g, " ").replace(/\s+/g, " ").trim().replace(/(^|\s)\S/g, (s) => s.toUpperCase());
1096
1524
  const renderButtonControl = (key, control, variant) => /* @__PURE__ */ jsx10(
1097
1525
  "div",
@@ -1258,7 +1686,7 @@ var ControlPanel = () => {
1258
1686
  height: "auto",
1259
1687
  flex: "0 0 auto"
1260
1688
  };
1261
- if (isHydrated) {
1689
+ if (isHydrated && !isControlsOnlyView) {
1262
1690
  if (isDesktop) {
1263
1691
  Object.assign(panelStyle, {
1264
1692
  position: "absolute",
@@ -1294,12 +1722,51 @@ var ControlPanel = () => {
1294
1722
  ([key, control]) => renderControl(key, control, "root")
1295
1723
  ),
1296
1724
  bottomFolderSections,
1725
+ hasCodeSnippet && /* @__PURE__ */ jsxs5("div", { className: "border border-stone-700/60 rounded-lg bg-stone-900/70", children: [
1726
+ /* @__PURE__ */ jsxs5(
1727
+ "button",
1728
+ {
1729
+ type: "button",
1730
+ onClick: handleToggleCodeVisibility,
1731
+ className: "w-full flex items-center justify-between px-4 py-3 text-left font-semibold text-stone-200 tracking-wide",
1732
+ "aria-expanded": isCodeVisible,
1733
+ children: [
1734
+ /* @__PURE__ */ jsx10("span", { children: isCodeVisible ? "Hide Code" : "Show Code" }),
1735
+ /* @__PURE__ */ jsx10(
1736
+ ChevronDown2,
1737
+ {
1738
+ className: `w-4 h-4 transition-transform duration-200 ${isCodeVisible ? "rotate-180" : ""}`
1739
+ }
1740
+ )
1741
+ ]
1742
+ }
1743
+ ),
1744
+ isCodeVisible && /* @__PURE__ */ jsxs5("div", { className: "relative border-t border-stone-700/60 bg-stone-950/60 px-4 py-4 rounded-b-lg", children: [
1745
+ /* @__PURE__ */ jsx10(
1746
+ "button",
1747
+ {
1748
+ type: "button",
1749
+ onClick: handleCodeCopy,
1750
+ className: "absolute top-3 right-3 flex items-center gap-1 rounded-md border border-stone-700 bg-stone-800 px-2 py-1 text-xs font-medium text-white shadow hover:bg-stone-700",
1751
+ children: codeCopied ? /* @__PURE__ */ jsxs5(Fragment, { children: [
1752
+ /* @__PURE__ */ jsx10(Check2, { className: "h-3.5 w-3.5" }),
1753
+ "Copied"
1754
+ ] }) : /* @__PURE__ */ jsxs5(Fragment, { children: [
1755
+ /* @__PURE__ */ jsx10(Copy, { className: "h-3.5 w-3.5" }),
1756
+ "Copy"
1757
+ ] })
1758
+ }
1759
+ ),
1760
+ /* @__PURE__ */ jsx10("pre", { className: "whitespace-pre overflow-x-auto text-xs md:text-sm text-stone-200 pr-14", children: /* @__PURE__ */ jsx10("code", { className: "block text-stone-200", children: highlightedCode ?? formattedCode }) })
1761
+ ] })
1762
+ ] }),
1297
1763
  shouldShowCopyButton && /* @__PURE__ */ jsx10("div", { className: "flex-1 pt-4", children: /* @__PURE__ */ jsx10(
1298
1764
  "button",
1299
1765
  {
1300
1766
  onClick: () => {
1301
- if (!copyText) return;
1302
- navigator.clipboard.writeText(copyText);
1767
+ const copyPayload = formattedCode || baseSnippet;
1768
+ if (!copyPayload) return;
1769
+ navigator.clipboard.writeText(copyPayload);
1303
1770
  setCopied(true);
1304
1771
  setTimeout(() => setCopied(false), 5e3);
1305
1772
  },
@@ -1314,19 +1781,34 @@ var ControlPanel = () => {
1314
1781
  }
1315
1782
  ) }, "control-panel-jsx")
1316
1783
  ] }),
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
- ) })
1784
+ previewUrl && /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", children: [
1785
+ /* @__PURE__ */ jsx10(Button, { asChild: true, className: "w-full", children: /* @__PURE__ */ jsxs5(
1786
+ "a",
1787
+ {
1788
+ href: previewUrl,
1789
+ target: "_blank",
1790
+ rel: "noopener noreferrer",
1791
+ 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",
1792
+ children: [
1793
+ /* @__PURE__ */ jsx10(SquareArrowOutUpRight, {}),
1794
+ " Open in a New Tab"
1795
+ ]
1796
+ }
1797
+ ) }),
1798
+ config?.showPresentationButton && presentationUrl && /* @__PURE__ */ jsxs5(
1799
+ Button,
1800
+ {
1801
+ type: "button",
1802
+ onClick: handlePresentationClick,
1803
+ variant: "secondary",
1804
+ className: "w-full bg-stone-800 text-white hover:bg-stone-700 border border-stone-700",
1805
+ children: [
1806
+ /* @__PURE__ */ jsx10(Presentation, {}),
1807
+ " Presentation Mode"
1808
+ ]
1809
+ }
1810
+ )
1811
+ ] })
1330
1812
  ] })
1331
1813
  }
1332
1814
  );
@@ -1334,7 +1816,7 @@ var ControlPanel = () => {
1334
1816
  var ControlPanel_default = ControlPanel;
1335
1817
 
1336
1818
  // src/components/PreviewContainer/PreviewContainer.tsx
1337
- import { useRef as useRef4 } from "react";
1819
+ import { useRef as useRef5 } from "react";
1338
1820
 
1339
1821
  // src/components/Grid/Grid.tsx
1340
1822
  import { jsx as jsx11 } from "react/jsx-runtime";
@@ -1361,7 +1843,7 @@ import { jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
1361
1843
  var PreviewContainer = ({ children, hideControls }) => {
1362
1844
  const { config } = useControlsContext();
1363
1845
  const { leftPanelWidth, isDesktop, isHydrated, containerRef } = useResizableLayout();
1364
- const previewRef = useRef4(null);
1846
+ const previewRef = useRef5(null);
1365
1847
  return /* @__PURE__ */ jsx12(
1366
1848
  "div",
1367
1849
  {
@@ -1382,25 +1864,42 @@ var PreviewContainer_default = PreviewContainer;
1382
1864
 
1383
1865
  // src/components/Playground/Playground.tsx
1384
1866
  import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
1385
- var NO_CONTROLS_PARAM = "nocontrols";
1867
+ var HiddenPreview = ({ children }) => /* @__PURE__ */ jsx13("div", { "aria-hidden": "true", className: "hidden", children });
1386
1868
  function Playground({ children }) {
1387
1869
  const [isHydrated, setIsHydrated] = useState5(false);
1388
1870
  const [copied, setCopied] = useState5(false);
1389
- useEffect5(() => {
1871
+ useEffect6(() => {
1390
1872
  setIsHydrated(true);
1391
1873
  }, []);
1392
- const hideControls = useMemo4(() => {
1393
- if (typeof window === "undefined") return false;
1394
- return new URLSearchParams(window.location.search).get(NO_CONTROLS_PARAM) === "true";
1874
+ const { showControls, isPresentationMode, isControlsOnly } = useMemo4(() => {
1875
+ if (typeof window === "undefined") {
1876
+ return {
1877
+ showControls: true,
1878
+ isPresentationMode: false,
1879
+ isControlsOnly: false
1880
+ };
1881
+ }
1882
+ const params = new URLSearchParams(window.location.search);
1883
+ const presentation = params.get(PRESENTATION_PARAM) === "true";
1884
+ const controlsOnly = params.get(CONTROLS_ONLY_PARAM) === "true";
1885
+ const noControlsParam = params.get(NO_CONTROLS_PARAM) === "true";
1886
+ const showControlsValue = controlsOnly || !presentation && !noControlsParam;
1887
+ return {
1888
+ showControls: showControlsValue,
1889
+ isPresentationMode: presentation,
1890
+ isControlsOnly: controlsOnly
1891
+ };
1395
1892
  }, []);
1893
+ const shouldShowShareButton = !showControls && !isPresentationMode;
1894
+ const layoutHideControls = !showControls || isControlsOnly;
1396
1895
  const handleCopy = () => {
1397
1896
  navigator.clipboard.writeText(window.location.href);
1398
1897
  setCopied(true);
1399
1898
  setTimeout(() => setCopied(false), 2e3);
1400
1899
  };
1401
1900
  if (!isHydrated) return null;
1402
- return /* @__PURE__ */ jsx13(ResizableLayout, { hideControls, children: /* @__PURE__ */ jsxs7(ControlsProvider, { children: [
1403
- hideControls && /* @__PURE__ */ jsxs7(
1901
+ return /* @__PURE__ */ jsx13(ResizableLayout, { hideControls: layoutHideControls, children: /* @__PURE__ */ jsxs7(ControlsProvider, { children: [
1902
+ shouldShowShareButton && /* @__PURE__ */ jsxs7(
1404
1903
  "button",
1405
1904
  {
1406
1905
  onClick: handleCopy,
@@ -1411,13 +1910,13 @@ function Playground({ children }) {
1411
1910
  ]
1412
1911
  }
1413
1912
  ),
1414
- /* @__PURE__ */ jsx13(PreviewContainer_default, { hideControls, children }),
1415
- !hideControls && /* @__PURE__ */ jsx13(ControlPanel_default, {})
1913
+ isControlsOnly ? /* @__PURE__ */ jsx13(HiddenPreview, { children }) : /* @__PURE__ */ jsx13(PreviewContainer_default, { hideControls: layoutHideControls, children }),
1914
+ showControls && /* @__PURE__ */ jsx13(ControlPanel_default, {})
1416
1915
  ] }) });
1417
1916
  }
1418
1917
 
1419
1918
  // src/hooks/useAdvancedPaletteControls.ts
1420
- import { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo5, useRef as useRef5, useState as useState6 } from "react";
1919
+ import { useCallback as useCallback4, useEffect as useEffect7, useMemo as useMemo5, useRef as useRef6, useState as useState6 } from "react";
1421
1920
  var cloneForCallbacks = (palette) => clonePalette(palette);
1422
1921
  var useAdvancedPaletteControls = (options = {}) => {
1423
1922
  const resolvedDefaultPalette = useMemo5(
@@ -1431,10 +1930,10 @@ var useAdvancedPaletteControls = (options = {}) => {
1431
1930
  const [palette, setPaletteState] = useState6(
1432
1931
  () => clonePalette(resolvedDefaultPalette)
1433
1932
  );
1434
- const defaultSignatureRef = useRef5(
1933
+ const defaultSignatureRef = useRef6(
1435
1934
  createPaletteSignature(resolvedDefaultPalette)
1436
1935
  );
1437
- useEffect6(() => {
1936
+ useEffect7(() => {
1438
1937
  const nextSignature = createPaletteSignature(resolvedDefaultPalette);
1439
1938
  if (defaultSignatureRef.current === nextSignature) return;
1440
1939
  defaultSignatureRef.current = nextSignature;