react-native-model-viewer-webview 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.
@@ -0,0 +1,86 @@
1
+ # API Reference For Agents
2
+
3
+ ## Component
4
+
5
+ ```tsx
6
+ <ModelViewerWebView
7
+ modelSource={source}
8
+ modelUri="https://example.com/model.glb"
9
+ htmlOptions={options}
10
+ onStatus={(status, event) => {}}
11
+ onModelLoaded={(status, event) => {}}
12
+ onModelError={(status, event) => {}}
13
+ webViewBaseUrl="https://localhost/"
14
+ />
15
+ ```
16
+
17
+ `ModelViewerWebView` accepts all `react-native-webview` props except `source`
18
+ and `onMessage`.
19
+
20
+ Prefer `modelSource` over `modelUri`.
21
+
22
+ ## ModelSource
23
+
24
+ ```ts
25
+ type ModelSource =
26
+ | string
27
+ | number
28
+ | {
29
+ localUri?: null | string;
30
+ uri?: null | string;
31
+ };
32
+ ```
33
+
34
+ Use:
35
+
36
+ - remote URL: `"https://example.com/model.glb"`
37
+ - data URI: `"data:model/gltf-binary;base64,..."`
38
+ - file URI: `"file:///.../model.glb"`
39
+ - static asset: `require("./assets/model.glb")`
40
+ - Expo asset object: `Asset.fromModule(require("./assets/model.glb"))`
41
+
42
+ ## HTML Options
43
+
44
+ ```ts
45
+ type ModelViewerHtmlOptions = {
46
+ additionalAttributes?: Record<string, boolean | number | string | null | undefined>;
47
+ autoRotate?: boolean;
48
+ autoRotateDelay?: number | string;
49
+ backgroundColor?: string;
50
+ cameraControls?: boolean;
51
+ cameraOrbit?: string;
52
+ disablePan?: boolean;
53
+ exposure?: number | string;
54
+ interactionPrompt?: "auto" | "none";
55
+ maxCameraOrbit?: string;
56
+ minCameraOrbit?: string;
57
+ modelUri: string;
58
+ modelViewerScript?: string;
59
+ modelViewerScriptUrl?: string;
60
+ posterColor?: string;
61
+ rotationPerSecond?: string;
62
+ shadowIntensity?: number | string;
63
+ };
64
+ ```
65
+
66
+ Consumers pass `htmlOptions` without `modelUri`; the component injects it.
67
+
68
+ ## Utility Functions
69
+
70
+ ```ts
71
+ buildModelViewerHtml(options)
72
+ parseModelViewerMessage(data)
73
+ isModelViewerErrorStatus(status)
74
+ resolveModelSourceUri(modelSource)
75
+ ```
76
+
77
+ Use utilities for tests or custom wrappers. Most apps should use the component.
78
+
79
+ ## Event Types
80
+
81
+ - `dom-ready`
82
+ - `model-loaded`
83
+ - `model-error`
84
+ - `page-error`
85
+
86
+ Statuses ending in `error` trigger `onModelError`.
@@ -0,0 +1,19 @@
1
+ import type { ReactElement } from "react";
2
+ import type { WebViewMessageEvent, WebViewProps } from "react-native-webview";
3
+
4
+ import type { ModelViewerHtmlOptions, ModelViewerStatus } from "./model-viewer-html";
5
+ import type { ModelSource } from "./model-source";
6
+
7
+ export type ModelViewerWebViewProps = Omit<WebViewProps, "onMessage" | "source"> & {
8
+ htmlOptions?: Omit<ModelViewerHtmlOptions, "modelUri">;
9
+ modelSource?: ModelSource;
10
+ modelUri?: string;
11
+ onModelError?: (status: ModelViewerStatus, event: WebViewMessageEvent) => void;
12
+ onModelLoaded?: (status: ModelViewerStatus, event: WebViewMessageEvent) => void;
13
+ onStatus?: (status: ModelViewerStatus, event: WebViewMessageEvent) => void;
14
+ webViewBaseUrl?: string;
15
+ };
16
+
17
+ export declare function ModelViewerWebView(
18
+ props: ModelViewerWebViewProps,
19
+ ): ReactElement;
@@ -0,0 +1,90 @@
1
+ const React = require("react");
2
+ const { WebView } = require("react-native-webview");
3
+
4
+ const {
5
+ buildModelViewerHtml,
6
+ isModelViewerErrorStatus,
7
+ MODEL_VIEWER_LOADED_EVENT,
8
+ parseModelViewerMessage,
9
+ } = require("./model-viewer-html");
10
+ const { resolveModelSourceUri } = require("./model-source");
11
+
12
+ function ModelViewerWebView({
13
+ allowFileAccess = true,
14
+ allowFileAccessFromFileURLs = true,
15
+ allowUniversalAccessFromFileURLs = true,
16
+ androidLayerType = "hardware",
17
+ bounces = false,
18
+ domStorageEnabled = true,
19
+ htmlOptions,
20
+ javaScriptEnabled = true,
21
+ mixedContentMode = "always",
22
+ modelSource,
23
+ modelUri,
24
+ onModelError,
25
+ onModelLoaded,
26
+ onStatus,
27
+ originWhitelist = ["*"],
28
+ scrollEnabled = false,
29
+ setSupportMultipleWindows = false,
30
+ webViewBaseUrl = "https://localhost/",
31
+ ...webViewProps
32
+ }) {
33
+ const resolvedModelUri = React.useMemo(() => {
34
+ if (modelSource !== undefined) {
35
+ return resolveModelSourceUri(modelSource);
36
+ }
37
+
38
+ if (modelUri !== undefined) {
39
+ return modelUri;
40
+ }
41
+
42
+ throw new Error("ModelViewerWebView requires modelSource or modelUri.");
43
+ }, [modelSource, modelUri]);
44
+
45
+ const html = React.useMemo(
46
+ () =>
47
+ buildModelViewerHtml({
48
+ ...htmlOptions,
49
+ modelUri: resolvedModelUri,
50
+ }),
51
+ [htmlOptions, resolvedModelUri],
52
+ );
53
+
54
+ function handleMessage(event) {
55
+ const status = parseModelViewerMessage(event.nativeEvent.data);
56
+
57
+ onStatus?.(status, event);
58
+
59
+ if (status.type === MODEL_VIEWER_LOADED_EVENT) {
60
+ onModelLoaded?.(status, event);
61
+ return;
62
+ }
63
+
64
+ if (isModelViewerErrorStatus(status)) {
65
+ onModelError?.(status, event);
66
+ }
67
+ }
68
+
69
+ return React.createElement(WebView, {
70
+ allowFileAccess,
71
+ allowFileAccessFromFileURLs,
72
+ allowUniversalAccessFromFileURLs,
73
+ androidLayerType,
74
+ bounces,
75
+ domStorageEnabled,
76
+ javaScriptEnabled,
77
+ mixedContentMode,
78
+ onMessage: handleMessage,
79
+ originWhitelist,
80
+ scrollEnabled,
81
+ setSupportMultipleWindows,
82
+ source: {
83
+ baseUrl: webViewBaseUrl,
84
+ html,
85
+ },
86
+ ...webViewProps,
87
+ });
88
+ }
89
+
90
+ exports.ModelViewerWebView = ModelViewerWebView;
@@ -0,0 +1,20 @@
1
+ export {
2
+ ModelViewerWebView,
3
+ type ModelViewerWebViewProps,
4
+ } from "./ModelViewerWebView";
5
+ export {
6
+ buildModelViewerHtml,
7
+ DEFAULT_MODEL_VIEWER_SCRIPT_URL,
8
+ isModelViewerErrorStatus,
9
+ MODEL_VIEWER_DOM_READY_EVENT,
10
+ MODEL_VIEWER_LOADED_EVENT,
11
+ MODEL_VIEWER_MODEL_ERROR_EVENT,
12
+ MODEL_VIEWER_PAGE_ERROR_EVENT,
13
+ parseModelViewerMessage,
14
+ type ModelViewerHtmlOptions,
15
+ type ModelViewerStatus,
16
+ } from "./model-viewer-html";
17
+ export {
18
+ resolveModelSourceUri,
19
+ type ModelSource,
20
+ } from "./model-source";
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ const modelViewerHtml = require("./model-viewer-html");
2
+
3
+ Object.assign(exports, modelViewerHtml);
4
+
5
+ Object.defineProperty(exports, "ModelViewerWebView", {
6
+ enumerable: true,
7
+ get() {
8
+ return require("./ModelViewerWebView").ModelViewerWebView;
9
+ },
10
+ });
11
+
12
+ Object.defineProperty(exports, "resolveModelSourceUri", {
13
+ enumerable: true,
14
+ get() {
15
+ return require("./model-source").resolveModelSourceUri;
16
+ },
17
+ });
@@ -0,0 +1,9 @@
1
+ export type ModelSource =
2
+ | string
3
+ | number
4
+ | {
5
+ localUri?: null | string;
6
+ uri?: null | string;
7
+ };
8
+
9
+ export declare function resolveModelSourceUri(modelSource: ModelSource): string;
@@ -0,0 +1,31 @@
1
+ function resolveModelSourceUri(modelSource) {
2
+ if (typeof modelSource === "string") {
3
+ return modelSource;
4
+ }
5
+
6
+ if (
7
+ typeof modelSource === "object" &&
8
+ modelSource.localUri &&
9
+ modelSource.localUri.length > 0
10
+ ) {
11
+ return modelSource.localUri;
12
+ }
13
+
14
+ if (
15
+ typeof modelSource === "object" &&
16
+ modelSource.uri &&
17
+ modelSource.uri.length > 0
18
+ ) {
19
+ return modelSource.uri;
20
+ }
21
+
22
+ const { Image } = require("react-native");
23
+ const resolved = Image.resolveAssetSource(modelSource);
24
+ if (resolved?.uri) {
25
+ return resolved.uri;
26
+ }
27
+
28
+ throw new Error("Unable to resolve model source URI.");
29
+ }
30
+
31
+ exports.resolveModelSourceUri = resolveModelSourceUri;
@@ -0,0 +1,35 @@
1
+ export declare const DEFAULT_MODEL_VIEWER_SCRIPT_URL = "https://ajax.googleapis.com/ajax/libs/model-viewer/4.2.0/model-viewer.min.js";
2
+ export declare const MODEL_VIEWER_DOM_READY_EVENT = "dom-ready";
3
+ export declare const MODEL_VIEWER_LOADED_EVENT = "model-loaded";
4
+ export declare const MODEL_VIEWER_MODEL_ERROR_EVENT = "model-error";
5
+ export declare const MODEL_VIEWER_PAGE_ERROR_EVENT = "page-error";
6
+
7
+ export type ModelViewerStatus = {
8
+ message?: string;
9
+ rawData?: string;
10
+ type: string;
11
+ };
12
+
13
+ export type ModelViewerHtmlOptions = {
14
+ additionalAttributes?: Record<string, boolean | number | string | null | undefined>;
15
+ autoRotate?: boolean;
16
+ autoRotateDelay?: number | string;
17
+ backgroundColor?: string;
18
+ cameraControls?: boolean;
19
+ cameraOrbit?: string;
20
+ disablePan?: boolean;
21
+ exposure?: number | string;
22
+ interactionPrompt?: "auto" | "none";
23
+ maxCameraOrbit?: string;
24
+ minCameraOrbit?: string;
25
+ modelUri: string;
26
+ modelViewerScript?: string;
27
+ modelViewerScriptUrl?: string;
28
+ posterColor?: string;
29
+ rotationPerSecond?: string;
30
+ shadowIntensity?: number | string;
31
+ };
32
+
33
+ export declare function buildModelViewerHtml(options: ModelViewerHtmlOptions): string;
34
+ export declare function parseModelViewerMessage(data: string): ModelViewerStatus;
35
+ export declare function isModelViewerErrorStatus(status: ModelViewerStatus): boolean;
@@ -0,0 +1,187 @@
1
+ const DEFAULT_MODEL_VIEWER_SCRIPT_URL =
2
+ "https://ajax.googleapis.com/ajax/libs/model-viewer/4.2.0/model-viewer.min.js";
3
+
4
+ const MODEL_VIEWER_DOM_READY_EVENT = "dom-ready";
5
+ const MODEL_VIEWER_LOADED_EVENT = "model-loaded";
6
+ const MODEL_VIEWER_MODEL_ERROR_EVENT = "model-error";
7
+ const MODEL_VIEWER_PAGE_ERROR_EVENT = "page-error";
8
+
9
+ function buildModelViewerHtml(options) {
10
+ const backgroundColor = sanitizeCssColor(options.backgroundColor ?? "#ffffff");
11
+ const posterColor = sanitizeCssColor(options.posterColor ?? backgroundColor);
12
+ const modelViewerScriptTag = getModelViewerScriptTag(options);
13
+ const attributes = [
14
+ htmlAttribute("src", options.modelUri),
15
+ htmlAttribute("camera-controls", options.cameraControls ?? true),
16
+ htmlAttribute("auto-rotate", options.autoRotate ?? false),
17
+ htmlAttribute("auto-rotate-delay", options.autoRotateDelay),
18
+ htmlAttribute("rotation-per-second", options.rotationPerSecond),
19
+ htmlAttribute("interaction-prompt", options.interactionPrompt ?? "none"),
20
+ htmlAttribute("camera-orbit", options.cameraOrbit),
21
+ htmlAttribute("min-camera-orbit", options.minCameraOrbit),
22
+ htmlAttribute("max-camera-orbit", options.maxCameraOrbit),
23
+ htmlAttribute("shadow-intensity", options.shadowIntensity),
24
+ htmlAttribute("exposure", options.exposure),
25
+ htmlAttribute("disable-pan", options.disablePan ?? false),
26
+ ...Object.entries(options.additionalAttributes ?? {}).map(([name, value]) =>
27
+ htmlAttribute(name, value),
28
+ ),
29
+ ].join("");
30
+
31
+ return `<!doctype html>
32
+ <html>
33
+ <head>
34
+ <meta charset="utf-8" />
35
+ <meta
36
+ name="viewport"
37
+ content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
38
+ />
39
+ ${modelViewerScriptTag}
40
+ <script>
41
+ function postStatus(type, message) {
42
+ window.ReactNativeWebView &&
43
+ window.ReactNativeWebView.postMessage(JSON.stringify({ type, message }));
44
+ }
45
+
46
+ window.addEventListener("error", function (event) {
47
+ postStatus("${MODEL_VIEWER_PAGE_ERROR_EVENT}", event.message || "Page error");
48
+ });
49
+
50
+ window.addEventListener("unhandledrejection", function (event) {
51
+ postStatus("${MODEL_VIEWER_PAGE_ERROR_EVENT}", String(event.reason || "Unhandled rejection"));
52
+ });
53
+
54
+ window.addEventListener("DOMContentLoaded", function () {
55
+ var viewer = document.querySelector("model-viewer");
56
+ postStatus("${MODEL_VIEWER_DOM_READY_EVENT}", "Model viewer DOM ready");
57
+
58
+ if (!viewer) {
59
+ postStatus("${MODEL_VIEWER_MODEL_ERROR_EVENT}", "model-viewer element was not created");
60
+ return;
61
+ }
62
+
63
+ viewer.addEventListener("load", function () {
64
+ postStatus("${MODEL_VIEWER_LOADED_EVENT}", "3D model loaded");
65
+ });
66
+
67
+ viewer.addEventListener("error", function (event) {
68
+ var detail = event.detail || {};
69
+ var sourceError = detail.sourceError || {};
70
+ postStatus(
71
+ "${MODEL_VIEWER_MODEL_ERROR_EVENT}",
72
+ sourceError.message || detail.message || "3D model failed to load"
73
+ );
74
+ });
75
+ });
76
+ </script>
77
+ <style>
78
+ html,
79
+ body {
80
+ background: ${backgroundColor};
81
+ height: 100%;
82
+ margin: 0;
83
+ overflow: hidden;
84
+ width: 100%;
85
+ }
86
+
87
+ model-viewer {
88
+ --poster-color: ${posterColor};
89
+ background: ${backgroundColor};
90
+ height: 100%;
91
+ width: 100%;
92
+ }
93
+ </style>
94
+ </head>
95
+ <body>
96
+ <model-viewer${attributes}></model-viewer>
97
+ </body>
98
+ </html>`;
99
+ }
100
+
101
+ function parseModelViewerMessage(data) {
102
+ try {
103
+ const parsed = JSON.parse(data);
104
+
105
+ if (parsed && typeof parsed.type === "string") {
106
+ return {
107
+ message: typeof parsed.message === "string" ? parsed.message : undefined,
108
+ rawData: data,
109
+ type: parsed.type,
110
+ };
111
+ }
112
+ } catch {
113
+ return {
114
+ message: data,
115
+ rawData: data,
116
+ type: MODEL_VIEWER_PAGE_ERROR_EVENT,
117
+ };
118
+ }
119
+
120
+ return {
121
+ message: data,
122
+ rawData: data,
123
+ type: MODEL_VIEWER_PAGE_ERROR_EVENT,
124
+ };
125
+ }
126
+
127
+ function isModelViewerErrorStatus(status) {
128
+ return status.type.endsWith("error");
129
+ }
130
+
131
+ function getModelViewerScriptTag(options) {
132
+ if (options.modelViewerScript) {
133
+ return `<script type="module">${escapeScriptContent(options.modelViewerScript)}</script>`;
134
+ }
135
+
136
+ const scriptUrl = options.modelViewerScriptUrl ?? DEFAULT_MODEL_VIEWER_SCRIPT_URL;
137
+ return `<script type="module" src="${escapeHtmlAttribute(scriptUrl)}"></script>`;
138
+ }
139
+
140
+ function htmlAttribute(name, value) {
141
+ if (!isSafeAttributeName(name)) {
142
+ return "";
143
+ }
144
+
145
+ if (value === false || value === null || value === undefined) {
146
+ return "";
147
+ }
148
+
149
+ if (value === true) {
150
+ return ` ${name}`;
151
+ }
152
+
153
+ return ` ${name}="${escapeHtmlAttribute(String(value))}"`;
154
+ }
155
+
156
+ function isSafeAttributeName(name) {
157
+ return /^[a-zA-Z][\w:.-]*$/.test(name);
158
+ }
159
+
160
+ function escapeHtmlAttribute(value) {
161
+ return value
162
+ .replace(/&/g, "&amp;")
163
+ .replace(/"/g, "&quot;")
164
+ .replace(/</g, "&lt;")
165
+ .replace(/>/g, "&gt;");
166
+ }
167
+
168
+ function escapeScriptContent(value) {
169
+ return value.replace(/<\/script/gi, "<\\/script");
170
+ }
171
+
172
+ function sanitizeCssColor(value) {
173
+ if (/^[#(),.%\w\s-]+$/.test(value)) {
174
+ return value;
175
+ }
176
+
177
+ return "#ffffff";
178
+ }
179
+
180
+ exports.DEFAULT_MODEL_VIEWER_SCRIPT_URL = DEFAULT_MODEL_VIEWER_SCRIPT_URL;
181
+ exports.MODEL_VIEWER_DOM_READY_EVENT = MODEL_VIEWER_DOM_READY_EVENT;
182
+ exports.MODEL_VIEWER_LOADED_EVENT = MODEL_VIEWER_LOADED_EVENT;
183
+ exports.MODEL_VIEWER_MODEL_ERROR_EVENT = MODEL_VIEWER_MODEL_ERROR_EVENT;
184
+ exports.MODEL_VIEWER_PAGE_ERROR_EVENT = MODEL_VIEWER_PAGE_ERROR_EVENT;
185
+ exports.buildModelViewerHtml = buildModelViewerHtml;
186
+ exports.isModelViewerErrorStatus = isModelViewerErrorStatus;
187
+ exports.parseModelViewerMessage = parseModelViewerMessage;
@@ -0,0 +1,124 @@
1
+ # AI Agent Usage
2
+
3
+ This package ships agent-facing context so AI coding agents can integrate it
4
+ without guessing at the API shape or overselling the package.
5
+
6
+ ## What We Ship
7
+
8
+ - `AGENTS.md`: package-level instructions for coding agents working inside this
9
+ repository or an unpacked npm tarball.
10
+ - `llms.txt`: a compact documentation index agents can read before choosing
11
+ deeper files.
12
+ - `agent-skills/react-native-model-viewer-webview/SKILL.md`: a portable Agent
13
+ Skill for tools that support filesystem skills.
14
+ - `agent-skills/react-native-model-viewer-webview/references/api.md`: a smaller
15
+ API reference loaded only when the skill needs it.
16
+ - `agent-skills/react-native-model-viewer-webview/agents/openai.yaml`:
17
+ UI-facing metadata for OpenAI/Codex-style skill lists.
18
+
19
+ ## Current Conventions
20
+
21
+ Agent Skills use a directory containing `SKILL.md` with YAML frontmatter and
22
+ Markdown instructions. The public Agent Skills specification defines `name` and
23
+ `description` as required fields, plus optional resources such as `references`,
24
+ `scripts`, and `assets`. Claude Code documents the same `SKILL.md` workflow and
25
+ project/personal skill locations.
26
+
27
+ `AGENTS.md` is the cross-agent repository instruction file. It is plain Markdown
28
+ and is intended to hold build commands, test commands, code style, security
29
+ notes, and package-specific constraints.
30
+
31
+ `llms.txt` is an emerging Markdown index convention for LLM-readable docs. It is
32
+ useful because it gives agents a curated map of the docs, but it is not a W3C or
33
+ IETF standard and no package manager automatically wires it into agent clients.
34
+
35
+ Useful references:
36
+
37
+ - Agent Skills overview: https://agentskills.io/
38
+ - Agent Skills specification: https://agentskills.io/specification
39
+ - Claude Code skills docs: https://code.claude.com/docs/en/skills
40
+ - AGENTS.md format: https://agents.md/
41
+ - llms.txt reference: https://llmtxt.info/
42
+
43
+ ## Claude Code Support
44
+
45
+ Installing the npm package does not automatically install a skill into a user's
46
+ agent. That is intentional: agent clients keep skills in their own trusted
47
+ locations, and users should review third-party skills before enabling them.
48
+
49
+ Claude Code supports this skill because it is a directory containing
50
+ `SKILL.md` with YAML frontmatter, Markdown instructions, and optional supporting
51
+ files. We do not use `allowed-tools` in the skill frontmatter, because Claude
52
+ Code supports it but the Claude Agent SDK handles tool access through SDK
53
+ options instead.
54
+
55
+ Personal Claude Code install:
56
+
57
+ ```bash
58
+ mkdir -p ~/.claude/skills
59
+ cp -R node_modules/react-native-model-viewer-webview/agent-skills/react-native-model-viewer-webview ~/.claude/skills/
60
+ ls ~/.claude/skills/react-native-model-viewer-webview/SKILL.md
61
+ ```
62
+
63
+ Project Claude Code install, for teams that want to commit the skill into an app
64
+ repository:
65
+
66
+ ```bash
67
+ mkdir -p .claude/skills
68
+ cp -R node_modules/react-native-model-viewer-webview/agent-skills/react-native-model-viewer-webview .claude/skills/
69
+ ls .claude/skills/react-native-model-viewer-webview/SKILL.md
70
+ ```
71
+
72
+ After installing, ask Claude Code:
73
+
74
+ ```text
75
+ What Skills are available?
76
+ ```
77
+
78
+ or invoke it directly:
79
+
80
+ ```text
81
+ /react-native-model-viewer-webview add a GLB preview to this Expo screen
82
+ ```
83
+
84
+ If Claude does not list or use the skill, verify the filesystem path and run:
85
+
86
+ ```bash
87
+ claude --debug
88
+ ```
89
+
90
+ Claude Agent SDK users should make sure user or project settings are loaded and
91
+ the skill is enabled. In Python SDK terms, that means `setting_sources` includes
92
+ `"user"` or `"project"` and `skills="all"` or a list containing
93
+ `"react-native-model-viewer-webview"`.
94
+
95
+ ## Other Agent Clients
96
+
97
+ After installing the package, users can copy or symlink the skill folder into
98
+ their agent's supported skills directory.
99
+
100
+ Codex-style clients that support filesystem skills can use the same skill folder
101
+ from their configured skills directory. If the client does not support skills,
102
+ point the agent at `llms.txt`, `AGENTS.md`, and the package README instead.
103
+
104
+ ## What Agents Should Do
105
+
106
+ When a user asks to add this package to a React Native or Expo app:
107
+
108
+ 1. Confirm the requirement is a simple GLB/glTF preview, not a native 3D engine.
109
+ 2. Install `react-native-model-viewer-webview` and `react-native-webview`.
110
+ 3. Use `modelSource` for URLs, static asset module numbers, file/data URIs, or
111
+ Expo asset-like objects.
112
+ 4. For local `.glb` or `.gltf` imports, add those extensions to Metro asset
113
+ extensions.
114
+ 5. Use `onModelLoaded`, `onModelError`, and `onStatus` for instrumentation.
115
+ 6. Recommend Filament, React Three Fiber Native, Three.js, or Expo GLView when
116
+ the user needs native rendering or full scene control.
117
+
118
+ ## Maintenance Checklist
119
+
120
+ - Keep `SKILL.md` concise and put detailed API material in `references/api.md`.
121
+ - Keep `agents/openai.yaml` in sync with `SKILL.md`.
122
+ - Keep `llms.txt` links aligned with published docs.
123
+ - Keep `AGENTS.md` aligned with package scripts and release policy.
124
+ - Run `npm test` after changing agent assets; tests pin the expected files.