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.
- package/AGENTS.md +53 -0
- package/CHANGELOG.md +67 -0
- package/CONTRIBUTING.md +79 -0
- package/LICENSE +21 -0
- package/README.md +288 -0
- package/SECURITY.md +31 -0
- package/agent-skills/react-native-model-viewer-webview/SKILL.md +134 -0
- package/agent-skills/react-native-model-viewer-webview/agents/openai.yaml +4 -0
- package/agent-skills/react-native-model-viewer-webview/references/api.md +86 -0
- package/dist/ModelViewerWebView.d.ts +19 -0
- package/dist/ModelViewerWebView.js +90 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +17 -0
- package/dist/model-source.d.ts +9 -0
- package/dist/model-source.js +31 -0
- package/dist/model-viewer-html.d.ts +35 -0
- package/dist/model-viewer-html.js +187 -0
- package/docs/AGENT_USAGE.md +124 -0
- package/docs/COMPATIBILITY.md +112 -0
- package/docs/HOW_IT_WORKS.md +126 -0
- package/docs/RELEASE.md +169 -0
- package/example/App.tsx +41 -0
- package/example/README.md +43 -0
- package/llms.txt +79 -0
- package/package.json +76 -0
- package/src/ModelViewerWebView.tsx +103 -0
- package/src/index.ts +20 -0
- package/src/model-source.ts +39 -0
- package/src/model-viewer-html.ts +207 -0
- package/tsconfig.json +13 -0
|
@@ -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;
|
package/dist/index.d.ts
ADDED
|
@@ -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,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, "&")
|
|
163
|
+
.replace(/"/g, """)
|
|
164
|
+
.replace(/</g, "<")
|
|
165
|
+
.replace(/>/g, ">");
|
|
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.
|