@toriistudio/v0-playground 0.6.0 → 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.d.mts CHANGED
@@ -119,6 +119,7 @@ type ControlsConfig = {
119
119
  mainLabel?: string;
120
120
  showGrid?: boolean;
121
121
  showPresentationButton?: boolean;
122
+ showCodeSnippet?: boolean;
122
123
  addAdvancedPaletteControl?: ResolvedAdvancedPaletteConfig;
123
124
  };
124
125
  type UseControlsConfig = Omit<ControlsConfig, "addAdvancedPaletteControl"> & {
package/dist/index.d.ts CHANGED
@@ -119,6 +119,7 @@ type ControlsConfig = {
119
119
  mainLabel?: string;
120
120
  showGrid?: boolean;
121
121
  showPresentationButton?: boolean;
122
+ showCodeSnippet?: boolean;
122
123
  addAdvancedPaletteControl?: ResolvedAdvancedPaletteConfig;
123
124
  };
124
125
  type UseControlsConfig = Omit<ControlsConfig, "addAdvancedPaletteControl"> & {
package/dist/index.js CHANGED
@@ -448,7 +448,8 @@ var ControlsProvider = ({ children }) => {
448
448
  const [schema, setSchema] = (0, import_react2.useState)({});
449
449
  const [values, setValues] = (0, import_react2.useState)({});
450
450
  const [config, setConfig] = (0, import_react2.useState)({
451
- showCopyButton: true
451
+ showCopyButton: true,
452
+ showCodeSnippet: false
452
453
  });
453
454
  const [componentName, setComponentName] = (0, import_react2.useState)();
454
455
  const [channelName, setChannelName] = (0, import_react2.useState)(null);
@@ -1092,11 +1093,227 @@ var AdvancedPaletteControl_default = AdvancedPaletteControl;
1092
1093
 
1093
1094
  // src/components/ControlPanel/ControlPanel.tsx
1094
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
+ };
1095
1308
  var ControlPanel = () => {
1096
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);
1097
1312
  const [folderStates, setFolderStates] = (0, import_react5.useState)({});
1313
+ const codeCopyTimeoutRef = (0, import_react5.useRef)(null);
1098
1314
  const { leftPanelWidth, isDesktop, isHydrated } = useResizableLayout();
1099
1315
  const { schema, setValue, values, componentName, config } = useControlsContext();
1316
+ const isControlsOnlyView = typeof window !== "undefined" && new URLSearchParams(window.location.search).get(CONTROLS_ONLY_PARAM) === "true";
1100
1317
  const previewUrl = usePreviewUrl(values);
1101
1318
  const buildUrl = (0, import_react5.useCallback)(
1102
1319
  (modifier) => {
@@ -1270,6 +1487,65 @@ var ControlPanel = () => {
1270
1487
  jsonToComponentString
1271
1488
  }) ?? jsx14;
1272
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]);
1273
1549
  const labelize = (key) => key.replace(/([A-Z])/g, " $1").replace(/[\-_]/g, " ").replace(/\s+/g, " ").trim().replace(/(^|\s)\S/g, (s) => s.toUpperCase());
1274
1550
  const renderButtonControl = (key, control, variant) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1275
1551
  "div",
@@ -1436,7 +1712,7 @@ var ControlPanel = () => {
1436
1712
  height: "auto",
1437
1713
  flex: "0 0 auto"
1438
1714
  };
1439
- if (isHydrated) {
1715
+ if (isHydrated && !isControlsOnlyView) {
1440
1716
  if (isDesktop) {
1441
1717
  Object.assign(panelStyle, {
1442
1718
  position: "absolute",
@@ -1472,12 +1748,51 @@ var ControlPanel = () => {
1472
1748
  ([key, control]) => renderControl(key, control, "root")
1473
1749
  ),
1474
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
+ ] }),
1475
1789
  shouldShowCopyButton && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex-1 pt-4", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1476
1790
  "button",
1477
1791
  {
1478
1792
  onClick: () => {
1479
- if (!copyText) return;
1480
- navigator.clipboard.writeText(copyText);
1793
+ const copyPayload = formattedCode || baseSnippet;
1794
+ if (!copyPayload) return;
1795
+ navigator.clipboard.writeText(copyPayload);
1481
1796
  setCopied(true);
1482
1797
  setTimeout(() => setCopied(false), 5e3);
1483
1798
  },
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
@@ -411,7 +411,8 @@ var ControlsProvider = ({ children }) => {
411
411
  const [schema, setSchema] = useState2({});
412
412
  const [values, setValues] = useState2({});
413
413
  const [config, setConfig] = useState2({
414
- showCopyButton: true
414
+ showCopyButton: true,
415
+ showCodeSnippet: false
415
416
  });
416
417
  const [componentName, setComponentName] = useState2();
417
418
  const [channelName, setChannelName] = useState2(null);
@@ -629,7 +630,7 @@ var useControls = (schema, options) => {
629
630
  var useUrlSyncedControls = useControls;
630
631
 
631
632
  // src/components/ControlPanel/ControlPanel.tsx
632
- import { useState as useState4, useMemo as useMemo3, useCallback as useCallback3 } from "react";
633
+ import { useState as useState4, useMemo as useMemo3, useCallback as useCallback3, useEffect as useEffect5, useRef as useRef4 } from "react";
633
634
  import {
634
635
  Check as Check2,
635
636
  Copy,
@@ -1066,11 +1067,227 @@ var AdvancedPaletteControl_default = AdvancedPaletteControl;
1066
1067
 
1067
1068
  // src/components/ControlPanel/ControlPanel.tsx
1068
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
+ };
1069
1282
  var ControlPanel = () => {
1070
1283
  const [copied, setCopied] = useState4(false);
1284
+ const [codeCopied, setCodeCopied] = useState4(false);
1285
+ const [isCodeVisible, setIsCodeVisible] = useState4(false);
1071
1286
  const [folderStates, setFolderStates] = useState4({});
1287
+ const codeCopyTimeoutRef = useRef4(null);
1072
1288
  const { leftPanelWidth, isDesktop, isHydrated } = useResizableLayout();
1073
1289
  const { schema, setValue, values, componentName, config } = useControlsContext();
1290
+ const isControlsOnlyView = typeof window !== "undefined" && new URLSearchParams(window.location.search).get(CONTROLS_ONLY_PARAM) === "true";
1074
1291
  const previewUrl = usePreviewUrl(values);
1075
1292
  const buildUrl = useCallback3(
1076
1293
  (modifier) => {
@@ -1244,6 +1461,65 @@ var ControlPanel = () => {
1244
1461
  jsonToComponentString
1245
1462
  }) ?? jsx14;
1246
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]);
1247
1523
  const labelize = (key) => key.replace(/([A-Z])/g, " $1").replace(/[\-_]/g, " ").replace(/\s+/g, " ").trim().replace(/(^|\s)\S/g, (s) => s.toUpperCase());
1248
1524
  const renderButtonControl = (key, control, variant) => /* @__PURE__ */ jsx10(
1249
1525
  "div",
@@ -1410,7 +1686,7 @@ var ControlPanel = () => {
1410
1686
  height: "auto",
1411
1687
  flex: "0 0 auto"
1412
1688
  };
1413
- if (isHydrated) {
1689
+ if (isHydrated && !isControlsOnlyView) {
1414
1690
  if (isDesktop) {
1415
1691
  Object.assign(panelStyle, {
1416
1692
  position: "absolute",
@@ -1446,12 +1722,51 @@ var ControlPanel = () => {
1446
1722
  ([key, control]) => renderControl(key, control, "root")
1447
1723
  ),
1448
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
+ ] }),
1449
1763
  shouldShowCopyButton && /* @__PURE__ */ jsx10("div", { className: "flex-1 pt-4", children: /* @__PURE__ */ jsx10(
1450
1764
  "button",
1451
1765
  {
1452
1766
  onClick: () => {
1453
- if (!copyText) return;
1454
- navigator.clipboard.writeText(copyText);
1767
+ const copyPayload = formattedCode || baseSnippet;
1768
+ if (!copyPayload) return;
1769
+ navigator.clipboard.writeText(copyPayload);
1455
1770
  setCopied(true);
1456
1771
  setTimeout(() => setCopied(false), 5e3);
1457
1772
  },
@@ -1501,7 +1816,7 @@ var ControlPanel = () => {
1501
1816
  var ControlPanel_default = ControlPanel;
1502
1817
 
1503
1818
  // src/components/PreviewContainer/PreviewContainer.tsx
1504
- import { useRef as useRef4 } from "react";
1819
+ import { useRef as useRef5 } from "react";
1505
1820
 
1506
1821
  // src/components/Grid/Grid.tsx
1507
1822
  import { jsx as jsx11 } from "react/jsx-runtime";
@@ -1528,7 +1843,7 @@ import { jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
1528
1843
  var PreviewContainer = ({ children, hideControls }) => {
1529
1844
  const { config } = useControlsContext();
1530
1845
  const { leftPanelWidth, isDesktop, isHydrated, containerRef } = useResizableLayout();
1531
- const previewRef = useRef4(null);
1846
+ const previewRef = useRef5(null);
1532
1847
  return /* @__PURE__ */ jsx12(
1533
1848
  "div",
1534
1849
  {
@@ -1553,7 +1868,7 @@ var HiddenPreview = ({ children }) => /* @__PURE__ */ jsx13("div", { "aria-hidde
1553
1868
  function Playground({ children }) {
1554
1869
  const [isHydrated, setIsHydrated] = useState5(false);
1555
1870
  const [copied, setCopied] = useState5(false);
1556
- useEffect5(() => {
1871
+ useEffect6(() => {
1557
1872
  setIsHydrated(true);
1558
1873
  }, []);
1559
1874
  const { showControls, isPresentationMode, isControlsOnly } = useMemo4(() => {
@@ -1601,7 +1916,7 @@ function Playground({ children }) {
1601
1916
  }
1602
1917
 
1603
1918
  // src/hooks/useAdvancedPaletteControls.ts
1604
- 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";
1605
1920
  var cloneForCallbacks = (palette) => clonePalette(palette);
1606
1921
  var useAdvancedPaletteControls = (options = {}) => {
1607
1922
  const resolvedDefaultPalette = useMemo5(
@@ -1615,10 +1930,10 @@ var useAdvancedPaletteControls = (options = {}) => {
1615
1930
  const [palette, setPaletteState] = useState6(
1616
1931
  () => clonePalette(resolvedDefaultPalette)
1617
1932
  );
1618
- const defaultSignatureRef = useRef5(
1933
+ const defaultSignatureRef = useRef6(
1619
1934
  createPaletteSignature(resolvedDefaultPalette)
1620
1935
  );
1621
- useEffect6(() => {
1936
+ useEffect7(() => {
1622
1937
  const nextSignature = createPaletteSignature(resolvedDefaultPalette);
1623
1938
  if (defaultSignatureRef.current === nextSignature) return;
1624
1939
  defaultSignatureRef.current = nextSignature;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toriistudio/v0-playground",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "V0 Playground",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",