@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 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
  });
@@ -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
- 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
+ };
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 jsx12 = (0, import_react5.useMemo)(() => {
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 || jsx12) && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
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
- 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)(
710
713
  "button",
711
714
  {
712
715
  onClick: () => {
713
- navigator.clipboard.writeText(jsx12);
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
- 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
+ };
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 jsx12 = useMemo2(() => {
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 || jsx12) && /* @__PURE__ */ jsxs4(
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
- 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(
684
683
  "button",
685
684
  {
686
685
  onClick: () => {
687
- navigator.clipboard.writeText(jsx12);
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.2.8",
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
@@ -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 };