@toriistudio/v0-playground 0.2.10 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,7 @@ Perfect for prototyping components, sharing usage examples, or building your own
22
22
  To use `@toriistudio/v0-playground`, you’ll need to install the following peer dependencies:
23
23
 
24
24
  ```bash
25
- yarn add @radix-ui/react-label @radix-ui/react-select @radix-ui/react-slider @radix-ui/react-slot @radix-ui/react-switch class-variance-authority clsx lucide-react tailwind-merge tailwindcss-animate
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 React, { ReactNode } from 'react';
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?: () => React.ReactNode;
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 React, { ReactNode } from 'react';
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?: () => React.ReactNode;
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
  });
@@ -214,35 +218,6 @@ var ControlsProvider = ({ children }) => {
214
218
  var useControls = (schema, options) => {
215
219
  const ctx = (0, import_react2.useContext)(ControlsContext);
216
220
  if (!ctx) throw new Error("useControls must be used within ControlsProvider");
217
- (0, import_react2.useEffect)(() => {
218
- ctx.registerSchema(schema, options);
219
- }, [JSON.stringify(schema), JSON.stringify(options)]);
220
- (0, import_react2.useEffect)(() => {
221
- for (const key in schema) {
222
- if (!(key in ctx.values) && "value" in schema[key]) {
223
- ctx.setValue(key, schema[key].value);
224
- }
225
- }
226
- }, [JSON.stringify(schema), JSON.stringify(ctx.values)]);
227
- const typedValues = ctx.values;
228
- const jsx12 = (0, import_react2.useCallback)(() => {
229
- if (!options?.componentName) return "";
230
- const props = Object.entries(typedValues).map(([key, val]) => {
231
- if (typeof val === "string") return `${key}="${val}"`;
232
- if (typeof val === "boolean") return `${key}={${val}}`;
233
- return `${key}={${JSON.stringify(val)}}`;
234
- }).join(" ");
235
- return `<${options.componentName} ${props} />`;
236
- }, [options?.componentName, JSON.stringify(typedValues)]);
237
- return {
238
- ...typedValues,
239
- controls: ctx.values,
240
- schema: ctx.schema,
241
- setValue: ctx.setValue,
242
- jsx: jsx12
243
- };
244
- };
245
- var useUrlSyncedControls = (schema, options) => {
246
221
  const urlParams = getUrlParams();
247
222
  const mergedSchema = Object.fromEntries(
248
223
  Object.entries(schema).map(([key, control]) => {
@@ -265,8 +240,35 @@ var useUrlSyncedControls = (schema, options) => {
265
240
  ];
266
241
  })
267
242
  );
268
- return useControls(mergedSchema, options);
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
+ };
269
270
  };
271
+ var useUrlSyncedControls = useControls;
270
272
 
271
273
  // src/components/PreviewContainer/PreviewContainer.tsx
272
274
  var import_react3 = require("react");
@@ -283,7 +285,7 @@ var PreviewContainer = ({ children, hideControls }) => {
283
285
  width: `${100 - leftPanelWidth}%`,
284
286
  marginLeft: `${leftPanelWidth}%`
285
287
  } : {},
286
- children
288
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "w-screen h-screen flex items-center justify-center", children })
287
289
  }
288
290
  );
289
291
  };
@@ -576,7 +578,7 @@ var ControlPanel = () => {
576
578
  const buttonControls = Object.entries(schema).filter(
577
579
  ([, control]) => control.type === "button" && !control.hidden
578
580
  );
579
- const jsx12 = (0, import_react5.useMemo)(() => {
581
+ const jsx15 = (0, import_react5.useMemo)(() => {
580
582
  if (!componentName) return "";
581
583
  const props = Object.entries(values).map(([key, val]) => {
582
584
  if (typeof val === "string") return `${key}="${val}"`;
@@ -702,16 +704,16 @@ var ControlPanel = () => {
702
704
  return null;
703
705
  }
704
706
  }),
705
- (buttonControls.length > 0 || jsx12) && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
707
+ (buttonControls.length > 0 || jsx15) && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
706
708
  "div",
707
709
  {
708
710
  className: `${normalControls.length > 0 ? "border-t" : ""} border-stone-700`,
709
711
  children: [
710
- jsx12 && config?.showCopyButton !== false && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex-1 pt-4", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
712
+ jsx15 && config?.showCopyButton !== false && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex-1 pt-4", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
711
713
  "button",
712
714
  {
713
715
  onClick: () => {
714
- navigator.clipboard.writeText(jsx12);
716
+ navigator.clipboard.writeText(jsx15);
715
717
  setCopied(true);
716
718
  setTimeout(() => setCopied(false), 5e3);
717
719
  },
@@ -800,10 +802,127 @@ function Playground({ children }) {
800
802
  !hideControls && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ControlPanel_default, {})
801
803
  ] }) });
802
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;
803
918
  // Annotate the CommonJS export names for ESM import in node:
804
919
  0 && (module.exports = {
920
+ Button,
921
+ CameraLogger,
922
+ Canvas,
805
923
  ControlsProvider,
806
924
  Playground,
925
+ PlaygroundCanvas,
807
926
  useControls,
808
927
  useUrlSyncedControls
809
928
  });
package/dist/index.mjs CHANGED
@@ -188,35 +188,6 @@ var ControlsProvider = ({ children }) => {
188
188
  var useControls = (schema, options) => {
189
189
  const ctx = useContext2(ControlsContext);
190
190
  if (!ctx) throw new Error("useControls must be used within ControlsProvider");
191
- useEffect2(() => {
192
- ctx.registerSchema(schema, options);
193
- }, [JSON.stringify(schema), JSON.stringify(options)]);
194
- useEffect2(() => {
195
- for (const key in schema) {
196
- if (!(key in ctx.values) && "value" in schema[key]) {
197
- ctx.setValue(key, schema[key].value);
198
- }
199
- }
200
- }, [JSON.stringify(schema), JSON.stringify(ctx.values)]);
201
- const typedValues = ctx.values;
202
- const jsx12 = useCallback(() => {
203
- if (!options?.componentName) return "";
204
- const props = Object.entries(typedValues).map(([key, val]) => {
205
- if (typeof val === "string") return `${key}="${val}"`;
206
- if (typeof val === "boolean") return `${key}={${val}}`;
207
- return `${key}={${JSON.stringify(val)}}`;
208
- }).join(" ");
209
- return `<${options.componentName} ${props} />`;
210
- }, [options?.componentName, JSON.stringify(typedValues)]);
211
- return {
212
- ...typedValues,
213
- controls: ctx.values,
214
- schema: ctx.schema,
215
- setValue: ctx.setValue,
216
- jsx: jsx12
217
- };
218
- };
219
- var useUrlSyncedControls = (schema, options) => {
220
191
  const urlParams = getUrlParams();
221
192
  const mergedSchema = Object.fromEntries(
222
193
  Object.entries(schema).map(([key, control]) => {
@@ -239,8 +210,35 @@ var useUrlSyncedControls = (schema, options) => {
239
210
  ];
240
211
  })
241
212
  );
242
- return useControls(mergedSchema, options);
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
+ };
243
240
  };
241
+ var useUrlSyncedControls = useControls;
244
242
 
245
243
  // src/components/PreviewContainer/PreviewContainer.tsx
246
244
  import { useRef as useRef2 } from "react";
@@ -257,7 +255,7 @@ var PreviewContainer = ({ children, hideControls }) => {
257
255
  width: `${100 - leftPanelWidth}%`,
258
256
  marginLeft: `${leftPanelWidth}%`
259
257
  } : {},
260
- children
258
+ children: /* @__PURE__ */ jsx3("div", { className: "w-screen h-screen flex items-center justify-center", children })
261
259
  }
262
260
  );
263
261
  };
@@ -550,7 +548,7 @@ var ControlPanel = () => {
550
548
  const buttonControls = Object.entries(schema).filter(
551
549
  ([, control]) => control.type === "button" && !control.hidden
552
550
  );
553
- const jsx12 = useMemo2(() => {
551
+ const jsx15 = useMemo2(() => {
554
552
  if (!componentName) return "";
555
553
  const props = Object.entries(values).map(([key, val]) => {
556
554
  if (typeof val === "string") return `${key}="${val}"`;
@@ -676,16 +674,16 @@ var ControlPanel = () => {
676
674
  return null;
677
675
  }
678
676
  }),
679
- (buttonControls.length > 0 || jsx12) && /* @__PURE__ */ jsxs4(
677
+ (buttonControls.length > 0 || jsx15) && /* @__PURE__ */ jsxs4(
680
678
  "div",
681
679
  {
682
680
  className: `${normalControls.length > 0 ? "border-t" : ""} border-stone-700`,
683
681
  children: [
684
- jsx12 && config?.showCopyButton !== false && /* @__PURE__ */ jsx10("div", { className: "flex-1 pt-4", children: /* @__PURE__ */ jsx10(
682
+ jsx15 && config?.showCopyButton !== false && /* @__PURE__ */ jsx10("div", { className: "flex-1 pt-4", children: /* @__PURE__ */ jsx10(
685
683
  "button",
686
684
  {
687
685
  onClick: () => {
688
- navigator.clipboard.writeText(jsx12);
686
+ navigator.clipboard.writeText(jsx15);
689
687
  setCopied(true);
690
688
  setTimeout(() => setCopied(false), 5e3);
691
689
  },
@@ -774,9 +772,126 @@ function Playground({ children }) {
774
772
  !hideControls && /* @__PURE__ */ jsx11(ControlPanel_default, {})
775
773
  ] }) });
776
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;
777
888
  export {
889
+ Button,
890
+ CameraLogger,
891
+ Canvas_default as Canvas,
778
892
  ControlsProvider,
779
893
  Playground,
894
+ PlaygroundCanvas_default as PlaygroundCanvas,
780
895
  useControls,
781
896
  useUrlSyncedControls
782
897
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toriistudio/v0-playground",
3
- "version": "0.2.10",
3
+ "version": "0.3.1",
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
@@ -1,5 +0,0 @@
1
- import { Config } from 'tailwindcss';
2
-
3
- declare const preset: Partial<Config>;
4
-
5
- export { preset as default };
package/dist/preset.d.ts DELETED
@@ -1,5 +0,0 @@
1
- import { Config } from 'tailwindcss';
2
-
3
- declare const preset: Partial<Config>;
4
-
5
- export { preset as default };