@orb3d/react 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Amir Abushanab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,44 @@
1
- # Temporary Holding Version
1
+ # @orb3d/react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A drop-in glass orb with states for React.
4
+
5
+ ```sh
6
+ pnpm add @orb3d/react
7
+ ```
8
+
9
+ ```tsx
10
+ import { Orb3D } from "@orb3d/react";
11
+
12
+ export function Status({ state }: { state: "idle" | "thinking" | "speaking" }) {
13
+ return <Orb3D preset="aura" state={state} poster="/orb.png" style={{ width: 96, height: 96 }} />;
14
+ }
15
+ ```
16
+
17
+ Props: `preset` (a name, lazy-loaded, or a function, tree-shaken: `clear`, the default, `aura`,
18
+ `landscape`, `sunset`, `dawn`, `storm`, `night`), `state`, `level` (0 to 1), `config` (merged last,
19
+ block by block), the flat `size`, `background`, `speed` and `paused`, and
20
+ the shell's mount-time options: `poster`, `posterFit`, `fadeMs`, `lazy`, `webgl`,
21
+ `respectReducedMotion`, `adaptiveQuality`, `minSizeForWebGL`, `onReady`, `onFallback`, `onPhaseChange`.
22
+
23
+ `adaptiveQuality` is opt-in for large orbs: it lowers live resolution during sustained slow frames
24
+ and gradually recovers, while snapshots retain the authored resolution.
25
+
26
+ Changing `state` springs the orb to it and rebuilds nothing. For audio, drive the level through the
27
+ ref rather than a prop, so React doesn't re-render every frame:
28
+
29
+ ```tsx
30
+ const orb = useRef<OrbHandle>(null);
31
+ useEffect(() => analyser.onLevel((rms) => orb.current?.setLevel(rms * 6)), []);
32
+ return <Orb3D ref={orb} preset="aura" state="speaking" />;
33
+ ```
34
+
35
+ Anything can go in the glass through `config`:
36
+
37
+ ```tsx
38
+ <Orb3D config={{ contents: { type: "image", src: "/avatar.jpg" } }} />
39
+ <Orb3D config={{ contents: { type: "video", src: "/loop.mp4" } }} />
40
+ <Orb3D preset="aura" config={{ glass: { frost: 0.3, tint: "#cfe3ff" } }} />
41
+ ```
42
+
43
+ It renders a `<div>`, so it is safe to server-render; pass `<img data-orb3d-poster src="…" />` as
44
+ a child for a poster in the server HTML, which the shell adopts.
@@ -0,0 +1,64 @@
1
+ import { CSSProperties, ReactNode } from "react";
2
+ import { FallbackReason, FallbackReason as FallbackReason$1, OrbConfig, OrbConfigInput, OrbHandle, OrbHandle as OrbHandle$1, OrbPhase, OrbPhase as OrbPhase$1, OrbRenderer, PosterFit, SnapshotOptions } from "@orb3d/core";
3
+ //#region src/index.d.ts
4
+ /** Flat props for the settings people actually reach for; anything else goes through `config`. */
5
+ interface FlatProps {
6
+ /** The orb's diameter as a share of the box's shorter side. Below 1 leaves room for a halo. */
7
+ size?: number;
8
+ /** `"transparent"` (the default) composites over the page; a colour fills behind the orb. */
9
+ background?: string;
10
+ /** Scales every motion at once. */
11
+ speed?: number;
12
+ paused?: boolean;
13
+ }
14
+ interface Orb3DProps extends FlatProps {
15
+ /** A preset: a function (tree-shakeable) or a name string (lazy-imports the presets chunk). */
16
+ preset?: string | (() => OrbConfigInput);
17
+ /**
18
+ * The state to show, by name (`idle`, `listening`, `thinking`, `speaking`, `muted` in the
19
+ * built-in presets). Changing it springs the orb to the new state; it never rebuilds anything.
20
+ */
21
+ state?: string;
22
+ /**
23
+ * The input level, 0 to 1. Fine for a value that changes now and then; for audio, call
24
+ * `ref.current.setLevel()` from your analyser loop instead, so React doesn't re-render per frame.
25
+ */
26
+ level?: number;
27
+ /**
28
+ * Escape hatch: a full or partial config, applied last and merged block by block, so
29
+ * `config={{ glass: { frost: 0.5 } }}` keeps the rest of the glass, and
30
+ * `config={{ contents: { type: "image", src: "/photo.jpg" } }}` puts a picture in it.
31
+ * Precedence: default ← preset ← flat props ← config.
32
+ */
33
+ config?: OrbConfigInput;
34
+ poster?: string;
35
+ /** Poster `object-fit`. Default `"fill"` (matches the canvas, so the handoff is seamless). */
36
+ posterFit?: PosterFit;
37
+ /** Poster→canvas crossfade in ms. Default 300. */
38
+ fadeMs?: number;
39
+ lazy?: boolean;
40
+ webgl?: "auto" | "force" | "off";
41
+ respectReducedMotion?: boolean;
42
+ adaptiveQuality?: boolean;
43
+ /** Stay a poster below this box size in CSS pixels. */
44
+ minSizeForWebGL?: number;
45
+ className?: string;
46
+ style?: CSSProperties;
47
+ /** Custom SSR poster markup, e.g. `<img data-orb3d-poster src="…" />`; the shell adopts it. */
48
+ children?: ReactNode;
49
+ onReady?: (renderer: OrbRenderer) => void;
50
+ onFallback?: (reason: FallbackReason$1) => void;
51
+ onPhaseChange?: (phase: OrbPhase$1) => void;
52
+ }
53
+ /**
54
+ * A drop-in orb. Renders a `<div>` (SSR-safe; pass an `<img data-orb3d-poster>` child for a
55
+ * server-rendered poster) and, on the client, mounts the shell: poster-first, lazy, WebGL2,
56
+ * reduced-motion and Save-Data aware, with the engine code-split out.
57
+ *
58
+ * A `ref` receives the {@link OrbHandle}: `setState()`, `setLevel()`, `snapshot()`, `set()`,
59
+ * `play()`, `pause()`.
60
+ */
61
+ declare const Orb3D: import("react").ForwardRefExoticComponent<Orb3DProps & import("react").RefAttributes<OrbHandle$1>>;
62
+ //#endregion
63
+ export { type FallbackReason, Orb3D, Orb3D as default, Orb3DProps, type OrbConfig, type OrbHandle, type OrbPhase, type SnapshotOptions };
64
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,165 @@
1
+ "use client";
2
+ import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from "react";
3
+ import { createDefaultConfig, createOrb, ensureOrbConfig, mergeOrbConfig } from "@orb3d/core";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region src/index.tsx
6
+ /** Resolve the base config: function preset (sync) → its config; string preset → lazy-load. */
7
+ async function resolveBase(preset) {
8
+ if (typeof preset === "string") {
9
+ const { PRESETS, isPresetName } = await import("@orb3d/core/presets");
10
+ if (isPresetName(preset)) return PRESETS[preset]();
11
+ }
12
+ return syncBase(preset);
13
+ }
14
+ /** The base we can build synchronously (function preset or default); a string preset resolves later. */
15
+ function syncBase(preset) {
16
+ return typeof preset === "function" ? ensureOrbConfig(mergeOrbConfig(createDefaultConfig(), preset())) : createDefaultConfig();
17
+ }
18
+ /** Apply the flat props (and the config escape hatch) onto a full base config. */
19
+ function buildConfig(base, props) {
20
+ const out = structuredClone(base);
21
+ if (props.size !== void 0) out.size = props.size;
22
+ if (props.background !== void 0) out.background = props.background;
23
+ if (props.speed !== void 0) out.speed = props.speed;
24
+ if (props.paused !== void 0) out.paused = props.paused;
25
+ delete out.state;
26
+ const config = props.config ? mergeOrbConfig(out, props.config) : out;
27
+ if (props.state !== void 0) delete config.state;
28
+ return config;
29
+ }
30
+ /**
31
+ * Function presets are keyed by identity: swapping one function for another must re-run the update
32
+ * effect, and a string cannot say which function it saw. An inline function gets a fresh id each
33
+ * render; memoize the preset if you want the effect to skip entirely.
34
+ */
35
+ const presetIds = /* @__PURE__ */ new WeakMap();
36
+ let nextPresetId = 0;
37
+ function presetKey(preset) {
38
+ if (typeof preset !== "function") return preset;
39
+ let id = presetIds.get(preset);
40
+ if (id === void 0) presetIds.set(preset, id = nextPresetId++);
41
+ return `fn#${id}`;
42
+ }
43
+ /** A stable string that changes whenever the resolved config would; keys the update effect. */
44
+ function configKey(props) {
45
+ return JSON.stringify({
46
+ preset: presetKey(props.preset),
47
+ flat: [
48
+ props.size,
49
+ props.background,
50
+ props.speed,
51
+ props.paused
52
+ ],
53
+ config: props.config
54
+ });
55
+ }
56
+ function Orb3DInner(props, ref) {
57
+ const { className, style, children } = props;
58
+ const containerRef = useRef(null);
59
+ const handleRef = useRef(null);
60
+ const cbRef = useRef({});
61
+ cbRef.current.onReady = props.onReady;
62
+ cbRef.current.onFallback = props.onFallback;
63
+ cbRef.current.onPhaseChange = props.onPhaseChange;
64
+ const key = useMemo(() => configKey(props), [
65
+ props.preset,
66
+ props.size,
67
+ props.background,
68
+ props.speed,
69
+ props.paused,
70
+ props.config
71
+ ]);
72
+ const appliedKeyRef = useRef(null);
73
+ const configRevisionRef = useRef(0);
74
+ useImperativeHandle(ref, () => ({
75
+ get phase() {
76
+ return handleRef.current?.phase ?? "poster";
77
+ },
78
+ get renderer() {
79
+ return handleRef.current?.renderer ?? null;
80
+ },
81
+ get state() {
82
+ return handleRef.current?.state ?? props.state ?? "idle";
83
+ },
84
+ setState: (name) => handleRef.current?.setState(name),
85
+ setLevel: (value) => handleRef.current?.setLevel(value),
86
+ setWeather: (snapshot, options) => handleRef.current?.setWeather(snapshot, options),
87
+ snapshot: (opts) => handleRef.current?.snapshot(opts) ?? Promise.resolve(null),
88
+ set: (config) => handleRef.current?.set(config),
89
+ play: () => handleRef.current?.play(),
90
+ pause: () => handleRef.current?.pause(),
91
+ destroy: () => handleRef.current?.destroy()
92
+ }), []);
93
+ useEffect(() => {
94
+ const container = containerRef.current;
95
+ if (!container) return;
96
+ let cancelled = false;
97
+ const revision = ++configRevisionRef.current;
98
+ const options = {
99
+ poster: props.poster,
100
+ posterFit: props.posterFit,
101
+ fadeMs: props.fadeMs,
102
+ lazy: props.lazy,
103
+ webgl: props.webgl,
104
+ respectReducedMotion: props.respectReducedMotion,
105
+ adaptiveQuality: props.adaptiveQuality,
106
+ minSizeForWebGL: props.minSizeForWebGL,
107
+ onReady: (r) => cbRef.current.onReady?.(r),
108
+ onFallback: (reason) => cbRef.current.onFallback?.(reason),
109
+ onPhaseChange: (phase) => cbRef.current.onPhaseChange?.(phase)
110
+ };
111
+ const initial = buildConfig(syncBase(props.preset), props);
112
+ if (props.state !== void 0) initial.state = props.state;
113
+ const handle = createOrb(container, initial, options);
114
+ handleRef.current = handle;
115
+ appliedKeyRef.current = key;
116
+ if (props.level !== void 0) handle.setLevel(props.level);
117
+ if (typeof props.preset === "string") resolveBase(props.preset).then((base) => {
118
+ if (!cancelled && handleRef.current === handle && configRevisionRef.current === revision) handle.set(buildConfig(base, props));
119
+ });
120
+ return () => {
121
+ cancelled = true;
122
+ configRevisionRef.current++;
123
+ handle.destroy();
124
+ handleRef.current = null;
125
+ appliedKeyRef.current = null;
126
+ };
127
+ }, []);
128
+ useEffect(() => {
129
+ const handle = handleRef.current;
130
+ if (!handle || appliedKeyRef.current === key) return;
131
+ appliedKeyRef.current = key;
132
+ const revision = ++configRevisionRef.current;
133
+ const apply = (base) => {
134
+ if (handleRef.current === handle && configRevisionRef.current === revision) handle.set(buildConfig(base, props));
135
+ };
136
+ if (typeof props.preset === "string") resolveBase(props.preset).then(apply);
137
+ else apply(syncBase(props.preset));
138
+ }, [key]);
139
+ useEffect(() => {
140
+ if (props.state !== void 0) handleRef.current?.setState(props.state);
141
+ }, [props.state]);
142
+ useEffect(() => {
143
+ if (props.level !== void 0) handleRef.current?.setLevel(props.level);
144
+ }, [props.level]);
145
+ return /* @__PURE__ */ jsx("div", {
146
+ ref: containerRef,
147
+ className,
148
+ style,
149
+ children
150
+ });
151
+ }
152
+ /**
153
+ * A drop-in orb. Renders a `<div>` (SSR-safe; pass an `<img data-orb3d-poster>` child for a
154
+ * server-rendered poster) and, on the client, mounts the shell: poster-first, lazy, WebGL2,
155
+ * reduced-motion and Save-Data aware, with the engine code-split out.
156
+ *
157
+ * A `ref` receives the {@link OrbHandle}: `setState()`, `setLevel()`, `snapshot()`, `set()`,
158
+ * `play()`, `pause()`.
159
+ */
160
+ const Orb3D = forwardRef(Orb3DInner);
161
+ Orb3D.displayName = "Orb3D";
162
+ //#endregion
163
+ export { Orb3D, Orb3D as default };
164
+
165
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from \"react\";\nimport type { CSSProperties, ForwardedRef, ReactElement, ReactNode } from \"react\";\nimport { createDefaultConfig, createOrb, ensureOrbConfig, mergeOrbConfig } from \"@orb3d/core\";\nimport type {\n FallbackReason,\n OrbConfigInput,\n OrbConfig,\n OrbHandle,\n OrbOptions,\n OrbPhase,\n OrbRenderer,\n PosterFit,\n} from \"@orb3d/core\";\n\n/** Flat props for the settings people actually reach for; anything else goes through `config`. */\ninterface FlatProps {\n /** The orb's diameter as a share of the box's shorter side. Below 1 leaves room for a halo. */\n size?: number;\n /** `\"transparent\"` (the default) composites over the page; a colour fills behind the orb. */\n background?: string;\n /** Scales every motion at once. */\n speed?: number;\n paused?: boolean;\n}\n\nexport interface Orb3DProps extends FlatProps {\n /** A preset: a function (tree-shakeable) or a name string (lazy-imports the presets chunk). */\n preset?: string | (() => OrbConfigInput);\n /**\n * The state to show, by name (`idle`, `listening`, `thinking`, `speaking`, `muted` in the\n * built-in presets). Changing it springs the orb to the new state; it never rebuilds anything.\n */\n state?: string;\n /**\n * The input level, 0 to 1. Fine for a value that changes now and then; for audio, call\n * `ref.current.setLevel()` from your analyser loop instead, so React doesn't re-render per frame.\n */\n level?: number;\n /**\n * Escape hatch: a full or partial config, applied last and merged block by block, so\n * `config={{ glass: { frost: 0.5 } }}` keeps the rest of the glass, and\n * `config={{ contents: { type: \"image\", src: \"/photo.jpg\" } }}` puts a picture in it.\n * Precedence: default ← preset ← flat props ← config.\n */\n config?: OrbConfigInput;\n poster?: string;\n /** Poster `object-fit`. Default `\"fill\"` (matches the canvas, so the handoff is seamless). */\n posterFit?: PosterFit;\n /** Poster→canvas crossfade in ms. Default 300. */\n fadeMs?: number;\n lazy?: boolean;\n webgl?: \"auto\" | \"force\" | \"off\";\n respectReducedMotion?: boolean;\n adaptiveQuality?: boolean;\n /** Stay a poster below this box size in CSS pixels. */\n minSizeForWebGL?: number;\n className?: string;\n style?: CSSProperties;\n /** Custom SSR poster markup, e.g. `<img data-orb3d-poster src=\"…\" />`; the shell adopts it. */\n children?: ReactNode;\n onReady?: (renderer: OrbRenderer) => void;\n onFallback?: (reason: FallbackReason) => void;\n onPhaseChange?: (phase: OrbPhase) => void;\n}\n\n/** Resolve the base config: function preset (sync) → its config; string preset → lazy-load. */\nasync function resolveBase(preset: Orb3DProps[\"preset\"]): Promise<OrbConfig> {\n if (typeof preset === \"string\") {\n const { PRESETS, isPresetName } = await import(\"@orb3d/core/presets\");\n if (isPresetName(preset)) return PRESETS[preset]();\n }\n return syncBase(preset);\n}\n\n/** The base we can build synchronously (function preset or default); a string preset resolves later. */\nfunction syncBase(preset: Orb3DProps[\"preset\"]): OrbConfig {\n return typeof preset === \"function\"\n ? ensureOrbConfig(mergeOrbConfig(createDefaultConfig(), preset()))\n : createDefaultConfig();\n}\n\n/** Apply the flat props (and the config escape hatch) onto a full base config. */\nfunction buildConfig(base: OrbConfig, props: Orb3DProps): OrbConfigInput {\n // A function preset may hand back the same object on every call, so copy before writing.\n const out: OrbConfigInput = structuredClone(base);\n if (props.size !== undefined) out.size = props.size;\n if (props.background !== undefined) out.background = props.background;\n if (props.speed !== undefined) out.speed = props.speed;\n if (props.paused !== undefined) out.paused = props.paused;\n // The state prop is applied through setState, never through the config: a config change cuts\n // the dials, and a state change should spring.\n delete out.state;\n const config = props.config ? mergeOrbConfig(out, props.config) : out;\n if (props.state !== undefined) delete config.state;\n return config;\n}\n\n/**\n * Function presets are keyed by identity: swapping one function for another must re-run the update\n * effect, and a string cannot say which function it saw. An inline function gets a fresh id each\n * render; memoize the preset if you want the effect to skip entirely.\n */\nconst presetIds = new WeakMap<object, number>();\nlet nextPresetId = 0;\nfunction presetKey(preset: Orb3DProps[\"preset\"]): string | undefined {\n if (typeof preset !== \"function\") return preset;\n let id = presetIds.get(preset);\n if (id === undefined) presetIds.set(preset, (id = nextPresetId++));\n return `fn#${id}`;\n}\n\n/** A stable string that changes whenever the resolved config would; keys the update effect. */\nfunction configKey(props: Orb3DProps): string {\n return JSON.stringify({\n preset: presetKey(props.preset),\n flat: [props.size, props.background, props.speed, props.paused],\n config: props.config,\n });\n}\n\nfunction Orb3DInner(props: Orb3DProps, ref: ForwardedRef<OrbHandle>): ReactElement {\n const { className, style, children } = props;\n const containerRef = useRef<HTMLDivElement>(null);\n const handleRef = useRef<OrbHandle | null>(null);\n // Keep the latest callbacks in a ref so they never force a remount.\n const cbRef = useRef<Pick<Orb3DProps, \"onReady\" | \"onFallback\" | \"onPhaseChange\">>({});\n cbRef.current.onReady = props.onReady;\n cbRef.current.onFallback = props.onFallback;\n cbRef.current.onPhaseChange = props.onPhaseChange;\n\n const key = useMemo(\n () => configKey(props),\n [props.preset, props.size, props.background, props.speed, props.paused, props.config],\n );\n // The key the live handle was last given, so the update effect can tell a real change from its\n // own first run (and from StrictMode's replay), both of which the mount effect already covered.\n const appliedKeyRef = useRef<string | null>(null);\n const configRevisionRef = useRef(0);\n\n // A stable object that delegates to whichever handle is live, so a ref taken before the shell\n // mounted, or across a StrictMode remount, keeps working.\n useImperativeHandle(\n ref,\n () => ({\n get phase() {\n return handleRef.current?.phase ?? \"poster\";\n },\n get renderer() {\n return handleRef.current?.renderer ?? null;\n },\n get state() {\n return handleRef.current?.state ?? props.state ?? \"idle\";\n },\n setState: (name) => handleRef.current?.setState(name),\n setLevel: (value) => handleRef.current?.setLevel(value),\n setWeather: (snapshot, options) => handleRef.current?.setWeather(snapshot, options),\n snapshot: (opts) => handleRef.current?.snapshot(opts) ?? Promise.resolve(null),\n set: (config) => handleRef.current?.set(config),\n play: () => handleRef.current?.play(),\n pause: () => handleRef.current?.pause(),\n destroy: () => handleRef.current?.destroy(),\n }),\n [],\n );\n\n // Mount once. StrictMode's double mount is safe: destroy() aborts a pending upgrade, and the\n // pre-upgrade create/destroy is DOM-only (poster + IntersectionObserver).\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n let cancelled = false;\n const revision = ++configRevisionRef.current;\n const options: OrbOptions = {\n poster: props.poster,\n posterFit: props.posterFit,\n fadeMs: props.fadeMs,\n lazy: props.lazy,\n webgl: props.webgl,\n respectReducedMotion: props.respectReducedMotion,\n adaptiveQuality: props.adaptiveQuality,\n minSizeForWebGL: props.minSizeForWebGL,\n onReady: (r) => cbRef.current.onReady?.(r),\n onFallback: (reason) => cbRef.current.onFallback?.(reason),\n onPhaseChange: (phase) => cbRef.current.onPhaseChange?.(phase),\n };\n const initial = buildConfig(syncBase(props.preset), props);\n if (props.state !== undefined) initial.state = props.state;\n const handle = createOrb(container, initial, options);\n handleRef.current = handle;\n appliedKeyRef.current = key;\n if (props.level !== undefined) handle.setLevel(props.level);\n // A string preset resolves asynchronously; stage the real config once it loads. This is the one\n // set a mount makes: the update effect below sees the key it already applied and skips.\n if (typeof props.preset === \"string\") {\n void resolveBase(props.preset).then((base) => {\n if (!cancelled && handleRef.current === handle && configRevisionRef.current === revision)\n handle.set(buildConfig(base, props));\n });\n }\n return () => {\n cancelled = true;\n configRevisionRef.current++;\n handle.destroy();\n handleRef.current = null;\n appliedKeyRef.current = null;\n };\n // Mount-time only: options are captured once; config, state and level flow through the\n // effects below.\n }, []);\n\n // Push config changes (flat props, config, preset) to the live handle.\n useEffect(() => {\n const handle = handleRef.current;\n if (!handle || appliedKeyRef.current === key) return;\n appliedKeyRef.current = key;\n const revision = ++configRevisionRef.current;\n const apply = (base: OrbConfig): void => {\n if (handleRef.current === handle && configRevisionRef.current === revision)\n handle.set(buildConfig(base, props));\n };\n if (typeof props.preset === \"string\") void resolveBase(props.preset).then(apply);\n else apply(syncBase(props.preset));\n }, [key]);\n\n useEffect(() => {\n if (props.state !== undefined) handleRef.current?.setState(props.state);\n }, [props.state]);\n\n useEffect(() => {\n if (props.level !== undefined) handleRef.current?.setLevel(props.level);\n }, [props.level]);\n\n return (\n <div ref={containerRef} className={className} style={style}>\n {children}\n </div>\n );\n}\n\n/**\n * A drop-in orb. Renders a `<div>` (SSR-safe; pass an `<img data-orb3d-poster>` child for a\n * server-rendered poster) and, on the client, mounts the shell: poster-first, lazy, WebGL2,\n * reduced-motion and Save-Data aware, with the engine code-split out.\n *\n * A `ref` receives the {@link OrbHandle}: `setState()`, `setLevel()`, `snapshot()`, `set()`,\n * `play()`, `pause()`.\n */\nexport const Orb3D = forwardRef(Orb3DInner);\nOrb3D.displayName = \"Orb3D\";\n\nexport default Orb3D;\nexport type { OrbConfig, OrbHandle, OrbPhase, FallbackReason, SnapshotOptions } from \"@orb3d/core\";\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@orb3d/react",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Drop-in glass orb with states for React: the <Orb3D> component",
5
+ "keywords": [
6
+ "agent",
7
+ "avatar",
8
+ "component",
9
+ "glass",
10
+ "orb",
11
+ "react",
12
+ "status",
13
+ "voice",
14
+ "webgl"
15
+ ],
16
+ "homepage": "https://github.com/Amir-Abushanab/orb3d/tree/main/packages/react#readme",
17
+ "bugs": "https://github.com/Amir-Abushanab/orb3d/issues",
18
+ "license": "MIT",
19
+ "author": "Amir Abushanab (https://github.com/Amir-Abushanab)",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/Amir-Abushanab/orb3d.git",
23
+ "directory": "packages/react"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "type": "module",
29
+ "sideEffects": false,
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ }
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "@orb3d/core": "^0.1.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/react": "^19.0.0",
44
+ "react": "^19.0.0"
45
+ },
46
+ "peerDependencies": {
47
+ "@types/react": ">=18",
48
+ "react": ">=18"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "@types/react": {
52
+ "optional": true
53
+ }
54
+ },
55
+ "engines": {
56
+ "node": ">=20.19"
57
+ },
58
+ "scripts": {
59
+ "build": "tsdown",
60
+ "typecheck": "tsc --noEmit"
61
+ },
62
+ "main": "./dist/index.js",
63
+ "types": "./dist/index.d.ts"
6
64
  }