@anvpx/motion-scenes 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +39 -2
- package/dist/catalog.d.ts +17 -0
- package/dist/catalog.js +51 -0
- package/dist/custom.d.ts +86 -0
- package/dist/custom.js +176 -0
- package/dist/define-scene.d.ts +31 -0
- package/dist/define-scene.js +128 -0
- package/dist/html.d.ts +5 -0
- package/dist/html.js +14 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/position.d.ts +21 -0
- package/dist/position.js +16 -0
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +68 -0
- package/dist/runtime/compose.d.ts +21 -0
- package/dist/runtime/compose.js +43 -0
- package/dist/runtime/easing.d.ts +14 -0
- package/dist/runtime/easing.js +45 -0
- package/dist/runtime/mount.d.ts +50 -0
- package/dist/runtime/mount.js +153 -0
- package/dist/runtime/timeline.d.ts +147 -0
- package/dist/runtime/timeline.js +199 -0
- package/dist/runtime-files.d.ts +4 -0
- package/dist/runtime-files.js +18 -0
- package/dist/scene-contract.d.ts +7 -0
- package/dist/scene-contract.js +104 -0
- package/dist/scene3d/post.d.ts +12 -0
- package/dist/scene3d/post.js +16 -0
- package/dist/scene3d/reflector.d.ts +9 -0
- package/dist/scene3d/reflector.js +17 -0
- package/dist/scene3d/textures.d.ts +9 -0
- package/dist/scene3d/textures.js +11 -0
- package/dist/scene3d/volumetric.d.ts +9 -0
- package/dist/scene3d/volumetric.js +16 -0
- package/dist/scenes/bug/index.d.ts +1 -0
- package/dist/scenes/bug/index.js +45 -0
- package/dist/scenes/bug/scene.d.ts +2 -0
- package/dist/scenes/bug/scene.js +6 -0
- package/dist/scenes/bug/style.css +30 -0
- package/dist/scenes/bullets/index.d.ts +1 -0
- package/dist/scenes/bullets/index.js +128 -0
- package/dist/scenes/bullets/scene.d.ts +2 -0
- package/dist/scenes/bullets/scene.js +6 -0
- package/dist/scenes/bullets/style.css +91 -0
- package/dist/scenes/callout/index.d.ts +9 -0
- package/dist/scenes/callout/index.js +95 -0
- package/dist/scenes/callout/scene.d.ts +2 -0
- package/dist/scenes/callout/scene.js +43 -0
- package/dist/scenes/callout/style.css +97 -0
- package/dist/scenes/caption/index.d.ts +1 -0
- package/dist/scenes/caption/index.js +72 -0
- package/dist/scenes/caption/scene.d.ts +11 -0
- package/dist/scenes/caption/scene.js +33 -0
- package/dist/scenes/caption/style.css +33 -0
- package/dist/scenes/chapter/index.d.ts +1 -0
- package/dist/scenes/chapter/index.js +29 -0
- package/dist/scenes/chapter/scene.d.ts +2 -0
- package/dist/scenes/chapter/scene.js +6 -0
- package/dist/scenes/chapter/style.css +25 -0
- package/dist/scenes/chart/index.d.ts +1 -0
- package/dist/scenes/chart/index.js +24 -0
- package/dist/scenes/chart/render.d.ts +4 -0
- package/dist/scenes/chart/render.js +257 -0
- package/dist/scenes/chart/scale.d.ts +11 -0
- package/dist/scenes/chart/scale.js +53 -0
- package/dist/scenes/chart/scene.d.ts +2 -0
- package/dist/scenes/chart/scene.js +73 -0
- package/dist/scenes/chart/schema.d.ts +36 -0
- package/dist/scenes/chart/schema.js +109 -0
- package/dist/scenes/chart/style.css +425 -0
- package/dist/scenes/code/index.d.ts +1 -0
- package/dist/scenes/code/index.js +109 -0
- package/dist/scenes/code/scene.d.ts +2 -0
- package/dist/scenes/code/scene.js +108 -0
- package/dist/scenes/code/style.css +155 -0
- package/dist/scenes/code/tokenize.d.ts +8 -0
- package/dist/scenes/code/tokenize.js +145 -0
- package/dist/scenes/compare/index.d.ts +1 -0
- package/dist/scenes/compare/index.js +128 -0
- package/dist/scenes/compare/scene.d.ts +2 -0
- package/dist/scenes/compare/scene.js +58 -0
- package/dist/scenes/compare/style.css +187 -0
- package/dist/scenes/countdown/index.d.ts +1 -0
- package/dist/scenes/countdown/index.js +46 -0
- package/dist/scenes/countdown/scene.d.ts +2 -0
- package/dist/scenes/countdown/scene.js +21 -0
- package/dist/scenes/countdown/style.css +32 -0
- package/dist/scenes/flow/index.d.ts +1 -0
- package/dist/scenes/flow/index.js +66 -0
- package/dist/scenes/flow/inspect.d.ts +26 -0
- package/dist/scenes/flow/inspect.js +25 -0
- package/dist/scenes/flow/layout.d.ts +83 -0
- package/dist/scenes/flow/layout.js +222 -0
- package/dist/scenes/flow/measure.d.ts +15 -0
- package/dist/scenes/flow/measure.js +33 -0
- package/dist/scenes/flow/plan.d.ts +42 -0
- package/dist/scenes/flow/plan.js +117 -0
- package/dist/scenes/flow/scene.d.ts +2 -0
- package/dist/scenes/flow/scene.js +6 -0
- package/dist/scenes/flow/schema.d.ts +49 -0
- package/dist/scenes/flow/schema.js +60 -0
- package/dist/scenes/flow/style.css +54 -0
- package/dist/scenes/hero/index.d.ts +3 -0
- package/dist/scenes/hero/index.js +77 -0
- package/dist/scenes/hero/scene.d.ts +2 -0
- package/dist/scenes/hero/scene.js +6 -0
- package/dist/scenes/hero/style.css +143 -0
- package/dist/scenes/icon-grid/index.d.ts +1 -0
- package/dist/scenes/icon-grid/index.js +69 -0
- package/dist/scenes/icon-grid/scene.d.ts +2 -0
- package/dist/scenes/icon-grid/scene.js +6 -0
- package/dist/scenes/icon-grid/style.css +74 -0
- package/dist/scenes/kinetic/index.d.ts +1 -0
- package/dist/scenes/kinetic/index.js +62 -0
- package/dist/scenes/kinetic/scene.d.ts +2 -0
- package/dist/scenes/kinetic/scene.js +98 -0
- package/dist/scenes/kinetic/style.css +94 -0
- package/dist/scenes/kpi-grid/index.d.ts +1 -0
- package/dist/scenes/kpi-grid/index.js +161 -0
- package/dist/scenes/kpi-grid/scene.d.ts +2 -0
- package/dist/scenes/kpi-grid/scene.js +6 -0
- package/dist/scenes/kpi-grid/style.css +141 -0
- package/dist/scenes/lower-third/index.d.ts +1 -0
- package/dist/scenes/lower-third/index.js +33 -0
- package/dist/scenes/lower-third/scene.d.ts +2 -0
- package/dist/scenes/lower-third/scene.js +6 -0
- package/dist/scenes/lower-third/style.css +45 -0
- package/dist/scenes/media-frame/index.d.ts +1 -0
- package/dist/scenes/media-frame/index.js +99 -0
- package/dist/scenes/media-frame/scene.d.ts +2 -0
- package/dist/scenes/media-frame/scene.js +85 -0
- package/dist/scenes/media-frame/style.css +275 -0
- package/dist/scenes/outro/index.d.ts +1 -0
- package/dist/scenes/outro/index.js +54 -0
- package/dist/scenes/outro/scene.d.ts +2 -0
- package/dist/scenes/outro/scene.js +6 -0
- package/dist/scenes/outro/style.css +92 -0
- package/dist/scenes/poster/index.d.ts +3 -0
- package/dist/scenes/poster/index.js +62 -0
- package/dist/scenes/poster/scene.d.ts +2 -0
- package/dist/scenes/poster/scene.js +6 -0
- package/dist/scenes/poster/style.css +102 -0
- package/dist/scenes/progress/index.d.ts +1 -0
- package/dist/scenes/progress/index.js +40 -0
- package/dist/scenes/progress/scene.d.ts +2 -0
- package/dist/scenes/progress/scene.js +17 -0
- package/dist/scenes/progress/style.css +31 -0
- package/dist/scenes/quote/index.d.ts +3 -0
- package/dist/scenes/quote/index.js +69 -0
- package/dist/scenes/quote/scene.d.ts +2 -0
- package/dist/scenes/quote/scene.js +6 -0
- package/dist/scenes/quote/style.css +86 -0
- package/dist/scenes/scene3d/index.d.ts +1 -0
- package/dist/scenes/scene3d/index.js +59 -0
- package/dist/scenes/scene3d/scene.d.ts +2 -0
- package/dist/scenes/scene3d/scene.js +436 -0
- package/dist/scenes/scene3d/style.css +43 -0
- package/dist/scenes/stat/index.d.ts +4 -0
- package/dist/scenes/stat/index.js +72 -0
- package/dist/scenes/stat/scene.d.ts +2 -0
- package/dist/scenes/stat/scene.js +6 -0
- package/dist/scenes/stat/style.css +95 -0
- package/dist/scenes/steps/index.d.ts +1 -0
- package/dist/scenes/steps/index.js +147 -0
- package/dist/scenes/steps/scene.d.ts +2 -0
- package/dist/scenes/steps/scene.js +6 -0
- package/dist/scenes/steps/style.css +190 -0
- package/dist/scenes/sticker/index.d.ts +1 -0
- package/dist/scenes/sticker/index.js +48 -0
- package/dist/scenes/sticker/scene.d.ts +2 -0
- package/dist/scenes/sticker/scene.js +57 -0
- package/dist/scenes/sticker/style.css +88 -0
- package/dist/scenes/table/index.d.ts +1 -0
- package/dist/scenes/table/index.js +131 -0
- package/dist/scenes/table/scene.d.ts +2 -0
- package/dist/scenes/table/scene.js +6 -0
- package/dist/scenes/table/style.css +102 -0
- package/dist/scenes/timeline/index.d.ts +1 -0
- package/dist/scenes/timeline/index.js +68 -0
- package/dist/scenes/timeline/scene.d.ts +2 -0
- package/dist/scenes/timeline/scene.js +6 -0
- package/dist/scenes/timeline/style.css +161 -0
- package/dist/scenes/title/index.d.ts +1 -0
- package/dist/scenes/title/index.js +52 -0
- package/dist/scenes/title/scene.d.ts +2 -0
- package/dist/scenes/title/scene.js +6 -0
- package/dist/scenes/title/style.css +80 -0
- package/dist/scenes/waveform/index.d.ts +1 -0
- package/dist/scenes/waveform/index.js +39 -0
- package/dist/scenes/waveform/scene.d.ts +2 -0
- package/dist/scenes/waveform/scene.js +35 -0
- package/dist/scenes/waveform/style.css +44 -0
- package/dist/shared/fit-text.d.ts +24 -0
- package/dist/shared/fit-text.js +82 -0
- package/dist/shared/format.d.ts +24 -0
- package/dist/shared/format.js +59 -0
- package/dist/shared/frame.d.ts +14 -0
- package/dist/shared/frame.js +21 -0
- package/dist/shared/icon-search.d.ts +9 -0
- package/dist/shared/icon-search.js +26 -0
- package/dist/shared/icon.d.ts +15 -0
- package/dist/shared/icon.js +28 -0
- package/dist/shared/index.d.ts +11 -0
- package/dist/shared/index.js +7 -0
- package/dist/shared/markup.d.ts +12 -0
- package/dist/shared/markup.js +43 -0
- package/dist/shared/measure.d.ts +7 -0
- package/dist/shared/measure.js +34 -0
- package/dist/shared/motion.d.ts +94 -0
- package/dist/shared/motion.js +117 -0
- package/dist/source-hash.d.ts +4 -0
- package/dist/source-hash.js +26 -0
- package/dist/test-fixtures.d.ts +9 -0
- package/dist/test-fixtures.js +22 -0
- package/dist/text-file.d.ts +1 -0
- package/dist/text-file.js +3 -0
- package/dist/words.d.ts +11 -0
- package/dist/words.js +93 -0
- package/package.json +36 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 anvpx
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,40 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @anvpx/motion-scenes
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Scene plugins (`ScenePlugin` from `@anvpx/motion-schema`) and the shared browser runtime. Base scenes: `title`, `bullets`, `stat`, `flow`, `outro`. Overlay scenes: `lower-third`, `bug`, `progress`, `countdown`, `chapter`.
|
|
4
|
+
|
|
5
|
+
- `sceneRegistry` / `getScene(type)` / `sceneTypes`: build consumes only this barrel.
|
|
6
|
+
- `runtimeFiles()`: shared modules (`runtime/easing.js`, `timeline.js`, `mount.js`), emitted once by build.
|
|
7
|
+
- `emit(entry, ctx)` returns, per scene id: `scenes/<id>/fragment.html`, `style.css`, `scene.js`.
|
|
8
|
+
|
|
9
|
+
Fragment root is `<section id="scene-<id>">` without a `clip` class; build wraps timing.
|
|
10
|
+
|
|
11
|
+
## Add a scene
|
|
12
|
+
|
|
13
|
+
1. New folder `src/scenes/<type>/` with `index.ts`, `style.css`, `scene.js`, `index.test.ts`.
|
|
14
|
+
2. Build the plugin with `defineScene` (`type`, `configSchema`, `meta`, `style`, `script`, `render`); run `describeSceneContract` in its test.
|
|
15
|
+
3. Add one import and one entry to the `plugins` list in `src/registry.ts`.
|
|
16
|
+
4. Run `mk catalog --write` to refresh the root `CATALOG.md` (a test fails on drift).
|
|
17
|
+
|
|
18
|
+
## Overlay scenes
|
|
19
|
+
|
|
20
|
+
An overlay is a transparent layer above the base timeline (`layer: "overlay"` on the storyboard entry; the plugin declares `layer: "overlay"` in `defineScene`). The root has no background, ignores pointer events and keeps the 5 % safe margin as padding, so content stays inside 90 % of the frame.
|
|
21
|
+
|
|
22
|
+
## Catalog metadata
|
|
23
|
+
|
|
24
|
+
Each scene passes `meta: { purpose, example }` to `defineScene`: a one-line purpose and the smallest props object that passes the schema. `sceneDocs()` derives everything else (layer, fields, required/optional, types, enums, defaults) from `configSchema` via `z.toJSONSchema`, so the schema stays the single source of truth. `mk catalog` and the generated `CATALOG.md` at the repo root consume it. Cross-field rules (for example `bug`: exactly one of `text` or `image`) are not derivable; state them in `purpose`.
|
|
25
|
+
|
|
26
|
+
## Flow layout
|
|
27
|
+
|
|
28
|
+
`flow` sizes its cards from the label text with a deterministic width estimate (`scenes/flow/measure.ts`, no font loading): labels wrap only at spaces, into at most two lines, and a word is never broken; a card widens for a long word and the text steps down `lg` -> `md` -> `sm` (theme steps, so always >= 28 px) before it is declared crowded. Edge labels sit on or beside their own edge and slide along it (then to either side) until they clear every card, earlier label and other edge; the room between layers is 2.2x the longest label. `planFlow` is the single layout; the scene renders it and `inspectFlow` (exported) feeds the lint rules below.
|
|
29
|
+
|
|
30
|
+
## Runtime contract
|
|
31
|
+
|
|
32
|
+
`createScene(root, config, ctx)` returns `{ seek(t), dispose(), ready }`. `seek(t)` is a pure function of `t`. Config falls back to `data-*` attributes on the root (ease, move, focus, fps, duration). Browser code is plain ES modules (`// @ts-check` plus JSDoc), shipped as text. No `requestAnimationFrame`, `Date.now` or unseeded random.
|
|
33
|
+
|
|
34
|
+
## Theme and text rules
|
|
35
|
+
|
|
36
|
+
CSS uses only `--mk-*` custom properties from `@anvpx/motion-themes`; text is at least 28 px and uses one theme font family; at most 40 on-screen words per scene (`limitWords`). Props are strict (unknown keys rejected).
|
|
37
|
+
|
|
38
|
+
## Shared helpers
|
|
39
|
+
|
|
40
|
+
`src/shared/` holds the helpers every scene reuses (one implementation, no per-scene copies): `staggerStart`/`staggerDuration` (offset always < item duration), `countUp` (ease-out, exact end), `progress`/`scale`/`lerp`/`clamp`, `wipe`, `drawOn`, `revealTimings`/`revealProgress` (word by word), `fmt` (locale-free tabular numbers: unit, decimals, sign, compact), `fitText` (deterministic width estimate, balanced wrap, never breaks a word, last line >= 35 % of the widest), `sceneIcon` (config error with the prop pointer and 3 suggestions), and `esc`/`accentHtml` (`[x]` accent markup; every character escaped, only `<span class="accent">` is emitted). `motion.js` and `format.js` are plain browser ES modules (they import `runtime/easing.js`); emitting them next to `runtime/` is wired by the first scene that needs them. `@anvpx/motion-assets` (a leaf) is imported only by `shared/icon.ts`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface FieldDoc {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
/** Compact type: `text`, `number`, `int 0..99`, `a|b` (enum), `text[1..6]`, `{id,label}[2..8]`. */
|
|
4
|
+
readonly type: string;
|
|
5
|
+
readonly required: boolean;
|
|
6
|
+
readonly default?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface SceneDoc {
|
|
9
|
+
readonly type: string;
|
|
10
|
+
readonly layer: "base" | "overlay";
|
|
11
|
+
readonly purpose: string;
|
|
12
|
+
readonly fields: readonly FieldDoc[];
|
|
13
|
+
/** Minimal props that pass the scene's schema. */
|
|
14
|
+
readonly example: Readonly<Record<string, unknown>>;
|
|
15
|
+
}
|
|
16
|
+
/** Catalog data for every registered scene, derived from each plugin's configSchema and meta. */
|
|
17
|
+
export declare const sceneDocs: () => SceneDoc[];
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { sceneRegistry } from "./registry.js";
|
|
3
|
+
const range = (min, max) => min === undefined && max === undefined ? "" : `${min ?? ""}..${max ?? ""}`;
|
|
4
|
+
const textType = (node) => node.maxLength === undefined ? "text" : `text<=${node.maxLength}`;
|
|
5
|
+
const numberType = (node) => {
|
|
6
|
+
const base = node.type === "integer" ? "int" : "number";
|
|
7
|
+
const bounds = range(node.minimum, node.maximum);
|
|
8
|
+
return bounds === "" ? base : `${base} ${bounds}`;
|
|
9
|
+
};
|
|
10
|
+
const arrayType = (node) => {
|
|
11
|
+
const bounds = range(node.minItems, node.maxItems);
|
|
12
|
+
return `${typeOf(node.items ?? {})}${bounds === "" ? "[]" : `[${bounds}]`}`;
|
|
13
|
+
};
|
|
14
|
+
const scalarType = (node) => {
|
|
15
|
+
if (node.type === "string")
|
|
16
|
+
return textType(node);
|
|
17
|
+
if (node.type === "number" || node.type === "integer")
|
|
18
|
+
return numberType(node);
|
|
19
|
+
return node.type === "boolean" ? "boolean" : "any";
|
|
20
|
+
};
|
|
21
|
+
/** Compact, single-line type of a JSON Schema node. */
|
|
22
|
+
function typeOf(node) {
|
|
23
|
+
if (node.enum)
|
|
24
|
+
return node.enum.map(String).join("|");
|
|
25
|
+
if (node.anyOf)
|
|
26
|
+
return node.anyOf.map(typeOf).join("|");
|
|
27
|
+
if (node.type === "array")
|
|
28
|
+
return arrayType(node);
|
|
29
|
+
if (node.type === "object")
|
|
30
|
+
return `{${Object.keys(node.properties ?? {}).join(",")}}`;
|
|
31
|
+
return scalarType(node);
|
|
32
|
+
}
|
|
33
|
+
const fieldsOf = (plugin) => {
|
|
34
|
+
const schema = z.toJSONSchema(plugin.configSchema, { io: "input" });
|
|
35
|
+
const required = new Set(schema.required ?? []);
|
|
36
|
+
return Object.entries(schema.properties ?? {}).map(([name, node]) => ({
|
|
37
|
+
name,
|
|
38
|
+
type: typeOf(node),
|
|
39
|
+
required: required.has(name),
|
|
40
|
+
...("default" in node ? { default: node.default } : {}),
|
|
41
|
+
}));
|
|
42
|
+
};
|
|
43
|
+
const docOf = (plugin) => ({
|
|
44
|
+
type: plugin.type,
|
|
45
|
+
layer: plugin.layer ?? "base",
|
|
46
|
+
purpose: plugin.meta?.purpose ?? "",
|
|
47
|
+
fields: fieldsOf(plugin),
|
|
48
|
+
example: plugin.meta?.example ?? {},
|
|
49
|
+
});
|
|
50
|
+
/** Catalog data for every registered scene, derived from each plugin's configSchema and meta. */
|
|
51
|
+
export const sceneDocs = () => [...sceneRegistry.values()].map(docOf);
|
package/dist/custom.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type EmitContext, type SceneEntry, type VendorLib } from "@anvpx/motion-schema";
|
|
2
|
+
/** The storyboard `type` that means "the author wrote this scene". */
|
|
3
|
+
export declare const CUSTOM_SCENE_TYPE = "custom";
|
|
4
|
+
/** One extra file an authored scene imports by relative path (path is scene-relative, forward slash). */
|
|
5
|
+
export interface CustomSceneFile {
|
|
6
|
+
readonly path: string;
|
|
7
|
+
readonly text: string;
|
|
8
|
+
}
|
|
9
|
+
/** An authored scene's three files, already read from disk by the caller. */
|
|
10
|
+
export interface CustomSceneSource {
|
|
11
|
+
readonly html: string;
|
|
12
|
+
readonly css?: string;
|
|
13
|
+
readonly js: string;
|
|
14
|
+
/** Browser libraries `js` imports by bare specifier; the build vendors each (ADR-0006). */
|
|
15
|
+
readonly vendor?: readonly VendorLib[];
|
|
16
|
+
/**
|
|
17
|
+
* Relative-import closure of `js` (its own helpers, and theirs). Emitted beside
|
|
18
|
+
* `scene.js`, so `import { x } from "./helper.js"` resolves in the built project.
|
|
19
|
+
*/
|
|
20
|
+
readonly files?: readonly CustomSceneFile[];
|
|
21
|
+
}
|
|
22
|
+
export interface VendorScan {
|
|
23
|
+
/** Vendored libraries the script imports, sorted and deduplicated. */
|
|
24
|
+
readonly libs: readonly VendorLib[];
|
|
25
|
+
/** Bare specifiers that nothing vendors, so the page would 404 on them. */
|
|
26
|
+
readonly unknown: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* What a scene script imports. Relative paths are the emitted project's own files; a bare
|
|
30
|
+
* specifier only works if the build wrote an import map entry for it.
|
|
31
|
+
*/
|
|
32
|
+
export declare const scanVendorImports: (js: string) => VendorScan;
|
|
33
|
+
export type SceneDirResult = {
|
|
34
|
+
readonly ok: true;
|
|
35
|
+
readonly dir: string;
|
|
36
|
+
} | {
|
|
37
|
+
readonly ok: false;
|
|
38
|
+
readonly message: string;
|
|
39
|
+
};
|
|
40
|
+
/** Relative module specifiers a scene script imports, split from those that would escape. */
|
|
41
|
+
export interface RelativeImportScan {
|
|
42
|
+
/** Specifiers as written (`./lib/x.js`), deduplicated and sorted. */
|
|
43
|
+
readonly paths: readonly string[];
|
|
44
|
+
/** Absolute or drive-qualified specifiers: never a scene-local helper. */
|
|
45
|
+
readonly escaping: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The `./x` specifiers one script imports. Bare specifiers are the vendored ones
|
|
49
|
+
* (`scanVendorImports`); an absolute or drive path is reported as escaping because
|
|
50
|
+
* packaging can only copy files inside the scene directory.
|
|
51
|
+
*/
|
|
52
|
+
export declare const scanRelativeImports: (js: string) => RelativeImportScan;
|
|
53
|
+
/** Packaging limits: a scene is a few files, not a bundled application. */
|
|
54
|
+
export declare const HELPER_LIMITS: {
|
|
55
|
+
readonly files: 32;
|
|
56
|
+
readonly chars: 262144;
|
|
57
|
+
};
|
|
58
|
+
export interface RelativeClosure {
|
|
59
|
+
/** Helpers found, sorted by scene-relative path. */
|
|
60
|
+
readonly files: readonly CustomSceneFile[];
|
|
61
|
+
/** Specifiers that point outside the scene directory. */
|
|
62
|
+
readonly escaping: readonly string[];
|
|
63
|
+
/** Helper paths that do not exist beside the scene. */
|
|
64
|
+
readonly missing: readonly string[];
|
|
65
|
+
/** True when the file/byte limit stopped the walk. */
|
|
66
|
+
readonly overflow: boolean;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Walks the relative-import closure of an authored scene's script, reading each
|
|
70
|
+
* helper through the injected `read` (so this stays free of filesystem access).
|
|
71
|
+
* Nested helpers resolve against their own directory; the walk is breadth-first,
|
|
72
|
+
* deduplicated and bounded by {@link HELPER_LIMITS}.
|
|
73
|
+
*/
|
|
74
|
+
export declare const readRelativeClosure: (entryJs: string, read: (path: string) => Promise<string | undefined>) => Promise<RelativeClosure>;
|
|
75
|
+
/**
|
|
76
|
+
* Resolves `props.dir` against the storyboard's directory and refuses anything that
|
|
77
|
+
* escapes it. A storyboard is data until it points at code, so the containment check
|
|
78
|
+
* is the boundary between "a document" and "arbitrary code execution".
|
|
79
|
+
*/
|
|
80
|
+
export declare const resolveSceneDir: (baseDir: string, requested: string) => SceneDirResult;
|
|
81
|
+
/**
|
|
82
|
+
* Emits an authored scene as the same three files a catalog scene produces, so the
|
|
83
|
+
* rest of the pipeline (fragment guard, boot, manifest, cache) cannot tell them apart.
|
|
84
|
+
* `#scene-root` in the author's CSS is rewritten to this scene's selector.
|
|
85
|
+
*/
|
|
86
|
+
export declare const emitCustomScene: (entry: SceneEntry, ctx: EmitContext, source: CustomSceneSource) => Map<string, string>;
|
package/dist/custom.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { isAbsolute, posix, relative, resolve } from "node:path";
|
|
2
|
+
import { SceneEntryBaseSchema, } from "@anvpx/motion-schema";
|
|
3
|
+
import { rootElement, safePadding } from "./define-scene.js";
|
|
4
|
+
import { isSharedModulePath } from "./runtime-files.js";
|
|
5
|
+
/** The storyboard `type` that means "the author wrote this scene". */
|
|
6
|
+
export const CUSTOM_SCENE_TYPE = "custom";
|
|
7
|
+
/** The bare specifiers a scene script may import; everything else must be a relative path. */
|
|
8
|
+
const VENDORED = new Set(["three", "postprocessing"]);
|
|
9
|
+
const SPECIFIER = /(?:from\s*|import\s*\(\s*|import\s+)["']([^"']+)["']/g;
|
|
10
|
+
/**
|
|
11
|
+
* What a scene script imports. Relative paths are the emitted project's own files; a bare
|
|
12
|
+
* specifier only works if the build wrote an import map entry for it.
|
|
13
|
+
*/
|
|
14
|
+
export const scanVendorImports = (js) => {
|
|
15
|
+
const libs = new Set();
|
|
16
|
+
const unknown = new Set();
|
|
17
|
+
for (const match of js.matchAll(SPECIFIER)) {
|
|
18
|
+
const specifier = match[1] ?? "";
|
|
19
|
+
if (specifier === "" || specifier.startsWith(".") || specifier.startsWith("/"))
|
|
20
|
+
continue;
|
|
21
|
+
if (VENDORED.has(specifier))
|
|
22
|
+
libs.add(specifier);
|
|
23
|
+
else
|
|
24
|
+
unknown.add(specifier);
|
|
25
|
+
}
|
|
26
|
+
return { libs: [...libs].sort(), unknown: [...unknown].sort() };
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The `./x` specifiers one script imports. Bare specifiers are the vendored ones
|
|
30
|
+
* (`scanVendorImports`); an absolute or drive path is reported as escaping because
|
|
31
|
+
* packaging can only copy files inside the scene directory.
|
|
32
|
+
*/
|
|
33
|
+
export const scanRelativeImports = (js) => {
|
|
34
|
+
const paths = new Set();
|
|
35
|
+
const escaping = new Set();
|
|
36
|
+
for (const match of js.matchAll(SPECIFIER)) {
|
|
37
|
+
const specifier = match[1] ?? "";
|
|
38
|
+
if (specifier === "")
|
|
39
|
+
continue;
|
|
40
|
+
if (specifier.startsWith("/") || specifier.startsWith("\\") || /^[a-zA-Z]:/.test(specifier)) {
|
|
41
|
+
escaping.add(specifier);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!specifier.startsWith("."))
|
|
45
|
+
continue;
|
|
46
|
+
paths.add(specifier);
|
|
47
|
+
}
|
|
48
|
+
return { paths: [...paths].sort(), escaping: [...escaping].sort() };
|
|
49
|
+
};
|
|
50
|
+
/** Resolves a relative specifier against the importing file's scene-relative directory. */
|
|
51
|
+
const resolveRelative = (base, specifier) => {
|
|
52
|
+
const joined = posix.normalize(posix.join(base === "" ? "." : base, specifier));
|
|
53
|
+
if (joined === "." || joined === "" || joined.startsWith("../") || posix.isAbsolute(joined)) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return joined;
|
|
57
|
+
};
|
|
58
|
+
/** Packaging limits: a scene is a few files, not a bundled application. */
|
|
59
|
+
export const HELPER_LIMITS = { files: 32, chars: 262_144 };
|
|
60
|
+
/**
|
|
61
|
+
* Walks the relative-import closure of an authored scene's script, reading each
|
|
62
|
+
* helper through the injected `read` (so this stays free of filesystem access).
|
|
63
|
+
* Nested helpers resolve against their own directory; the walk is breadth-first,
|
|
64
|
+
* deduplicated and bounded by {@link HELPER_LIMITS}.
|
|
65
|
+
*/
|
|
66
|
+
export const readRelativeClosure = async (entryJs, read) => {
|
|
67
|
+
const found = new Map();
|
|
68
|
+
const missing = new Set();
|
|
69
|
+
const escaping = new Set();
|
|
70
|
+
let chars = 0;
|
|
71
|
+
let overflow = false;
|
|
72
|
+
const queue = [{ text: entryJs, base: "" }];
|
|
73
|
+
while (queue.length > 0 && !overflow) {
|
|
74
|
+
const item = queue.shift();
|
|
75
|
+
if (item === undefined)
|
|
76
|
+
break;
|
|
77
|
+
const scan = scanRelativeImports(item.text);
|
|
78
|
+
for (const specifier of scan.escaping)
|
|
79
|
+
escaping.add(specifier);
|
|
80
|
+
for (const specifier of scan.paths) {
|
|
81
|
+
// Shared modules are virtual build outputs, never reads from the source
|
|
82
|
+
// filesystem. Resolve from the same scenes/<id>/ location used by emit.
|
|
83
|
+
const emitted = posix.normalize(posix.join("scenes/custom", item.base, specifier));
|
|
84
|
+
if (isSharedModulePath(emitted))
|
|
85
|
+
continue;
|
|
86
|
+
const path = resolveRelative(item.base, specifier);
|
|
87
|
+
if (path === undefined) {
|
|
88
|
+
escaping.add(specifier);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (found.has(path))
|
|
92
|
+
continue;
|
|
93
|
+
if (found.size >= HELPER_LIMITS.files) {
|
|
94
|
+
overflow = true;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
const text = await read(path);
|
|
98
|
+
if (text === undefined) {
|
|
99
|
+
missing.add(path);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (chars + text.length > HELPER_LIMITS.chars) {
|
|
103
|
+
overflow = true;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
chars += text.length;
|
|
107
|
+
found.set(path, text);
|
|
108
|
+
queue.push({ text, base: posix.dirname(path) });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
files: [...found.entries()]
|
|
113
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
114
|
+
.map(([path, text]) => ({ path, text })),
|
|
115
|
+
escaping: [...escaping].sort(),
|
|
116
|
+
missing: [...missing].sort(),
|
|
117
|
+
overflow,
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
const ESCAPE = "props.dir must stay inside the storyboard directory";
|
|
121
|
+
const ABSOLUTE = "props.dir must be relative to the storyboard";
|
|
122
|
+
/**
|
|
123
|
+
* Resolves `props.dir` against the storyboard's directory and refuses anything that
|
|
124
|
+
* escapes it. A storyboard is data until it points at code, so the containment check
|
|
125
|
+
* is the boundary between "a document" and "arbitrary code execution".
|
|
126
|
+
*/
|
|
127
|
+
export const resolveSceneDir = (baseDir, requested) => {
|
|
128
|
+
const trimmed = requested.trim();
|
|
129
|
+
if (trimmed === "")
|
|
130
|
+
return { ok: false, message: "props.dir is empty" };
|
|
131
|
+
if (isAbsolute(trimmed) || /^[a-zA-Z]:/.test(trimmed) || trimmed.startsWith("\\\\")) {
|
|
132
|
+
return { ok: false, message: ABSOLUTE };
|
|
133
|
+
}
|
|
134
|
+
const base = resolve(baseDir);
|
|
135
|
+
const target = resolve(base, trimmed);
|
|
136
|
+
const rel = relative(base, target);
|
|
137
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
|
|
138
|
+
return { ok: false, message: ESCAPE };
|
|
139
|
+
}
|
|
140
|
+
return { ok: true, dir: target };
|
|
141
|
+
};
|
|
142
|
+
/** Root styles for an authored scene: fills the frame and inherits the theme, nothing else. */
|
|
143
|
+
const baseCss = (root, ctx) => `${root} {
|
|
144
|
+
position: absolute;
|
|
145
|
+
inset: 0;
|
|
146
|
+
box-sizing: border-box;
|
|
147
|
+
display: flex;
|
|
148
|
+
flex-direction: column;
|
|
149
|
+
justify-content: center;
|
|
150
|
+
overflow: hidden;
|
|
151
|
+
padding: ${safePadding(ctx)};
|
|
152
|
+
color: var(--mk-color-fg);
|
|
153
|
+
font-family: var(--mk-font-sans);
|
|
154
|
+
line-height: 1.2;
|
|
155
|
+
}
|
|
156
|
+
${root} * { margin: 0; box-sizing: border-box; }
|
|
157
|
+
`;
|
|
158
|
+
/**
|
|
159
|
+
* Emits an authored scene as the same three files a catalog scene produces, so the
|
|
160
|
+
* rest of the pipeline (fragment guard, boot, manifest, cache) cannot tell them apart.
|
|
161
|
+
* `#scene-root` in the author's CSS is rewritten to this scene's selector.
|
|
162
|
+
*/
|
|
163
|
+
export const emitCustomScene = (entry, ctx, source) => {
|
|
164
|
+
const id = SceneEntryBaseSchema.shape.id.parse(entry.id);
|
|
165
|
+
const root = `#scene-${id}`;
|
|
166
|
+
const dir = `scenes/${id}`;
|
|
167
|
+
const css = (source.css ?? "").replaceAll("#scene-root", root);
|
|
168
|
+
const files = new Map([
|
|
169
|
+
[`${dir}/fragment.html`, rootElement(entry, ctx, { type: CUSTOM_SCENE_TYPE }, source.html)],
|
|
170
|
+
[`${dir}/style.css`, baseCss(root, ctx) + css],
|
|
171
|
+
[`${dir}/scene.js`, source.js],
|
|
172
|
+
]);
|
|
173
|
+
for (const helper of source.files ?? [])
|
|
174
|
+
files.set(`${dir}/${helper.path}`, helper.text);
|
|
175
|
+
return files;
|
|
176
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type EmitContext, type SceneEntry, type SceneMeta, type ScenePlugin, type VendorLib } from "@anvpx/motion-schema";
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
export interface Rendered {
|
|
4
|
+
/** Children of the scene root element. */
|
|
5
|
+
readonly body: string;
|
|
6
|
+
/** Extra CSS (already scoped to the root selector). */
|
|
7
|
+
readonly css?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SceneSpec<S extends z.ZodType> {
|
|
10
|
+
readonly type: string;
|
|
11
|
+
/** "overlay": transparent layer above the base timeline (no background, no camera art). */
|
|
12
|
+
readonly layer?: "overlay";
|
|
13
|
+
readonly configSchema: S;
|
|
14
|
+
/** Catalog documentation: purpose line and minimal example props. */
|
|
15
|
+
readonly meta: SceneMeta;
|
|
16
|
+
/** Static CSS; `#scene-root` is replaced by the scene's root selector. */
|
|
17
|
+
readonly style: string;
|
|
18
|
+
/** Static ES module text: `export function createScene(root, config, ctx)`. */
|
|
19
|
+
readonly script: string;
|
|
20
|
+
/** Browser libraries the script imports by bare specifier; the build vendors them (ADR-0006). */
|
|
21
|
+
readonly vendor?: readonly VendorLib[];
|
|
22
|
+
render(props: z.output<S>, ctx: EmitContext, root: string): Rendered;
|
|
23
|
+
}
|
|
24
|
+
/** Safe-margin padding of the 5 % rule, in px. */
|
|
25
|
+
export declare const safePadding: (ctx: EmitContext) => string;
|
|
26
|
+
export declare const rootElement: (entry: SceneEntry, ctx: EmitContext, spec: {
|
|
27
|
+
type: string;
|
|
28
|
+
layer?: "overlay";
|
|
29
|
+
}, body: string) => string;
|
|
30
|
+
/** Builds a ScenePlugin: validates props, renders the fragment, assembles the three files. */
|
|
31
|
+
export declare const defineScene: <S extends z.ZodType>(spec: SceneSpec<S>) => ScenePlugin;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { SceneEntryBaseSchema, } from "@anvpx/motion-schema";
|
|
2
|
+
import { esc } from "./html.js";
|
|
3
|
+
const SAFE_MARGIN = 0.05;
|
|
4
|
+
const DEFAULT_EASE = "out";
|
|
5
|
+
const DEFAULT_MOVE = "static";
|
|
6
|
+
/** Safe-margin padding of the 5 % rule, in px. */
|
|
7
|
+
export const safePadding = (ctx) => {
|
|
8
|
+
const { insets } = ctx;
|
|
9
|
+
const side = (frac, size) => Math.round(size * Math.max(SAFE_MARGIN, frac ?? 0));
|
|
10
|
+
if (insets === undefined)
|
|
11
|
+
return `${side(0, ctx.height)}px ${side(0, ctx.width)}px`;
|
|
12
|
+
const { top, right, bottom, left } = insets;
|
|
13
|
+
return [
|
|
14
|
+
side(top, ctx.height),
|
|
15
|
+
side(right, ctx.width),
|
|
16
|
+
side(bottom, ctx.height),
|
|
17
|
+
side(left, ctx.width),
|
|
18
|
+
]
|
|
19
|
+
.map((px) => `${px}px`)
|
|
20
|
+
.join(" ");
|
|
21
|
+
};
|
|
22
|
+
const baseCss = (root, ctx) => `${root} {
|
|
23
|
+
position: absolute;
|
|
24
|
+
inset: 0;
|
|
25
|
+
box-sizing: border-box;
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
justify-content: center;
|
|
29
|
+
padding: ${safePadding(ctx)};
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
isolation: isolate;
|
|
32
|
+
background: var(--mk-grain-image, none), linear-gradient(160deg, var(--mk-color-bg) 0%, var(--mk-color-bgEnd) 100%);
|
|
33
|
+
color: var(--mk-color-fg);
|
|
34
|
+
font-family: var(--mk-font-sans);
|
|
35
|
+
line-height: 1.2;
|
|
36
|
+
}
|
|
37
|
+
${root}::before {
|
|
38
|
+
content: "";
|
|
39
|
+
position: absolute;
|
|
40
|
+
inset: -12%;
|
|
41
|
+
z-index: -1;
|
|
42
|
+
pointer-events: none;
|
|
43
|
+
background:
|
|
44
|
+
radial-gradient(48% 56% at calc(80% + var(--drift, 0) * 3%) calc(28% - var(--drift, 0) * 4%), var(--mk-color-glow) 0%, transparent 100%),
|
|
45
|
+
radial-gradient(40% 50% at calc(8% - var(--drift, 0) * 3%) calc(104% + var(--drift, 0) * 3%), var(--mk-color-glow2) 0%, transparent 100%);
|
|
46
|
+
}
|
|
47
|
+
${root}::after {
|
|
48
|
+
content: "";
|
|
49
|
+
position: absolute;
|
|
50
|
+
inset: 0;
|
|
51
|
+
z-index: -1;
|
|
52
|
+
pointer-events: none;
|
|
53
|
+
background-image: radial-gradient(color-mix(in srgb, var(--mk-color-fg) 12%, transparent) 2px, transparent 2.5px);
|
|
54
|
+
background-size: 64px 64px;
|
|
55
|
+
background-position: calc(var(--drift, 0) * 8px) 0;
|
|
56
|
+
mask-image: radial-gradient(75% 75% at 50% 50%, transparent 30%, black 100%);
|
|
57
|
+
}
|
|
58
|
+
${root} * { margin: 0; box-sizing: border-box; }
|
|
59
|
+
${root} [data-anim] { opacity: 0; }
|
|
60
|
+
${root} [data-anim="draw"] { stroke-dasharray: 1; stroke-dashoffset: 1; }
|
|
61
|
+
${root} [data-anim="grow"] { transform: scaleX(0); }
|
|
62
|
+
${root} .mk-rule {
|
|
63
|
+
display: block;
|
|
64
|
+
flex: none;
|
|
65
|
+
width: 160px;
|
|
66
|
+
height: 8px;
|
|
67
|
+
border-radius: var(--mk-radius-full);
|
|
68
|
+
background: linear-gradient(90deg, var(--mk-color-accent), var(--mk-color-accent2));
|
|
69
|
+
transform-origin: left center;
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
/** Transparent, click-through layer; content stays inside the safe margin. */
|
|
73
|
+
const overlayCss = (root, ctx) => `${root} {
|
|
74
|
+
position: absolute;
|
|
75
|
+
inset: 0;
|
|
76
|
+
box-sizing: border-box;
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
padding: ${safePadding(ctx)};
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
pointer-events: none;
|
|
82
|
+
color: var(--mk-color-fg);
|
|
83
|
+
font-family: var(--mk-font-sans);
|
|
84
|
+
font-size: var(--mk-font-size-sm);
|
|
85
|
+
line-height: 1.2;
|
|
86
|
+
}
|
|
87
|
+
${root} * { margin: 0; box-sizing: border-box; }
|
|
88
|
+
${root} [data-anim] { opacity: 0; }
|
|
89
|
+
${root} [data-anim="grow"] { transform: scaleX(0); }
|
|
90
|
+
`;
|
|
91
|
+
export const rootElement = (entry, ctx, spec, body) => {
|
|
92
|
+
const { type, layer } = spec;
|
|
93
|
+
const attrs = [
|
|
94
|
+
`id="scene-${entry.id}"`,
|
|
95
|
+
`class="${layer === "overlay" ? "mk-scene mk-overlay" : "mk-scene"} mk-${type}"`,
|
|
96
|
+
`data-ease="${esc(entry.ease ?? DEFAULT_EASE)}"`,
|
|
97
|
+
`data-move="${esc(entry.move ?? DEFAULT_MOVE)}"`,
|
|
98
|
+
`data-focus="${esc(entry.focus ?? "")}"`,
|
|
99
|
+
`data-fps="${ctx.fps}"`,
|
|
100
|
+
`data-duration="${ctx.duration}"`,
|
|
101
|
+
];
|
|
102
|
+
return `<section ${attrs.join(" ")}>\n${body}\n</section>\n`;
|
|
103
|
+
};
|
|
104
|
+
/** Builds a ScenePlugin: validates props, renders the fragment, assembles the three files. */
|
|
105
|
+
export const defineScene = (spec) => ({
|
|
106
|
+
type: spec.type,
|
|
107
|
+
...(spec.layer === undefined ? {} : { layer: spec.layer }),
|
|
108
|
+
configSchema: spec.configSchema,
|
|
109
|
+
meta: spec.meta,
|
|
110
|
+
...(spec.vendor === undefined ? {} : { vendor: spec.vendor }),
|
|
111
|
+
emit(entry, ctx) {
|
|
112
|
+
const id = SceneEntryBaseSchema.shape.id.parse(entry.id);
|
|
113
|
+
const props = spec.configSchema.parse(entry.props);
|
|
114
|
+
const root = `#scene-${id}`;
|
|
115
|
+
const { body, css = "" } = spec.render(props, ctx, root);
|
|
116
|
+
const dir = `scenes/${id}`;
|
|
117
|
+
return new Map([
|
|
118
|
+
[`${dir}/fragment.html`, rootElement(entry, ctx, spec, body)],
|
|
119
|
+
[
|
|
120
|
+
`${dir}/style.css`,
|
|
121
|
+
(spec.layer === "overlay" ? overlayCss(root, ctx) : baseCss(root, ctx)) +
|
|
122
|
+
spec.style.replaceAll("#scene-root", root) +
|
|
123
|
+
css,
|
|
124
|
+
],
|
|
125
|
+
[`${dir}/scene.js`, spec.script],
|
|
126
|
+
]);
|
|
127
|
+
},
|
|
128
|
+
});
|
package/dist/html.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Escapes text for element content and double- or single-quoted attributes. */
|
|
2
|
+
export declare const esc: (text: string) => string;
|
|
3
|
+
export type AnimKind = "rise" | "fade" | "draw" | "grow";
|
|
4
|
+
/** Attributes the runtime reads: entrance kind, stagger step and focus keys. */
|
|
5
|
+
export declare const animAttrs: (kind: AnimKind, step: number, focusKeys?: string) => string;
|
package/dist/html.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const ESCAPES = {
|
|
2
|
+
"&": "&",
|
|
3
|
+
"<": "<",
|
|
4
|
+
">": ">",
|
|
5
|
+
'"': """,
|
|
6
|
+
"'": "'",
|
|
7
|
+
};
|
|
8
|
+
/** Escapes text for element content and double- or single-quoted attributes. */
|
|
9
|
+
export const esc = (text) => text.replace(/[&<>"']/g, (char) => ESCAPES[char] ?? char);
|
|
10
|
+
/** Attributes the runtime reads: entrance kind, stagger step and focus keys. */
|
|
11
|
+
export const animAttrs = (kind, step, focusKeys = "") => {
|
|
12
|
+
const keys = focusKeys ? ` data-focus-key="${esc(focusKeys)}"` : "";
|
|
13
|
+
return `data-anim="${kind}" data-step="${step}"${keys}`;
|
|
14
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { type FieldDoc, type SceneDoc, sceneDocs } from "./catalog.ts";
|
|
2
|
+
export { CUSTOM_SCENE_TYPE, type CustomSceneFile, type CustomSceneSource, emitCustomScene, HELPER_LIMITS, type RelativeClosure, type RelativeImportScan, readRelativeClosure, resolveSceneDir, type SceneDirResult, scanRelativeImports, scanVendorImports, type VendorScan, } from "./custom.ts";
|
|
3
|
+
export { esc } from "./html.ts";
|
|
4
|
+
export { getScene, sceneRegistry, sceneTypes } from "./registry.ts";
|
|
5
|
+
export { runtimeFiles } from "./runtime-files.ts";
|
|
6
|
+
export { type FlowReport, inspectFlow } from "./scenes/flow/inspect.ts";
|
|
7
|
+
export { collectIcons, iconLedgerRows, LUCIDE_LICENCE_TEXT } from "./shared/icon.ts";
|
|
8
|
+
export { iconGroups, listIcons, searchIconNames } from "./shared/icon-search.ts";
|
|
9
|
+
export { textWidth } from "./shared/measure.ts";
|
|
10
|
+
export { sceneSourceHash } from "./source-hash.ts";
|
|
11
|
+
export { sceneWords } from "./words.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { sceneDocs } from "./catalog.js";
|
|
2
|
+
export { CUSTOM_SCENE_TYPE, emitCustomScene, HELPER_LIMITS, readRelativeClosure, resolveSceneDir, scanRelativeImports, scanVendorImports, } from "./custom.js";
|
|
3
|
+
export { esc } from "./html.js";
|
|
4
|
+
export { getScene, sceneRegistry, sceneTypes } from "./registry.js";
|
|
5
|
+
export { runtimeFiles } from "./runtime-files.js";
|
|
6
|
+
export { inspectFlow } from "./scenes/flow/inspect.js";
|
|
7
|
+
export { collectIcons, iconLedgerRows, LUCIDE_LICENCE_TEXT } from "./shared/icon.js";
|
|
8
|
+
export { iconGroups, listIcons, searchIconNames } from "./shared/icon-search.js";
|
|
9
|
+
export { textWidth } from "./shared/measure.js";
|
|
10
|
+
export { sceneSourceHash } from "./source-hash.js";
|
|
11
|
+
export { sceneWords } from "./words.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const CORNERS: readonly ["top-left", "top-right", "bottom-left", "bottom-right"];
|
|
3
|
+
export declare const POSITIONS: readonly ["top-left", "top-right", "bottom-left", "bottom-right", "center"];
|
|
4
|
+
export declare const cornerSchema: z.ZodEnum<{
|
|
5
|
+
"bottom-left": "bottom-left";
|
|
6
|
+
"bottom-right": "bottom-right";
|
|
7
|
+
"top-left": "top-left";
|
|
8
|
+
"top-right": "top-right";
|
|
9
|
+
}>;
|
|
10
|
+
export declare const positionSchema: z.ZodEnum<{
|
|
11
|
+
"bottom-left": "bottom-left";
|
|
12
|
+
"bottom-right": "bottom-right";
|
|
13
|
+
center: "center";
|
|
14
|
+
"top-left": "top-left";
|
|
15
|
+
"top-right": "top-right";
|
|
16
|
+
}>;
|
|
17
|
+
export type Position = (typeof POSITIONS)[number];
|
|
18
|
+
/** Class that places a child of an overlay root (a flex column inside the safe margin). */
|
|
19
|
+
export declare const positionClass: (position: Position) => string;
|
|
20
|
+
/** Placement rules for the `mk-pos-*` classes, scoped to the scene root. */
|
|
21
|
+
export declare const positionCss: (root: string) => string;
|
package/dist/position.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const CORNERS = ["top-left", "top-right", "bottom-left", "bottom-right"];
|
|
3
|
+
export const POSITIONS = [...CORNERS, "center"];
|
|
4
|
+
export const cornerSchema = z.enum(CORNERS);
|
|
5
|
+
export const positionSchema = z.enum(POSITIONS);
|
|
6
|
+
/** Class that places a child of an overlay root (a flex column inside the safe margin). */
|
|
7
|
+
export const positionClass = (position) => `mk-pos-${position}`;
|
|
8
|
+
/** Placement rules for the `mk-pos-*` classes, scoped to the scene root. */
|
|
9
|
+
export const positionCss = (root) => [
|
|
10
|
+
`${root} .mk-pos-top-left { align-self: flex-start; margin-bottom: auto; }`,
|
|
11
|
+
`${root} .mk-pos-top-right { align-self: flex-end; margin-bottom: auto; }`,
|
|
12
|
+
`${root} .mk-pos-bottom-left { align-self: flex-start; margin-top: auto; }`,
|
|
13
|
+
`${root} .mk-pos-bottom-right { align-self: flex-end; margin-top: auto; }`,
|
|
14
|
+
`${root} .mk-pos-center { align-self: center; margin-block: auto; }`,
|
|
15
|
+
"",
|
|
16
|
+
].join("\n");
|