@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/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,8 @@ type ControlsConfig = {
118
118
  showCopyButtonFn?: (args: CopyButtonFnArgs) => string | null | undefined;
119
119
  mainLabel?: string;
120
120
  showGrid?: boolean;
121
+ showPresentationButton?: boolean;
122
+ showCodeSnippet?: boolean;
121
123
  addAdvancedPaletteControl?: ResolvedAdvancedPaletteConfig;
122
124
  };
123
125
  type UseControlsConfig = Omit<ControlsConfig, "addAdvancedPaletteControl"> & {
package/dist/index.d.ts CHANGED
@@ -118,6 +118,8 @@ type ControlsConfig = {
118
118
  showCopyButtonFn?: (args: CopyButtonFnArgs) => string | null | undefined;
119
119
  mainLabel?: string;
120
120
  showGrid?: boolean;
121
+ showPresentationButton?: boolean;
122
+ showCodeSnippet?: boolean;
121
123
  addAdvancedPaletteControl?: ResolvedAdvancedPaletteConfig;
122
124
  };
123
125
  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 = {
@@ -426,9 +448,22 @@ var ControlsProvider = ({ children }) => {
426
448
  const [schema, setSchema] = (0, import_react2.useState)({});
427
449
  const [values, setValues] = (0, import_react2.useState)({});
428
450
  const [config, setConfig] = (0, import_react2.useState)({
429
- showCopyButton: true
451
+ showCopyButton: true,
452
+ showCodeSnippet: false
430
453
  });
431
454
  const [componentName, setComponentName] = (0, import_react2.useState)();
455
+ const [channelName, setChannelName] = (0, import_react2.useState)(null);
456
+ const channelRef = (0, import_react2.useRef)(null);
457
+ const instanceIdRef = (0, import_react2.useRef)(null);
458
+ const skipBroadcastRef = (0, import_react2.useRef)(false);
459
+ const latestValuesRef = (0, import_react2.useRef)(values);
460
+ (0, import_react2.useEffect)(() => {
461
+ latestValuesRef.current = values;
462
+ }, [values]);
463
+ (0, import_react2.useEffect)(() => {
464
+ if (typeof window === "undefined") return;
465
+ setChannelName(getControlsChannelName());
466
+ }, []);
432
467
  const setValue = (key, value) => {
433
468
  setValues((prev) => ({ ...prev, [key]: value }));
434
469
  };
@@ -463,6 +498,66 @@ var ControlsProvider = ({ children }) => {
463
498
  return updated;
464
499
  });
465
500
  };
501
+ (0, import_react2.useEffect)(() => {
502
+ if (!channelName) return;
503
+ if (typeof window === "undefined") return;
504
+ if (typeof window.BroadcastChannel === "undefined") return;
505
+ const instanceId = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : Math.random().toString(36).slice(2);
506
+ instanceIdRef.current = instanceId;
507
+ const channel = new BroadcastChannel(channelName);
508
+ channelRef.current = channel;
509
+ const sendValues = () => {
510
+ if (!instanceIdRef.current) return;
511
+ channel.postMessage({
512
+ type: "controls-sync-values",
513
+ source: instanceIdRef.current,
514
+ values: latestValuesRef.current
515
+ });
516
+ };
517
+ const handleMessage = (event) => {
518
+ const data = event.data;
519
+ if (!data || data.source === instanceIdRef.current) return;
520
+ if (data.type === "controls-sync-request") {
521
+ sendValues();
522
+ return;
523
+ }
524
+ if (data.type === "controls-sync-values" && data.values) {
525
+ const incoming = data.values;
526
+ setValues((prev) => {
527
+ const prevKeys = Object.keys(prev);
528
+ const incomingKeys = Object.keys(incoming);
529
+ const sameLength = prevKeys.length === incomingKeys.length;
530
+ const sameValues = sameLength && incomingKeys.every((key) => prev[key] === incoming[key]);
531
+ if (sameValues) return prev;
532
+ skipBroadcastRef.current = true;
533
+ return { ...incoming };
534
+ });
535
+ }
536
+ };
537
+ channel.addEventListener("message", handleMessage);
538
+ channel.postMessage({
539
+ type: "controls-sync-request",
540
+ source: instanceId
541
+ });
542
+ return () => {
543
+ channel.removeEventListener("message", handleMessage);
544
+ channel.close();
545
+ channelRef.current = null;
546
+ instanceIdRef.current = null;
547
+ };
548
+ }, [channelName]);
549
+ (0, import_react2.useEffect)(() => {
550
+ if (!channelRef.current || !instanceIdRef.current) return;
551
+ if (skipBroadcastRef.current) {
552
+ skipBroadcastRef.current = false;
553
+ return;
554
+ }
555
+ channelRef.current.postMessage({
556
+ type: "controls-sync-values",
557
+ source: instanceIdRef.current,
558
+ values
559
+ });
560
+ }, [values]);
466
561
  const contextValue = (0, import_react2.useMemo)(
467
562
  () => ({
468
563
  schema,
@@ -582,7 +677,7 @@ var usePreviewUrl = (values, basePath = "") => {
582
677
  (0, import_react3.useEffect)(() => {
583
678
  if (typeof window === "undefined") return;
584
679
  const params = new URLSearchParams();
585
- params.set("nocontrols", "true");
680
+ params.set(NO_CONTROLS_PARAM, "true");
586
681
  for (const [key, value] of Object.entries(values)) {
587
682
  if (value !== void 0 && value !== null) {
588
683
  params.set(key, value.toString());
@@ -998,12 +1093,280 @@ var AdvancedPaletteControl_default = AdvancedPaletteControl;
998
1093
 
999
1094
  // src/components/ControlPanel/ControlPanel.tsx
1000
1095
  var import_jsx_runtime10 = require("react/jsx-runtime");
1096
+ var splitPropsString = (input) => {
1097
+ const props = [];
1098
+ let current = "";
1099
+ let curlyDepth = 0;
1100
+ let squareDepth = 0;
1101
+ let parenDepth = 0;
1102
+ let inSingleQuote = false;
1103
+ let inDoubleQuote = false;
1104
+ let inBacktick = false;
1105
+ let escapeNext = false;
1106
+ for (const char of input) {
1107
+ if (escapeNext) {
1108
+ current += char;
1109
+ escapeNext = false;
1110
+ continue;
1111
+ }
1112
+ if (char === "\\") {
1113
+ current += char;
1114
+ escapeNext = true;
1115
+ continue;
1116
+ }
1117
+ if (char === "'" && !inDoubleQuote && !inBacktick) {
1118
+ inSingleQuote = !inSingleQuote;
1119
+ current += char;
1120
+ continue;
1121
+ }
1122
+ if (char === '"' && !inSingleQuote && !inBacktick) {
1123
+ inDoubleQuote = !inDoubleQuote;
1124
+ current += char;
1125
+ continue;
1126
+ }
1127
+ if (char === "`" && !inSingleQuote && !inDoubleQuote) {
1128
+ inBacktick = !inBacktick;
1129
+ current += char;
1130
+ continue;
1131
+ }
1132
+ if (!inSingleQuote && !inDoubleQuote && !inBacktick) {
1133
+ if (char === "{") {
1134
+ curlyDepth += 1;
1135
+ } else if (char === "}") {
1136
+ curlyDepth = Math.max(0, curlyDepth - 1);
1137
+ } else if (char === "[") {
1138
+ squareDepth += 1;
1139
+ } else if (char === "]") {
1140
+ squareDepth = Math.max(0, squareDepth - 1);
1141
+ } else if (char === "(") {
1142
+ parenDepth += 1;
1143
+ } else if (char === ")") {
1144
+ parenDepth = Math.max(0, parenDepth - 1);
1145
+ }
1146
+ }
1147
+ const atTopLevel = !inSingleQuote && !inDoubleQuote && !inBacktick && curlyDepth === 0 && squareDepth === 0 && parenDepth === 0;
1148
+ if (atTopLevel && /\s/.test(char)) {
1149
+ if (current.trim()) {
1150
+ props.push(current.trim());
1151
+ }
1152
+ current = "";
1153
+ continue;
1154
+ }
1155
+ current += char;
1156
+ }
1157
+ if (current.trim()) {
1158
+ props.push(current.trim());
1159
+ }
1160
+ return props;
1161
+ };
1162
+ var formatJsxCodeSnippet = (input) => {
1163
+ const trimmed = input.trim();
1164
+ if (!trimmed) return "";
1165
+ if (trimmed.includes("\n")) {
1166
+ return trimmed;
1167
+ }
1168
+ if (!trimmed.startsWith("<") || !trimmed.endsWith(">")) {
1169
+ return trimmed;
1170
+ }
1171
+ if (!trimmed.endsWith("/>")) {
1172
+ return trimmed;
1173
+ }
1174
+ const inner = trimmed.slice(1, -2).trim();
1175
+ const firstSpaceIndex = inner.indexOf(" ");
1176
+ if (firstSpaceIndex === -1) {
1177
+ return `<${inner} />`;
1178
+ }
1179
+ const componentName = inner.slice(0, firstSpaceIndex);
1180
+ const propsString = inner.slice(firstSpaceIndex + 1).trim();
1181
+ if (!propsString) {
1182
+ return `<${componentName} />`;
1183
+ }
1184
+ const propsList = splitPropsString(propsString);
1185
+ if (propsList.length === 0) {
1186
+ return `<${componentName} ${propsString} />`;
1187
+ }
1188
+ const formattedProps = propsList.map((prop) => ` ${prop}`).join("\n");
1189
+ return `<${componentName}
1190
+ ${formattedProps}
1191
+ />`;
1192
+ };
1193
+ var isWhitespace = (char) => /\s/.test(char);
1194
+ var isAttrNameChar = (char) => /[A-Za-z0-9_$\-.:]/.test(char);
1195
+ var isAlphaStart = (char) => /[A-Za-z_$]/.test(char);
1196
+ var tokenizeJsx = (input) => {
1197
+ const tokens = [];
1198
+ let i = 0;
1199
+ while (i < input.length) {
1200
+ const char = input[i];
1201
+ if (char === "<") {
1202
+ tokens.push({ type: "punctuation", value: "<" });
1203
+ i += 1;
1204
+ if (input[i] === "/") {
1205
+ tokens.push({ type: "punctuation", value: "/" });
1206
+ i += 1;
1207
+ }
1208
+ const start = i;
1209
+ while (i < input.length && isAttrNameChar(input[i])) {
1210
+ i += 1;
1211
+ }
1212
+ if (i > start) {
1213
+ tokens.push({ type: "tag", value: input.slice(start, i) });
1214
+ }
1215
+ continue;
1216
+ }
1217
+ if (char === "/" && input[i + 1] === ">") {
1218
+ tokens.push({ type: "punctuation", value: "/>" });
1219
+ i += 2;
1220
+ continue;
1221
+ }
1222
+ if (char === ">") {
1223
+ tokens.push({ type: "punctuation", value: ">" });
1224
+ i += 1;
1225
+ continue;
1226
+ }
1227
+ if (char === "=") {
1228
+ tokens.push({ type: "punctuation", value: "=" });
1229
+ i += 1;
1230
+ continue;
1231
+ }
1232
+ if (char === '"' || char === "'" || char === "`") {
1233
+ const quote = char;
1234
+ let j = i + 1;
1235
+ let value = quote;
1236
+ while (j < input.length) {
1237
+ const current = input[j];
1238
+ value += current;
1239
+ if (current === quote && input[j - 1] !== "\\") {
1240
+ break;
1241
+ }
1242
+ j += 1;
1243
+ }
1244
+ tokens.push({ type: "string", value });
1245
+ i = j + 1;
1246
+ continue;
1247
+ }
1248
+ if (char === "{") {
1249
+ let depth = 1;
1250
+ let j = i + 1;
1251
+ while (j < input.length && depth > 0) {
1252
+ if (input[j] === "{") {
1253
+ depth += 1;
1254
+ } else if (input[j] === "}") {
1255
+ depth -= 1;
1256
+ }
1257
+ j += 1;
1258
+ }
1259
+ const expression = input.slice(i, j);
1260
+ tokens.push({ type: "expression", value: expression });
1261
+ i = j;
1262
+ continue;
1263
+ }
1264
+ if (isAlphaStart(char)) {
1265
+ const start = i;
1266
+ i += 1;
1267
+ while (i < input.length && isAttrNameChar(input[i])) {
1268
+ i += 1;
1269
+ }
1270
+ const word = input.slice(start, i);
1271
+ let k = i;
1272
+ while (k < input.length && isWhitespace(input[k])) {
1273
+ k += 1;
1274
+ }
1275
+ if (input[k] === "=") {
1276
+ tokens.push({ type: "attrName", value: word });
1277
+ } else {
1278
+ tokens.push({ type: "plain", value: word });
1279
+ }
1280
+ continue;
1281
+ }
1282
+ tokens.push({ type: "plain", value: char });
1283
+ i += 1;
1284
+ }
1285
+ return tokens;
1286
+ };
1287
+ var TOKEN_CLASS_MAP = {
1288
+ tag: "text-sky-300",
1289
+ attrName: "text-amber-200",
1290
+ string: "text-emerald-300",
1291
+ expression: "text-purple-300",
1292
+ punctuation: "text-stone-400"
1293
+ };
1294
+ var highlightJsx = (input) => {
1295
+ const tokens = tokenizeJsx(input);
1296
+ const nodes = [];
1297
+ tokens.forEach((token, index) => {
1298
+ if (token.type === "plain") {
1299
+ nodes.push(token.value);
1300
+ } else {
1301
+ nodes.push(
1302
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: TOKEN_CLASS_MAP[token.type], children: token.value }, `token-${index}`)
1303
+ );
1304
+ }
1305
+ });
1306
+ return nodes;
1307
+ };
1001
1308
  var ControlPanel = () => {
1002
1309
  const [copied, setCopied] = (0, import_react5.useState)(false);
1310
+ const [codeCopied, setCodeCopied] = (0, import_react5.useState)(false);
1311
+ const [isCodeVisible, setIsCodeVisible] = (0, import_react5.useState)(false);
1003
1312
  const [folderStates, setFolderStates] = (0, import_react5.useState)({});
1313
+ const codeCopyTimeoutRef = (0, import_react5.useRef)(null);
1004
1314
  const { leftPanelWidth, isDesktop, isHydrated } = useResizableLayout();
1005
1315
  const { schema, setValue, values, componentName, config } = useControlsContext();
1316
+ const isControlsOnlyView = typeof window !== "undefined" && new URLSearchParams(window.location.search).get(CONTROLS_ONLY_PARAM) === "true";
1006
1317
  const previewUrl = usePreviewUrl(values);
1318
+ const buildUrl = (0, import_react5.useCallback)(
1319
+ (modifier) => {
1320
+ if (!previewUrl) return "";
1321
+ const [path, search = ""] = previewUrl.split("?");
1322
+ const params = new URLSearchParams(search);
1323
+ modifier(params);
1324
+ const query = params.toString();
1325
+ return query ? `${path}?${query}` : path;
1326
+ },
1327
+ [previewUrl]
1328
+ );
1329
+ const presentationUrl = (0, import_react5.useMemo)(() => {
1330
+ if (!previewUrl) return "";
1331
+ return buildUrl((params) => {
1332
+ params.set(PRESENTATION_PARAM, "true");
1333
+ });
1334
+ }, [buildUrl, previewUrl]);
1335
+ const controlsOnlyUrl = (0, import_react5.useMemo)(() => {
1336
+ if (!previewUrl) return "";
1337
+ return buildUrl((params) => {
1338
+ params.delete(NO_CONTROLS_PARAM);
1339
+ params.delete(PRESENTATION_PARAM);
1340
+ params.set(CONTROLS_ONLY_PARAM, "true");
1341
+ });
1342
+ }, [buildUrl, previewUrl]);
1343
+ const handlePresentationClick = (0, import_react5.useCallback)(() => {
1344
+ if (typeof window === "undefined" || !presentationUrl) return;
1345
+ window.open(presentationUrl, "_blank", "noopener,noreferrer");
1346
+ if (controlsOnlyUrl) {
1347
+ const viewportWidth = window.innerWidth || 1200;
1348
+ const viewportHeight = window.innerHeight || 900;
1349
+ const controlsWidth = Math.max(
1350
+ 320,
1351
+ Math.min(
1352
+ 600,
1353
+ Math.round(viewportWidth * leftPanelWidth / 100)
1354
+ )
1355
+ );
1356
+ const controlsHeight = Math.max(600, viewportHeight);
1357
+ const controlsFeatures = [
1358
+ "noopener",
1359
+ "noreferrer",
1360
+ "toolbar=0",
1361
+ "menubar=0",
1362
+ "resizable=yes",
1363
+ "scrollbars=yes",
1364
+ `width=${controlsWidth}`,
1365
+ `height=${controlsHeight}`
1366
+ ].join(",");
1367
+ window.open(controlsOnlyUrl, "v0-controls", controlsFeatures);
1368
+ }
1369
+ }, [controlsOnlyUrl, leftPanelWidth, presentationUrl]);
1007
1370
  const jsx14 = (0, import_react5.useMemo)(() => {
1008
1371
  if (!componentName) return "";
1009
1372
  const props = Object.entries(values).map(([key, val]) => {
@@ -1124,6 +1487,65 @@ var ControlPanel = () => {
1124
1487
  jsonToComponentString
1125
1488
  }) ?? jsx14;
1126
1489
  const shouldShowCopyButton = config?.showCopyButton !== false && Boolean(copyText);
1490
+ const baseSnippet = copyText || jsx14;
1491
+ const formattedCode = (0, import_react5.useMemo)(
1492
+ () => formatJsxCodeSnippet(baseSnippet),
1493
+ [baseSnippet]
1494
+ );
1495
+ const hasCodeSnippet = Boolean(config?.showCodeSnippet && formattedCode);
1496
+ const highlightedCode = (0, import_react5.useMemo)(
1497
+ () => formattedCode ? highlightJsx(formattedCode) : null,
1498
+ [formattedCode]
1499
+ );
1500
+ (0, import_react5.useEffect)(() => {
1501
+ if (!hasCodeSnippet) {
1502
+ setIsCodeVisible(false);
1503
+ }
1504
+ }, [hasCodeSnippet]);
1505
+ (0, import_react5.useEffect)(() => {
1506
+ setCodeCopied(false);
1507
+ if (codeCopyTimeoutRef.current) {
1508
+ clearTimeout(codeCopyTimeoutRef.current);
1509
+ codeCopyTimeoutRef.current = null;
1510
+ }
1511
+ }, [formattedCode]);
1512
+ (0, import_react5.useEffect)(() => {
1513
+ return () => {
1514
+ if (codeCopyTimeoutRef.current) {
1515
+ clearTimeout(codeCopyTimeoutRef.current);
1516
+ }
1517
+ };
1518
+ }, []);
1519
+ const handleToggleCodeVisibility = (0, import_react5.useCallback)(() => {
1520
+ setIsCodeVisible((prev) => {
1521
+ const next = !prev;
1522
+ if (!next) {
1523
+ setCodeCopied(false);
1524
+ if (codeCopyTimeoutRef.current) {
1525
+ clearTimeout(codeCopyTimeoutRef.current);
1526
+ codeCopyTimeoutRef.current = null;
1527
+ }
1528
+ }
1529
+ return next;
1530
+ });
1531
+ }, []);
1532
+ const handleCodeCopy = (0, import_react5.useCallback)(() => {
1533
+ if (!formattedCode) return;
1534
+ if (typeof navigator === "undefined" || !navigator.clipboard || typeof navigator.clipboard.writeText !== "function") {
1535
+ return;
1536
+ }
1537
+ navigator.clipboard.writeText(formattedCode).then(() => {
1538
+ setCodeCopied(true);
1539
+ if (codeCopyTimeoutRef.current) {
1540
+ clearTimeout(codeCopyTimeoutRef.current);
1541
+ }
1542
+ codeCopyTimeoutRef.current = setTimeout(() => {
1543
+ setCodeCopied(false);
1544
+ codeCopyTimeoutRef.current = null;
1545
+ }, 3e3);
1546
+ }).catch(() => {
1547
+ });
1548
+ }, [formattedCode]);
1127
1549
  const labelize = (key) => key.replace(/([A-Z])/g, " $1").replace(/[\-_]/g, " ").replace(/\s+/g, " ").trim().replace(/(^|\s)\S/g, (s) => s.toUpperCase());
1128
1550
  const renderButtonControl = (key, control, variant) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1129
1551
  "div",
@@ -1290,7 +1712,7 @@ var ControlPanel = () => {
1290
1712
  height: "auto",
1291
1713
  flex: "0 0 auto"
1292
1714
  };
1293
- if (isHydrated) {
1715
+ if (isHydrated && !isControlsOnlyView) {
1294
1716
  if (isDesktop) {
1295
1717
  Object.assign(panelStyle, {
1296
1718
  position: "absolute",
@@ -1326,12 +1748,51 @@ var ControlPanel = () => {
1326
1748
  ([key, control]) => renderControl(key, control, "root")
1327
1749
  ),
1328
1750
  bottomFolderSections,
1751
+ hasCodeSnippet && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "border border-stone-700/60 rounded-lg bg-stone-900/70", children: [
1752
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1753
+ "button",
1754
+ {
1755
+ type: "button",
1756
+ onClick: handleToggleCodeVisibility,
1757
+ className: "w-full flex items-center justify-between px-4 py-3 text-left font-semibold text-stone-200 tracking-wide",
1758
+ "aria-expanded": isCodeVisible,
1759
+ children: [
1760
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: isCodeVisible ? "Hide Code" : "Show Code" }),
1761
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1762
+ import_lucide_react3.ChevronDown,
1763
+ {
1764
+ className: `w-4 h-4 transition-transform duration-200 ${isCodeVisible ? "rotate-180" : ""}`
1765
+ }
1766
+ )
1767
+ ]
1768
+ }
1769
+ ),
1770
+ isCodeVisible && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "relative border-t border-stone-700/60 bg-stone-950/60 px-4 py-4 rounded-b-lg", children: [
1771
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1772
+ "button",
1773
+ {
1774
+ type: "button",
1775
+ onClick: handleCodeCopy,
1776
+ 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",
1777
+ children: codeCopied ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
1778
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react3.Check, { className: "h-3.5 w-3.5" }),
1779
+ "Copied"
1780
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
1781
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react3.Copy, { className: "h-3.5 w-3.5" }),
1782
+ "Copy"
1783
+ ] })
1784
+ }
1785
+ ),
1786
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("pre", { className: "whitespace-pre overflow-x-auto text-xs md:text-sm text-stone-200 pr-14", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { className: "block text-stone-200", children: highlightedCode ?? formattedCode }) })
1787
+ ] })
1788
+ ] }),
1329
1789
  shouldShowCopyButton && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex-1 pt-4", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1330
1790
  "button",
1331
1791
  {
1332
1792
  onClick: () => {
1333
- if (!copyText) return;
1334
- navigator.clipboard.writeText(copyText);
1793
+ const copyPayload = formattedCode || baseSnippet;
1794
+ if (!copyPayload) return;
1795
+ navigator.clipboard.writeText(copyPayload);
1335
1796
  setCopied(true);
1336
1797
  setTimeout(() => setCopied(false), 5e3);
1337
1798
  },
@@ -1346,19 +1807,34 @@ var ControlPanel = () => {
1346
1807
  }
1347
1808
  ) }, "control-panel-jsx")
1348
1809
  ] }),
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
- ) })
1810
+ previewUrl && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex flex-col gap-2", children: [
1811
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Button, { asChild: true, className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1812
+ "a",
1813
+ {
1814
+ href: previewUrl,
1815
+ target: "_blank",
1816
+ rel: "noopener noreferrer",
1817
+ 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",
1818
+ children: [
1819
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react3.SquareArrowOutUpRight, {}),
1820
+ " Open in a New Tab"
1821
+ ]
1822
+ }
1823
+ ) }),
1824
+ config?.showPresentationButton && presentationUrl && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1825
+ Button,
1826
+ {
1827
+ type: "button",
1828
+ onClick: handlePresentationClick,
1829
+ variant: "secondary",
1830
+ className: "w-full bg-stone-800 text-white hover:bg-stone-700 border border-stone-700",
1831
+ children: [
1832
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_lucide_react3.Presentation, {}),
1833
+ " Presentation Mode"
1834
+ ]
1835
+ }
1836
+ )
1837
+ ] })
1362
1838
  ] })
1363
1839
  }
1364
1840
  );
@@ -1414,25 +1890,42 @@ var PreviewContainer_default = PreviewContainer;
1414
1890
 
1415
1891
  // src/components/Playground/Playground.tsx
1416
1892
  var import_jsx_runtime13 = require("react/jsx-runtime");
1417
- var NO_CONTROLS_PARAM = "nocontrols";
1893
+ var HiddenPreview = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { "aria-hidden": "true", className: "hidden", children });
1418
1894
  function Playground({ children }) {
1419
1895
  const [isHydrated, setIsHydrated] = (0, import_react7.useState)(false);
1420
1896
  const [copied, setCopied] = (0, import_react7.useState)(false);
1421
1897
  (0, import_react7.useEffect)(() => {
1422
1898
  setIsHydrated(true);
1423
1899
  }, []);
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";
1900
+ const { showControls, isPresentationMode, isControlsOnly } = (0, import_react7.useMemo)(() => {
1901
+ if (typeof window === "undefined") {
1902
+ return {
1903
+ showControls: true,
1904
+ isPresentationMode: false,
1905
+ isControlsOnly: false
1906
+ };
1907
+ }
1908
+ const params = new URLSearchParams(window.location.search);
1909
+ const presentation = params.get(PRESENTATION_PARAM) === "true";
1910
+ const controlsOnly = params.get(CONTROLS_ONLY_PARAM) === "true";
1911
+ const noControlsParam = params.get(NO_CONTROLS_PARAM) === "true";
1912
+ const showControlsValue = controlsOnly || !presentation && !noControlsParam;
1913
+ return {
1914
+ showControls: showControlsValue,
1915
+ isPresentationMode: presentation,
1916
+ isControlsOnly: controlsOnly
1917
+ };
1427
1918
  }, []);
1919
+ const shouldShowShareButton = !showControls && !isPresentationMode;
1920
+ const layoutHideControls = !showControls || isControlsOnly;
1428
1921
  const handleCopy = () => {
1429
1922
  navigator.clipboard.writeText(window.location.href);
1430
1923
  setCopied(true);
1431
1924
  setTimeout(() => setCopied(false), 2e3);
1432
1925
  };
1433
1926
  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)(
1927
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ResizableLayout, { hideControls: layoutHideControls, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(ControlsProvider, { children: [
1928
+ shouldShowShareButton && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1436
1929
  "button",
1437
1930
  {
1438
1931
  onClick: handleCopy,
@@ -1443,8 +1936,8 @@ function Playground({ children }) {
1443
1936
  ]
1444
1937
  }
1445
1938
  ),
1446
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PreviewContainer_default, { hideControls, children }),
1447
- !hideControls && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ControlPanel_default, {})
1939
+ isControlsOnly ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(HiddenPreview, { children }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PreviewContainer_default, { hideControls: layoutHideControls, children }),
1940
+ showControls && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ControlPanel_default, {})
1448
1941
  ] }) });
1449
1942
  }
1450
1943