@toriistudio/v0-playground 0.2.8 → 0.3.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 +20 -1
- package/dist/index.d.mts +36 -3
- package/dist/index.d.ts +36 -3
- package/dist/index.js +171 -34
- package/dist/index.mjs +167 -34
- package/package.json +13 -2
- package/dist/preset.d.mts +0 -5
- package/dist/preset.d.ts +0 -5
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 @react-three/drei @react-three/fiber three lodash
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
Or automate it with:
|
|
@@ -83,6 +83,25 @@ export default function App() {
|
|
|
83
83
|
}
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
### R3F Canvas
|
|
87
|
+
|
|
88
|
+
`PlaygroundCanvas` wraps the playground with a react-three-fiber canvas. Pass any
|
|
89
|
+
`Canvas` props through `mediaProps`:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { PlaygroundCanvas } from "@toriistudio/v0-playground";
|
|
93
|
+
|
|
94
|
+
export default function Scene() {
|
|
95
|
+
return (
|
|
96
|
+
<PlaygroundCanvas mediaProps={{ size: { width: 300, height: 300 } }}>
|
|
97
|
+
<MyR3FComponent />
|
|
98
|
+
</PlaygroundCanvas>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
See [`examples/r3f-canvas`](./examples/r3f-canvas) for a full working example.
|
|
104
|
+
|
|
86
105
|
## 💡 Example Use Cases
|
|
87
106
|
|
|
88
107
|
- Build custom component sandboxes
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import React__default, { ReactNode } from 'react';
|
|
4
|
+
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
5
|
+
import { VariantProps } from 'class-variance-authority';
|
|
3
6
|
|
|
4
7
|
declare function Playground({ children }: {
|
|
5
8
|
children: ReactNode;
|
|
6
9
|
}): react_jsx_runtime.JSX.Element | null;
|
|
7
10
|
|
|
11
|
+
type CanvasMediaProps = {
|
|
12
|
+
debugOrbit?: boolean;
|
|
13
|
+
size: {
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
} | null;
|
|
17
|
+
};
|
|
18
|
+
type CanvasProps = {
|
|
19
|
+
mediaProps?: CanvasMediaProps;
|
|
20
|
+
children: React__default.ReactNode;
|
|
21
|
+
};
|
|
22
|
+
declare const Canvas: React__default.FC<CanvasProps>;
|
|
23
|
+
|
|
24
|
+
type PlaygroundCanvasProps = {
|
|
25
|
+
children: React__default.ReactNode;
|
|
26
|
+
mediaProps?: CanvasMediaProps;
|
|
27
|
+
};
|
|
28
|
+
declare const PlaygroundCanvas: React__default.FC<PlaygroundCanvasProps>;
|
|
29
|
+
|
|
30
|
+
declare function CameraLogger(): react_jsx_runtime.JSX.Element;
|
|
31
|
+
|
|
32
|
+
declare const buttonVariants: (props?: ({
|
|
33
|
+
variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
|
|
34
|
+
size?: "default" | "sm" | "lg" | "icon" | null | undefined;
|
|
35
|
+
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
36
|
+
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
|
37
|
+
asChild?: boolean;
|
|
38
|
+
}
|
|
39
|
+
declare const Button: React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
40
|
+
|
|
8
41
|
type BaseControl = {
|
|
9
42
|
hidden?: boolean;
|
|
10
43
|
};
|
|
@@ -31,7 +64,7 @@ type ControlType = ({
|
|
|
31
64
|
type: "button";
|
|
32
65
|
onClick?: () => void;
|
|
33
66
|
label?: string;
|
|
34
|
-
render?: () =>
|
|
67
|
+
render?: () => React__default.ReactNode;
|
|
35
68
|
} & BaseControl);
|
|
36
69
|
type ControlsSchema = Record<string, ControlType>;
|
|
37
70
|
type ControlsConfig = {
|
|
@@ -64,4 +97,4 @@ declare const useUrlSyncedControls: <T extends ControlsSchema>(schema: T, option
|
|
|
64
97
|
jsx: () => string;
|
|
65
98
|
};
|
|
66
99
|
|
|
67
|
-
export { type ControlType, ControlsProvider, type ControlsSchema, Playground, useControls, useUrlSyncedControls };
|
|
100
|
+
export { Button, CameraLogger, Canvas, type ControlType, ControlsProvider, type ControlsSchema, Playground, PlaygroundCanvas, useControls, useUrlSyncedControls };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import React__default, { ReactNode } from 'react';
|
|
4
|
+
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
5
|
+
import { VariantProps } from 'class-variance-authority';
|
|
3
6
|
|
|
4
7
|
declare function Playground({ children }: {
|
|
5
8
|
children: ReactNode;
|
|
6
9
|
}): react_jsx_runtime.JSX.Element | null;
|
|
7
10
|
|
|
11
|
+
type CanvasMediaProps = {
|
|
12
|
+
debugOrbit?: boolean;
|
|
13
|
+
size: {
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
} | null;
|
|
17
|
+
};
|
|
18
|
+
type CanvasProps = {
|
|
19
|
+
mediaProps?: CanvasMediaProps;
|
|
20
|
+
children: React__default.ReactNode;
|
|
21
|
+
};
|
|
22
|
+
declare const Canvas: React__default.FC<CanvasProps>;
|
|
23
|
+
|
|
24
|
+
type PlaygroundCanvasProps = {
|
|
25
|
+
children: React__default.ReactNode;
|
|
26
|
+
mediaProps?: CanvasMediaProps;
|
|
27
|
+
};
|
|
28
|
+
declare const PlaygroundCanvas: React__default.FC<PlaygroundCanvasProps>;
|
|
29
|
+
|
|
30
|
+
declare function CameraLogger(): react_jsx_runtime.JSX.Element;
|
|
31
|
+
|
|
32
|
+
declare const buttonVariants: (props?: ({
|
|
33
|
+
variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
|
|
34
|
+
size?: "default" | "sm" | "lg" | "icon" | null | undefined;
|
|
35
|
+
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
36
|
+
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
|
37
|
+
asChild?: boolean;
|
|
38
|
+
}
|
|
39
|
+
declare const Button: React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
40
|
+
|
|
8
41
|
type BaseControl = {
|
|
9
42
|
hidden?: boolean;
|
|
10
43
|
};
|
|
@@ -31,7 +64,7 @@ type ControlType = ({
|
|
|
31
64
|
type: "button";
|
|
32
65
|
onClick?: () => void;
|
|
33
66
|
label?: string;
|
|
34
|
-
render?: () =>
|
|
67
|
+
render?: () => React__default.ReactNode;
|
|
35
68
|
} & BaseControl);
|
|
36
69
|
type ControlsSchema = Record<string, ControlType>;
|
|
37
70
|
type ControlsConfig = {
|
|
@@ -64,4 +97,4 @@ declare const useUrlSyncedControls: <T extends ControlsSchema>(schema: T, option
|
|
|
64
97
|
jsx: () => string;
|
|
65
98
|
};
|
|
66
99
|
|
|
67
|
-
export { type ControlType, ControlsProvider, type ControlsSchema, Playground, useControls, useUrlSyncedControls };
|
|
100
|
+
export { Button, CameraLogger, Canvas, type ControlType, ControlsProvider, type ControlsSchema, Playground, PlaygroundCanvas, useControls, useUrlSyncedControls };
|
package/dist/index.js
CHANGED
|
@@ -30,8 +30,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
+
Button: () => Button,
|
|
34
|
+
CameraLogger: () => CameraLogger,
|
|
35
|
+
Canvas: () => Canvas_default,
|
|
33
36
|
ControlsProvider: () => ControlsProvider,
|
|
34
37
|
Playground: () => Playground,
|
|
38
|
+
PlaygroundCanvas: () => PlaygroundCanvas_default,
|
|
35
39
|
useControls: () => useControls,
|
|
36
40
|
useUrlSyncedControls: () => useUrlSyncedControls
|
|
37
41
|
});
|
|
@@ -39,6 +43,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
39
43
|
|
|
40
44
|
// src/components/Playground/Playground.tsx
|
|
41
45
|
var import_react6 = require("react");
|
|
46
|
+
var import_lucide_react4 = require("lucide-react");
|
|
42
47
|
|
|
43
48
|
// src/context/ResizableLayout.tsx
|
|
44
49
|
var import_react = require("react");
|
|
@@ -213,35 +218,6 @@ var ControlsProvider = ({ children }) => {
|
|
|
213
218
|
var useControls = (schema, options) => {
|
|
214
219
|
const ctx = (0, import_react2.useContext)(ControlsContext);
|
|
215
220
|
if (!ctx) throw new Error("useControls must be used within ControlsProvider");
|
|
216
|
-
(0, import_react2.useEffect)(() => {
|
|
217
|
-
ctx.registerSchema(schema, options);
|
|
218
|
-
}, [JSON.stringify(schema), JSON.stringify(options)]);
|
|
219
|
-
(0, import_react2.useEffect)(() => {
|
|
220
|
-
for (const key in schema) {
|
|
221
|
-
if (!(key in ctx.values) && "value" in schema[key]) {
|
|
222
|
-
ctx.setValue(key, schema[key].value);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}, [JSON.stringify(schema), JSON.stringify(ctx.values)]);
|
|
226
|
-
const typedValues = ctx.values;
|
|
227
|
-
const jsx12 = (0, import_react2.useCallback)(() => {
|
|
228
|
-
if (!options?.componentName) return "";
|
|
229
|
-
const props = Object.entries(typedValues).map(([key, val]) => {
|
|
230
|
-
if (typeof val === "string") return `${key}="${val}"`;
|
|
231
|
-
if (typeof val === "boolean") return `${key}={${val}}`;
|
|
232
|
-
return `${key}={${JSON.stringify(val)}}`;
|
|
233
|
-
}).join(" ");
|
|
234
|
-
return `<${options.componentName} ${props} />`;
|
|
235
|
-
}, [options?.componentName, JSON.stringify(typedValues)]);
|
|
236
|
-
return {
|
|
237
|
-
...typedValues,
|
|
238
|
-
controls: ctx.values,
|
|
239
|
-
schema: ctx.schema,
|
|
240
|
-
setValue: ctx.setValue,
|
|
241
|
-
jsx: jsx12
|
|
242
|
-
};
|
|
243
|
-
};
|
|
244
|
-
var useUrlSyncedControls = (schema, options) => {
|
|
245
221
|
const urlParams = getUrlParams();
|
|
246
222
|
const mergedSchema = Object.fromEntries(
|
|
247
223
|
Object.entries(schema).map(([key, control]) => {
|
|
@@ -264,8 +240,35 @@ var useUrlSyncedControls = (schema, options) => {
|
|
|
264
240
|
];
|
|
265
241
|
})
|
|
266
242
|
);
|
|
267
|
-
|
|
243
|
+
(0, import_react2.useEffect)(() => {
|
|
244
|
+
ctx.registerSchema(mergedSchema, options);
|
|
245
|
+
}, [JSON.stringify(mergedSchema), JSON.stringify(options)]);
|
|
246
|
+
(0, import_react2.useEffect)(() => {
|
|
247
|
+
for (const key in mergedSchema) {
|
|
248
|
+
if (!(key in ctx.values) && "value" in mergedSchema[key]) {
|
|
249
|
+
ctx.setValue(key, mergedSchema[key].value);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}, [JSON.stringify(mergedSchema), JSON.stringify(ctx.values)]);
|
|
253
|
+
const typedValues = ctx.values;
|
|
254
|
+
const jsx15 = (0, import_react2.useCallback)(() => {
|
|
255
|
+
if (!options?.componentName) return "";
|
|
256
|
+
const props = Object.entries(typedValues).map(([key, val]) => {
|
|
257
|
+
if (typeof val === "string") return `${key}="${val}"`;
|
|
258
|
+
if (typeof val === "boolean") return `${key}={${val}}`;
|
|
259
|
+
return `${key}={${JSON.stringify(val)}}`;
|
|
260
|
+
}).join(" ");
|
|
261
|
+
return `<${options.componentName} ${props} />`;
|
|
262
|
+
}, [options?.componentName, JSON.stringify(typedValues)]);
|
|
263
|
+
return {
|
|
264
|
+
...typedValues,
|
|
265
|
+
controls: ctx.values,
|
|
266
|
+
schema: ctx.schema,
|
|
267
|
+
setValue: ctx.setValue,
|
|
268
|
+
jsx: jsx15
|
|
269
|
+
};
|
|
268
270
|
};
|
|
271
|
+
var useUrlSyncedControls = useControls;
|
|
269
272
|
|
|
270
273
|
// src/components/PreviewContainer/PreviewContainer.tsx
|
|
271
274
|
var import_react3 = require("react");
|
|
@@ -575,7 +578,7 @@ var ControlPanel = () => {
|
|
|
575
578
|
const buttonControls = Object.entries(schema).filter(
|
|
576
579
|
([, control]) => control.type === "button" && !control.hidden
|
|
577
580
|
);
|
|
578
|
-
const
|
|
581
|
+
const jsx15 = (0, import_react5.useMemo)(() => {
|
|
579
582
|
if (!componentName) return "";
|
|
580
583
|
const props = Object.entries(values).map(([key, val]) => {
|
|
581
584
|
if (typeof val === "string") return `${key}="${val}"`;
|
|
@@ -701,16 +704,16 @@ var ControlPanel = () => {
|
|
|
701
704
|
return null;
|
|
702
705
|
}
|
|
703
706
|
}),
|
|
704
|
-
(buttonControls.length > 0 ||
|
|
707
|
+
(buttonControls.length > 0 || jsx15) && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
|
|
705
708
|
"div",
|
|
706
709
|
{
|
|
707
710
|
className: `${normalControls.length > 0 ? "border-t" : ""} border-stone-700`,
|
|
708
711
|
children: [
|
|
709
|
-
|
|
712
|
+
jsx15 && config?.showCopyButton !== false && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex-1 pt-4", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
710
713
|
"button",
|
|
711
714
|
{
|
|
712
715
|
onClick: () => {
|
|
713
|
-
navigator.clipboard.writeText(
|
|
716
|
+
navigator.clipboard.writeText(jsx15);
|
|
714
717
|
setCopied(true);
|
|
715
718
|
setTimeout(() => setCopied(false), 5e3);
|
|
716
719
|
},
|
|
@@ -769,6 +772,7 @@ var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
|
769
772
|
var NO_CONTROLS_PARAM = "nocontrols";
|
|
770
773
|
function Playground({ children }) {
|
|
771
774
|
const [isHydrated, setIsHydrated] = (0, import_react6.useState)(false);
|
|
775
|
+
const [copied, setCopied] = (0, import_react6.useState)(false);
|
|
772
776
|
(0, import_react6.useEffect)(() => {
|
|
773
777
|
setIsHydrated(true);
|
|
774
778
|
}, []);
|
|
@@ -776,16 +780,149 @@ function Playground({ children }) {
|
|
|
776
780
|
if (typeof window === "undefined") return false;
|
|
777
781
|
return new URLSearchParams(window.location.search).get(NO_CONTROLS_PARAM) === "true";
|
|
778
782
|
}, []);
|
|
783
|
+
const handleCopy = () => {
|
|
784
|
+
navigator.clipboard.writeText(window.location.href);
|
|
785
|
+
setCopied(true);
|
|
786
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
787
|
+
};
|
|
779
788
|
if (!isHydrated) return null;
|
|
780
789
|
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ResizableLayout, { hideControls, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(ControlsProvider, { children: [
|
|
790
|
+
hideControls && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
|
|
791
|
+
"button",
|
|
792
|
+
{
|
|
793
|
+
onClick: handleCopy,
|
|
794
|
+
className: "absolute top-4 right-4 z-50 flex items-center gap-1 rounded bg-black/70 px-3 py-1 text-white hover:bg-black",
|
|
795
|
+
children: [
|
|
796
|
+
copied ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.Check, { size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react4.Copy, { size: 16 }),
|
|
797
|
+
copied ? "Copied!" : "Share"
|
|
798
|
+
]
|
|
799
|
+
}
|
|
800
|
+
),
|
|
781
801
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PreviewContainer_default, { hideControls, children }),
|
|
782
802
|
!hideControls && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ControlPanel_default, {})
|
|
783
803
|
] }) });
|
|
784
804
|
}
|
|
805
|
+
|
|
806
|
+
// src/components/Canvas/Canvas.tsx
|
|
807
|
+
var import_react8 = __toESM(require("react"));
|
|
808
|
+
var import_fiber2 = require("@react-three/fiber");
|
|
809
|
+
var import_fiber3 = require("@react-three/fiber");
|
|
810
|
+
|
|
811
|
+
// src/components/CameraLogger/CameraLogger.tsx
|
|
812
|
+
var import_react7 = require("react");
|
|
813
|
+
var import_drei = require("@react-three/drei");
|
|
814
|
+
var import_fiber = require("@react-three/fiber");
|
|
815
|
+
var import_lodash = require("lodash");
|
|
816
|
+
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
817
|
+
function CameraLogger() {
|
|
818
|
+
const { camera } = (0, import_fiber.useThree)();
|
|
819
|
+
const controlsRef = (0, import_react7.useRef)(null);
|
|
820
|
+
const logRef = (0, import_react7.useRef)(null);
|
|
821
|
+
(0, import_react7.useEffect)(() => {
|
|
822
|
+
logRef.current = (0, import_lodash.debounce)(() => {
|
|
823
|
+
console.info("Camera position:", camera.position.toArray());
|
|
824
|
+
}, 200);
|
|
825
|
+
}, [camera]);
|
|
826
|
+
(0, import_react7.useEffect)(() => {
|
|
827
|
+
const controls = controlsRef.current;
|
|
828
|
+
const handler = logRef.current;
|
|
829
|
+
if (!controls || !handler) return;
|
|
830
|
+
controls.addEventListener("change", handler);
|
|
831
|
+
return () => controls.removeEventListener("change", handler);
|
|
832
|
+
}, []);
|
|
833
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_drei.OrbitControls, { ref: controlsRef });
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/components/Canvas/Canvas.tsx
|
|
837
|
+
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
838
|
+
var ResponsiveCamera = ({
|
|
839
|
+
height,
|
|
840
|
+
width
|
|
841
|
+
}) => {
|
|
842
|
+
const { camera } = (0, import_fiber2.useThree)();
|
|
843
|
+
(0, import_react8.useEffect)(() => {
|
|
844
|
+
const isMobile = width < 768;
|
|
845
|
+
const zoomFactor = isMobile ? 70 : 100;
|
|
846
|
+
camera.position.z = height / zoomFactor;
|
|
847
|
+
camera.updateProjectionMatrix();
|
|
848
|
+
}, [height, camera, width]);
|
|
849
|
+
return null;
|
|
850
|
+
};
|
|
851
|
+
var Canvas = ({ mediaProps, children }) => {
|
|
852
|
+
const canvasRef = (0, import_react8.useRef)(null);
|
|
853
|
+
const [parentSize, setParentSize] = (0, import_react8.useState)(null);
|
|
854
|
+
(0, import_react8.useEffect)(() => {
|
|
855
|
+
let observer = null;
|
|
856
|
+
const tryObserve = () => {
|
|
857
|
+
const node = canvasRef.current;
|
|
858
|
+
if (!node || !node.parentElement) {
|
|
859
|
+
setTimeout(tryObserve, 50);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const parent = node.parentElement;
|
|
863
|
+
observer = new ResizeObserver(([entry]) => {
|
|
864
|
+
const { width, height } = entry.contentRect;
|
|
865
|
+
setParentSize({ width, height });
|
|
866
|
+
});
|
|
867
|
+
observer.observe(parent);
|
|
868
|
+
};
|
|
869
|
+
tryObserve();
|
|
870
|
+
return () => {
|
|
871
|
+
if (observer) observer.disconnect();
|
|
872
|
+
};
|
|
873
|
+
}, []);
|
|
874
|
+
const mergedMediaProps = {
|
|
875
|
+
...mediaProps || {},
|
|
876
|
+
size: mediaProps?.size || { width: 400, height: 400 }
|
|
877
|
+
};
|
|
878
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
879
|
+
"div",
|
|
880
|
+
{
|
|
881
|
+
ref: canvasRef,
|
|
882
|
+
className: "w-full h-full pointer-events-none relative touch-none",
|
|
883
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
|
|
884
|
+
import_fiber2.Canvas,
|
|
885
|
+
{
|
|
886
|
+
resize: { polyfill: ResizeObserver },
|
|
887
|
+
style: { width: parentSize?.width, height: parentSize?.height },
|
|
888
|
+
gl: { preserveDrawingBuffer: true },
|
|
889
|
+
children: [
|
|
890
|
+
parentSize?.height && parentSize?.width && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
891
|
+
ResponsiveCamera,
|
|
892
|
+
{
|
|
893
|
+
height: parentSize.height,
|
|
894
|
+
width: parentSize.width
|
|
895
|
+
}
|
|
896
|
+
),
|
|
897
|
+
mediaProps?.debugOrbit && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(CameraLogger, {}),
|
|
898
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("ambientLight", { intensity: 1 }),
|
|
899
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("pointLight", { position: [10, 10, 10] }),
|
|
900
|
+
import_react8.default.cloneElement(children, mergedMediaProps)
|
|
901
|
+
]
|
|
902
|
+
}
|
|
903
|
+
)
|
|
904
|
+
}
|
|
905
|
+
);
|
|
906
|
+
};
|
|
907
|
+
var Canvas_default = Canvas;
|
|
908
|
+
|
|
909
|
+
// src/components/PlaygroundCanvas/PlaygroundCanvas.tsx
|
|
910
|
+
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
911
|
+
var PlaygroundCanvas = ({
|
|
912
|
+
children,
|
|
913
|
+
mediaProps
|
|
914
|
+
}) => {
|
|
915
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Playground, { children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Canvas_default, { mediaProps, children }) });
|
|
916
|
+
};
|
|
917
|
+
var PlaygroundCanvas_default = PlaygroundCanvas;
|
|
785
918
|
// Annotate the CommonJS export names for ESM import in node:
|
|
786
919
|
0 && (module.exports = {
|
|
920
|
+
Button,
|
|
921
|
+
CameraLogger,
|
|
922
|
+
Canvas,
|
|
787
923
|
ControlsProvider,
|
|
788
924
|
Playground,
|
|
925
|
+
PlaygroundCanvas,
|
|
789
926
|
useControls,
|
|
790
927
|
useUrlSyncedControls
|
|
791
928
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/components/Playground/Playground.tsx
|
|
2
2
|
import { useEffect as useEffect4, useMemo as useMemo3, useState as useState5 } from "react";
|
|
3
|
+
import { Check as Check3, Copy as Copy2 } from "lucide-react";
|
|
3
4
|
|
|
4
5
|
// src/context/ResizableLayout.tsx
|
|
5
6
|
import {
|
|
@@ -187,35 +188,6 @@ var ControlsProvider = ({ children }) => {
|
|
|
187
188
|
var useControls = (schema, options) => {
|
|
188
189
|
const ctx = useContext2(ControlsContext);
|
|
189
190
|
if (!ctx) throw new Error("useControls must be used within ControlsProvider");
|
|
190
|
-
useEffect2(() => {
|
|
191
|
-
ctx.registerSchema(schema, options);
|
|
192
|
-
}, [JSON.stringify(schema), JSON.stringify(options)]);
|
|
193
|
-
useEffect2(() => {
|
|
194
|
-
for (const key in schema) {
|
|
195
|
-
if (!(key in ctx.values) && "value" in schema[key]) {
|
|
196
|
-
ctx.setValue(key, schema[key].value);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}, [JSON.stringify(schema), JSON.stringify(ctx.values)]);
|
|
200
|
-
const typedValues = ctx.values;
|
|
201
|
-
const jsx12 = useCallback(() => {
|
|
202
|
-
if (!options?.componentName) return "";
|
|
203
|
-
const props = Object.entries(typedValues).map(([key, val]) => {
|
|
204
|
-
if (typeof val === "string") return `${key}="${val}"`;
|
|
205
|
-
if (typeof val === "boolean") return `${key}={${val}}`;
|
|
206
|
-
return `${key}={${JSON.stringify(val)}}`;
|
|
207
|
-
}).join(" ");
|
|
208
|
-
return `<${options.componentName} ${props} />`;
|
|
209
|
-
}, [options?.componentName, JSON.stringify(typedValues)]);
|
|
210
|
-
return {
|
|
211
|
-
...typedValues,
|
|
212
|
-
controls: ctx.values,
|
|
213
|
-
schema: ctx.schema,
|
|
214
|
-
setValue: ctx.setValue,
|
|
215
|
-
jsx: jsx12
|
|
216
|
-
};
|
|
217
|
-
};
|
|
218
|
-
var useUrlSyncedControls = (schema, options) => {
|
|
219
191
|
const urlParams = getUrlParams();
|
|
220
192
|
const mergedSchema = Object.fromEntries(
|
|
221
193
|
Object.entries(schema).map(([key, control]) => {
|
|
@@ -238,8 +210,35 @@ var useUrlSyncedControls = (schema, options) => {
|
|
|
238
210
|
];
|
|
239
211
|
})
|
|
240
212
|
);
|
|
241
|
-
|
|
213
|
+
useEffect2(() => {
|
|
214
|
+
ctx.registerSchema(mergedSchema, options);
|
|
215
|
+
}, [JSON.stringify(mergedSchema), JSON.stringify(options)]);
|
|
216
|
+
useEffect2(() => {
|
|
217
|
+
for (const key in mergedSchema) {
|
|
218
|
+
if (!(key in ctx.values) && "value" in mergedSchema[key]) {
|
|
219
|
+
ctx.setValue(key, mergedSchema[key].value);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}, [JSON.stringify(mergedSchema), JSON.stringify(ctx.values)]);
|
|
223
|
+
const typedValues = ctx.values;
|
|
224
|
+
const jsx15 = useCallback(() => {
|
|
225
|
+
if (!options?.componentName) return "";
|
|
226
|
+
const props = Object.entries(typedValues).map(([key, val]) => {
|
|
227
|
+
if (typeof val === "string") return `${key}="${val}"`;
|
|
228
|
+
if (typeof val === "boolean") return `${key}={${val}}`;
|
|
229
|
+
return `${key}={${JSON.stringify(val)}}`;
|
|
230
|
+
}).join(" ");
|
|
231
|
+
return `<${options.componentName} ${props} />`;
|
|
232
|
+
}, [options?.componentName, JSON.stringify(typedValues)]);
|
|
233
|
+
return {
|
|
234
|
+
...typedValues,
|
|
235
|
+
controls: ctx.values,
|
|
236
|
+
schema: ctx.schema,
|
|
237
|
+
setValue: ctx.setValue,
|
|
238
|
+
jsx: jsx15
|
|
239
|
+
};
|
|
242
240
|
};
|
|
241
|
+
var useUrlSyncedControls = useControls;
|
|
243
242
|
|
|
244
243
|
// src/components/PreviewContainer/PreviewContainer.tsx
|
|
245
244
|
import { useRef as useRef2 } from "react";
|
|
@@ -549,7 +548,7 @@ var ControlPanel = () => {
|
|
|
549
548
|
const buttonControls = Object.entries(schema).filter(
|
|
550
549
|
([, control]) => control.type === "button" && !control.hidden
|
|
551
550
|
);
|
|
552
|
-
const
|
|
551
|
+
const jsx15 = useMemo2(() => {
|
|
553
552
|
if (!componentName) return "";
|
|
554
553
|
const props = Object.entries(values).map(([key, val]) => {
|
|
555
554
|
if (typeof val === "string") return `${key}="${val}"`;
|
|
@@ -675,16 +674,16 @@ var ControlPanel = () => {
|
|
|
675
674
|
return null;
|
|
676
675
|
}
|
|
677
676
|
}),
|
|
678
|
-
(buttonControls.length > 0 ||
|
|
677
|
+
(buttonControls.length > 0 || jsx15) && /* @__PURE__ */ jsxs4(
|
|
679
678
|
"div",
|
|
680
679
|
{
|
|
681
680
|
className: `${normalControls.length > 0 ? "border-t" : ""} border-stone-700`,
|
|
682
681
|
children: [
|
|
683
|
-
|
|
682
|
+
jsx15 && config?.showCopyButton !== false && /* @__PURE__ */ jsx10("div", { className: "flex-1 pt-4", children: /* @__PURE__ */ jsx10(
|
|
684
683
|
"button",
|
|
685
684
|
{
|
|
686
685
|
onClick: () => {
|
|
687
|
-
navigator.clipboard.writeText(
|
|
686
|
+
navigator.clipboard.writeText(jsx15);
|
|
688
687
|
setCopied(true);
|
|
689
688
|
setTimeout(() => setCopied(false), 5e3);
|
|
690
689
|
},
|
|
@@ -743,6 +742,7 @@ import { jsx as jsx11, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
|
743
742
|
var NO_CONTROLS_PARAM = "nocontrols";
|
|
744
743
|
function Playground({ children }) {
|
|
745
744
|
const [isHydrated, setIsHydrated] = useState5(false);
|
|
745
|
+
const [copied, setCopied] = useState5(false);
|
|
746
746
|
useEffect4(() => {
|
|
747
747
|
setIsHydrated(true);
|
|
748
748
|
}, []);
|
|
@@ -750,15 +750,148 @@ function Playground({ children }) {
|
|
|
750
750
|
if (typeof window === "undefined") return false;
|
|
751
751
|
return new URLSearchParams(window.location.search).get(NO_CONTROLS_PARAM) === "true";
|
|
752
752
|
}, []);
|
|
753
|
+
const handleCopy = () => {
|
|
754
|
+
navigator.clipboard.writeText(window.location.href);
|
|
755
|
+
setCopied(true);
|
|
756
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
757
|
+
};
|
|
753
758
|
if (!isHydrated) return null;
|
|
754
759
|
return /* @__PURE__ */ jsx11(ResizableLayout, { hideControls, children: /* @__PURE__ */ jsxs5(ControlsProvider, { children: [
|
|
760
|
+
hideControls && /* @__PURE__ */ jsxs5(
|
|
761
|
+
"button",
|
|
762
|
+
{
|
|
763
|
+
onClick: handleCopy,
|
|
764
|
+
className: "absolute top-4 right-4 z-50 flex items-center gap-1 rounded bg-black/70 px-3 py-1 text-white hover:bg-black",
|
|
765
|
+
children: [
|
|
766
|
+
copied ? /* @__PURE__ */ jsx11(Check3, { size: 16 }) : /* @__PURE__ */ jsx11(Copy2, { size: 16 }),
|
|
767
|
+
copied ? "Copied!" : "Share"
|
|
768
|
+
]
|
|
769
|
+
}
|
|
770
|
+
),
|
|
755
771
|
/* @__PURE__ */ jsx11(PreviewContainer_default, { hideControls, children }),
|
|
756
772
|
!hideControls && /* @__PURE__ */ jsx11(ControlPanel_default, {})
|
|
757
773
|
] }) });
|
|
758
774
|
}
|
|
775
|
+
|
|
776
|
+
// src/components/Canvas/Canvas.tsx
|
|
777
|
+
import React11, { useEffect as useEffect6, useRef as useRef4, useState as useState6 } from "react";
|
|
778
|
+
import { Canvas as ThreeCanvas, useThree as useThree2 } from "@react-three/fiber";
|
|
779
|
+
import "@react-three/fiber";
|
|
780
|
+
|
|
781
|
+
// src/components/CameraLogger/CameraLogger.tsx
|
|
782
|
+
import { useRef as useRef3, useEffect as useEffect5 } from "react";
|
|
783
|
+
import { OrbitControls } from "@react-three/drei";
|
|
784
|
+
import { useThree } from "@react-three/fiber";
|
|
785
|
+
import { debounce } from "lodash";
|
|
786
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
787
|
+
function CameraLogger() {
|
|
788
|
+
const { camera } = useThree();
|
|
789
|
+
const controlsRef = useRef3(null);
|
|
790
|
+
const logRef = useRef3(null);
|
|
791
|
+
useEffect5(() => {
|
|
792
|
+
logRef.current = debounce(() => {
|
|
793
|
+
console.info("Camera position:", camera.position.toArray());
|
|
794
|
+
}, 200);
|
|
795
|
+
}, [camera]);
|
|
796
|
+
useEffect5(() => {
|
|
797
|
+
const controls = controlsRef.current;
|
|
798
|
+
const handler = logRef.current;
|
|
799
|
+
if (!controls || !handler) return;
|
|
800
|
+
controls.addEventListener("change", handler);
|
|
801
|
+
return () => controls.removeEventListener("change", handler);
|
|
802
|
+
}, []);
|
|
803
|
+
return /* @__PURE__ */ jsx12(OrbitControls, { ref: controlsRef });
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/components/Canvas/Canvas.tsx
|
|
807
|
+
import { jsx as jsx13, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
808
|
+
var ResponsiveCamera = ({
|
|
809
|
+
height,
|
|
810
|
+
width
|
|
811
|
+
}) => {
|
|
812
|
+
const { camera } = useThree2();
|
|
813
|
+
useEffect6(() => {
|
|
814
|
+
const isMobile = width < 768;
|
|
815
|
+
const zoomFactor = isMobile ? 70 : 100;
|
|
816
|
+
camera.position.z = height / zoomFactor;
|
|
817
|
+
camera.updateProjectionMatrix();
|
|
818
|
+
}, [height, camera, width]);
|
|
819
|
+
return null;
|
|
820
|
+
};
|
|
821
|
+
var Canvas = ({ mediaProps, children }) => {
|
|
822
|
+
const canvasRef = useRef4(null);
|
|
823
|
+
const [parentSize, setParentSize] = useState6(null);
|
|
824
|
+
useEffect6(() => {
|
|
825
|
+
let observer = null;
|
|
826
|
+
const tryObserve = () => {
|
|
827
|
+
const node = canvasRef.current;
|
|
828
|
+
if (!node || !node.parentElement) {
|
|
829
|
+
setTimeout(tryObserve, 50);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const parent = node.parentElement;
|
|
833
|
+
observer = new ResizeObserver(([entry]) => {
|
|
834
|
+
const { width, height } = entry.contentRect;
|
|
835
|
+
setParentSize({ width, height });
|
|
836
|
+
});
|
|
837
|
+
observer.observe(parent);
|
|
838
|
+
};
|
|
839
|
+
tryObserve();
|
|
840
|
+
return () => {
|
|
841
|
+
if (observer) observer.disconnect();
|
|
842
|
+
};
|
|
843
|
+
}, []);
|
|
844
|
+
const mergedMediaProps = {
|
|
845
|
+
...mediaProps || {},
|
|
846
|
+
size: mediaProps?.size || { width: 400, height: 400 }
|
|
847
|
+
};
|
|
848
|
+
return /* @__PURE__ */ jsx13(
|
|
849
|
+
"div",
|
|
850
|
+
{
|
|
851
|
+
ref: canvasRef,
|
|
852
|
+
className: "w-full h-full pointer-events-none relative touch-none",
|
|
853
|
+
children: /* @__PURE__ */ jsxs6(
|
|
854
|
+
ThreeCanvas,
|
|
855
|
+
{
|
|
856
|
+
resize: { polyfill: ResizeObserver },
|
|
857
|
+
style: { width: parentSize?.width, height: parentSize?.height },
|
|
858
|
+
gl: { preserveDrawingBuffer: true },
|
|
859
|
+
children: [
|
|
860
|
+
parentSize?.height && parentSize?.width && /* @__PURE__ */ jsx13(
|
|
861
|
+
ResponsiveCamera,
|
|
862
|
+
{
|
|
863
|
+
height: parentSize.height,
|
|
864
|
+
width: parentSize.width
|
|
865
|
+
}
|
|
866
|
+
),
|
|
867
|
+
mediaProps?.debugOrbit && /* @__PURE__ */ jsx13(CameraLogger, {}),
|
|
868
|
+
/* @__PURE__ */ jsx13("ambientLight", { intensity: 1 }),
|
|
869
|
+
/* @__PURE__ */ jsx13("pointLight", { position: [10, 10, 10] }),
|
|
870
|
+
React11.cloneElement(children, mergedMediaProps)
|
|
871
|
+
]
|
|
872
|
+
}
|
|
873
|
+
)
|
|
874
|
+
}
|
|
875
|
+
);
|
|
876
|
+
};
|
|
877
|
+
var Canvas_default = Canvas;
|
|
878
|
+
|
|
879
|
+
// src/components/PlaygroundCanvas/PlaygroundCanvas.tsx
|
|
880
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
881
|
+
var PlaygroundCanvas = ({
|
|
882
|
+
children,
|
|
883
|
+
mediaProps
|
|
884
|
+
}) => {
|
|
885
|
+
return /* @__PURE__ */ jsx14(Playground, { children: /* @__PURE__ */ jsx14(Canvas_default, { mediaProps, children }) });
|
|
886
|
+
};
|
|
887
|
+
var PlaygroundCanvas_default = PlaygroundCanvas;
|
|
759
888
|
export {
|
|
889
|
+
Button,
|
|
890
|
+
CameraLogger,
|
|
891
|
+
Canvas_default as Canvas,
|
|
760
892
|
ControlsProvider,
|
|
761
893
|
Playground,
|
|
894
|
+
PlaygroundCanvas_default as PlaygroundCanvas,
|
|
762
895
|
useControls,
|
|
763
896
|
useUrlSyncedControls
|
|
764
897
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toriistudio/v0-playground",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "V0 Playground",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -59,13 +59,18 @@
|
|
|
59
59
|
"@radix-ui/react-slider": "^1.3.4",
|
|
60
60
|
"@radix-ui/react-slot": "^1.2.3",
|
|
61
61
|
"@radix-ui/react-switch": "^1.2.4",
|
|
62
|
+
"@react-three/drei": "^10.3.0",
|
|
63
|
+
"@react-three/fiber": "^9.1.2",
|
|
62
64
|
"class-variance-authority": "^0.7.1",
|
|
63
65
|
"clsx": "^2.1.1",
|
|
66
|
+
"lodash": "^4.17.21",
|
|
64
67
|
"lucide-react": "^0.522.0",
|
|
65
68
|
"react": "^19.1.0",
|
|
66
69
|
"react-dom": "^19.1.0",
|
|
67
70
|
"tailwind-merge": "^3.3.1",
|
|
68
|
-
"tailwindcss-animate": "^1.0.7"
|
|
71
|
+
"tailwindcss-animate": "^1.0.7",
|
|
72
|
+
"three": "^0.177.0",
|
|
73
|
+
"three-stdlib": "^2.36.0"
|
|
69
74
|
},
|
|
70
75
|
"devDependencies": {
|
|
71
76
|
"@radix-ui/react-label": "^2.1.6",
|
|
@@ -73,15 +78,21 @@
|
|
|
73
78
|
"@radix-ui/react-slider": "^1.3.4",
|
|
74
79
|
"@radix-ui/react-slot": "^1.2.3",
|
|
75
80
|
"@radix-ui/react-switch": "^1.2.4",
|
|
81
|
+
"@react-three/drei": "^10.3.0",
|
|
82
|
+
"@react-three/fiber": "^9.1.2",
|
|
83
|
+
"@types/lodash": "^4.17.19",
|
|
76
84
|
"@types/node": "^24.0.3",
|
|
77
85
|
"@types/react": "^19.1.2",
|
|
78
86
|
"@types/react-dom": "^19.1.3",
|
|
79
87
|
"class-variance-authority": "^0.7.1",
|
|
80
88
|
"clsx": "^2.1.1",
|
|
89
|
+
"lodash": "^4.17.21",
|
|
81
90
|
"lucide-react": "^0.522.0",
|
|
82
91
|
"tailwind-merge": "^3.3.1",
|
|
83
92
|
"tailwindcss": "^4.1.10",
|
|
84
93
|
"tailwindcss-animate": "^1.0.7",
|
|
94
|
+
"three": "^0.177.0",
|
|
95
|
+
"three-stdlib": "^2.36.0",
|
|
85
96
|
"tsup": "^8.4.0",
|
|
86
97
|
"typescript": "^5.8.3"
|
|
87
98
|
},
|
package/dist/preset.d.mts
DELETED