@chestnutlabs/gcode-preview-react 0.1.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 ADDED
@@ -0,0 +1,60 @@
1
+ # @chestnutlabs/gcode-preview-react
2
+
3
+ Thin React integration for the Chestnut Labs G-code viewer — a reactivity bridge over
4
+ `@chestnutlabs/gcode-preview-core` with **capability parity** to the Vue and Svelte adapters
5
+ (same engine, same state model, same TypeScript contracts; DD-007 D1 amendment).
6
+
7
+ ```sh
8
+ npm install @chestnutlabs/gcode-preview-react three
9
+ ```
10
+
11
+ (`three` is a peerDependency of the renderer, supported range `^0.178.0` — npm ≥ 7 installs
12
+ it automatically; pnpm/yarn users add it explicitly. See the
13
+ [support policy](../../docs/reference/support-policy.md).)
14
+
15
+ ## Ready-to-use component
16
+
17
+ ```tsx
18
+ import { GcodePreview } from '@chestnutlabs/gcode-preview-react';
19
+
20
+ function Viewer({ file }: { file: File | null }) {
21
+ return (
22
+ <div style={{ height: '70vh' }}>
23
+ <GcodePreview source={file} onReady={(s) => console.log(`${s.segments} segments`)} />
24
+ </div>
25
+ );
26
+ }
27
+ ```
28
+
29
+ That is a complete viewer (StrictMode-safe). The full surface is optional props with sensible
30
+ defaults — `source`, `parseOptions`, `buildVolume` (consumer-wins bed precedence), `quality`,
31
+ `colorMode`, `layerRange`, `scrub`, `showTravel`, `progress` (DD-006 observation), `createWorker`
32
+ — with matching callbacks (`onReady`, `onParseError`, `onBuildComplete`, `onQualityFallback`,
33
+ `onMachineGeometryMismatch`/`Discovered`, `onProgressPresentationChanged`, `onDisclosure`,
34
+ `onError`). The full handle is reachable via `ref`.
35
+
36
+ ## Headless hook (build your own controls)
37
+
38
+ ```tsx
39
+ import { useGcodePreview } from '@chestnutlabs/gcode-preview-react';
40
+
41
+ function CustomViewer() {
42
+ const preview = useGcodePreview();
43
+ // <canvas ref={preview.canvasRef} /> — bind; unmount disposes automatically
44
+ // await preview.parse(bytes); preview.controls.setScrubPosition(n);
45
+ // preview.observeProgress(obs); preview.state (useSyncExternalStore-subscribed)
46
+ return <canvas ref={preview.canvasRef} style={{ width: '100%', height: '100%' }} />;
47
+ }
48
+ ```
49
+
50
+ ## Workers
51
+
52
+ The batteries default (all dialect adapters + `.gcode.3mf`) needs zero setup under Vite; pass
53
+ `createWorker` for the slim build, custom adapters, other bundlers, or strict CSP. See the
54
+ `@chestnutlabs/gcode-preview-vue` README for the shared worker documentation — the options are
55
+ identical across adapters by design.
56
+
57
+ ## Example
58
+
59
+ `tools/example-react` in the repository is a complete Vite + React app (corpus load, layer/scrub,
60
+ simulated live progress) running under `<StrictMode>`.
@@ -0,0 +1,63 @@
1
+ import type { BuildVolumeDef, ColorMode, QualityMode, TubeOptions } from '@chestnutlabs/gcode-renderer-three';
2
+ import type { MachineGeometry, ProgressObservation } from '@chestnutlabs/toolpath-core';
3
+ import type { WireParseOptions, WorkerLike } from '@chestnutlabs/gcode-parser';
4
+ import { type GcodePreviewHandle, type UseGcodePreviewOptions } from './use-gcode-preview.js';
5
+ export type GcodePreviewSource = Uint8Array | ArrayBuffer | File | null;
6
+ export interface GcodePreviewProps {
7
+ /** The one prop needed in practice: bytes/File to parse. Changing it re-parses. */
8
+ source?: GcodePreviewSource;
9
+ parseOptions?: WireParseOptions;
10
+ /** Consumer-configured volume — wins over file-discovered geometry (DD-005 precedence). */
11
+ buildVolume?: BuildVolumeDef | MachineGeometry;
12
+ quality?: QualityMode | 'auto';
13
+ colorMode?: ColorMode;
14
+ tube?: TubeOptions;
15
+ /** Inclusive [start, end]; null/undefined shows every layer. */
16
+ layerRange?: [number, number] | null;
17
+ /** Scrub cut (IR segment index); null shows everything up to the layer range. */
18
+ scrub?: number | null;
19
+ showTravel?: boolean;
20
+ /** DD-006 live progress observation; null hides the overlay. */
21
+ progress?: ProgressObservation | null;
22
+ /** Worker factory escape hatch (DD-005 slim/custom entries; D2). */
23
+ createWorker?: () => WorkerLike;
24
+ /** Advanced/test renderer injectables (pass-throughs of the renderer contract). */
25
+ rendererOptions?: Omit<NonNullable<UseGcodePreviewOptions['renderer']>, 'buildVolume' | 'quality' | 'colorMode' | 'tube'>;
26
+ onReady?: (summary: {
27
+ segments: number;
28
+ layers: number;
29
+ complete: boolean;
30
+ }) => void;
31
+ onParseError?: (e: {
32
+ code: string;
33
+ message: string;
34
+ }) => void;
35
+ onParseCancelled?: () => void;
36
+ onParseProgress?: (p: {
37
+ bytesProcessed: number;
38
+ totalBytes: number;
39
+ }) => void;
40
+ onBuildComplete?: (e: {
41
+ segments: number;
42
+ quality: QualityMode;
43
+ }) => void;
44
+ onQualityFallback?: (e: {
45
+ from: QualityMode;
46
+ to: QualityMode;
47
+ reason: string;
48
+ }) => void;
49
+ onMachineGeometryMismatch?: (message: string) => void;
50
+ onMachineGeometryDiscovered?: (machine: MachineGeometry) => void;
51
+ onProgressPresentationChanged?: (e: {
52
+ mode: string;
53
+ reason?: string;
54
+ }) => void;
55
+ onDisclosure?: (text: string) => void;
56
+ onError?: (e: {
57
+ code: string;
58
+ message: string;
59
+ }) => void;
60
+ }
61
+ /** Ready-to-use viewer: `<GcodePreview source={file} />` works alone (D4). */
62
+ export declare const GcodePreview: import("react").ForwardRefExoticComponent<GcodePreviewProps & import("react").RefAttributes<GcodePreviewHandle>>;
63
+ //# sourceMappingURL=gcode-preview-component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gcode-preview-component.d.ts","sourceRoot":"","sources":["../src/gcode-preview-component.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * <GcodePreview> — the ready-to-use React component (DD-007 D1 amendment, phase 6).
3
+ *
4
+ * Strictly a SHELL over `useGcodePreview` (D1: never a separate implementation): every
5
+ * prop maps to a hook/controls call, every callback re-fires a controller event, and the
6
+ * full handle is reachable via `ref`. Advanced props are optional with sensible defaults,
7
+ * so `<GcodePreview source={file} />` alone is a working viewer (D4).
8
+ *
9
+ * createElement (no JSX) on purpose: the template is a single full-size <canvas>, and
10
+ * staying pure TypeScript keeps the package on the plain tsc build.
11
+ */
12
+ import { createElement, forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
13
+ import { useGcodePreview } from './use-gcode-preview.js';
14
+ function GcodePreviewImpl(props, ref) {
15
+ const isMachine = props.buildVolume !== undefined && 'bed' in props.buildVolume;
16
+ const preview = useGcodePreview({
17
+ createWorker: props.createWorker,
18
+ renderer: {
19
+ buildVolume: isMachine ? undefined : props.buildVolume,
20
+ quality: props.quality ?? 'auto',
21
+ colorMode: props.colorMode,
22
+ tube: props.tube,
23
+ ...props.rendererOptions
24
+ },
25
+ parseDefaults: props.parseOptions
26
+ });
27
+ useImperativeHandle(ref, () => preview, [preview]);
28
+ // Callbacks live in a ref so the event subscription is stable across renders.
29
+ const cbRef = useRef(props);
30
+ cbRef.current = props;
31
+ useEffect(() => {
32
+ return preview.onEvent((e) => {
33
+ const p = cbRef.current;
34
+ switch (e.type) {
35
+ case 'parse-complete':
36
+ p.onReady?.({ segments: e.segments, layers: e.layers, complete: e.complete });
37
+ break;
38
+ case 'parse-error':
39
+ p.onParseError?.({ code: e.code, message: e.message });
40
+ break;
41
+ case 'parse-cancelled':
42
+ p.onParseCancelled?.();
43
+ break;
44
+ case 'parse-progress':
45
+ p.onParseProgress?.({ bytesProcessed: e.progress.bytesProcessed, totalBytes: e.progress.totalBytes });
46
+ break;
47
+ case 'buildComplete':
48
+ p.onBuildComplete?.({ segments: e.segments, quality: e.quality });
49
+ p.onDisclosure?.(preview.state.disclosure);
50
+ break;
51
+ case 'qualityFallback':
52
+ p.onQualityFallback?.({ from: e.from, to: e.to, reason: e.reason });
53
+ break;
54
+ case 'progress-presentation-changed':
55
+ p.onProgressPresentationChanged?.(e.reason === undefined ? { mode: e.mode } : { mode: e.mode, reason: e.reason });
56
+ break;
57
+ case 'machine-geometry-discovered':
58
+ p.onMachineGeometryDiscovered?.(e.machine);
59
+ break;
60
+ case 'error':
61
+ if (e.code === 'machine-geometry-mismatch')
62
+ p.onMachineGeometryMismatch?.(e.message);
63
+ else
64
+ p.onError?.({ code: e.code, message: e.message });
65
+ break;
66
+ default:
67
+ break;
68
+ }
69
+ });
70
+ }, []);
71
+ // ---- prop wiring: each prop is a thin call into the hook (D1 shell rule) ----
72
+ const { source, layerRange, scrub, showTravel, colorMode, quality, buildVolume, progress } = props;
73
+ useEffect(() => {
74
+ if (source !== null && source !== undefined)
75
+ void preview.parse(source, cbRef.current.parseOptions);
76
+ }, [source]);
77
+ useEffect(() => {
78
+ if (layerRange === null || layerRange === undefined)
79
+ preview.controls.setLayerRange(0, Number.POSITIVE_INFINITY);
80
+ else
81
+ preview.controls.setLayerRange(layerRange[0], layerRange[1]);
82
+ }, [layerRange]);
83
+ useEffect(() => {
84
+ preview.controls.setScrubPosition(scrub ?? null);
85
+ }, [scrub]);
86
+ useEffect(() => {
87
+ preview.controls.setKindVisible('travel', showTravel ?? true);
88
+ }, [showTravel]);
89
+ useEffect(() => {
90
+ if (colorMode !== undefined)
91
+ preview.controls.setColorMode(colorMode);
92
+ }, [colorMode]);
93
+ useEffect(() => {
94
+ if (quality !== undefined)
95
+ preview.controls.setQuality(quality);
96
+ }, [quality]);
97
+ useEffect(() => {
98
+ if (buildVolume !== undefined && 'bed' in buildVolume)
99
+ preview.controls.setBuildVolume(buildVolume);
100
+ }, [buildVolume]);
101
+ useEffect(() => {
102
+ if (progress === null || progress === undefined)
103
+ preview.clearProgress();
104
+ else
105
+ preview.observeProgress(progress);
106
+ }, [progress]);
107
+ return createElement('canvas', {
108
+ ref: preview.canvasRef,
109
+ style: { width: '100%', height: '100%', display: 'block', touchAction: 'none' },
110
+ 'aria-label': '3D G-code toolpath preview'
111
+ });
112
+ }
113
+ /** Ready-to-use viewer: `<GcodePreview source={file} />` works alone (D4). */
114
+ export const GcodePreview = forwardRef(GcodePreviewImpl);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @chestnutlabs/gcode-preview-react — thin React integration (DD-007 D1 amendment).
3
+ *
4
+ * `useGcodePreview` hook + `<GcodePreview>` component, both reactivity bridges over
5
+ * @chestnutlabs/gcode-preview-core — never a separate viewer implementation. Shared
6
+ * contracts are re-exported from core.
7
+ */
8
+ export * from './use-gcode-preview.js';
9
+ export { GcodePreview, type GcodePreviewProps, type GcodePreviewSource } from './gcode-preview-component.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,cAAc,wBAAwB,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,KAAK,kBAAkB,EAAE,MAAM,8BAA8B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @chestnutlabs/gcode-preview-react — thin React integration (DD-007 D1 amendment).
3
+ *
4
+ * `useGcodePreview` hook + `<GcodePreview>` component, both reactivity bridges over
5
+ * @chestnutlabs/gcode-preview-core — never a separate viewer implementation. Shared
6
+ * contracts are re-exported from core.
7
+ */
8
+ export * from './use-gcode-preview.js';
9
+ export { GcodePreview } from './gcode-preview-component.js';
@@ -0,0 +1,27 @@
1
+ import type { GcodeParseSession, WireParseOptions } from '@chestnutlabs/gcode-parser';
2
+ import type { ToolpathRenderer } from '@chestnutlabs/gcode-renderer-three';
3
+ import type { MappedProgress, ProgressObservation } from '@chestnutlabs/toolpath-core';
4
+ import { type GcodePreviewControls, type GcodePreviewState, type ParseOutcome, type PreviewControllerOptions, type PreviewEvent } from '@chestnutlabs/gcode-preview-core';
5
+ export type { GcodePreviewControls, GcodePreviewState, ParseOutcome, PreviewEvent } from '@chestnutlabs/gcode-preview-core';
6
+ /** React-facing options — identical to the controller's (the alias is the public name). */
7
+ export type UseGcodePreviewOptions = PreviewControllerOptions;
8
+ export interface GcodePreviewHandle {
9
+ /** Attach as `<canvas ref={preview.canvasRef} />`; null detaches safely. */
10
+ canvasRef: (el: HTMLCanvasElement | null) => void;
11
+ parse(input: Uint8Array | ArrayBuffer | File, opts?: WireParseOptions): Promise<ParseOutcome>;
12
+ cancel(): void;
13
+ observeProgress(obs: ProgressObservation): MappedProgress | null;
14
+ tickProgress(nowMs: number): MappedProgress | null;
15
+ clearProgress(): void;
16
+ /** Current state snapshot — render-subscribed via useSyncExternalStore. */
17
+ state: GcodePreviewState;
18
+ controls: GcodePreviewControls;
19
+ raw: {
20
+ session: GcodeParseSession;
21
+ renderer: () => ToolpathRenderer | null;
22
+ };
23
+ onEvent(cb: (e: PreviewEvent) => void): () => void;
24
+ dispose(): void;
25
+ }
26
+ export declare function useGcodePreview(options?: UseGcodePreviewOptions): GcodePreviewHandle;
27
+ //# sourceMappingURL=use-gcode-preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-gcode-preview.d.ts","sourceRoot":"","sources":["../src/use-gcode-preview.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACtF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvF,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EAEjB,KAAK,wBAAwB,EAC7B,KAAK,YAAY,EAClB,MAAM,kCAAkC,CAAC;AAG1C,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACb,MAAM,kCAAkC,CAAC;AAE1C,2FAA2F;AAC3F,MAAM,MAAM,sBAAsB,GAAG,wBAAwB,CAAC;AAE9D,MAAM,WAAW,kBAAkB;IACjC,4EAA4E;IAC5E,SAAS,EAAE,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI,KAAK,IAAI,CAAC;IAClD,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,WAAW,GAAG,IAAI,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC9F,MAAM,IAAI,IAAI,CAAC;IACf,eAAe,CAAC,GAAG,EAAE,mBAAmB,GAAG,cAAc,GAAG,IAAI,CAAC;IACjE,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAAC;IACnD,aAAa,IAAI,IAAI,CAAC;IACtB,2EAA2E;IAC3E,KAAK,EAAE,iBAAiB,CAAC;IACzB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,GAAG,EAAE;QAAE,OAAO,EAAE,iBAAiB,CAAC;QAAC,QAAQ,EAAE,MAAM,gBAAgB,GAAG,IAAI,CAAA;KAAE,CAAC;IAC7E,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACnD,OAAO,IAAI,IAAI,CAAC;CACjB;AAUD,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,kBAAkB,CAiExF"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * useGcodePreview — the React reactivity bridge over the framework-neutral controller
3
+ * (DD-007 §4.6 D1 amendment, phase 6, issue #113). This file owns ONLY the React-isms:
4
+ * a `useSyncExternalStore` subscription to controller snapshots, a callback canvas ref,
5
+ * and effect-scoped disposal. Engine mechanics and contracts live in
6
+ * @chestnutlabs/gcode-preview-core and are RE-EXPORTED, never redeclared.
7
+ *
8
+ * StrictMode safety: React 18 dev mounts effects twice (setup → cleanup → setup). Every
9
+ * delegate goes through `ensure()`, which lazily (re)creates the controller after a
10
+ * cleanup disposed it — the remounted subscription lands on the fresh controller.
11
+ */
12
+ import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
13
+ import { createPreviewController } from '@chestnutlabs/gcode-preview-core';
14
+ export function useGcodePreview(options = {}) {
15
+ const optionsRef = useRef(options);
16
+ optionsRef.current = options;
17
+ const slot = useRef({ controller: null, disposed: false, canvas: null });
18
+ const ensure = useCallback(() => {
19
+ const s = slot.current;
20
+ if (s.controller === null || s.disposed) {
21
+ s.controller = createPreviewController(optionsRef.current);
22
+ s.disposed = false;
23
+ }
24
+ return s.controller;
25
+ }, []);
26
+ const subscribe = useCallback((cb) => ensure().onStateChange(() => cb()), [ensure]);
27
+ const getSnapshot = useCallback(() => ensure().getState(), [ensure]);
28
+ const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
29
+ useEffect(() => {
30
+ const c = ensure(); // (re)create on mount — StrictMode's second setup lands here
31
+ // Refs attached BEFORE StrictMode's dispose/recreate cycle: rebind the canvas the
32
+ // ref delivered so the fresh controller actually renders (refs never re-fire).
33
+ if (slot.current.canvas !== null && c.raw.renderer() === null) {
34
+ c.bindCanvas(slot.current.canvas);
35
+ }
36
+ return () => {
37
+ slot.current.controller?.dispose();
38
+ slot.current.disposed = true;
39
+ };
40
+ }, [ensure]);
41
+ // The handle (incl. canvasRef) must be IDENTITY-STABLE across state changes: a fresh
42
+ // canvasRef each render would make React detach/reattach the ref, rebuilding the
43
+ // renderer per snapshot — an infinite loop. `state` reads through a per-render ref.
44
+ const stateRef = useRef(state);
45
+ stateRef.current = state;
46
+ return useMemo(() => ({
47
+ canvasRef: (el) => {
48
+ slot.current.canvas = el;
49
+ if (el === null)
50
+ slot.current.controller?.bindCanvas(null);
51
+ else
52
+ ensure().bindCanvas(el);
53
+ },
54
+ parse: (input, opts) => ensure().parse(input, opts),
55
+ cancel: () => slot.current.controller?.cancel(),
56
+ observeProgress: (obs) => slot.current.controller?.observeProgress(obs) ?? null,
57
+ tickProgress: (nowMs) => slot.current.controller?.tickProgress(nowMs) ?? null,
58
+ clearProgress: () => slot.current.controller?.clearProgress(),
59
+ get state() {
60
+ return stateRef.current;
61
+ },
62
+ get controls() {
63
+ return ensure().controls;
64
+ },
65
+ get raw() {
66
+ return ensure().raw;
67
+ },
68
+ onEvent: (cb) => ensure().onEvent(cb),
69
+ dispose: () => {
70
+ slot.current.controller?.dispose();
71
+ slot.current.disposed = true;
72
+ }
73
+ }), [ensure]);
74
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@chestnutlabs/gcode-preview-react",
3
+ "version": "0.1.0",
4
+ "description": "Thin React integration for the Chestnut Labs G-code viewer (DD-007 D1 amendment): useGcodePreview hook + GcodePreview component as a reactivity bridge over @chestnutlabs/gcode-preview-core.",
5
+ "keywords": [
6
+ "gcode",
7
+ "3d-printing",
8
+ "toolpath",
9
+ "cnc",
10
+ "chestnutlabs",
11
+ "react",
12
+ "hooks",
13
+ "component",
14
+ "viewer",
15
+ "preview"
16
+ ],
17
+ "author": "Chestnut Labs",
18
+ "homepage": "https://github.com/ChestnutLabs/gcode-preview#readme",
19
+ "bugs": "https://github.com/ChestnutLabs/gcode-preview/issues",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/ChestnutLabs/gcode-preview.git",
23
+ "directory": "packages/gcode-preview-react"
24
+ },
25
+ "license": "MIT",
26
+ "engines": {
27
+ "node": ">=22"
28
+ },
29
+ "type": "module",
30
+ "sideEffects": false,
31
+ "main": "./dist/index.js",
32
+ "module": "./dist/index.js",
33
+ "types": "./dist/index.d.ts",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js"
38
+ }
39
+ },
40
+ "files": [
41
+ "dist"
42
+ ],
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "scripts": {
47
+ "build": "tsc -p tsconfig.json",
48
+ "typecheck": "tsc -p tsconfig.json --noEmit",
49
+ "test": "vitest run"
50
+ },
51
+ "dependencies": {
52
+ "@chestnutlabs/gcode-parser": "0.1.0",
53
+ "@chestnutlabs/gcode-preview-core": "0.1.0",
54
+ "@chestnutlabs/gcode-renderer-three": "0.1.0",
55
+ "@chestnutlabs/toolpath-core": "0.1.0"
56
+ },
57
+ "peerDependencies": {
58
+ "react": "^18.0.0 || ^19.0.0"
59
+ },
60
+ "devDependencies": {
61
+ "@types/react": "^18.3.0",
62
+ "react": "^18.3.0",
63
+ "react-dom": "^18.3.0"
64
+ }
65
+ }