@corvidlabs/three-md-element 1.7.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -0
- package/dist/three-md.d.ts +86 -0
- package/dist/three-md.js +219 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# @corvidlabs/three-md-element
|
|
2
|
+
|
|
3
|
+
The canonical `<three-md>` web component: a framework-agnostic interactive
|
|
4
|
+
renderer for the [3md format](https://github.com/CorvidLabs/3md). Parsing is
|
|
5
|
+
delegated to [`@corvidlabs/threemd`](../js) (the shared, conformance-tested data
|
|
6
|
+
layer); this element owns only the visual - a 3D stack of planes you scrub,
|
|
7
|
+
drag, and step through along the document's Z axis.
|
|
8
|
+
|
|
9
|
+
It is a custom element, so it works the same in plain HTML, React, Vue, Svelte,
|
|
10
|
+
and Angular. There is one tested renderer instead of a per-app reimplementation.
|
|
11
|
+
|
|
12
|
+
## Why a web component
|
|
13
|
+
|
|
14
|
+
The 3md parser is shared across Swift, TypeScript, and Rust and pinned by a
|
|
15
|
+
conformance suite. The interactive renderer used to be bespoke inline script,
|
|
16
|
+
copied between the demo and the marketing site, and it drifted: a Safari and
|
|
17
|
+
Low-Power bug where the focused plane never came to the front had to be fixed in
|
|
18
|
+
each copy. Shipping the renderer as one component fixes that at the root.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
Published to GitHub Packages. Point the `@corvidlabs` scope at the GitHub
|
|
23
|
+
registry once (in a project or user `.npmrc`), then install:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
echo "@corvidlabs:registry=https://npm.pkg.github.com" >> .npmrc
|
|
27
|
+
bun add @corvidlabs/three-md-element
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The published package is a single self-contained module (the parser is bundled
|
|
31
|
+
in, no other dependency), so you can also vendor `dist/three-md.js` and load it
|
|
32
|
+
directly:
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<script type="module" src="/three-md.js"></script>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
### Plain HTML
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<script type="module" src="/three-md.js"></script>
|
|
44
|
+
|
|
45
|
+
<!-- inline source -->
|
|
46
|
+
<three-md>---
|
|
47
|
+
3md: 1.0
|
|
48
|
+
axis: time
|
|
49
|
+
title: My week
|
|
50
|
+
---
|
|
51
|
+
@plane z=0 label="Monday"
|
|
52
|
+
# Monday
|
|
53
|
+
- [ ] Standup
|
|
54
|
+
</three-md>
|
|
55
|
+
|
|
56
|
+
<!-- or load a file -->
|
|
57
|
+
<three-md src="/plan.3md"></three-md>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### React
|
|
61
|
+
|
|
62
|
+
The element works as a normal tag. Pass inline source as children, or set `src`.
|
|
63
|
+
Listen for plane changes with a ref.
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { useEffect, useRef } from "react";
|
|
67
|
+
import "@corvidlabs/three-md-element";
|
|
68
|
+
|
|
69
|
+
export function Plan() {
|
|
70
|
+
const ref = useRef<HTMLElement>(null);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const el = ref.current;
|
|
73
|
+
const onChange = (e: Event) => console.log((e as CustomEvent).detail);
|
|
74
|
+
el?.addEventListener("planechange", onChange);
|
|
75
|
+
return () => el?.removeEventListener("planechange", onChange);
|
|
76
|
+
}, []);
|
|
77
|
+
return <three-md ref={ref} src="/plan.3md" />;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Vue
|
|
82
|
+
|
|
83
|
+
```vue
|
|
84
|
+
<script setup>
|
|
85
|
+
import "@corvidlabs/three-md-element";
|
|
86
|
+
function onChange(e) { console.log(e.detail); }
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<template>
|
|
90
|
+
<three-md src="/plan.3md" @planechange="onChange" />
|
|
91
|
+
</template>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Tell Vue to treat it as a custom element (in `vite.config`):
|
|
95
|
+
`vue({ template: { compilerOptions: { isCustomElement: (t) => t === "three-md" } } })`.
|
|
96
|
+
|
|
97
|
+
### Svelte
|
|
98
|
+
|
|
99
|
+
```svelte
|
|
100
|
+
<script>
|
|
101
|
+
import "@corvidlabs/three-md-element";
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<three-md src="/plan.3md" on:planechange={(e) => console.log(e.detail)} />
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Angular
|
|
108
|
+
|
|
109
|
+
Add `CUSTOM_ELEMENTS_SCHEMA` to the component/module, import the package once,
|
|
110
|
+
then use the tag:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import "@corvidlabs/three-md-element";
|
|
114
|
+
// @Component({ ..., schemas: [CUSTOM_ELEMENTS_SCHEMA] })
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```html
|
|
118
|
+
<three-md src="/plan.3md" (planechange)="onChange($event)"></three-md>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## API
|
|
122
|
+
|
|
123
|
+
### Attributes
|
|
124
|
+
|
|
125
|
+
| Attribute | Description |
|
|
126
|
+
|-----------|-------------|
|
|
127
|
+
| `src` | URL of a `.3md` file to fetch and render. |
|
|
128
|
+
| `mode` | Override the render mode. One of `stack`, `play`, `single`, `present`, `blend`, `map`, `layers`, `elevator`. Retired aliases such as `scene`, `parallax`, and `deck` are still accepted and mapped to current modes. Defaults to a mode chosen from the document's `axis`. |
|
|
129
|
+
| `autoplay` | Start auto-advancing as soon as content loads. `mode="play"` also auto-runs. |
|
|
130
|
+
|
|
131
|
+
Inline text content (the `.3md` source between the tags) is used when there is
|
|
132
|
+
no `src`.
|
|
133
|
+
|
|
134
|
+
### Properties and methods
|
|
135
|
+
|
|
136
|
+
- `document` - the parsed `Document`, or `null` before content loads.
|
|
137
|
+
- `error` - the parse or load error from the most recent failed load, or `null`.
|
|
138
|
+
- `errorLine` - the 1-based source line for parser errors when available.
|
|
139
|
+
- `errorCode` - the stable parser error code when available.
|
|
140
|
+
- `voxelizable` - whether the current document can render cleanly in `blend` mode.
|
|
141
|
+
- `currentIndex` - the index of the focused plane.
|
|
142
|
+
- `mode` - the active render mode.
|
|
143
|
+
- `playing` - whether playback is running.
|
|
144
|
+
- `goTo(index)` - focus a plane by index.
|
|
145
|
+
- `toggleFullscreen()` - toggle fullscreen for the whole component.
|
|
146
|
+
- `setSource(text)` - replace the rendered document with new 3md source.
|
|
147
|
+
- `play()` / `pause()` - control auto-advancing playback.
|
|
148
|
+
|
|
149
|
+
### Events
|
|
150
|
+
|
|
151
|
+
- `planechange` - dispatched when the focused plane changes. `event.detail` is
|
|
152
|
+
`{ index, z, label, plane }`.
|
|
153
|
+
|
|
154
|
+
### Interaction
|
|
155
|
+
|
|
156
|
+
- Scrub the slider, use the prev/next buttons, or the arrow keys.
|
|
157
|
+
- On the stage, drag vertically to move along Z and horizontally to orbit. Touch
|
|
158
|
+
works the same. The stage uses `touch-action: none` so a finger drives the
|
|
159
|
+
model rather than scrolling the page.
|
|
160
|
+
|
|
161
|
+
## Theming
|
|
162
|
+
|
|
163
|
+
The component uses Shadow DOM for style isolation. Theme it with CSS custom
|
|
164
|
+
properties:
|
|
165
|
+
|
|
166
|
+
```css
|
|
167
|
+
three-md {
|
|
168
|
+
--three-md-accent: #0e6f66;
|
|
169
|
+
--three-md-bg: #0c0f12;
|
|
170
|
+
--three-md-surface: #161a1e;
|
|
171
|
+
--three-md-text: #f4f3ef;
|
|
172
|
+
--three-md-muted: #9aa3a8;
|
|
173
|
+
--three-md-hairline: rgba(255, 255, 255, 0.12);
|
|
174
|
+
--three-md-height: 460px;
|
|
175
|
+
--three-md-plane-width: 320px;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Or target internals with `::part()`: `stage`, `scene`, `plane`, `plane-title`,
|
|
180
|
+
`plane-body`, `controls`, `scrubber`, `prev`, `next`, `readout`, `axis`, `hint`.
|
|
181
|
+
|
|
182
|
+
## Guarantees
|
|
183
|
+
|
|
184
|
+
The component is tested in CI (`uitests/`) in both Chromium and WebKit:
|
|
185
|
+
|
|
186
|
+
- the focused plane is frontmost in true Z while scrubbing (so it is correct in
|
|
187
|
+
Safari, which paints by Z and ignores `z-index` inside `preserve-3d`),
|
|
188
|
+
- it works with `requestAnimationFrame` paused (iOS Low Power Mode),
|
|
189
|
+
- it does not overflow horizontally from 320px to 1440px,
|
|
190
|
+
- the console stays clean.
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT (c) CorvidLabs.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Type declarations for @corvidlabs/three-md-element.
|
|
2
|
+
//
|
|
3
|
+
// Hand-authored and self-contained (no external imports) so the published
|
|
4
|
+
// package's types resolve without depending on the parser package. The shapes
|
|
5
|
+
// mirror @corvidlabs/threemd's Document and Plane.
|
|
6
|
+
|
|
7
|
+
export type Mode =
|
|
8
|
+
| "stack"
|
|
9
|
+
| "play"
|
|
10
|
+
| "single"
|
|
11
|
+
| "present"
|
|
12
|
+
| "blend"
|
|
13
|
+
| "map"
|
|
14
|
+
| "layers"
|
|
15
|
+
| "elevator";
|
|
16
|
+
|
|
17
|
+
export interface ThreeMDPlane {
|
|
18
|
+
readonly z: number;
|
|
19
|
+
readonly label: string | null;
|
|
20
|
+
readonly x: number | null;
|
|
21
|
+
readonly y: number | null;
|
|
22
|
+
readonly attributes: Readonly<Record<string, string>>;
|
|
23
|
+
readonly body: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ThreeMDDocument {
|
|
27
|
+
readonly version: string;
|
|
28
|
+
readonly axis: string;
|
|
29
|
+
readonly title: string | null;
|
|
30
|
+
readonly metadata: Readonly<Record<string, string>>;
|
|
31
|
+
readonly preamble: string | null;
|
|
32
|
+
readonly planes: readonly ThreeMDPlane[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Detail of the `planechange` event dispatched when the focused plane changes. */
|
|
36
|
+
export interface PlaneChangeDetail {
|
|
37
|
+
readonly index: number;
|
|
38
|
+
readonly z: number;
|
|
39
|
+
readonly label: string | null;
|
|
40
|
+
readonly plane: ThreeMDPlane;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The `<three-md>` custom element: an interactive renderer for the 3md format.
|
|
45
|
+
* Importing the package registers the element via `customElements.define`.
|
|
46
|
+
*/
|
|
47
|
+
export declare class ThreeMDElement extends HTMLElement {
|
|
48
|
+
static get observedAttributes(): string[];
|
|
49
|
+
/** The parsed document, or null before content has loaded. */
|
|
50
|
+
get document(): ThreeMDDocument | null;
|
|
51
|
+
/** The parse error from the most recent load, or null if it parsed cleanly. */
|
|
52
|
+
get error(): string | null;
|
|
53
|
+
/** The 1-based source line a parse error was attributed to, or null. */
|
|
54
|
+
get errorLine(): number | null;
|
|
55
|
+
/** The stable parser error code from the most recent failed load, or null. */
|
|
56
|
+
get errorCode(): string | null;
|
|
57
|
+
/** Whether the current document can render cleanly in blend/3D mode. */
|
|
58
|
+
get voxelizable(): boolean;
|
|
59
|
+
/** The index of the currently focused plane. */
|
|
60
|
+
get currentIndex(): number;
|
|
61
|
+
/** The active render mode. */
|
|
62
|
+
get mode(): Mode;
|
|
63
|
+
/** Whether playback is currently running. */
|
|
64
|
+
get playing(): boolean;
|
|
65
|
+
/** Focus a plane by index, clamped to range. */
|
|
66
|
+
goTo(index: number): void;
|
|
67
|
+
/** Toggle fullscreen for the element. */
|
|
68
|
+
toggleFullscreen(): void;
|
|
69
|
+
/** Replace the rendered document with new 3md source text. */
|
|
70
|
+
setSource(source: string): void;
|
|
71
|
+
/** Start auto-advancing through the planes. */
|
|
72
|
+
play(): void;
|
|
73
|
+
/** Stop auto-advancing. */
|
|
74
|
+
pause(): void;
|
|
75
|
+
/** Apply the current state to the DOM synchronously. */
|
|
76
|
+
render(): void;
|
|
77
|
+
connectedCallback(): void;
|
|
78
|
+
disconnectedCallback(): void;
|
|
79
|
+
attributeChangedCallback(name: string, oldValue: string | null, value: string | null): void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
declare global {
|
|
83
|
+
interface HTMLElementTagNameMap {
|
|
84
|
+
"three-md": ThreeMDElement;
|
|
85
|
+
}
|
|
86
|
+
}
|
package/dist/three-md.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
class _ extends Error{code;line;detail;constructor(J,U,j,$){super(U);this.name="ParseError",this.code=J,this.line=j,this.detail=$,Object.setPrototypeOf(this,_.prototype)}}var c=new Set(["z","x","y","label"]);function R(J){return J.replace(/^[ \t]+/,"").replace(/[ \t]+$/,"")}function o(J){for(let U of J.split(/[ \t]+/))if(U.length>0)return U;return""}function d(J,U){let j=[],$="",B=null,Y=!1;for(let X of J)if(B!==null){if($+=X,Y)Y=!1;else if(X==="\\")Y=!0;else if(X===B)B=null}else if(X==='"'||X==="'")B=X,$+=X;else if(X===" "||X==="\t"){if($.length>0)j.push($),$=""}else $+=X;if(B!==null)throw new _("invalidPlaneDirective",`Invalid @plane directive on line ${U}: unterminated quote in '${J}'`,U,`unterminated quote in '${J}'`);if($.length>0)j.push($);return j}function b(J){if(J.length<2)return J;let U=J[0],j=J[J.length-1];if(U==='"'&&j==='"'||U==="'"&&j==="'")return s(J.slice(1,-1));return J}function s(J){let U="",j=!1;for(let $ of J)if(j)U+=$,j=!1;else if($==="\\")j=!0;else U+=$;if(j)U+="\\";return U}function n(J){if(J.startsWith("```"))return"`";if(J.startsWith("~~~"))return"~";return null}function w(J){let U=0,j=J.length;while(U<j&&R(J[U]??"")==="")U+=1;while(j>U&&R(J[j-1]??"")==="")j-=1;if(U>=j)return null;return J.slice(U,j).join(`
|
|
2
|
+
`)}function k(J){if(!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(J))return null;let j=Number(J);return Number.isFinite(j)?j:null}function i(J){let U=0;while(U<J.length&&R(J[U]??"")==="")U+=1;if(U>=J.length||R(J[U]??"")!=="---")throw new _("missingFrontmatter","3md documents must begin with a '---' frontmatter block.");U+=1;let j=[];while(U<J.length){let $=R(J[U]??"");if($==="---"){let B=U+1,Y=B<J.length?J.slice(B):[];return{fields:j,bodyStartLine:U+2,body:Y}}if($!==""&&!$.startsWith("#")){let B=$.indexOf(":");if(B<0)throw new _("invalidFrontmatter",`Invalid frontmatter: expected 'key: value', found '${$}'`,void 0,`expected 'key: value', found '${$}'`);let Y=R($.slice(0,B)),X=R($.slice(B+1));j.push({key:Y,value:b(X)})}U+=1}throw new _("invalidFrontmatter","Invalid frontmatter: frontmatter block was not closed with '---'",void 0,"frontmatter block was not closed with '---'")}function a(J){let U=null,j="layer",$=null,B=Object.create(null);for(let Y of J)switch(Y.key.toLowerCase()){case"3md":U=Y.value;break;case"axis":j=R(Y.value).toLowerCase();break;case"title":$=Y.value;break;default:B[Y.key]=Y.value;break}if(U===null||U.length===0)throw new _("missingVersion","Frontmatter is missing the required '3md' version key.");return{version:U,axis:j,title:$,metadata:B}}function t(J,U){let j=R(J.slice(6)),$=Object.create(null);for(let B of d(j,U)){let Y=B.indexOf("=");if(Y<0)throw new _("invalidPlaneDirective",`Invalid @plane directive on line ${U}: expected key=value, found '${B}'`,U,`expected key=value, found '${B}'`);let X=R(B.slice(0,Y)).toLowerCase(),G=b(R(B.slice(Y+1)));if(X.length===0)throw new _("invalidPlaneDirective",`Invalid @plane directive on line ${U}: empty attribute key in '${B}'`,U,`empty attribute key in '${B}'`);$[X]=G}return $}function E(J,U,j){if(J===void 0)return null;let $=k(J);if($===null)throw new _("invalidPlaneDirective",`Invalid @plane directive on line ${j}: ${U} must be a finite decimal number, found '${J}'`,j,`${U} must be a finite decimal number, found '${J}'`);return $}function r(J){let{attributes:U,lineNumber:j}=J,$=U.z;if($===void 0)throw new _("missingPlanePosition",`The @plane directive on line ${j} is missing a 'z' position.`,j);let B=k($);if(B===null)throw new _("invalidPlaneDirective",`Invalid @plane directive on line ${j}: z must be a finite decimal number, found '${$}'`,j,`z must be a finite decimal number, found '${$}'`);let Y=E(U.x,"x",j),X=E(U.y,"y",j),G=U.label,Q=G===void 0?null:G,F=Object.create(null);for(let q of Object.keys(U))if(!c.has(q)){let H=U[q];if(H!==void 0)F[q]=H}let K=w(J.bodyLines)??"";return{z:B,label:Q,x:Y,y:X,attributes:F,body:K}}function l(J,U){let j=new Set,$=[],B=null,Y=[],X=null,G=()=>{if(B===null)return;let F=r(B);if(j.has(F.z))throw new _("duplicatePlane",`Two planes share the same z position: ${F.z}`,void 0,String(F.z));j.add(F.z),$.push(F)};J.forEach((F,K)=>{let q=U+K,H=R(F),T=!1;if(X!==null){if(H.startsWith(X.repeat(3)))X=null}else{let V=n(H);if(V!==null)X=V;else if(F[0]!==" "&&F[0]!=="\t"&&o(F)==="@plane")T=!0}if(!T){if(B!==null)B.bodyLines.push(F);else Y.push(F);return}G();let A=t(H,q);B={lineNumber:q,attributes:A,bodyLines:[]}}),G();let Q=w(Y);if($.length===0){if(Q===null)return{preamble:null,planes:[]};return{preamble:null,planes:[{z:0,label:null,x:null,y:null,attributes:Object.create(null),body:Q}]}}return{preamble:Q,planes:$}}function g(J){let U=J.replace(/\r\n/g,`
|
|
3
|
+
`);if(U.charCodeAt(0)===65279)U=U.slice(1);let j=U.split(`
|
|
4
|
+
`),$=i(j),B=a($.fields),{preamble:Y,planes:X}=l($.body,$.bodyStartLine);return{version:B.version,axis:B.axis,title:B.title,metadata:B.metadata,preamble:Y,planes:X}}var e=["stack","play","single","present","blend","map","layers","elevator"],f={parallax:"stack",scene:"map",deck:"present"},P={time:"stack",frame:"play",frames:"play",layer:"layers",layers:"layers",depth:"stack",space:"map",scene:"map",slide:"present",slides:"present",deck:"present",floor:"elevator",floors:"elevator"},S=new Set(["stack","blend","map","layers","elevator"]),v={stack:"drag to orbit · WASD / arrows move · scroll to zoom · slider scrubs Z",layers:"aligned overlays — drag to orbit the stack · slider brings a layer to front",elevator:"floors stacked vertically — drag to orbit · slider rides the floors",map:"the full board · tap a tile to read it · tap empty space or Esc for the board · drag to orbit · scroll to zoom",blend:"drag to orbit the object · scroll to zoom",play:"animation — play / pause and loop in the controls",present:"arrows or space to advance slides",single:"scroll to read · arrows or slider change plane"};function z(J){return J.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}function y(J){return z(J).replace(/"/g,""").replace(/'/g,"'")}function JJ(J){return J.replace(/\[\[z=([-+0-9eE.]{1,40})(?:\|([^\]\n]{0,400}))?\]\]/g,(U,j,$)=>`<a class="xlink" part="link" data-z="${j}">${$||"z="+j}</a>`).replace(/(^|[^!])\[([^\]]+)\]\((https?:\/\/[^)\s"'<>`]+)\)/g,(U,j,$,B)=>`${j}<a class="xlink" part="link" href="${y(B)}" target="_blank" rel="noopener">${$}</a>`).replace(/`([^`]+)`/g,"<code>$1</code>").replace(/\*\*([^*]+)\*\*/g,"<strong>$1</strong>").replace(/\*([^*]+)\*/g,"<em>$1</em>")}function I(J){return JJ(z(J))}function UJ(J){let U={};if(!J)return U;for(let j of J.trim().split(/[\s,]+/)){let $=j.indexOf("=");if($<=0)continue;let Y=[...j.slice(0,$).replace(/^["']|["']$/g,"")][0],X=j.slice($+1).replace(/^["']|["']$/g,"");if(Y)U[Y]=X===""?" ":z(X)}return U}function $J(J,U){let j=Object.keys(U);if(!j.length)return J;let $=j.map((B)=>B.replace(/[.*+?^${}()|[\]\\\-]/g,"\\$&")).join("");return J.replace(new RegExp(`[${$}]`,"g"),(B)=>U[B]??B)}function h(J,U={}){let j=J.split(`
|
|
5
|
+
`),$=(F)=>`<pre class="code" part="code">${F.map((K)=>$J(z(K),U)).join(`
|
|
6
|
+
`)}</pre>`,B=(F)=>/^\s*\|.*\|\s*$/.test(F),Y=(F)=>/-/.test(F)&&/^\s*\|?[\s:|-]*\|[\s:|-]*$/.test(F),X=(F)=>F.trim().replace(/^\||\|$/g,"").split("|").map((K)=>K.trim()),G=[],Q=null;for(let F=0;F<j.length;F++){let K=j[F];if(K.startsWith("```")){if(Q===null)Q=[];else G.push($(Q)),Q=null;continue}if(Q!==null){Q.push(K);continue}if(B(K)&&F+1<j.length&&Y(j[F+1])){let q=X(K);F+=2;let H=[];while(F<j.length&&B(j[F]))H.push(X(j[F])),F++;F--,G.push(`<table class="tbl" part="table"><thead><tr>${q.map((T)=>`<th>${I(T)}</th>`).join("")}</tr></thead><tbody>${H.map((T)=>`<tr>${T.map((A)=>`<td>${I(A)}</td>`).join("")}</tr>`).join("")}</tbody></table>`);continue}if(K.startsWith("# "))G.push(`<div class="ph" part="plane-title">${I(K.slice(2))}</div>`);else if(K.startsWith("## "))G.push(`<div class="ph2" part="plane-subtitle">${I(K.slice(3))}</div>`);else if(K.startsWith("### "))G.push(`<div class="ph2" part="plane-subtitle">${I(K.slice(4))}</div>`);else if(K.startsWith("- [x] ")||K.startsWith("- [X] "))G.push(`<div class="li"><span class="box">▣</span><span class="done">${I(K.slice(6))}</span></div>`);else if(K.startsWith("- [ ] "))G.push(`<div class="li"><span class="box">▢</span><span>${I(K.slice(6))}</span></div>`);else if(/^\s*[-*] /.test(K)){let q=K.replace(/^(\s*)[-*] /,"$1"),H=(K.match(/^\s*/)?.[0].length||0)>0;G.push(`<div class="li${H?" sub":""}"><span class="box">•</span><span>${I(q.trim())}</span></div>`)}else if(/^\d+\. /.test(K))G.push(`<div class="li"><span class="box">${z(K.slice(0,K.indexOf(".")))}</span><span>${I(K.slice(K.indexOf(".")+2))}</span></div>`);else if(K.startsWith("> "))G.push(`<div class="quote">${I(K.slice(2))}</div>`);else if(/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(K))G.push('<hr class="rule">');else if(/^!\[[^\]]*\]\([^)\s]+\)\s*$/.test(K.trim())){let q=K.trim().match(/^!\[([^\]]*)\]\(([^)\s]+)\)/),H=q?y(q[1]):"",T=q?q[2]:"",V=/^(https?:\/\/|\/|\.{0,2}\/|data:image\/)/i.test(T)&&!/["'<>`\s]/.test(T)?y(T):"";if(V)G.push(`<img class="img" part="image" src="${V}" alt="${H}" loading="lazy">`)}else if(K.trim()!=="")G.push(`<div>${I(K)}</div>`)}if(Q!==null)G.push($(Q));return`<div class="md" part="plane-body">${G.join("")}</div>`}var W=(J,U=!1)=>`<svg viewBox="0 0 24 24" ${U?'fill="currentColor" stroke="none"':'fill="none" stroke="currentColor" stroke-width="2"'} stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${J}</svg>`,O={prev:W('<path d="M15 5l-7 7 7 7"/>'),next:W('<path d="M9 5l7 7-7 7"/>'),play:W('<path d="M8 5.5v13l11-6.5z"/>',!0),pause:W('<path d="M9 5v14M15 5v14"/>'),loop:W('<path d="M3.5 12a8.5 8.5 0 0 1 14.5-6M20.5 12a8.5 8.5 0 0 1-14.5 6"/><path d="M18 2.5V6h-3.5M6 21.5V18h3.5"/>'),fullscreen:W('<path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/>')},jJ=`
|
|
7
|
+
:host {
|
|
8
|
+
display: block;
|
|
9
|
+
max-width: 100%;
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
/* Themeable surface. Override these (or use ::part) to restyle. */
|
|
12
|
+
--three-md-bg: var(--three-md-surface-strong, #131619);
|
|
13
|
+
--three-md-surface: #1b2024;
|
|
14
|
+
--three-md-text: #f4f3ef;
|
|
15
|
+
--three-md-muted: #9aa3a8;
|
|
16
|
+
--three-md-faint: #6b7479;
|
|
17
|
+
--three-md-accent: #45d0bc;
|
|
18
|
+
--three-md-hairline: rgba(244, 243, 239, 0.14);
|
|
19
|
+
font-family: var(--three-md-font, ui-monospace, "Spline Sans Mono", SFMono-Regular, Menlo, monospace);
|
|
20
|
+
color: var(--three-md-text);
|
|
21
|
+
}
|
|
22
|
+
* { box-sizing: border-box; }
|
|
23
|
+
:host(:focus) { outline: none; } /* no harsh ring on pointer/drag while flying */
|
|
24
|
+
/* Keyboard focus DOES get a visible indicator (WCAG 2.4.7); focus-visible never
|
|
25
|
+
fires on pointer interaction, so it doesn't reappear while dragging. */
|
|
26
|
+
:host(:focus-visible) { outline: none; box-shadow: 0 0 0 2px var(--three-md-accent); border-radius: 8px; }
|
|
27
|
+
/* An errored or empty document has no live scene: hide its stale controls. */
|
|
28
|
+
:host([data-state="error"]) .controls, :host([data-state="empty"]) .controls,
|
|
29
|
+
:host([data-state="error"]) .hint, :host([data-state="empty"]) .hint,
|
|
30
|
+
:host([data-state="error"]) .readout, :host([data-state="empty"]) .readout { display: none; }
|
|
31
|
+
@media (prefers-reduced-motion: reduce) {
|
|
32
|
+
.plane, .scene, .floattext { transition-duration: .001ms !important; }
|
|
33
|
+
}
|
|
34
|
+
.wrap { width: 100%; max-width: 100%; }
|
|
35
|
+
.axis { font-size: 12px; letter-spacing: .14em; text-transform: uppercase; color: var(--three-md-accent); margin: 0 0 8px; }
|
|
36
|
+
.stage {
|
|
37
|
+
position: relative; width: 100%; height: var(--three-md-height, 440px);
|
|
38
|
+
border: 1px solid var(--three-md-hairline); border-radius: 8px;
|
|
39
|
+
background: var(--three-md-bg); overflow: hidden;
|
|
40
|
+
perspective: 1300px; perspective-origin: 50% 44%;
|
|
41
|
+
cursor: grab; touch-action: none; user-select: none; -webkit-user-select: none;
|
|
42
|
+
}
|
|
43
|
+
.stage:active { cursor: grabbing; }
|
|
44
|
+
.scene { position: absolute; inset: 0; transform-style: preserve-3d; }
|
|
45
|
+
/* Flat 2D overlay for the focus-to-read card: lives OUTSIDE the perspective'd
|
|
46
|
+
scene so the read card is never skewed by the board's tilt. Clicks pass through
|
|
47
|
+
to the board behind, except on the card itself. */
|
|
48
|
+
.detail { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; pointer-events: none; z-index: 5; }
|
|
49
|
+
:host([data-mode="map"]) .detail { display: flex; }
|
|
50
|
+
.detail .plane.popped { pointer-events: auto; position: relative; inset: auto; left: auto; top: auto; right: auto; bottom: auto;
|
|
51
|
+
margin: 0; height: max-content; max-height: none; transform-origin: center center; }
|
|
52
|
+
/* The hint sits BELOW the stage, never over the plane content. */
|
|
53
|
+
.hint { margin: 8px 2px 0; font-size: 11px; color: var(--three-md-faint); }
|
|
54
|
+
.fs { margin-left: auto; }
|
|
55
|
+
.md .img, [part="image"] { display: block; max-width: 100%; height: auto; border-radius: 6px; margin: 8px 0; }
|
|
56
|
+
:host(:fullscreen) { background: var(--three-md-bg); padding: 2.5vh 3vw; box-sizing: border-box; }
|
|
57
|
+
:host(:fullscreen) .wrap { height: 100%; display: flex; flex-direction: column; }
|
|
58
|
+
:host(:fullscreen) .stage { flex: 1 1 auto; height: auto; }
|
|
59
|
+
:host(:fullscreen) .hint { display: none; } /* no instructional text over a presentation */
|
|
60
|
+
/* Fullscreen is a real reading/slideshow experience, not a bigger thumbnail:
|
|
61
|
+
scale the type up, and turn card views into a wide, centered slide. */
|
|
62
|
+
:host(:fullscreen) .md, :host(:fullscreen) .grid { font-size: clamp(14px, 1.6vw, 21px); line-height: 1.7; }
|
|
63
|
+
:host(:fullscreen) .md .ph { font-size: clamp(20px, 2.6vw, 34px); }
|
|
64
|
+
:host(:fullscreen) .md .ph2 { font-size: clamp(15px, 1.7vw, 22px); }
|
|
65
|
+
:host(:fullscreen) .md .code { font-size: clamp(12px, 1.2vw, 16px); }
|
|
66
|
+
:host(:fullscreen) .ptag { font-size: clamp(11px, 1vw, 14px); }
|
|
67
|
+
:host(:fullscreen) .navbtn { width: 44px; height: 40px; }
|
|
68
|
+
:host(:fullscreen) .navbtn svg { width: 18px; height: 18px; }
|
|
69
|
+
/* Reader (single) and present become big centered slides. */
|
|
70
|
+
:host(:fullscreen[data-mode="single"]) .plane.reader {
|
|
71
|
+
width: min(70vw, 900px); max-width: min(70vw, 900px); padding: clamp(20px, 3vw, 44px);
|
|
72
|
+
}
|
|
73
|
+
:host(:fullscreen[data-mode="present"]) .plane.hot {
|
|
74
|
+
max-width: min(82vw, 1100px); padding: clamp(20px, 3vw, 44px);
|
|
75
|
+
}
|
|
76
|
+
.plane {
|
|
77
|
+
position: absolute; left: 50%; top: 50%;
|
|
78
|
+
/* One width for the whole document (--three-md-card-w, computed to fit the widest
|
|
79
|
+
content, capped) so every card - deck and focused - is the SAME width. Falls
|
|
80
|
+
back to the default before it is measured. */
|
|
81
|
+
width: var(--three-md-card-w, min(var(--three-md-plane-width, 320px), 84%));
|
|
82
|
+
margin-left: calc(var(--three-md-card-w, min(var(--three-md-plane-width, 320px), 84%)) / -2); margin-top: -104px;
|
|
83
|
+
/* Keep cards inside the stage: cap height and clip; the focused card scrolls. */
|
|
84
|
+
max-height: calc(100% - 18px); overflow: hidden;
|
|
85
|
+
border-radius: 8px; padding: 14px 16px;
|
|
86
|
+
background: var(--three-md-surface); border: 1px solid var(--three-md-accent);
|
|
87
|
+
box-shadow: 0 18px 44px rgba(0,0,0,.45); cursor: pointer;
|
|
88
|
+
transition: opacity .3s, box-shadow .3s, filter .3s;
|
|
89
|
+
}
|
|
90
|
+
.plane.hot { overflow-y: auto; } /* the focused card can scroll long content */
|
|
91
|
+
/* In card modes the focused card hugs its CONTENT (so a short plane is a small
|
|
92
|
+
card, not a big empty box), stays vertically centered via auto margins, and only
|
|
93
|
+
caps + scrolls when the content is genuinely taller than the stage. */
|
|
94
|
+
:host([data-mode="stack"]) .plane.hot,
|
|
95
|
+
:host([data-mode="elevator"]) .plane.hot,
|
|
96
|
+
:host([data-mode="present"]) .plane.hot {
|
|
97
|
+
top: 0; bottom: 0; left: 0; right: 0; height: max-content; max-height: calc(100% - 20px);
|
|
98
|
+
margin: auto; overflow-y: auto;
|
|
99
|
+
/* Width comes from the shared --three-md-card-w (same as the deck), so the focused
|
|
100
|
+
card matches the cards behind it - no wide-front / narrow-back mismatch. */
|
|
101
|
+
}
|
|
102
|
+
/* Layers: EVERY overlay (not just the focused one) is a centered, stage-height,
|
|
103
|
+
scrollable box so a layer of any length fits in frame; they sit perfectly
|
|
104
|
+
aligned, with depth + opacity (not a vertical fan) giving the stacked look. */
|
|
105
|
+
:host([data-mode="layers"]) .plane {
|
|
106
|
+
top: 8px; bottom: 8px; height: auto; margin-top: 0; max-height: none; overflow-y: auto;
|
|
107
|
+
}
|
|
108
|
+
/* Map: each tile is a bounded, centered, scrollable card (margin-top matches half
|
|
109
|
+
the cap) so a tall tile stays on the board instead of spilling off the bottom. */
|
|
110
|
+
:host([data-mode="map"]) .plane {
|
|
111
|
+
max-height: 180px; margin-top: -90px; overflow-y: auto;
|
|
112
|
+
}
|
|
113
|
+
/* Layers: aligned translucent overlays seen together (not one-at-a-time). */
|
|
114
|
+
.layerchips { display: none; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
|
|
115
|
+
:host([data-mode="layers"]) .layerchips { display: flex; }
|
|
116
|
+
.layerchip { font: inherit; font-size: 11px; color: var(--three-md-muted); background: var(--three-md-surface);
|
|
117
|
+
border: 1px solid var(--three-md-hairline); border-radius: 999px; padding: 4px 11px; cursor: pointer;
|
|
118
|
+
transition: color .15s, border-color .15s, opacity .15s; }
|
|
119
|
+
.layerchip:hover { color: var(--three-md-text); border-color: var(--three-md-accent); }
|
|
120
|
+
.layerchip[aria-pressed="true"] { color: var(--three-md-text); border-color: var(--three-md-accent); }
|
|
121
|
+
.layerchip[aria-pressed="false"] { opacity: .45; text-decoration: line-through; }
|
|
122
|
+
.plane.dim { opacity: .18; filter: saturate(.5); }
|
|
123
|
+
.plane.hot { box-shadow: 0 0 0 2px var(--three-md-accent), 0 18px 44px rgba(0,0,0,.5); }
|
|
124
|
+
/* Reader: the focused plane in single-card view fills the stage and scrolls its
|
|
125
|
+
own body, so a plane of any length stays fully readable. Navigation moves to
|
|
126
|
+
the slider, arrows, and keyboard; the body scrolls with wheel or touch. */
|
|
127
|
+
/* Reader (single): every plane shares ONE fixed, scrollable box, so changing
|
|
128
|
+
plane never resizes or shifts the card - the content just swaps and scrolls. */
|
|
129
|
+
:host([data-mode="single"]) .plane {
|
|
130
|
+
top: 50%; left: 50%; transform: translate(-50%, -50%) !important;
|
|
131
|
+
width: min(var(--three-md-plane-width, 560px), 92%); height: calc(100% - 14px);
|
|
132
|
+
margin: 0; max-width: none; max-height: none; overflow-y: auto; overscroll-behavior: contain;
|
|
133
|
+
touch-action: pan-y; -webkit-overflow-scrolling: touch; cursor: auto; transition: opacity .15s;
|
|
134
|
+
-webkit-user-select: text; user-select: text;
|
|
135
|
+
}
|
|
136
|
+
/* Flipbook frame (animations): a FIXED-size centered card (not content-sized, so
|
|
137
|
+
it never grows/shrinks between frames; not full-bleed, so small animations are
|
|
138
|
+
not lost in a huge box). Content is centered; swaps are instant (no ghosting). */
|
|
139
|
+
.plane.frame {
|
|
140
|
+
top: 50%; left: 50%;
|
|
141
|
+
/* Natural content size (measured in JS) scaled to fit the stage, so every frame
|
|
142
|
+
is the same size AND the whole animation is visible with NO scrollbars. */
|
|
143
|
+
transform: translate(-50%, -50%) scale(var(--three-md-frame-scale, 1)) !important;
|
|
144
|
+
width: var(--three-md-frame-w, min(var(--three-md-plane-width, 360px), 88%));
|
|
145
|
+
height: var(--three-md-frame-h, min(72%, 340px)); margin: 0;
|
|
146
|
+
display: flex; flex-direction: column; justify-content: center; align-items: center;
|
|
147
|
+
/* Real animations fit via the scale, so no scrollbar appears; only content that
|
|
148
|
+
genuinely cannot fit (e.g. a long text doc forced into animation) scrolls. */
|
|
149
|
+
overflow: auto; transition: none;
|
|
150
|
+
}
|
|
151
|
+
:host([data-mode="play"]) .plane { transition: none; } /* instant frame swaps */
|
|
152
|
+
/* No accidental text selection while orbiting/flying; the reader stays selectable. */
|
|
153
|
+
.scene, .plane { -webkit-user-select: none; user-select: none; }
|
|
154
|
+
.plane.reader { -webkit-user-select: text; user-select: text; }
|
|
155
|
+
.ptag { font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--three-md-accent); display: flex; justify-content: space-between; margin-bottom: 8px; }
|
|
156
|
+
.ptag b { color: var(--three-md-text); font-weight: 700; }
|
|
157
|
+
.md, .grid { font-size: 12.5px; line-height: 1.65; color: var(--three-md-muted); }
|
|
158
|
+
.md .ph { font-family: var(--three-md-display, inherit); font-weight: 700; font-size: 16px; margin: 2px 0 8px; color: var(--three-md-text); }
|
|
159
|
+
.md .ph2 { font-weight: 700; font-size: 13px; margin: 6px 0 6px; color: var(--three-md-text); }
|
|
160
|
+
.md .code { margin: 6px 0; padding: 8px 10px; background: var(--three-md-hairline); border-radius: 4px; font-size: 11.5px; line-height: 1.5; white-space: pre; overflow-x: auto; max-width: 100%; color: var(--three-md-text); }
|
|
161
|
+
.md .li { display: flex; gap: 8px; }
|
|
162
|
+
.md .box { color: var(--three-md-accent); }
|
|
163
|
+
.md .done { color: var(--three-md-faint); text-decoration: line-through; }
|
|
164
|
+
.md .quote { border-left: 2px solid var(--three-md-hairline); padding-left: 10px; color: var(--three-md-faint); font-style: italic; }
|
|
165
|
+
.md .li.sub { padding-left: 16px; }
|
|
166
|
+
.md .rule { border: 0; border-top: 1px solid var(--three-md-hairline); margin: 8px 0; }
|
|
167
|
+
.md .tbl { border-collapse: collapse; margin: 8px 0; font-size: 12px; max-width: 100%; }
|
|
168
|
+
.md .tbl th, .md .tbl td { border: 1px solid var(--three-md-hairline); padding: 4px 8px; text-align: left; vertical-align: top; }
|
|
169
|
+
.md .tbl th { color: var(--three-md-text); font-weight: 700; background: var(--three-md-hairline); }
|
|
170
|
+
.md .tbl td { color: var(--three-md-muted); }
|
|
171
|
+
.md strong { color: var(--three-md-text); font-weight: 700; }
|
|
172
|
+
.md em { font-style: italic; }
|
|
173
|
+
.md code { font-family: inherit; background: var(--three-md-hairline); padding: 1px 5px; border-radius: 3px; color: var(--three-md-text); }
|
|
174
|
+
.grid { font-size: 16px; line-height: 1.25; letter-spacing: 3px; color: var(--three-md-faint); white-space: pre; max-width: 100%; overflow-x: auto; }
|
|
175
|
+
.grid .dot { color: var(--three-md-accent); }
|
|
176
|
+
/* A voxel IS its character glyph (legend-remapped): emoji/symbols render in full
|
|
177
|
+
colour, plain chars in the accent. Depth is cued by opacity (near = focused). */
|
|
178
|
+
.voxel { position: absolute; left: 50%; top: 50%; width: 14px; height: 14px; margin: -7px 0 0 -7px;
|
|
179
|
+
display: flex; align-items: center; justify-content: center;
|
|
180
|
+
font-family: var(--three-md-mono, ui-monospace, "Spline Sans Mono", monospace);
|
|
181
|
+
font-size: 13px; line-height: 1; color: var(--three-md-accent); opacity: .4;
|
|
182
|
+
transition: opacity .2s; pointer-events: none; }
|
|
183
|
+
.voxel.near { opacity: 1; }
|
|
184
|
+
.floattext { position: absolute; left: 50%; top: 50%; font-size: 11px; line-height: 1.5;
|
|
185
|
+
color: var(--three-md-faint); opacity: .5; white-space: pre; pointer-events: none; }
|
|
186
|
+
/* nowrap so the button row never reflows and shifts under the cursor. */
|
|
187
|
+
.controls { display: flex; align-items: center; gap: 8px; margin-top: 12px; flex-wrap: nowrap; }
|
|
188
|
+
.navbtn { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; color: var(--three-md-text); background: var(--three-md-surface); border: 1px solid var(--three-md-hairline); width: 38px; height: 34px; border-radius: 4px; cursor: pointer; }
|
|
189
|
+
.navbtn svg { width: 16px; height: 16px; display: block; }
|
|
190
|
+
.navbtn:hover { border-color: var(--three-md-accent); color: var(--three-md-accent); }
|
|
191
|
+
.navbtn.loop[aria-pressed="true"] { color: var(--three-md-accent); border-color: var(--three-md-accent); }
|
|
192
|
+
.navbtn.loop[aria-pressed="false"] { opacity: .55; }
|
|
193
|
+
input[type=range] { flex: 1 1 auto; min-width: 48px; accent-color: var(--three-md-accent); cursor: pointer; }
|
|
194
|
+
/* readout on its own line, so its changing width never moves the buttons. */
|
|
195
|
+
.readout { font-size: 12.5px; color: var(--three-md-muted); margin-top: 8px; min-height: 16px; }
|
|
196
|
+
.readout b { color: var(--three-md-accent); }
|
|
197
|
+
.md .xlink, [part="link"] { color: var(--three-md-accent); text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }
|
|
198
|
+
.md .xlink:hover { opacity: .8; }
|
|
199
|
+
.err { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
|
|
200
|
+
text-align: center; padding: 24px; color: var(--three-md-error, #ff8f8f); font-size: 13px; line-height: 1.6; }
|
|
201
|
+
:host([data-state="empty"]) .err { color: var(--three-md-muted); } /* empty is not an error */
|
|
202
|
+
`;class x extends HTMLElement{static get observedAttributes(){return["src","mode","autoplay"]}_root;_scene;_stage;_detail;_axisEl;_scrub;_readout;_hintEl;_wrap;_doc=null;_planes=[];_els=[];_voxels=[];_billboard=!1;_mode="stack";_legend={};_focus=0;_target=0;_yaw=0;_pitch=0;_panX=0;_panY=0;_zoom=0;_dragging=!1;_mapOverview=!0;_downTarget=null;_lastX=0;_lastY=0;_dragStartX=0;_dragStartY=0;_dragStartTarget=0;_dragAxis=null;_pointerId=null;_pendingDrag=!1;_hiddenLayers=new Set;_chipsEl;_keys=new Set;_camRaf=null;_lastEmitted=-1;_error=null;_errorLine=null;_errorCode=null;_loadToken=0;_playBtn;_loopBtn;_loop=!0;_playing=!1;_playTimer=null;constructor(){super();this._root=this.attachShadow({mode:"open"})}connectedCallback(){this._build();let J=this.getAttribute("src");if(J)this._loadFromSrc(J);else this._loadFromText(this.textContent||"")}disconnectedCallback(){this._stopPlay()}attributeChangedCallback(J,U,j){if(!this._scene)return;if(J==="src"&&j)this._loadFromSrc(j);else if(J==="mode"){let $=this._mode==="blend";if(this._applyMode(),$||this._mode==="blend")this._buildPlanes();this._resetCamera(),this._buildLayerChips(),this.render()}else if(J==="autoplay")if(j!==null)this._startPlay();else this._stopPlay()}get document(){return this._error?null:this._doc}get errorLine(){return this._errorLine}get errorCode(){return this._errorCode}get voxelizable(){let J=this._planes.map((U)=>this._gridOf(U.body)).filter((U)=>U!==null);return J.length>0&&J.every((U)=>U.w<=36&&U.h<=36)}get currentIndex(){return Math.round(this._focus)}get mode(){return this._mode}get error(){return this._error}goTo(J){this._setTarget(J)}toggleFullscreen(){let J=document,U=this;if(J.fullscreenElement||J.webkitFullscreenElement)(J.exitFullscreen||J.webkitExitFullscreen)?.call(J);else(U.requestFullscreen||U.webkitRequestFullscreen)?.call(U)}setSource(J){if(!this._scene)this._build();this._loadFromText(J)}play(){this._startPlay()}pause(){this._stopPlay()}get playing(){return this._playing}async _loadFromSrc(J){let U=++this._loadToken;try{let j=await fetch(J);if(!j.ok)throw Error(`HTTP ${j.status}`);let $=await j.text();if(U!==this._loadToken)return;this._loadFromText($)}catch(j){if(U!==this._loadToken)return;this._showError(`Could not load ${J}: ${j.message}`)}}_loadFromText(J){let U=J.trim();if(!U){this._error="Nothing to render yet — start typing or load a 3md document.",this._errorLine=null,this._errorCode=null,this._doc=null,this._planes=[],this._stopPlay(),this.setAttribute("data-state","empty"),this._showError(this._error);return}let j;try{j=g(U)}catch(B){let Y=B;this._error=Y.message??String(B),this._errorLine=typeof Y.line==="number"?Y.line:null,this._errorCode=typeof Y.code==="string"?Y.code:null,this._doc=null,this._planes=[],this._stopPlay(),this.setAttribute("data-state","error"),this._showError(`Invalid 3md: ${this._error}`);return}this._error=null,this._errorLine=null,this._errorCode=null,this.setAttribute("data-state","ready"),this._doc=j,this._planes=j.planes,this._legend=UJ(j.metadata?.legend);let $=String(j.metadata?.billboard??"").toLowerCase();if(this._billboard=$==="true"||$==="yes"||$==="1",this._applyMode(),this._stopPlay(),this._focus=0,this._target=0,this._resetCamera(),this._keys.clear(),this._hiddenLayers.clear(),this._camRaf!=null)cancelAnimationFrame(this._camRaf),this._camRaf=null;if(this._dragAxis=null,this._dragging=!1,this._pendingDrag=!1,this._lastEmitted=-1,this._buildPlanes(),this.render(),this.hasAttribute("autoplay")||this._mode==="play")this._startPlay()}_applyMode(){let J=(j)=>{let $=(j||"").toLowerCase();if(!$)return null;if(f[$])return f[$];if(e.includes($))return $;if(P[$])return P[$];return null},U=this._doc?this._doc.metadata.view??this._doc.metadata.display:null;if(this._mode=J(this.getAttribute("mode"))||J(U)||(this._doc?P[this._doc.axis]||"stack":"stack"),this.setAttribute("data-mode",this._mode),this._hintEl)this._hintEl.textContent=v[this._mode]??""}_build(){if(this._scene)return;let J=document.createElement("style");J.textContent=jJ,this._root.appendChild(J),this._wrap=document.createElement("div"),this._wrap.className="wrap",this._wrap.setAttribute("part","wrap"),this._wrap.innerHTML=`
|
|
203
|
+
<div class="axis" part="axis"></div>
|
|
204
|
+
<div class="stage" part="stage">
|
|
205
|
+
<div class="scene" part="scene"></div>
|
|
206
|
+
<div class="detail" part="detail"></div>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="hint" part="hint">drag to orbit · WASD / arrows move · slider or ↑↓ scrub Z</div>
|
|
209
|
+
<div class="controls" part="controls">
|
|
210
|
+
<button class="navbtn" part="prev" type="button" aria-label="previous plane">${O.prev}</button>
|
|
211
|
+
<input type="range" part="scrubber" min="0" max="0" step="0.001" value="0" aria-label="scrub the Z axis" />
|
|
212
|
+
<button class="navbtn" part="next" type="button" aria-label="next plane">${O.next}</button>
|
|
213
|
+
<button class="navbtn" part="play" type="button" aria-label="play">${O.play}</button>
|
|
214
|
+
<button class="navbtn loop" part="loop" type="button" aria-label="toggle loop" title="Loop playback" aria-pressed="true">${O.loop}</button>
|
|
215
|
+
<button class="navbtn fs" part="fullscreen" type="button" aria-label="fullscreen" title="Fullscreen">${O.fullscreen}</button>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="readout" part="readout"></div>
|
|
218
|
+
<div class="layerchips" part="layer-toggles"></div>`,this._root.appendChild(this._wrap),this._axisEl=this._wrap.querySelector(".axis"),this._stage=this._wrap.querySelector(".stage"),this._scene=this._wrap.querySelector(".scene"),this._detail=this._wrap.querySelector(".detail"),this._scrub=this._wrap.querySelector("input"),this._readout=this._wrap.querySelector(".readout"),this._chipsEl=this._wrap.querySelector(".layerchips"),this._hintEl=this._wrap.querySelector(".hint"),this._playBtn=this._wrap.querySelector('[part="play"]'),this._scrub.addEventListener("input",()=>{this._stopPlay(),this._target=parseFloat(this._scrub.value),this._focus=this._target,this.render()}),this._playBtn.addEventListener("click",()=>this._togglePlay()),this._wrap.querySelector('[part="prev"]').addEventListener("click",()=>this._setTarget(Math.round(this._target)-1)),this._wrap.querySelector('[part="next"]').addEventListener("click",()=>this._setTarget(Math.round(this._target)+1)),this._wrap.querySelector('[part="fullscreen"]').addEventListener("click",()=>this.toggleFullscreen()),this._loopBtn=this._wrap.querySelector('[part="loop"]'),this._loopBtn.addEventListener("click",()=>{this._loop=!this._loop,this._loopBtn.setAttribute("aria-pressed",String(this._loop))}),this._stage.addEventListener("click",(U)=>{let j=U.target?.closest?.(".xlink");if(!j)return;U.preventDefault(),U.stopPropagation();let $=Number(j.dataset.z),B=this._planes.findIndex((Y)=>Y.z===$);if(B>=0)this._setTarget(B)},!0),this.tabIndex=this.tabIndex<0?0:this.tabIndex,this.addEventListener("keydown",(U)=>this._onKey(U)),this.addEventListener("keyup",(U)=>this._onKeyUp(U)),this.addEventListener("blur",()=>this._keys.clear()),this._stage.addEventListener("pointerdown",(U)=>this._onPointerDown(U)),this._stage.addEventListener("pointermove",(U)=>this._onPointerMove(U)),this._stage.addEventListener("pointerup",(U)=>this._onPointerUp(U)),this._stage.addEventListener("pointercancel",(U)=>this._onPointerUp(U)),this._stage.addEventListener("wheel",(U)=>this._onWheel(U),{passive:!1})}_showError(J){if(!this._wrap)this._build();if(this._scene.innerHTML="",this._detail)this._detail.innerHTML="";if(this._els=[],this._scene.style.transform="",this._axisEl)this._axisEl.textContent="";if(this._readout)this._readout.textContent="";let U=document.createElement("div");U.className="err",U.setAttribute("part","error"),U.textContent=J,this._scene.appendChild(U)}_buildPlanes(){if(this._scene.innerHTML="",this._detail)this._detail.innerHTML="";if(this._els=[],this._voxels=[],this._axisEl.textContent=this._doc?`axis = ${this._doc.axis}`:"",this._mode==="blend")if(!this.voxelizable){if(this._mode="stack",this.setAttribute("data-mode","stack"),this._hintEl)this._hintEl.textContent=v.stack}else this._buildBlend();if(this._mode!=="blend")this._planes.forEach((J,U)=>{let j=document.createElement("div");j.className="plane",j.setAttribute("part","plane");let $=J.label?J.label:`z ${J.z}`;j.innerHTML=`<div class="ptag"><span>z = ${J.z}</span><b>${z($)}</b></div>${h(J.body,this._legend)}`,j.addEventListener("click",()=>{if(this._mode==="map")this._mapOverview=!1;this._setTarget(U)}),this._scene.appendChild(j),this._els.push(j)}),this._measureCardWidth();this._scrub.max=String(Math.max(0,this._planes.length-1)),this._scrub.value="0",this._measureFrames(),this._buildLayerChips(),this._updateReadout()}_measureCardWidth(){if(!this._els.length||!this._stage)return;this._wrap.style.removeProperty("--three-md-card-w");let J=this._stage.clientWidth||600,U=Math.round(J*0.92),j=Math.min(320,Math.round(J*0.84)),$=0;for(let Y of this._els){for(let X of Y.querySelectorAll(".code"))$=Math.max($,X.scrollWidth);for(let X of Y.querySelectorAll(".tbl, table")){let G=X.style.cssText;X.style.cssText="width:max-content",$=Math.max($,X.offsetWidth),X.style.cssText=G}}let B=$?Math.max(j,Math.min($+36,U)):j;this._wrap.style.setProperty("--three-md-card-w",B+"px")}_buildLayerChips(){if(!this._chipsEl)return;if(this._chipsEl.innerHTML="",this._mode!=="layers")return;this._planes.forEach((J,U)=>{let j=document.createElement("button");j.type="button",j.className="layerchip",j.setAttribute("part","layer-toggle"),j.textContent=J.label||`z${J.z}`,j.setAttribute("aria-pressed",String(!this._hiddenLayers.has(U))),j.addEventListener("click",()=>{if(this._hiddenLayers.has(U))this._hiddenLayers.delete(U);else this._hiddenLayers.add(U);j.setAttribute("aria-pressed",String(!this._hiddenLayers.has(U))),this.render()}),this._chipsEl.appendChild(j)})}_measureFrames(){if(this.style.removeProperty("--three-md-frame-w"),this.style.removeProperty("--three-md-frame-h"),this.style.removeProperty("--three-md-frame-scale"),this._mode!=="play"||!this._els.length)return;let J=(this._stage?.clientWidth||9999)-24,U=(this._stage?.clientHeight||9999)-24,j=0;for(let Y of this._els){let X=Y.style.cssText;Y.classList.remove("frame"),Y.style.transform="none",Y.style.opacity="0",Y.style.width="max-content",Y.style.height="auto",Y.style.maxWidth="none",Y.style.maxHeight="none",j=Math.max(j,Y.offsetWidth),Y.style.cssText=X}if(j<=0)return;let $=0;for(let Y of this._els){let X=Y.style.cssText;Y.classList.remove("frame"),Y.style.transform="none",Y.style.opacity="0",Y.style.width=j+"px",Y.style.height="auto",Y.style.maxWidth="none",Y.style.maxHeight="none",$=Math.max($,Y.offsetHeight),Y.style.cssText=X}if($<=0)return;let B=Math.min(J/j,U/$,1);this.style.setProperty("--three-md-frame-w",j+"px"),this.style.setProperty("--three-md-frame-h",$+"px"),this.style.setProperty("--three-md-frame-scale",B.toFixed(3))}_gridOf(J){let U=J.split(`
|
|
219
|
+
`),j=U.findIndex((G)=>G.startsWith("```"));if(j<0)return null;let $=[];for(let G=j+1;G<U.length&&!U[G].startsWith("```");G++)$.push(U[G]);if(!$.length)return null;let B=new Set;for(let G of $)for(let Q of G)if(Q!==" ")B.add(Q);if(B.size>12)return null;let Y=Math.max(...$.map((G)=>G.length)),X=$.map((G)=>{let Q=[];for(let F=0;F<Y;F++){let K=G[F]??" ";Q.push(K!==" "&&K!==".")}return Q});return{w:Y,h:$.length,cells:X,rows:$}}_buildBlend(){let U=this._planes.map((Y)=>this._gridOf(Y.body)),j=Math.max(1,...U.map((Y)=>Y?Y.w:0)),$=Math.max(1,...U.map((Y)=>Y?Y.h:0)),B=this._planes.length;this._planes.forEach((Y,X)=>{let G=U[X],Q=(X-(B-1)/2)*13;if(G)for(let F=0;F<G.h;F++)for(let K=0;K<G.w;K++){if(!G.cells[F][K])continue;let q=G.rows[F]?.[K]??"",H=this._legend[q]??q,T=document.createElement("div");T.className="voxel",T.setAttribute("part","voxel"),T.dataset.z=String(X),T.textContent=H;let A=(K-(j-1)/2)*13,V=(F-($-1)/2)*13;T.dataset.tx=A.toFixed(1),T.dataset.ty=V.toFixed(1),T.dataset.tz=Q.toFixed(1),T.style.transform=`translate3d(${A.toFixed(1)}px,${V.toFixed(1)}px,${Q.toFixed(1)}px)`,this._scene.appendChild(T),this._voxels.push(T)}else{let F=document.createElement("div");F.className="floattext",F.setAttribute("part","floattext"),F.dataset.z=String(X),F.style.transform=`translate3d(${-j*13/2}px,0,${Q.toFixed(1)}px)`,F.innerHTML=h(Y.body,this._legend),this._scene.appendChild(F),this._voxels.push(F)}})}_step(){let J=this._planes.length;if(J<=1)return;let U=Math.round(this._target)+1;if(U>=J){if(!this._loop){this._stopPlay();return}this._target=0}else this._target=U;this._focus=this._target,this._scrub.value=String(this._target),this.render()}_resetCamera(){if(this._panX=0,this._panY=0,this._zoom=0,this._mapOverview=!0,this._mode==="blend")this._yaw=-28,this._pitch=14;else if(this._mode==="stack")this._yaw=-18,this._pitch=8;else if(this._mode==="layers")this._yaw=-22,this._pitch=12;else if(this._mode==="elevator")this._yaw=-10,this._pitch=6;else if(this._mode==="map")this._yaw=0,this._pitch=30,this._zoom=0;else this._yaw=0,this._pitch=0}_cameraTransform(J){return`translate3d(${this._panX.toFixed(1)}px, ${this._panY.toFixed(1)}px, ${(J+this._zoom).toFixed(1)}px) rotateX(${this._pitch.toFixed(2)}deg) rotateY(${this._yaw.toFixed(2)}deg)`}_startPlay(){if(this._playing||this._planes.length<=1)return;if(this._playing=!0,this._playBtn)this._playBtn.innerHTML=O.pause,this._playBtn.setAttribute("aria-label","pause");let J=this._doc?parseInt(this._doc.metadata?.fps??"",10):NaN,U=J>0?Math.min(1400,Math.max(135,1000/J)):600;this._playTimer=setInterval(()=>this._step(),U)}_stopPlay(){if(this._playing=!1,this._playTimer)clearInterval(this._playTimer),this._playTimer=null;if(this._playBtn)this._playBtn.innerHTML=O.play,this._playBtn.setAttribute("aria-label","play")}_togglePlay(){if(this._playing)this._stopPlay();else this._startPlay()}_setTarget(J){this._stopPlay();let U=this._planes.length-1;this._target=Math.max(0,Math.min(U,J)),this._focus=this._target,this._scrub.value=String(this._target),this.render()}_onKey(J){if(S.has(this._mode)){let j=J.key.length===1?J.key.toLowerCase():J.key;if(j==="Escape"&&this._mode==="map"&&!this._mapOverview){this._mapOverview=!0,this.render(),J.preventDefault();return}if(j==="+"||j==="="){this._zoom=Math.min(600,this._zoom+40),this.render(),J.preventDefault();return}if(j==="-"||j==="_"){this._zoom=Math.max(-400,this._zoom-40),this.render(),J.preventDefault();return}if(j==="PageDown"){this._setTarget(Math.round(this._target)+1),J.preventDefault();return}if(j==="PageUp"){this._setTarget(Math.round(this._target)-1),J.preventDefault();return}if("wasdqe".includes(j)||j==="ArrowUp"||j==="ArrowDown"||j==="ArrowLeft"||j==="ArrowRight")this._keys.add(j),this._startCamLoop(),J.preventDefault();return}let U=J.key===" "&&this._mode!=="single";if(J.key==="ArrowRight"||J.key==="ArrowUp"||J.key==="PageDown"||U)this._setTarget(Math.round(this._target)+1),J.preventDefault();else if(J.key==="ArrowLeft"||J.key==="ArrowDown"||J.key==="PageUp")this._setTarget(Math.round(this._target)-1),J.preventDefault()}_onPointerUpMap(J){if(J||this._mode!=="map")return;if(!this._downTarget?.closest?.(".plane")&&!this._mapOverview)this._mapOverview=!0}_onPointerDown(J){if(this._downTarget=J.target,!S.has(this._mode))return;this._pendingDrag=!0,this._dragging=!1,this._pointerId=J.pointerId,this._lastX=J.clientX,this._lastY=J.clientY,this._dragStartX=J.clientX,this._dragStartY=J.clientY}_onPointerMove(J){if(this._pointerId!==null&&J.pointerId!==this._pointerId)return;if(this._pendingDrag&&!this._dragging){if(Math.hypot(J.clientX-this._dragStartX,J.clientY-this._dragStartY)<8)return;this._dragging=!0,this._stopPlay();try{this._stage.setPointerCapture(J.pointerId)}catch{}}if(!this._dragging)return;if(this._yaw+=(J.clientX-this._lastX)*0.4,this._pitch=Math.max(-85,Math.min(85,this._pitch-(J.clientY-this._lastY)*0.3)),this._mode==="map")this._yaw=Math.max(-30,Math.min(30,this._yaw)),this._pitch=Math.max(12,Math.min(52,this._pitch));this._lastX=J.clientX,this._lastY=J.clientY,this.render()}_onPointerUp(J){let U=this._dragging;this._pendingDrag=!1,this._dragging=!1,this._pointerId=null;try{this._stage.releasePointerCapture(J.pointerId)}catch{}this._onPointerUpMap(U),this.render()}_onWheel(J){if(!S.has(this._mode))return;J.preventDefault();let U=J.deltaMode===1?16:J.deltaMode===2?400:1,j=J.ctrlKey?6:2.2;this._zoom=Math.max(-420,Math.min(720,this._zoom-J.deltaY*U*j)),this.render()}_startCamLoop(){if(this._camRaf!=null)return;let J=()=>{if(!this._keys.size){this._camRaf=null;return}let U=7,j=1.6,$=(B)=>this._keys.has(B);if($("w")||$("ArrowUp"))this._panY+=U;if($("s")||$("ArrowDown"))this._panY-=U;if($("a")||$("ArrowLeft"))this._panX+=U;if($("d")||$("ArrowRight"))this._panX-=U;if($("q"))this._yaw-=j;if($("e"))this._yaw+=j;this.render(),this._camRaf=requestAnimationFrame(J)};this._camRaf=requestAnimationFrame(J)}_onKeyUp(J){this._keys.delete(J.key.length===1?J.key.toLowerCase():J.key)}render(){let J=this._mode,U=this._focus,j=Math.round(U);for(let $ of this._els)if($.parentNode===this._detail)$.classList.remove("popped"),$.removeAttribute("style"),this._scene.appendChild($);if(J==="blend"){if(!this._voxels.length)return;for(let $ of this._voxels)$.classList.toggle("near",Number($.dataset.z)===j);if(this._billboard)for(let $ of this._voxels){if($.dataset.tx===void 0)continue;$.style.transform=`translate3d(${$.dataset.tx}px,${$.dataset.ty}px,${$.dataset.tz}px) rotateY(${(-this._yaw).toFixed(2)}deg) rotateX(${(-this._pitch).toFixed(2)}deg)`}this._scene.style.transform=this._cameraTransform(-30),this._updateReadout(),this._maybeEmit(j);return}if(!this._els.length)return;if(J!=="layers")for(let $ of this._els)$.style.pointerEvents="";if(J==="single"){this._els.forEach(($,B)=>{let Y=B===j;$.style.transform=Y?"":"translate3d(0px,0px,0px) scale(1)",$.style.opacity=Y?"1":"0",$.style.zIndex=Y?"10":"0",$.classList.toggle("hot",!1),$.classList.toggle("dim",!1),$.classList.toggle("reader",Y)}),this._scene.style.transform="translateZ(0px)",this._updateReadout(),this._maybeEmit(j);return}if(J==="play"){this._els.forEach(($,B)=>{let Y=B===j;$.style.transform=Y?"":"translate3d(0px,0px,0px)",$.style.opacity=Y?"1":"0",$.style.zIndex=Y?"10":"0",$.classList.toggle("frame",Y),$.classList.toggle("hot",!1),$.classList.toggle("dim",!1)}),this._scene.style.transform="translateZ(0px)",this._updateReadout(),this._maybeEmit(j);return}if(J==="layers"){this._els.forEach(($,B)=>{let Y=this._hiddenLayers.has(B),X=B-U,G=B===j,Q=-Math.min(Math.abs(X),4)*45;$.style.transform=`translate3d(0px, 0px, ${Q.toFixed(1)}px)`,$.style.opacity=Y?"0":G?"0.97":"0.42",$.style.zIndex=G?"300":String(120-Math.abs(j-B)),$.style.pointerEvents=Y?"none":"auto",$.classList.toggle("hot",G&&!Y),$.classList.toggle("dim",!1),$.classList.toggle("reader",!1),$.classList.toggle("frame",!1)}),this._scene.style.transform=this._cameraTransform(-150),this._updateReadout(),this._maybeEmit(j);return}if(J==="elevator"){this._els.forEach(($,B)=>{let Y=B-U,X=B===j;$.style.transform=`translate3d(0px, ${(Y*150).toFixed(1)}px, ${(-Math.abs(Y)*70).toFixed(1)}px) scale(${X?1:0.84})`,$.style.opacity=Math.max(0.14,1-Math.abs(Y)*0.42).toFixed(2),$.style.zIndex=X?"300":String(120-Math.abs(j-B)),$.classList.toggle("hot",X),$.classList.toggle("dim",!1),$.classList.toggle("reader",!1),$.classList.toggle("frame",!1)}),this._scene.style.transform=this._cameraTransform(-160),this._updateReadout(),this._maybeEmit(j);return}if(J==="map"){this._yaw=Math.max(-30,Math.min(30,this._yaw)),this._pitch=Math.max(12,Math.min(52,this._pitch));let $=this._els[0]?.offsetWidth||300,B=this._els[0]?.offsetHeight||180,Y=$*1.1,X=B*1.25,G=this._planes.some((Z)=>Z.x!=null||Z.y!=null),Q=Math.max(1,Math.ceil(Math.sqrt(this._els.length))),F=Math.ceil(this._els.length/Q),K=(Z)=>[(Z%Q-(Q-1)/2)*Y,(Math.floor(Z/Q)-(F-1)/2)*X],q;if(G){let Z=[...new Set(this._planes.filter((L)=>L.x!=null).map((L)=>L.x))].sort((L,D)=>L-D),C=[...new Set(this._planes.filter((L)=>L.y!=null).map((L)=>L.y))].sort((L,D)=>L-D),M=Math.max(1,Z.length),N=Math.max(1,C.length);q=(L)=>{let D=this._planes[L];if(D.x==null&&D.y==null)return K(L);let u=D.x!=null?Z.indexOf(D.x):M,p=D.y!=null?C.indexOf(D.y):N;return[(u-(M-1)/2)*Y,(p-(N-1)/2)*X]}}else q=K;let H=this._stage.clientWidth||1,T=this._stage.clientHeight||1,A=$/2,V=B/2;this._els.forEach((Z,C)=>{let[M,N]=q(C);A=Math.max(A,Math.abs(M)+$*0.45),V=Math.max(V,Math.abs(N)+B*0.45)});let m=Math.min(1,(H/2-16)/A,(T/2-16)/V)*0.92;if(this.setAttribute("data-map-overview",String(this._mapOverview)),this._mapOverview)this._els.forEach((Z,C)=>{let[M,N]=q(C);Z.style.transform=`translate3d(${M.toFixed(1)}px, ${N.toFixed(1)}px, 0px) scale(0.82)`,Z.style.opacity="1",Z.style.zIndex=String(100+C),Z.style.top="",Z.style.bottom="",Z.style.height="",Z.style.marginTop="",Z.style.marginBottom="",Z.style.maxHeight="",Z.classList.toggle("hot",!1),Z.classList.toggle("dim",!1),Z.classList.toggle("reader",!1),Z.classList.toggle("frame",!1)});else{this._els.forEach((C,M)=>{if(M===j)return;let[N,L]=q(M);C.style.transform=`translate3d(${N.toFixed(1)}px, ${L.toFixed(1)}px, 0px) scale(0.72)`,C.style.opacity="0.4",C.style.zIndex=String(100+M),C.style.top="",C.style.bottom="",C.style.height="",C.style.marginTop="",C.style.marginBottom="",C.style.maxHeight="",C.classList.toggle("hot",!1),C.classList.toggle("dim",!1),C.classList.toggle("reader",!1),C.classList.toggle("frame",!1)});let Z=this._els[j];if(Z){if(Z.classList.add("hot","popped"),Z.classList.toggle("dim",!1),Z.classList.toggle("reader",!1),Z.classList.toggle("frame",!1),Z.parentNode!==this._detail)this._detail.appendChild(Z);Z.style.cssText="position:relative;margin:0;top:auto;left:auto;right:auto;bottom:auto;height:max-content;max-height:none;width:var(--three-md-card-w, 340px);opacity:1;transform:none";let C=Math.max(1,Z.offsetHeight),M=Math.max(1,Z.offsetWidth),N=Math.max(0.6,Math.min(1.8,(T-22)/C,(H-22)/M));Z.style.transform=`scale(${N.toFixed(3)})`}}this._scene.style.transform=`${this._cameraTransform(-120)} scale(${m.toFixed(3)})`,this._updateReadout(),this._maybeEmit(j);return}this._els.forEach(($,B)=>{let Y=0,X=0,G=0,Q=1,F=1,K=!1,q=!1,H=B-U;if(J==="present")if(Math.abs(H)<0.5)G=0,Q=1,K=!0;else Y=H*70,G=-180-Math.abs(H)*50,Q=0.6,F=0.22,q=!0;else if(G=-Math.abs(H)*160,X=H*26,Y=H*30,B===j)Q=1,K=!0;$.style.transform=`translate3d(${Y}px,${X}px,${G}px) scale(${Q.toFixed(3)})`,$.style.opacity=String(F),$.style.zIndex=B===j?"300":String(120-Math.abs(j-B)),$.classList.toggle("hot",K),$.classList.toggle("dim",q),$.classList.toggle("reader",!1),$.classList.toggle("frame",!1)}),this._scene.style.transform=J==="present"?"translateZ(-110px)":this._cameraTransform(-160),this._updateReadout(),this._maybeEmit(j)}_updateReadout(){let J=Math.round(this._focus),U=this._planes[J];if(!U){this._readout.textContent="";return}let j=U.label?` ${U.label}`:"";this._readout.innerHTML=`Z = <b>${U.z}</b>${z(j)} <span>(${J+1}/${this._planes.length})</span>`}_maybeEmit(J){if(J===this._lastEmitted)return;this._lastEmitted=J;let U=this._planes[J];if(!U)return;this.dispatchEvent(new CustomEvent("planechange",{detail:{index:J,z:U.z,label:U.label,plane:U},bubbles:!0,composed:!0}))}}if(typeof customElements<"u"&&!customElements.get("three-md"))customElements.define("three-md",x);export{x as ThreeMDElement};
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@corvidlabs/three-md-element",
|
|
3
|
+
"version": "1.7.17",
|
|
4
|
+
"description": "The canonical <three-md> web component: a framework-agnostic interactive renderer for the 3md format, backed by @corvidlabs/threemd.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": ["3md", "web-component", "custom-element", "renderer", "markdown", "z-axis", "corvidlabs"],
|
|
8
|
+
"repository": { "type": "git", "url": "git+https://github.com/CorvidLabs/3md.git", "directory": "element" },
|
|
9
|
+
"homepage": "https://github.com/CorvidLabs/3md#readme",
|
|
10
|
+
"bugs": { "url": "https://github.com/CorvidLabs/3md/issues" },
|
|
11
|
+
"publishConfig": { "access": "public" },
|
|
12
|
+
"main": "./dist/three-md.js",
|
|
13
|
+
"module": "./dist/three-md.js",
|
|
14
|
+
"types": "./dist/three-md.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/three-md.d.ts",
|
|
18
|
+
"import": "./dist/three-md.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": ["dist"],
|
|
22
|
+
"sideEffects": ["./dist/three-md.js"],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build:dist": "bun build ./src/three-md.ts --outfile ./dist/three-md.js --format esm --target browser --minify",
|
|
25
|
+
"build:types": "mkdir -p dist && cp ./three-md.d.ts ./dist/three-md.d.ts",
|
|
26
|
+
"build:web": "cp ./dist/three-md.js ../web/assets/three-md.js",
|
|
27
|
+
"build": "bun run build:dist && bun run build:types && bun run build:web",
|
|
28
|
+
"prepublishOnly": "bun run build:dist && bun run build:types"
|
|
29
|
+
}
|
|
30
|
+
}
|