@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 +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +519 -26
- package/dist/index.mjs +534 -35
- 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,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(
|
|
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
|
-
|
|
1334
|
-
|
|
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.
|
|
1350
|
-
"
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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
|
|
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
|
|
1425
|
-
if (typeof window === "undefined")
|
|
1426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|