@khorsheed/dsh-inline-html-render 0.1.13
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/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.en.md +79 -0
- package/README.md +79 -0
- package/cordis.patch.yml +8 -0
- package/lib/client.js +421 -0
- package/lib/index.js +88 -0
- package/lib/invariant.js +25 -0
- package/lib/types/client/bridge.d.ts +28 -0
- package/lib/types/client/bridge.js +110 -0
- package/lib/types/client/index.d.ts +34 -0
- package/lib/types/client/index.js +60 -0
- package/lib/types/client/renderer.d.ts +52 -0
- package/lib/types/client/renderer.js +220 -0
- package/lib/types/client/srcdoc.d.ts +30 -0
- package/lib/types/client/srcdoc.js +88 -0
- package/lib/types/index.d.ts +32 -0
- package/lib/types/index.js +90 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +23 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +60 -0
- package/skills/3d-artifact/SKILL.md +52 -0
- package/skills/inline-html-card/SKILL.md +97 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandboxed srcDoc builder for inline HTML cards.
|
|
3
|
+
*
|
|
4
|
+
* The parent web shell has no Content-Security-Policy of its own and an
|
|
5
|
+
* iframe's `srcdoc` is not served as an HTTP response, so CSP inheritance
|
|
6
|
+
* cannot be relied on — the policy must be embedded IN the document. This
|
|
7
|
+
* helper guarantees exactly that for both shapes of input:
|
|
8
|
+
*
|
|
9
|
+
* - a bare fragment (no `<html>`/`<head>`) is wrapped into a full document
|
|
10
|
+
* whose head carries the CSP and the capability bridge;
|
|
11
|
+
* - a full document keeps its own structure and gets the CSP meta injected
|
|
12
|
+
* right after its `<head …>` tag (skipped when it already declares one,
|
|
13
|
+
* e.g. a skill-compliant output that embeds the same policy).
|
|
14
|
+
*
|
|
15
|
+
* The sandbox is `allow-scripts`, NEVER `allow-same-origin` — the opaque
|
|
16
|
+
* origin is what keeps the frame's scripts from touching the host, and the
|
|
17
|
+
* CSP is the network/capability boundary. This is deliberately stricter than
|
|
18
|
+
* file-preview's Tier1 (no whitelisted CDNs): an inline card should never
|
|
19
|
+
* reach the network at all.
|
|
20
|
+
* @module @khorsheed/dsh-inline-html-render
|
|
21
|
+
*/
|
|
22
|
+
/** Strict sandbox CSP: no network, no navigation, scripts only inline. */
|
|
23
|
+
export const CARD_CSP = 'default-src \'none\'; script-src \'unsafe-inline\'; style-src \'unsafe-inline\'; img-src data: blob:; font-src data:; connect-src \'none\'; worker-src blob:; object-src \'none\'; base-uri \'none\'; form-action \'none\'';
|
|
24
|
+
/** The capability bridge client, injected into the document. Exposes a tiny
|
|
25
|
+
* `window.dshBridge` (openLink / copy / download) backed by postMessage; the
|
|
26
|
+
* host validates every call. It also reports the document's content height on
|
|
27
|
+
* load (and on resize) so the parent can size the cross-origin iframe — the
|
|
28
|
+
* parent cannot read `contentDocument` on an opaque-origin frame, so the height
|
|
29
|
+
* must be posted from inside. No network and no host access exist outside
|
|
30
|
+
* these messages. */
|
|
31
|
+
const BRIDGE_SCRIPT = `<script id="dsh-inline-bridge">
|
|
32
|
+
(function () {
|
|
33
|
+
var seq = 0
|
|
34
|
+
function call (fn, args, target) {
|
|
35
|
+
return new Promise(function (resolve, reject) {
|
|
36
|
+
var id = 'dshb' + (++seq)
|
|
37
|
+
function on (e) {
|
|
38
|
+
if (e.data && e.data.id === id) {
|
|
39
|
+
removeEventListener('message', on)
|
|
40
|
+
if (e.data.ok) resolve(e.data.result)
|
|
41
|
+
else reject(new Error(e.data.error || 'bridge call failed'))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
addEventListener('message', on)
|
|
45
|
+
parent.postMessage({ id: id, fn: fn, args: args }, '*')
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
window.dshBridge = {
|
|
49
|
+
openLink: function (url) { return call('openLink', [url]) },
|
|
50
|
+
copy: function (text) { return call('copy', [text]) },
|
|
51
|
+
download: function (name, dataUrl) { return call('download', [name, dataUrl]) }
|
|
52
|
+
}
|
|
53
|
+
function report () {
|
|
54
|
+
// documentElement.scrollHeight reflects the viewport height, so a short
|
|
55
|
+
// card leaves a blank gap below. document.body.scrollHeight is the actual
|
|
56
|
+
// content height the card occupies. Use it, with documentElement only as a
|
|
57
|
+
// fallback for a body that holds no laid-out content of its own.
|
|
58
|
+
var bodyH = document.body ? document.body.scrollHeight : 0
|
|
59
|
+
var h = bodyH
|
|
60
|
+
if (h <= 0 && document.documentElement) h = document.documentElement.scrollHeight || 0
|
|
61
|
+
if (h > 0) parent.postMessage({ fn: 'resize', args: [h] }, '*')
|
|
62
|
+
}
|
|
63
|
+
if (document.readyState === 'complete') report()
|
|
64
|
+
else addEventListener('load', report)
|
|
65
|
+
addEventListener('resize', report)
|
|
66
|
+
})()
|
|
67
|
+
<\/script>`;
|
|
68
|
+
/** The CSP meta tag. */
|
|
69
|
+
const CSP_META = `<meta http-equiv="Content-Security-Policy" content="${CARD_CSP}">`;
|
|
70
|
+
const HEAD_OPEN = /<head[^>]*>/i;
|
|
71
|
+
const HAS_CSP = /http-equiv=["']Content-Security-Policy/i;
|
|
72
|
+
/**
|
|
73
|
+
* Wrap or annotate an untrusted HTML document so it runs inside the sandbox CSP.
|
|
74
|
+
* @param content - the card HTML, as authored inside the fenced block.
|
|
75
|
+
* @returns a complete document carrying the strict CSP and the bridge.
|
|
76
|
+
*/
|
|
77
|
+
export function buildCardSrcDoc(content) {
|
|
78
|
+
const headExtras = BRIDGE_SCRIPT;
|
|
79
|
+
const head = HEAD_OPEN.exec(content);
|
|
80
|
+
if (head === null) {
|
|
81
|
+
// Fragment: we own the whole document.
|
|
82
|
+
return `<!doctype html><html lang="zh"><head><meta charset="utf-8">${CSP_META}${headExtras}</head><body>${content}</body></html>`;
|
|
83
|
+
}
|
|
84
|
+
// Full document: inject into its own head (no duplicated CSP when it
|
|
85
|
+
// already declares one — skill-compliant outputs embed the same policy).
|
|
86
|
+
const headInjection = (HAS_CSP.test(content) ? '' : CSP_META) + headExtras;
|
|
87
|
+
return content.slice(0, head.index + head[0].length) + headInjection + content.slice(head.index + head[0].length);
|
|
88
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline HTML render host half. Registers the `inline-html-card` and
|
|
3
|
+
* `3d-artifact` skills, which
|
|
4
|
+
* documents the fenced-block protocol the browser half consumes: the agent
|
|
5
|
+
* writes a ` ```dsh-card ```` fenced block (or a `dsh-card <spec>` inline
|
|
6
|
+
* fence) whose body is a fully self-contained HTML document, and the client
|
|
7
|
+
* half swaps the rendered `<pre><code class="language-dsh-card">` for a
|
|
8
|
+
* sandboxed iframe that runs it.
|
|
9
|
+
*
|
|
10
|
+
* This half owns no DOM and no Remote — it only advertises the contract so
|
|
11
|
+
* the agent knows how to author a card. It degrades silently when the
|
|
12
|
+
* `skills` capability is absent (a minimal composition may lack it), exactly
|
|
13
|
+
* without widening the read-only file-preview host's responsibility.
|
|
14
|
+
* @module @khorsheed/dsh-inline-html-render
|
|
15
|
+
*/
|
|
16
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
17
|
+
/** The host half requires no service; `skills` is awaited, never baked into inject. */
|
|
18
|
+
export declare const inject: readonly string[];
|
|
19
|
+
/**
|
|
20
|
+
* Plugin body: register the card-authoring skill once the `skills` service is
|
|
21
|
+
* ready. `ctx.inject(['skills'], …)` waits for the service rather than probing
|
|
22
|
+
* it synchronously at apply time, so registration is not skipped when the host
|
|
23
|
+
* mounts this plugin before the skill registry is up. (The file-preview host is
|
|
24
|
+
* a service class instantiated at a later point; this plugin's `apply` runs
|
|
25
|
+
* earlier, so a plain synchronous `ctx.get('skills')` at apply time could see
|
|
26
|
+
* the registry absent and silently skip — which is why inline-html-card did not
|
|
27
|
+
* appear in the catalog while 3d-artifact did.)
|
|
28
|
+
* @param ctx - Cordis context carrying the `skills` service.
|
|
29
|
+
* @returns a disposer unwinding the registration effect.
|
|
30
|
+
*/
|
|
31
|
+
export declare function apply(ctx: Context): () => void;
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline HTML render host half. Registers the `inline-html-card` and
|
|
3
|
+
* `3d-artifact` skills, which
|
|
4
|
+
* documents the fenced-block protocol the browser half consumes: the agent
|
|
5
|
+
* writes a ` ```dsh-card ```` fenced block (or a `dsh-card <spec>` inline
|
|
6
|
+
* fence) whose body is a fully self-contained HTML document, and the client
|
|
7
|
+
* half swaps the rendered `<pre><code class="language-dsh-card">` for a
|
|
8
|
+
* sandboxed iframe that runs it.
|
|
9
|
+
*
|
|
10
|
+
* This half owns no DOM and no Remote — it only advertises the contract so
|
|
11
|
+
* the agent knows how to author a card. It degrades silently when the
|
|
12
|
+
* `skills` capability is absent (a minimal composition may lack it), exactly
|
|
13
|
+
* without widening the read-only file-preview host's responsibility.
|
|
14
|
+
* @module @khorsheed/dsh-inline-html-render
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
/**
|
|
20
|
+
* Read and parse one shipped SKILL.md (`inline-html-card` / `3d-artifact`) into
|
|
21
|
+
* a registration.
|
|
22
|
+
*
|
|
23
|
+
* This skill is content-only — it ships a single self-contained SKILL.md with
|
|
24
|
+
* no sibling scripts/assets/references. Per the capability-catalog registration
|
|
25
|
+
* protocol, a content-only skill OMITS `resourceBase` so the catalog renders it
|
|
26
|
+
* as a virtual single-SKILL.md node (it does NOT point at a directory that
|
|
27
|
+
* holds only SKILL.md). Add `resourceBase: { kind: 'directory', path }` only
|
|
28
|
+
* when a bundle with files beside SKILL.md is introduced later.
|
|
29
|
+
* @param ctx - plugin context (for logging).
|
|
30
|
+
* @returns the parsed skill, or undefined when the file is missing/malformed
|
|
31
|
+
* (a discovery aid must never take a boot down, so each failure warns).
|
|
32
|
+
*/
|
|
33
|
+
function readSkill(ctx, skillName) {
|
|
34
|
+
try {
|
|
35
|
+
const skillDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'skills', skillName);
|
|
36
|
+
const skillFile = join(skillDir, 'SKILL.md');
|
|
37
|
+
const raw = readFileSync(skillFile, 'utf8');
|
|
38
|
+
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(raw);
|
|
39
|
+
const name = /^name: (.+)$/m.exec(match?.[1] ?? '')?.[1]?.trim();
|
|
40
|
+
const description = /^description: (.+)$/m.exec(match?.[1] ?? '')?.[1]?.trim();
|
|
41
|
+
const content = match?.[2];
|
|
42
|
+
if (match === null || name === undefined || description === undefined || content === undefined) {
|
|
43
|
+
ctx.logger.warn(`inline-html-render: shipped SKILL.md is malformed — the ${skillName} skill is not registered`);
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
name,
|
|
48
|
+
description,
|
|
49
|
+
content,
|
|
50
|
+
source: 'runtime',
|
|
51
|
+
provider: 'inline-html-render',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
ctx.logger.warn(`inline-html-render: shipped SKILL.md unreadable (${String(error)}) — the ${skillName} skill is not registered`);
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** The host half requires no service; `skills` is awaited, never baked into inject. */
|
|
60
|
+
export const inject = [];
|
|
61
|
+
/**
|
|
62
|
+
* Plugin body: register the card-authoring skill once the `skills` service is
|
|
63
|
+
* ready. `ctx.inject(['skills'], …)` waits for the service rather than probing
|
|
64
|
+
* it synchronously at apply time, so registration is not skipped when the host
|
|
65
|
+
* mounts this plugin before the skill registry is up. (The file-preview host is
|
|
66
|
+
* a service class instantiated at a later point; this plugin's `apply` runs
|
|
67
|
+
* earlier, so a plain synchronous `ctx.get('skills')` at apply time could see
|
|
68
|
+
* the registry absent and silently skip — which is why inline-html-card did not
|
|
69
|
+
* appear in the catalog while 3d-artifact did.)
|
|
70
|
+
* @param ctx - Cordis context carrying the `skills` service.
|
|
71
|
+
* @returns a disposer unwinding the registration effect.
|
|
72
|
+
*/
|
|
73
|
+
export function apply(ctx) {
|
|
74
|
+
const disposers = [];
|
|
75
|
+
// Wait for the skill registry before registering. If it never appears, this
|
|
76
|
+
// fiber stays pending rather than throwing — a discovery aid must not take a
|
|
77
|
+
// boot down. The injected ctx is scope-addressed, so ctx.get('skills') is set.
|
|
78
|
+
ctx.inject(['skills'], (scoped) => {
|
|
79
|
+
const registry = scoped.get('skills');
|
|
80
|
+
if (registry === undefined)
|
|
81
|
+
return;
|
|
82
|
+
for (const skillName of ['inline-html-card', '3d-artifact']) {
|
|
83
|
+
const skill = readSkill(scoped, skillName);
|
|
84
|
+
if (skill !== undefined)
|
|
85
|
+
disposers.push(scoped.effect(() => registry.register(skill)));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return () => { for (const disposer of disposers.reverse())
|
|
89
|
+
disposer(); };
|
|
90
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@khorsheed/dsh-inline-html-render`.
|
|
3
|
+
* @module @khorsheed/dsh-inline-html-render/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "client-inline-html-render-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns The installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@khorsheed/dsh-inline-html-render`.
|
|
3
|
+
* @module @khorsheed/dsh-inline-html-render/invariant
|
|
4
|
+
*/
|
|
5
|
+
const PACKAGE_NAME = '@khorsheed/dsh-inline-html-render';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export const name = 'client-inline-html-render-invariant';
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export const inject = ['invariants'];
|
|
10
|
+
/**
|
|
11
|
+
* No runtime invariant: the renderer is a pure DOM-layer effect owning its own
|
|
12
|
+
* MutationObserver and a per-document iframe/`<pre>` pair; there is no durable
|
|
13
|
+
* state shared with other packages. Reserved here so the loader knows this
|
|
14
|
+
* package is alive (and so a future invariant can attach without a rename).
|
|
15
|
+
*/
|
|
16
|
+
const install = () => { };
|
|
17
|
+
/**
|
|
18
|
+
* Register this package's invariant companion.
|
|
19
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
20
|
+
* @returns The installed registration's disposer after setup succeeds.
|
|
21
|
+
*/
|
|
22
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
23
|
+
/* jscpd:ignore-end */
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@khorsheed/dsh-inline-html-render",
|
|
3
|
+
"description": "Render agent-authored ```dsh-card HTML as sandboxed interactive iframes inline in the conversation",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"dsh",
|
|
6
|
+
"dsh-plugin",
|
|
7
|
+
"cordis",
|
|
8
|
+
"html",
|
|
9
|
+
"render",
|
|
10
|
+
"card",
|
|
11
|
+
"sandbox"
|
|
12
|
+
],
|
|
13
|
+
"version": "0.1.13",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "lib/index.js",
|
|
16
|
+
"types": "lib/types/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./lib/types/index.d.ts",
|
|
20
|
+
"default": "./lib/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./invariant": {
|
|
23
|
+
"types": "./lib/types/invariant.d.ts",
|
|
24
|
+
"default": "./lib/invariant.js"
|
|
25
|
+
},
|
|
26
|
+
"./client": {
|
|
27
|
+
"types": "./lib/types/client/index.d.ts",
|
|
28
|
+
"default": "./lib/client.js"
|
|
29
|
+
},
|
|
30
|
+
"./src/*": "./src/*",
|
|
31
|
+
"./package.json": "./package.json",
|
|
32
|
+
"./locale/*.json": "./locale/*.json"
|
|
33
|
+
},
|
|
34
|
+
"dsh": {
|
|
35
|
+
"client": {
|
|
36
|
+
"inject": [],
|
|
37
|
+
"platform": "web"
|
|
38
|
+
},
|
|
39
|
+
"bundle": {
|
|
40
|
+
"patch": "./cordis.patch.yml"
|
|
41
|
+
},
|
|
42
|
+
"compat": {
|
|
43
|
+
"minHost": "0.1.2-rc.1",
|
|
44
|
+
"verifiedHost": "0.1.2-rc.1"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
50
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.6"
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"lib",
|
|
54
|
+
"cordis.patch.yml",
|
|
55
|
+
"skills/**/*.md",
|
|
56
|
+
"locale/*.json",
|
|
57
|
+
"LICENSE",
|
|
58
|
+
"CHANGELOG.md"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 3d-artifact
|
|
3
|
+
description: Generate sandbox-runnable interactive 3D / digital-twin single-file HTML that obeys the strict sandbox CSP — self-contained, zero runtime network, GLB-inline zero-fetch models. Load when the task asks for a 3D visualization, simulation, digital twin, or WebGL scene delivered as one standalone HTML page.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 3d-artifact — generating sandbox-runnable interactive 3D / digital-twin single-file HTML
|
|
7
|
+
|
|
8
|
+
## When to load
|
|
9
|
+
|
|
10
|
+
Load this skill when the task asks to produce a **self-contained HTML page that renders 3D content** — visualizations, simulations, digital twins, WebGL scenes (three.js, etc.) — that must run inside a sandboxed preview iframe under a strict CSP (for example, the dsh file-preview or deliverables preview).
|
|
11
|
+
|
|
12
|
+
## The hard contract — violations fail silently in the sandbox; this is the #1 cause of "dead" artifacts
|
|
13
|
+
|
|
14
|
+
1. **Single file, fully self-contained.** All JS/CSS inline. Images as `data:` URIs. No external stylesheets or fonts.
|
|
15
|
+
2. **Zero runtime network.** No `fetch` / XHR / WebSocket / EventSource. Never `loader.load(url)`.
|
|
16
|
+
- 3D models MUST be inlined as **GLB base64** and decoded by hand: `atob` → `Uint8Array` → `GLTFLoader.parse(arrayBuffer, ...)`.
|
|
17
|
+
- **Never** embed a `.gltf` JSON whose buffer uses a `data:...` URI. three.js's GLTFLoader resolves `data:` URIs through `fetch`, which the sandbox CSP (`connect-src 'none'`) blocks — verified against three r152.
|
|
18
|
+
3. **Libraries may load only from whitelisted CDNs** (the CSP's `script-src` allows exactly these): `https://cdn.jsdelivr.net` and `https://cdnjs.cloudflare.com`. Use ESM + an inline `<script type="importmap">`. Never derive a resource URL from user/data input at runtime.
|
|
19
|
+
4. **Embed this Tier1 CSP meta tag** in `<head>`:
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; connect-src 'none'; worker-src blob:; object-src 'none'; base-uri 'none'; form-action 'none'; upgrade-insecure-requests; block-all-mixed-content">
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
5. **Size red lines**: GLB ≤ 6 MB raw (≈8 MB base64); total page ≤ 16 MB. Base64 inflates ×4/3. Prefer procedural geometry, SVG, and compressed textures (KTX2/DRACO) over big assets.
|
|
26
|
+
6. **Performance**: cap `renderer.setPixelRatio` (≤2); keep the scene interactive within ~1 s of load; prefer `InstancedMesh` for repeated objects (vehicles, clamps, turbines); defer heavy DOM with `content-visibility`-style techniques; merge many small geometries (e.g. `BufferGeometryUtils.mergeGeometries`).
|
|
27
|
+
|
|
28
|
+
## Recommended stack
|
|
29
|
+
|
|
30
|
+
- three.js ESM + OrbitControls via the importmap pattern:
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
<script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.152.2/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.152.2/examples/jsm/"}}</script>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- Procedural geometry + vertex colors beat downloaded assets for reliability and size.
|
|
37
|
+
|
|
38
|
+
## Self-check before finishing
|
|
39
|
+
|
|
40
|
+
- [ ] No external URL anywhere except whitelisted CDN scripts used by the importmap.
|
|
41
|
+
- [ ] No `fetch(` / `XMLHttpRequest` / `WebSocket` / `loader.load(` anywhere in the page.
|
|
42
|
+
- [ ] Any model is GLB base64 decoded via `atob` → `parse(arrayBuffer)` — never a `data:` URI buffer.
|
|
43
|
+
- [ ] The Tier1 CSP meta tag above is present in `<head>`.
|
|
44
|
+
- [ ] Page + all inline assets ≤ 16 MB.
|
|
45
|
+
- [ ] Opens and renders in a plain browser within ~2 s — that is the sandbox experience.
|
|
46
|
+
|
|
47
|
+
## Authoring tips for realism (超写实 / 数字孪生 scenes)
|
|
48
|
+
|
|
49
|
+
- Golden-hour lighting: warm directional sun + hemisphere fill + exponential fog for depth.
|
|
50
|
+
- ACES filmic tone mapping + sRGB output (`renderer.outputEncoding`).
|
|
51
|
+
- Layered composition: hero object (bridge), midground (valley, road), background (mountain silhouettes, turbines, power towers), atmosphere (fog, mist).
|
|
52
|
+
- Digital-twin overlays read best as translucent DOM panels + in-scene markers (sensor nodes, point clouds, BIM wireframes, heatmaps, drone paths).
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: inline-html-card
|
|
3
|
+
description: Make a live, interactive HTML preview that appears right in the conversation. Use it whenever the user wants to see a result visually instead of reading about it — a sample UI, a component mockup, a color/spacing/design-token reference, a "what would this look like" idea, a small clickable widget, a before/after, a card showing the current state (like progress, stats, or a mini dashboard). This also covers design work: when the user is redesigning or optimizing something and you want to confirm a visual detail, show it inline and iterate — render just the changed part, not a whole deliverable. Default to an inline preview to confirm details with the user; only fall back to writing a full HTML file if the user asks for a file or the details are settled and they want a complete artifact. If the user says "show me", "make it look like", "render this", "preview", "compare a few visual options", or wants to confirm how a design change looks, this is the tool. It renders a self-contained HTML block inline and lets the user interact with it. Writes a ```dsh-card fenced block.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# inline-html-card — authoring a live inline HTML card
|
|
7
|
+
|
|
8
|
+
## When to load
|
|
9
|
+
|
|
10
|
+
Load this skill whenever the user wants to **see something visually, right here in the chat instead of just reading your reply** — a sample of a UI, a mockup, a color/typography/spacing reference, "what would this look like", a small interactive thing (a toggle, tabs, a live mini-chart), a before/after, or a card that summarizes the current state (progress, stats, a compact dashboard). This includes the user asking you to "render", "preview", "show me", "make it look like", or when they want to choose between a few visual options. It also covers **design work**: redesigning or optimizing a surface where you want to confirm a visual detail with the user. If it would help to draw a picture of the answer rather than describe it in words or ASCII, use this skill. Write the result as a ` ```dsh-card ```` fenced block (see below).
|
|
11
|
+
|
|
12
|
+
## Show it inline first — don't reach for a file
|
|
13
|
+
|
|
14
|
+
When the user wants to see or confirm how something **looks**, your default is an **inline preview** — not writing an HTML file to disk. Follow this order:
|
|
15
|
+
|
|
16
|
+
1. **Inline preview first (the default).** Render the thing with a ` ```dsh-card ```` block so the user sees it in the message and can react to it. For a **局部细节** (a changed section, a specific component, a single face of the redesign) render **just that part** — don't build a whole deliverable you're only trying to confirm.
|
|
17
|
+
2. **Iterate on the detail inline.** Confirm the visual, adjust, show again — all in the chat. Stay in the preview loop until the user is happy.
|
|
18
|
+
3. **Offer a full file only after the details are settled.** Once it's stable, you can ask the user whether they want a **complete HTML file** — or wait for them to ask.
|
|
19
|
+
4. **Only write a file when asked.** Generate `*.html` to the workspace **only** if the user explicitly requests a full HTML artifact (to reuse, edit, or ship). Don't default to a file for something you can confirm inline.
|
|
20
|
+
|
|
21
|
+
Reach for the file only when it's genuinely a deliverable, not when the goal is to show/confirm a look. If you're in doubt, show it inline and ask.
|
|
22
|
+
|
|
23
|
+
## The contract
|
|
24
|
+
|
|
25
|
+
Write exactly one fenced code block whose info string is `dsh-card` — **using three backticks**, never four:
|
|
26
|
+
|
|
27
|
+
````
|
|
28
|
+
```dsh-card
|
|
29
|
+
...your HTML here...
|
|
30
|
+
```
|
|
31
|
+
````
|
|
32
|
+
|
|
33
|
+
**Use exactly three backticks.** Do NOT use four backticks (````) — that is how this skill demonstrates the fence in a doc so it is not parsed, and writing it that way in your reply makes the parser treat it as an outer code block, so the `dsh-card` info string is never picked up and the card is not rendered. Write a plain top-level ` ```dsh-card …``` ` block, not a nested one.
|
|
34
|
+
|
|
35
|
+
Inside the fence, put a **fully self-contained HTML document** (fragment is fine; the renderer wraps missing `<html>`/`<head>` automatically). The browser half turns this into a sandboxed iframe and renders it inline where the block sits.
|
|
36
|
+
|
|
37
|
+
## The content IS the card — don't wrap it in a container
|
|
38
|
+
|
|
39
|
+
The user should see **the content itself**, not the content sitting inside another box you invented. Render the thing you're showing directly, at the width it needs, and let it sit naturally in the message.
|
|
40
|
+
|
|
41
|
+
- **Render the content, not a container.** Put your UI/text/visual directly in the body. Don't wrap it in an extra "window" (a title bar, ─ ✕ buttons, or a header row you added just to frame it) unless the user explicitly asked for a card/app window.
|
|
42
|
+
- **Width follows the content.** Let it be as wide as it needs to be (`width:100%` or a modest `max-width`, `margin:0`). Don't fix a narrow width and center it with large side margins — that leaves empty boxes around it.
|
|
43
|
+
- **Keep the content's own breathing room.** The padding/margins that are part of the content (e.g. a setting panel's `padding: 0 24px 24px`) are the content itself — preserve them. Only remove padding you added just to frame the content. "No fake window" means don't add a border/box around it, not "cram it to the very edge with zero padding."
|
|
44
|
+
- **No extra background box.** Don't give the top-level element a big solid background that spans the whole frame just for contrast; if you want contrast, color the content region itself, not a wrapper around it.
|
|
45
|
+
- **Height follows content, not a placeholder.** Don't set a large `min-height` just to fill space; let the layout be as tall as its real content.
|
|
46
|
+
- **Colors, theme, and fonts are yours to choose** — design them for the content and the user's intent, not for any assumed page background. Pick whatever palette/typography the thing itself calls for.
|
|
47
|
+
|
|
48
|
+
In short: what the block renders should be **exactly the content** — a mockup, a token reference, a mini dashboard — sized to itself and flush with the message, not a panel floating in a bigger box you added around it.
|
|
49
|
+
|
|
50
|
+
## The hard rules — violations fail silently (the #1 cause of "looks blank")
|
|
51
|
+
|
|
52
|
+
1. **Self-contained.** All CSS and JS inline. No external stylesheets, fonts, or scripts. Images as `data:` URIs.
|
|
53
|
+
2. **Zero runtime network.** No `fetch` / XHR / WebSocket / EventSource. The sandbox CSP sets `connect-src 'none'` — any network call just fails.
|
|
54
|
+
3. **No host access.** The frame is an opaque origin (`sandbox="allow-scripts"`, never `allow-same-origin`), so scripts cannot read parent DOM or cookies. The only way out is a tiny `window.dshBridge` (see below).
|
|
55
|
+
4. **No imposed look.** Style, colors, theme, and fonts are entirely up to you — design them for the content and the user's intent. What matters is that you render the content directly and don't add a meaningless container around it (no fixed narrow width centered with big margins, no extra solid-background wrapper). Leave the card's own design to the content, not to the surrounding chat.
|
|
56
|
+
5. **Size sane.** Keep the HTML small (a few KB). Avoid huge inline assets.
|
|
57
|
+
|
|
58
|
+
## Interactions that work
|
|
59
|
+
|
|
60
|
+
- **Want the card to respond to the user?** Add an **inline `<script>`** inside the card and bind with `addEventListener`. That's the normal, supported way to make buttons, tabs, timers, and state switches actually work — the card runs in a sandboxed iframe with scripts enabled (`allow-scripts`), so inline JS just works.
|
|
61
|
+
- Example:
|
|
62
|
+
```html
|
|
63
|
+
<button id="go">开始</button>
|
|
64
|
+
<span id="out">未开始</span>
|
|
65
|
+
<script>
|
|
66
|
+
document.getElementById('go').addEventListener('click', function () {
|
|
67
|
+
document.getElementById('out').textContent = '进行中…'
|
|
68
|
+
})
|
|
69
|
+
</script>
|
|
70
|
+
```
|
|
71
|
+
- If the card should animate or count down (a timer, a breathing ball, progress), drive it from JS too — `setInterval` / `requestAnimationFrame` / updating the DOM are all fine inside the frame.
|
|
72
|
+
- Hover, click, toggling, tabs, and charts drawn with inline SVG/canvas also work under `allow-scripts`.
|
|
73
|
+
- To open a link, copy text, or download a file, use the bridge:
|
|
74
|
+
- `await window.dshBridge.openLink('https://…')` (https: only)
|
|
75
|
+
- `await window.dshBridge.copy('text')`
|
|
76
|
+
- `await window.dshBridge.download('name.txt', 'data:…')`
|
|
77
|
+
These route through the host's validated bridge; anything else is blocked.
|
|
78
|
+
|
|
79
|
+
## Recommended: embed this CSP meta in `<head>`
|
|
80
|
+
|
|
81
|
+
The renderer injects its own CSP if you omit it, but embedding the same one is harmless and keeps the artifact self-describing:
|
|
82
|
+
|
|
83
|
+
```html
|
|
84
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; connect-src 'none'; worker-src blob:; object-src 'none'; base-uri 'none'; form-action 'none'">
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Self-check before finishing
|
|
88
|
+
|
|
89
|
+
- [ ] No external URL anywhere.
|
|
90
|
+
- [ ] No `fetch` / XHR / WebSocket.
|
|
91
|
+
- [ ] Fence info string is exactly `dsh-card`.
|
|
92
|
+
- [ ] Used **three** backticks ( ``` ), never four ( ```` ) — a four-backtick or nested fence makes the parser read it as an outer block and the card never renders.
|
|
93
|
+
- [ ] Defaulted to an inline preview to confirm the look; only wrote an HTML file if the user asked for one or the details were settled and they wanted a complete artifact.
|
|
94
|
+
- [ ] For a local detail, rendered just that part, not a whole deliverable.
|
|
95
|
+
- [ ] The content IS the card — no fixed narrow width centered in a big panel, no extra solid-background "window" wrapper, no large empty `min-height`. You chose colors/theme/fonts for the content, not for the surrounding chat. Kept the padding/margins that are genuinely part of the content (a panel's own inset is not a fake window).
|
|
96
|
+
- [ ] If the card is meant to respond to the user, each interactive element has an inline `<script>` handler (`addEventListener`) wired to it — don't rely on CSS alone for click/state behavior.
|
|
97
|
+
- [ ] Without the plugin, the block degrades to a normal code block (never a crash).
|