@squinch/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +41 -0
  3. package/dist/api.d.ts +91 -0
  4. package/dist/api.js +232 -0
  5. package/dist/browser.d.ts +3 -0
  6. package/dist/browser.js +9 -0
  7. package/dist/diff/diff.d.ts +30 -0
  8. package/dist/diff/diff.js +365 -0
  9. package/dist/fonts.generated.d.ts +1 -0
  10. package/dist/fonts.generated.js +6 -0
  11. package/dist/grammar/parser.js +22 -0
  12. package/dist/grammar/parser.terms.js +115 -0
  13. package/dist/index.d.ts +4 -0
  14. package/dist/index.js +4 -0
  15. package/dist/layout/layout.d.ts +197 -0
  16. package/dist/layout/layout.js +1721 -0
  17. package/dist/metrics.d.ts +20 -0
  18. package/dist/metrics.generated.d.ts +4 -0
  19. package/dist/metrics.generated.js +4 -0
  20. package/dist/metrics.js +57 -0
  21. package/dist/model/build.d.ts +8 -0
  22. package/dist/model/build.js +1343 -0
  23. package/dist/model/packs.d.ts +13 -0
  24. package/dist/model/packs.js +29 -0
  25. package/dist/model/source.d.ts +10 -0
  26. package/dist/model/source.js +28 -0
  27. package/dist/model/suggest.d.ts +2 -0
  28. package/dist/model/suggest.js +24 -0
  29. package/dist/model/types.d.ts +226 -0
  30. package/dist/model/types.js +24 -0
  31. package/dist/packs/node-fs.d.ts +1 -0
  32. package/dist/packs/node-fs.js +37 -0
  33. package/dist/packs/registry.d.ts +61 -0
  34. package/dist/packs/registry.js +122 -0
  35. package/dist/packs/sanitize.d.ts +12 -0
  36. package/dist/packs/sanitize.js +127 -0
  37. package/dist/packs/sysGlyphs.d.ts +2 -0
  38. package/dist/packs/sysGlyphs.js +21 -0
  39. package/dist/render/adaptive.d.ts +13 -0
  40. package/dist/render/adaptive.js +112 -0
  41. package/dist/render/html/runtime.d.ts +1 -0
  42. package/dist/render/html/runtime.generated.d.ts +1 -0
  43. package/dist/render/html/runtime.generated.js +6 -0
  44. package/dist/render/html/runtime.js +362 -0
  45. package/dist/render/html.d.ts +39 -0
  46. package/dist/render/html.js +235 -0
  47. package/dist/render/svg.d.ts +75 -0
  48. package/dist/render/svg.js +1403 -0
  49. package/dist/render/validate.d.ts +4 -0
  50. package/dist/render/validate.js +9 -0
  51. package/dist/themes/index.d.ts +84 -0
  52. package/dist/themes/index.js +90 -0
  53. package/dist/view/dive.d.ts +55 -0
  54. package/dist/view/dive.js +57 -0
  55. package/dist/view/navigate.d.ts +38 -0
  56. package/dist/view/navigate.js +81 -0
  57. package/dist/view/resolve.d.ts +92 -0
  58. package/dist/view/resolve.js +591 -0
  59. package/fonts/inter-400.ttf +0 -0
  60. package/fonts/inter-500.ttf +0 -0
  61. package/fonts/inter-600.ttf +0 -0
  62. package/fonts/mono-400.ttf +0 -0
  63. package/metrics.json +510 -0
  64. package/package.json +89 -0
@@ -0,0 +1,127 @@
1
+ // Pack SVGs are third-party content that ends up inside the SPA, VSCode webviews
2
+ // and committed files — so every asset is sanitized at load with an allowlist.
3
+ // Anything not explicitly permitted is dropped.
4
+ import { XMLParser, XMLBuilder } from "fast-xml-parser";
5
+ const ELEMENTS = new Set([
6
+ "svg", "g", "defs", "symbol", "use", "title", "desc",
7
+ "path", "rect", "circle", "ellipse", "line", "polyline", "polygon",
8
+ "linearGradient", "radialGradient", "stop", "clipPath", "mask", "pattern",
9
+ "text", "tspan",
10
+ ]);
11
+ const ATTRS = new Set([
12
+ "d", "fill", "stroke", "opacity", "transform", "viewBox",
13
+ "x", "y", "width", "height", "cx", "cy", "r", "rx", "ry",
14
+ "x1", "y1", "x2", "y2", "points", "offset",
15
+ "fill-rule", "clip-rule", "fill-opacity", "stroke-opacity",
16
+ "stroke-width", "stroke-linecap", "stroke-linejoin", "stroke-dasharray",
17
+ "stop-color", "stop-opacity", "gradientUnits", "gradientTransform",
18
+ "spreadMethod", "clipPathUnits", "maskUnits", "patternUnits",
19
+ "id", "clip-path", "mask", "filter", "font-size", "font-family",
20
+ "text-anchor", "dominant-baseline",
21
+ ]);
22
+ /** Attribute values that reference document ids as `url(#foo)`. */
23
+ const REF_ATTRS = new Set(["clip-path", "mask", "filter", "fill", "stroke"]);
24
+ /** Attributes whose value is a bare `#id` fragment rather than a colour.
25
+ *
26
+ * `href`/`xlink:href` are the only SVG attributes shaped that way, and ATTRS
27
+ * drops both today — so in practice nothing here fires. It stays an explicit
28
+ * allowlist because the alternative (rewrite any value starting with `#`,
29
+ * minus a denylist of known colour attributes) is what shipped, and it was
30
+ * wrong: `stop-color="#5ea0ef"` is a hex colour, not a reference to an element
31
+ * named `5ea0ef`. Namespacing it produced `#pack-5ea0ef`, an invalid colour
32
+ * that paints black — which is every gradient in a pack of 597 gradient
33
+ * icons, rendered as black blobs. A denylist can only ever be as complete as
34
+ * the last pack to expose it. */
35
+ const BARE_REF_ATTRS = new Set(["href", "xlink:href"]);
36
+ const parser = new XMLParser({
37
+ ignoreAttributes: false,
38
+ attributeNamePrefix: "@",
39
+ preserveOrder: true,
40
+ allowBooleanAttributes: true,
41
+ trimValues: false,
42
+ });
43
+ const builder = new XMLBuilder({
44
+ ignoreAttributes: false,
45
+ attributeNamePrefix: "@",
46
+ preserveOrder: true,
47
+ suppressEmptyNode: true,
48
+ });
49
+ /**
50
+ * Strip everything not on the allowlist and namespace internal ids so several
51
+ * icons can coexist in one document. Does not alter geometry or colour — the
52
+ * asset itself must stay byte-faithful (CC-BY-ND).
53
+ */
54
+ export function sanitizeIcon(svg, idPrefix) {
55
+ const tree = parser.parse(svg);
56
+ const root = findElement(tree, "svg");
57
+ if (!root)
58
+ throw new Error("pack asset has no <svg> root");
59
+ const viewBox = String(attrOf(root, "viewBox") ?? "0 0 80 80");
60
+ const clean = (nodes) => {
61
+ const out = [];
62
+ for (const node of nodes) {
63
+ const tag = Object.keys(node).find((k) => k !== ":@" && k !== "#text");
64
+ if (tag === undefined) {
65
+ if (typeof node["#text"] === "string" && node["#text"].trim())
66
+ out.push(node);
67
+ continue;
68
+ }
69
+ if (!ELEMENTS.has(tag))
70
+ continue; // drops script, foreignObject, image, …
71
+ const attrs = node[":@"] ?? {};
72
+ const kept = {};
73
+ for (const [rawName, value] of Object.entries(attrs)) {
74
+ const name = rawName.replace(/^@/, "");
75
+ if (!ATTRS.has(name))
76
+ continue; // drops on*, xlink:href, style, class, …
77
+ let v = String(value);
78
+ if (name === "id")
79
+ v = `${idPrefix}-${v}`;
80
+ else if (REF_ATTRS.has(name) && v.includes("url(#"))
81
+ v = v.replace(/url\(#([^)]+)\)/g, (_m, id) => `url(#${idPrefix}-${id})`);
82
+ else if (BARE_REF_ATTRS.has(name) && v.startsWith("#"))
83
+ v = `#${idPrefix}-${v.slice(1)}`;
84
+ kept[`@${name}`] = v;
85
+ }
86
+ const children = node[tag];
87
+ out.push({ [tag]: Array.isArray(children) ? clean(children) : children, ":@": kept });
88
+ }
89
+ return out;
90
+ };
91
+ // The root <svg>'s presentation attributes are *inherited* by everything
92
+ // inside it, and the body gets lifted out of that root into a <symbol> — so
93
+ // dropping them silently restyles the artwork. Stroke-only sets are where it
94
+ // shows: Lucide puts `fill="none" stroke="currentColor" stroke-width="2"` on
95
+ // the root and nothing on the paths, so without this the shapes fall back to
96
+ // the SVG defaults — filled black, unstroked — and every icon renders as a
97
+ // solid blob. Hoist them onto a wrapping <g> so inheritance survives the move.
98
+ //
99
+ // Only inheritable paint/stroke/text properties: geometry (`width`, `x`,
100
+ // `viewBox`) describes the root viewport, not its children, and carrying it
101
+ // down would move the artwork.
102
+ const INHERITED = [
103
+ "fill", "stroke", "stroke-width", "stroke-linecap", "stroke-linejoin",
104
+ "stroke-dasharray", "stroke-opacity", "fill-opacity", "fill-rule",
105
+ "clip-rule", "opacity", "font-size", "font-family", "text-anchor",
106
+ "dominant-baseline",
107
+ ];
108
+ const inherited = {};
109
+ for (const name of INHERITED) {
110
+ const v = attrOf(root, name);
111
+ if (v !== undefined && v !== null)
112
+ inherited[`@${name}`] = String(v);
113
+ }
114
+ const children = clean(root.svg);
115
+ const body = builder.build(Object.keys(inherited).length ? [{ g: children, ":@": inherited }] : children);
116
+ return { body, viewBox };
117
+ }
118
+ function findElement(nodes, tag) {
119
+ for (const node of nodes) {
120
+ if (node[tag])
121
+ return node;
122
+ }
123
+ return undefined;
124
+ }
125
+ function attrOf(node, name) {
126
+ return node[":@"]?.[`@${name}`];
127
+ }
@@ -0,0 +1,2 @@
1
+ export declare const SYS_GLYPH_ART: Record<string, Record<string, string>>;
2
+ export declare const SYS_GLYPH_VIEWBOX = "0 0 24 24";
@@ -0,0 +1,21 @@
1
+ // First-party glyph artwork for the `builtin` pseudo-pack: `person` and `box`,
2
+ // drawn as 24×24 monoline strokes with round caps/joins. Both paint with
3
+ // currentColor so themes tint them natively — muted on card badges, plate-text
4
+ // white on icon plates, ink in sketch.
5
+ //
6
+ // These two stay compiled into core because they are language sugar, not an icon
7
+ // choice: `person x "…"` and `= box` desugar to them, so they must resolve with
8
+ // nothing installed. The far larger `sys/*` set moved out to @squinch/pack-sys
9
+ // (Lucide, ISC) — 142 icons is more than belongs in an isomorphic bundle, and
10
+ // vendoring gives one coherent hand instead of one person's.
11
+ const S = `fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"`;
12
+ const F = `fill="currentColor" stroke="none"`;
13
+ const g = (inner) => `<g ${S}>${inner}</g>`;
14
+ export const SYS_GLYPH_ART = {
15
+ builtin: {
16
+ person: g(`<circle cx="12" cy="8" r="3.75"/><path d="M5 20c0-4.1 3.1-6.5 7-6.5s7 2.4 7 6.5"/>`),
17
+ box: g(`<path d="M12 3.5 20 7.75v8.5L12 20.5 4 16.25v-8.5Z"/>` +
18
+ `<path d="M4 7.75 12 12l8-4.25"/><path d="M12 12v8.5"/>`),
19
+ },
20
+ };
21
+ export const SYS_GLYPH_VIEWBOX = "0 0 24 24";
@@ -0,0 +1,13 @@
1
+ export declare class AdaptivePairError extends Error {
2
+ }
3
+ /**
4
+ * Fold a light and a dark render of the same diagram into one file.
5
+ *
6
+ * Both must be the same drawing — same geometry, same text, same icons —
7
+ * differing only in colour. That holds for themes sharing a font (light/dark,
8
+ * sketch/sketch-dark) because nothing in the render path branches on the theme
9
+ * beyond its palette and its sketch settings. It does *not* hold across fonts,
10
+ * since type metrics drive layout, so a mismatched pair is rejected rather
11
+ * than quietly emitting a file whose dark half is misaligned.
12
+ */
13
+ export declare function mergeAdaptive(light: string, dark: string): string;
@@ -0,0 +1,112 @@
1
+ // One SVG that carries both palettes and picks one with `prefers-color-scheme`.
2
+ //
3
+ // Why merge two renders instead of emitting CSS variables from the emitters:
4
+ // the artwork in an icon pack is verbatim third-party SVG (AWS ships CC-BY-ND),
5
+ // and it shares hex values with the theme — `#FFFFFF` appears in both a light
6
+ // card surface and inside the icons, unchanged between themes. A search and
7
+ // replace over the colour literals would silently recolour someone else's
8
+ // trademark. Rendering the pair and walking their attributes in lockstep tells
9
+ // us *positionally* which values are theme-driven: the ones that moved.
10
+ //
11
+ // The dark palette rides in a `<style>` media query rather than replacing the
12
+ // attributes, so the file degrades to exactly today's output. A renderer with
13
+ // no CSS support at all — resvg, which is what PNG export uses — still reads
14
+ // the presentation attributes and draws the light theme correctly. Nothing
15
+ // executes: this is a stylesheet, not script, so the no-JS contract holds.
16
+ const ATTR = /([a-zA-Z_][\w:.-]*)="([^"]*)"/g;
17
+ const TAG = /<[a-zA-Z]/g;
18
+ function attrs(svg) {
19
+ // Tag starts, so each attribute can be attributed to its element. Both
20
+ // strings come straight from our own emitter, which never puts a `<` inside
21
+ // an attribute value, so a scan is enough — no parser needed.
22
+ const tags = [];
23
+ for (const m of svg.matchAll(TAG))
24
+ tags.push(m.index);
25
+ const out = [];
26
+ let ti = 0;
27
+ for (const m of svg.matchAll(ATTR)) {
28
+ const at = m.index + m[1].length + 2;
29
+ while (ti + 1 < tags.length && tags[ti + 1] < m.index)
30
+ ti++;
31
+ out.push({ name: m[1], value: m[2], at, tag: tags[ti] });
32
+ }
33
+ return out;
34
+ }
35
+ /** Colour-bearing properties. A differing `d` or `x` would mean the geometry
36
+ * moved, which is a bug in the pairing rather than something to style. */
37
+ const COLOUR = new Set(["fill", "stroke", "stop-color", "color", "flood-color"]);
38
+ export class AdaptivePairError extends Error {
39
+ }
40
+ /**
41
+ * Fold a light and a dark render of the same diagram into one file.
42
+ *
43
+ * Both must be the same drawing — same geometry, same text, same icons —
44
+ * differing only in colour. That holds for themes sharing a font (light/dark,
45
+ * sketch/sketch-dark) because nothing in the render path branches on the theme
46
+ * beyond its palette and its sketch settings. It does *not* hold across fonts,
47
+ * since type metrics drive layout, so a mismatched pair is rejected rather
48
+ * than quietly emitting a file whose dark half is misaligned.
49
+ */
50
+ export function mergeAdaptive(light, dark) {
51
+ const la = attrs(light);
52
+ const da = attrs(dark);
53
+ const skeleton = (s) => s.replace(/="[^"]*"/g, '=""');
54
+ if (la.length !== da.length || skeleton(light) !== skeleton(dark))
55
+ throw new AdaptivePairError("the two themes did not draw the same diagram, so they cannot share one file — " +
56
+ "an adaptive pair must differ only in colour (check they use the same font)");
57
+ // Distinct (property, light, dark) triples become one CSS rule each; the
58
+ // class name is the triple's index in sorted order, so the output is a pure
59
+ // function of the input and stays byte-identical across runs.
60
+ const triples = new Map();
61
+ const hits = [];
62
+ for (let i = 0; i < la.length; i++) {
63
+ if (la[i].value === da[i].value)
64
+ continue;
65
+ const { name } = la[i];
66
+ if (!COLOUR.has(name))
67
+ throw new AdaptivePairError(`the two themes disagree on \`${name}\`, which is geometry rather than colour — ` +
68
+ "an adaptive pair must differ only in colour");
69
+ const key = `${name}|${la[i].value}|${da[i].value}`;
70
+ triples.set(key, { prop: name, dark: da[i].value });
71
+ hits.push({ tag: la[i].tag, key });
72
+ }
73
+ if (!triples.size)
74
+ return light; // identical palettes: nothing to adapt
75
+ const order = [...triples.keys()].sort();
76
+ const cls = new Map(order.map((k, i) => [k, `sq-t${i}`]));
77
+ // Group by element: one element can have both a themed fill and a themed
78
+ // stroke, and it needs both classes.
79
+ const byTag = new Map();
80
+ for (const h of hits) {
81
+ const name = cls.get(h.key);
82
+ const list = byTag.get(h.tag) ?? [];
83
+ if (!list.includes(name))
84
+ list.push(name);
85
+ byTag.set(h.tag, list);
86
+ }
87
+ // Rewrite back-to-front so earlier offsets stay valid.
88
+ let out = light;
89
+ for (const [tag, names] of [...byTag].sort((a, b) => b[0] - a[0])) {
90
+ const close = out.indexOf(">", tag);
91
+ const head = out.slice(tag, close);
92
+ // `class="sq-flow"` already exists on animated edges — extend it, never
93
+ // replace it, or the flow animation stops.
94
+ const existing = /\sclass="([^"]*)"/.exec(head);
95
+ if (existing)
96
+ out =
97
+ out.slice(0, tag + existing.index) +
98
+ ` class="${existing[1]} ${names.join(" ")}"` +
99
+ out.slice(tag + existing.index + existing[0].length);
100
+ else {
101
+ const nameEnd = tag + 1 + /^[a-zA-Z][\w:.-]*/.exec(out.slice(tag + 1))[0].length;
102
+ out = out.slice(0, nameEnd) + ` class="${names.join(" ")}"` + out.slice(nameEnd);
103
+ }
104
+ }
105
+ const rules = order
106
+ .map((k) => `.${cls.get(k)}{${triples.get(k).prop}:${triples.get(k).dark}}`)
107
+ .join("");
108
+ const style = `<style>@media (prefers-color-scheme: dark){${rules}}</style>`;
109
+ // After the opening tag, alongside the font faces.
110
+ const rootEnd = out.indexOf(">") + 1;
111
+ return out.slice(0, rootEnd) + "\n" + style + out.slice(rootEnd);
112
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare const RUNTIME_JS = "\"use strict\";(()=>{var Q={ms:460,ease:\"cubic-bezier(.32,.72,0,1)\"},X={ms:240,ease:\"cubic-bezier(.4,0,.2,1)\"},K=e=>({x:e.x+e.w/2,y:e.y+e.h/2}),Y=(e,n,i)=>Math.min(i,Math.max(n,e)),Z=(e,n)=>Y(Math.min(e.w/n.w,e.h/n.h),1.15,3.2);function U(e){let{view:n,ghostBox:i,liveBox:a,anchor:p,dir:o}=e,{ms:l,ease:d}=p?Q:X;if(!p)return{ms:l,ease:d,gOrigin:\"50% 50%\",lOrigin:\"50% 50%\",gEnd:\"scale(.97)\",lStart:\"scale(1.03)\"};let f=Z(n,p),T=1+(f-1)*.62,E=K(p),h=K(n),y=h.x-E.x,w=h.y-E.y,x=o===\"in\",g=x?E:h,k=x?h:E;return{ms:l,ease:d,gOrigin:`${g.x-i.x}px ${g.y-i.y}px`,lOrigin:`${k.x-a.x}px ${k.y-a.y}px`,gEnd:x?`translate(${y}px, ${w}px) scale(${f})`:`translate(${-y}px, ${-w}px) scale(${1/f})`,lStart:x?`translate(${-y}px, ${-w}px) scale(${1/T})`:`translate(${y}px, ${w}px) scale(${T})`}}function W(e,n){if(n){if(!e)return n.split(\".\")[0];if(!(n===e||!n.startsWith(`${e}.`)))return`${e}.${n.slice(e.length+1).split(\".\")[0]}`}}function ee(e){if(!e)return[];let n=e.split(\".\");return n.map((i,a)=>n.slice(0,a+1).join(\".\"))}function O(e,n,i){return e.find(a=>a.scope===i&&a.name!==n)}function _(e,n,i){let a=e.find(d=>d.name===n)?.scope,p=e.find(d=>d.name===i)?.scope,o=W(a,p);if(o)return{dir:\"in\",anchor:o};let l=W(p,a);return l?{dir:\"out\",anchor:l}:{dir:\"in\"}}function te(e,n){let i=[],a=e.find(o=>!o.scope);if(a&&i.push({label:\"landscape\",view:a.name}),!n)return i;let p=n.split(\".\");for(let[o,l]of ee(n).entries())i.push({label:p[o],view:e.find(d=>d.scope===l)?.name});return i}function S(e,n,i){return[...te(e,i)].reverse().find(a=>a.view&&a.view!==n)?.view}var L=e=>document.querySelector(e);function j(){let e=JSON.parse(L(\"#sq-data\").textContent||\"{}\"),n=L(\"#sq-live\"),i=L(\"#sq-ghost\"),a=L(\"#sq-stage\"),p=()=>matchMedia(\"(prefers-reduced-motion: reduce)\").matches,o=e.entry,l=e.themes[0],d=0,f=!1;if(e.themes.length>1&&matchMedia(\"(prefers-color-scheme: dark)\").matches){let t=e.themes.find(s=>s.includes(\"dark\"));t&&(l=t)}let T=`${e.entry}|${e.themes[0]}`,E=n.firstElementChild?.cloneNode(!0)??null,h=(t,s,c=0)=>{let r=c?`${t}|${s}|${c}`:`${t}|${s}`;if(r===T)return E?.cloneNode(!0)??null;let u=document.querySelector(`template[data-key=\"${CSS.escape(r)}\"]`);return u?u.content.cloneNode(!0):null},y=t=>e.views.find(s=>s.name===t)?.scope,w=t=>{let s=t.getBoundingClientRect(),c=a.getBoundingClientRect();return{x:s.left-c.left,y:s.top-c.top,w:s.width,h:s.height}};function x(){document.title=e.views.find(r=>r.name===o)?.title??document.title;let t=document.querySelector(\"#sq-tabs\");if(t){t.replaceChildren();for(let r of e.views){let u=document.createElement(\"button\");u.type=\"button\",u.className=r.name===o?\"on\":\"\",u.textContent=r.name,u.title=r.title??r.name,u.onclick=()=>g(r.name),t.append(u)}t.querySelector(\".on\")?.scrollIntoView?.({block:\"nearest\",inline:\"nearest\"})}for(let r of n.querySelectorAll(\"[data-path]\")){let u=r.getAttribute(\"data-path\");r.classList.toggle(\"sq-zoom\",!!(u&&O(e.views,o,u)))}let s=document.querySelector(\"#sq-step\"),c=e.flows[o]??0;s&&(s.textContent=f&&c?`${d||1} / ${c}`:\"\")}function g(t,s=!1){if(!t||t===o)return;let c=f&&e.flows[t]?s?e.flows[t]:1:0,r=h(t,l,c);if(!r)return;d=c;let{dir:u,anchor:v}=_(e.views,o,t),C=w(n),H=n.firstElementChild;if(p()||!H){n.replaceChildren(r),o=t,x();return}i.replaceChildren(H.cloneNode(!0)),i.style.cssText=`position:absolute;left:${C.x}px;top:${C.y}px;width:${C.w}px;height:${C.h}px;z-index:1;pointer-events:none`,n.replaceChildren(r),o=t,x();let G=w(n),P=v?(u===\"in\"?i:n).querySelector(`[data-path=\"${CSS.escape(v)}\"]`):null,m=U({view:{x:0,y:0,w:a.clientWidth,h:a.clientHeight},ghostBox:C,liveBox:G,anchor:P?w(P):void 0,dir:u}),b=i.style,$=n.style;b.transition=\"none\",b.transformOrigin=m.gOrigin,b.transform=\"none\",b.opacity=\"1\",$.transition=\"none\",$.transformOrigin=m.lOrigin,$.transform=m.lStart,$.opacity=\"0\",n.offsetHeight,b.transition=`transform ${m.ms}ms ${m.ease}, opacity ${Math.round(m.ms*.55)}ms ${m.ease}`,$.transition=`transform ${m.ms}ms ${m.ease}, opacity ${Math.round(m.ms*.6)}ms ${m.ease} ${Math.round(m.ms*.25)}ms`,b.transform=m.gEnd,b.opacity=\"0\",$.transform=\"none\",$.opacity=\"1\";let I=!1,q=0,R=()=>{I||(I=!0,cancelAnimationFrame(q),i.replaceChildren(),i.removeAttribute(\"style\"),n.removeAttribute(\"style\"))},z=()=>{getComputedStyle(n).transform===\"none\"?R():q=requestAnimationFrame(z)};q=requestAnimationFrame(z),setTimeout(R,m.ms+1e3)}function k(t){let s=t===l?null:h(o,t,d);s&&(l=t,document.documentElement.dataset.theme=t,n.replaceChildren(s))}function M(t){let s=e.flows[o]??0,c=d+t;if(s&&c>=1&&c<=s){let v=h(o,l,c);v&&(d=c,n.replaceChildren(v),x());return}let r=e.views.findIndex(v=>v.name===o),u=e.views[r+t];u&&g(u.name,t<0)}n.addEventListener(\"click\",t=>{let c=t.target.closest?.(\"[data-path]\")?.getAttribute(\"data-path\");if(c){let r=O(e.views,o,c);if(r)return g(r.name)}if(!c){let r=S(e.views,o,y(o));r&&g(r)}});let A=document.querySelector(\"#sq-theme\");A&&(A.onclick=()=>k(e.themes[(e.themes.indexOf(l)+1)%e.themes.length]));function B(t){if(t!==f){if(f=t,document.body.classList.toggle(\"presenting\",t),t){if(document.documentElement.requestFullscreen?.().catch(()=>{}),(e.flows[o]??0)&&!d){let c=h(o,l,1);c&&(d=1,n.replaceChildren(c))}}else if(document.fullscreenElement&&document.exitFullscreen?.().catch(()=>{}),d){let s=h(o,l,0);s&&(d=0,n.replaceChildren(s))}x()}}let N,V=()=>{document.body.classList.remove(\"idle\"),clearTimeout(N),f&&(N=setTimeout(()=>document.body.classList.add(\"idle\"),3500))};addEventListener(\"mousemove\",V),addEventListener(\"keydown\",V);let J=()=>k(e.themes[(e.themes.indexOf(l)+1)%e.themes.length]);addEventListener(\"keydown\",t=>{if(!(t.metaKey||t.ctrlKey||t.altKey))switch(t.key){case\"ArrowRight\":case\"PageDown\":case\" \":case\"Enter\":t.preventDefault(),M(1);break;case\"ArrowLeft\":case\"PageUp\":t.preventDefault(),M(-1);break;case\"ArrowUp\":case\"Backspace\":{let s=S(e.views,o,y(o));s&&(t.preventDefault(),g(s));break}case\"Home\":t.preventDefault(),g(e.views[0].name);break;case\"End\":t.preventDefault(),g(e.views[e.views.length-1].name,!0);break;case\"Escape\":f&&(t.preventDefault(),B(!1));break;case\"p\":case\"P\":B(!f);break;case\"f\":case\"F\":document.fullscreenElement?document.exitFullscreen?.().catch(()=>{}):document.documentElement.requestFullscreen?.().catch(()=>{});break;case\"t\":case\"T\":e.themes.length>1&&J();break}}),addEventListener(\"fullscreenchange\",()=>{!document.fullscreenElement&&f&&B(!1)});let D=document.querySelector(\"#sq-present\");D&&(D.onclick=()=>B(!f));let F=()=>{let t=decodeURIComponent(location.hash.slice(1));t&&t!==o&&e.views.some(s=>s.name===t)&&g(t)};if(addEventListener(\"hashchange\",F),document.documentElement.dataset.theme=l,l!==e.themes[0]){let t=h(o,l);t&&n.replaceChildren(t)}x(),F()}document.readyState===\"loading\"?addEventListener(\"DOMContentLoaded\",j):j();})();";
@@ -0,0 +1,6 @@
1
+ // GENERATED by scripts/gen-html-runtime.ts from src/render/html/runtime.ts —
2
+ // do not edit. Committed so that rendering never runs a bundler and the
3
+ // output cannot drift with an esbuild upgrade; CI re-runs the generator and
4
+ // diffs. It bundles view/dive.ts and view/navigate.ts, which is what makes
5
+ // "the export moves exactly like the playground" true by construction.
6
+ export const RUNTIME_JS = "\"use strict\";(()=>{var Q={ms:460,ease:\"cubic-bezier(.32,.72,0,1)\"},X={ms:240,ease:\"cubic-bezier(.4,0,.2,1)\"},K=e=>({x:e.x+e.w/2,y:e.y+e.h/2}),Y=(e,n,i)=>Math.min(i,Math.max(n,e)),Z=(e,n)=>Y(Math.min(e.w/n.w,e.h/n.h),1.15,3.2);function U(e){let{view:n,ghostBox:i,liveBox:a,anchor:p,dir:o}=e,{ms:l,ease:d}=p?Q:X;if(!p)return{ms:l,ease:d,gOrigin:\"50% 50%\",lOrigin:\"50% 50%\",gEnd:\"scale(.97)\",lStart:\"scale(1.03)\"};let f=Z(n,p),T=1+(f-1)*.62,E=K(p),h=K(n),y=h.x-E.x,w=h.y-E.y,x=o===\"in\",g=x?E:h,k=x?h:E;return{ms:l,ease:d,gOrigin:`${g.x-i.x}px ${g.y-i.y}px`,lOrigin:`${k.x-a.x}px ${k.y-a.y}px`,gEnd:x?`translate(${y}px, ${w}px) scale(${f})`:`translate(${-y}px, ${-w}px) scale(${1/f})`,lStart:x?`translate(${-y}px, ${-w}px) scale(${1/T})`:`translate(${y}px, ${w}px) scale(${T})`}}function W(e,n){if(n){if(!e)return n.split(\".\")[0];if(!(n===e||!n.startsWith(`${e}.`)))return`${e}.${n.slice(e.length+1).split(\".\")[0]}`}}function ee(e){if(!e)return[];let n=e.split(\".\");return n.map((i,a)=>n.slice(0,a+1).join(\".\"))}function O(e,n,i){return e.find(a=>a.scope===i&&a.name!==n)}function _(e,n,i){let a=e.find(d=>d.name===n)?.scope,p=e.find(d=>d.name===i)?.scope,o=W(a,p);if(o)return{dir:\"in\",anchor:o};let l=W(p,a);return l?{dir:\"out\",anchor:l}:{dir:\"in\"}}function te(e,n){let i=[],a=e.find(o=>!o.scope);if(a&&i.push({label:\"landscape\",view:a.name}),!n)return i;let p=n.split(\".\");for(let[o,l]of ee(n).entries())i.push({label:p[o],view:e.find(d=>d.scope===l)?.name});return i}function S(e,n,i){return[...te(e,i)].reverse().find(a=>a.view&&a.view!==n)?.view}var L=e=>document.querySelector(e);function j(){let e=JSON.parse(L(\"#sq-data\").textContent||\"{}\"),n=L(\"#sq-live\"),i=L(\"#sq-ghost\"),a=L(\"#sq-stage\"),p=()=>matchMedia(\"(prefers-reduced-motion: reduce)\").matches,o=e.entry,l=e.themes[0],d=0,f=!1;if(e.themes.length>1&&matchMedia(\"(prefers-color-scheme: dark)\").matches){let t=e.themes.find(s=>s.includes(\"dark\"));t&&(l=t)}let T=`${e.entry}|${e.themes[0]}`,E=n.firstElementChild?.cloneNode(!0)??null,h=(t,s,c=0)=>{let r=c?`${t}|${s}|${c}`:`${t}|${s}`;if(r===T)return E?.cloneNode(!0)??null;let u=document.querySelector(`template[data-key=\"${CSS.escape(r)}\"]`);return u?u.content.cloneNode(!0):null},y=t=>e.views.find(s=>s.name===t)?.scope,w=t=>{let s=t.getBoundingClientRect(),c=a.getBoundingClientRect();return{x:s.left-c.left,y:s.top-c.top,w:s.width,h:s.height}};function x(){document.title=e.views.find(r=>r.name===o)?.title??document.title;let t=document.querySelector(\"#sq-tabs\");if(t){t.replaceChildren();for(let r of e.views){let u=document.createElement(\"button\");u.type=\"button\",u.className=r.name===o?\"on\":\"\",u.textContent=r.name,u.title=r.title??r.name,u.onclick=()=>g(r.name),t.append(u)}t.querySelector(\".on\")?.scrollIntoView?.({block:\"nearest\",inline:\"nearest\"})}for(let r of n.querySelectorAll(\"[data-path]\")){let u=r.getAttribute(\"data-path\");r.classList.toggle(\"sq-zoom\",!!(u&&O(e.views,o,u)))}let s=document.querySelector(\"#sq-step\"),c=e.flows[o]??0;s&&(s.textContent=f&&c?`${d||1} / ${c}`:\"\")}function g(t,s=!1){if(!t||t===o)return;let c=f&&e.flows[t]?s?e.flows[t]:1:0,r=h(t,l,c);if(!r)return;d=c;let{dir:u,anchor:v}=_(e.views,o,t),C=w(n),H=n.firstElementChild;if(p()||!H){n.replaceChildren(r),o=t,x();return}i.replaceChildren(H.cloneNode(!0)),i.style.cssText=`position:absolute;left:${C.x}px;top:${C.y}px;width:${C.w}px;height:${C.h}px;z-index:1;pointer-events:none`,n.replaceChildren(r),o=t,x();let G=w(n),P=v?(u===\"in\"?i:n).querySelector(`[data-path=\"${CSS.escape(v)}\"]`):null,m=U({view:{x:0,y:0,w:a.clientWidth,h:a.clientHeight},ghostBox:C,liveBox:G,anchor:P?w(P):void 0,dir:u}),b=i.style,$=n.style;b.transition=\"none\",b.transformOrigin=m.gOrigin,b.transform=\"none\",b.opacity=\"1\",$.transition=\"none\",$.transformOrigin=m.lOrigin,$.transform=m.lStart,$.opacity=\"0\",n.offsetHeight,b.transition=`transform ${m.ms}ms ${m.ease}, opacity ${Math.round(m.ms*.55)}ms ${m.ease}`,$.transition=`transform ${m.ms}ms ${m.ease}, opacity ${Math.round(m.ms*.6)}ms ${m.ease} ${Math.round(m.ms*.25)}ms`,b.transform=m.gEnd,b.opacity=\"0\",$.transform=\"none\",$.opacity=\"1\";let I=!1,q=0,R=()=>{I||(I=!0,cancelAnimationFrame(q),i.replaceChildren(),i.removeAttribute(\"style\"),n.removeAttribute(\"style\"))},z=()=>{getComputedStyle(n).transform===\"none\"?R():q=requestAnimationFrame(z)};q=requestAnimationFrame(z),setTimeout(R,m.ms+1e3)}function k(t){let s=t===l?null:h(o,t,d);s&&(l=t,document.documentElement.dataset.theme=t,n.replaceChildren(s))}function M(t){let s=e.flows[o]??0,c=d+t;if(s&&c>=1&&c<=s){let v=h(o,l,c);v&&(d=c,n.replaceChildren(v),x());return}let r=e.views.findIndex(v=>v.name===o),u=e.views[r+t];u&&g(u.name,t<0)}n.addEventListener(\"click\",t=>{let c=t.target.closest?.(\"[data-path]\")?.getAttribute(\"data-path\");if(c){let r=O(e.views,o,c);if(r)return g(r.name)}if(!c){let r=S(e.views,o,y(o));r&&g(r)}});let A=document.querySelector(\"#sq-theme\");A&&(A.onclick=()=>k(e.themes[(e.themes.indexOf(l)+1)%e.themes.length]));function B(t){if(t!==f){if(f=t,document.body.classList.toggle(\"presenting\",t),t){if(document.documentElement.requestFullscreen?.().catch(()=>{}),(e.flows[o]??0)&&!d){let c=h(o,l,1);c&&(d=1,n.replaceChildren(c))}}else if(document.fullscreenElement&&document.exitFullscreen?.().catch(()=>{}),d){let s=h(o,l,0);s&&(d=0,n.replaceChildren(s))}x()}}let N,V=()=>{document.body.classList.remove(\"idle\"),clearTimeout(N),f&&(N=setTimeout(()=>document.body.classList.add(\"idle\"),3500))};addEventListener(\"mousemove\",V),addEventListener(\"keydown\",V);let J=()=>k(e.themes[(e.themes.indexOf(l)+1)%e.themes.length]);addEventListener(\"keydown\",t=>{if(!(t.metaKey||t.ctrlKey||t.altKey))switch(t.key){case\"ArrowRight\":case\"PageDown\":case\" \":case\"Enter\":t.preventDefault(),M(1);break;case\"ArrowLeft\":case\"PageUp\":t.preventDefault(),M(-1);break;case\"ArrowUp\":case\"Backspace\":{let s=S(e.views,o,y(o));s&&(t.preventDefault(),g(s));break}case\"Home\":t.preventDefault(),g(e.views[0].name);break;case\"End\":t.preventDefault(),g(e.views[e.views.length-1].name,!0);break;case\"Escape\":f&&(t.preventDefault(),B(!1));break;case\"p\":case\"P\":B(!f);break;case\"f\":case\"F\":document.fullscreenElement?document.exitFullscreen?.().catch(()=>{}):document.documentElement.requestFullscreen?.().catch(()=>{});break;case\"t\":case\"T\":e.themes.length>1&&J();break}}),addEventListener(\"fullscreenchange\",()=>{!document.fullscreenElement&&f&&B(!1)});let D=document.querySelector(\"#sq-present\");D&&(D.onclick=()=>B(!f));let F=()=>{let t=decodeURIComponent(location.hash.slice(1));t&&t!==o&&e.views.some(s=>s.name===t)&&g(t)};if(addEventListener(\"hashchange\",F),document.documentElement.dataset.theme=l,l!==e.themes[0]){let t=h(o,l);t&&n.replaceChildren(t)}x(),F()}document.readyState===\"loading\"?addEventListener(\"DOMContentLoaded\",j):j();})();";