@fundamental-engine/dom 0.7.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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -0
  3. package/dist/apply-recipe.d.ts +103 -0
  4. package/dist/apply-recipe.d.ts.map +1 -0
  5. package/dist/apply-recipe.js +271 -0
  6. package/dist/apply-recipe.js.map +1 -0
  7. package/dist/bind-data.d.ts +72 -0
  8. package/dist/bind-data.d.ts.map +1 -0
  9. package/dist/bind-data.js +164 -0
  10. package/dist/bind-data.js.map +1 -0
  11. package/dist/browser-host.d.ts +11 -0
  12. package/dist/browser-host.d.ts.map +1 -0
  13. package/dist/browser-host.js +41 -0
  14. package/dist/browser-host.js.map +1 -0
  15. package/dist/contours.d.ts +79 -0
  16. package/dist/contours.d.ts.map +1 -0
  17. package/dist/contours.js +88 -0
  18. package/dist/contours.js.map +1 -0
  19. package/dist/env.d.ts +39 -0
  20. package/dist/env.d.ts.map +1 -0
  21. package/dist/env.js +47 -0
  22. package/dist/env.js.map +1 -0
  23. package/dist/export-dom.d.ts +7 -0
  24. package/dist/export-dom.d.ts.map +1 -0
  25. package/dist/export-dom.js +28 -0
  26. package/dist/export-dom.js.map +1 -0
  27. package/dist/feedback.d.ts +57 -0
  28. package/dist/feedback.d.ts.map +1 -0
  29. package/dist/feedback.js +134 -0
  30. package/dist/feedback.js.map +1 -0
  31. package/dist/field-nav.d.ts +35 -0
  32. package/dist/field-nav.d.ts.map +1 -0
  33. package/dist/field-nav.js +82 -0
  34. package/dist/field-nav.js.map +1 -0
  35. package/dist/flip.d.ts +31 -0
  36. package/dist/flip.d.ts.map +1 -0
  37. package/dist/flip.js +65 -0
  38. package/dist/flip.js.map +1 -0
  39. package/dist/governor.d.ts +37 -0
  40. package/dist/governor.d.ts.map +1 -0
  41. package/dist/governor.js +72 -0
  42. package/dist/governor.js.map +1 -0
  43. package/dist/index.d.ts +39 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +42 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/lint.d.ts +78 -0
  48. package/dist/lint.d.ts.map +1 -0
  49. package/dist/lint.js +153 -0
  50. package/dist/lint.js.map +1 -0
  51. package/dist/measurement.d.ts +44 -0
  52. package/dist/measurement.d.ts.map +1 -0
  53. package/dist/measurement.js +95 -0
  54. package/dist/measurement.js.map +1 -0
  55. package/dist/metrics.d.ts +70 -0
  56. package/dist/metrics.d.ts.map +1 -0
  57. package/dist/metrics.js +119 -0
  58. package/dist/metrics.js.map +1 -0
  59. package/dist/overlays.d.ts +48 -0
  60. package/dist/overlays.d.ts.map +1 -0
  61. package/dist/overlays.js +48 -0
  62. package/dist/overlays.js.map +1 -0
  63. package/dist/perf.d.ts +62 -0
  64. package/dist/perf.d.ts.map +1 -0
  65. package/dist/perf.js +94 -0
  66. package/dist/perf.js.map +1 -0
  67. package/dist/platform.d.ts +40 -0
  68. package/dist/platform.d.ts.map +1 -0
  69. package/dist/platform.js +61 -0
  70. package/dist/platform.js.map +1 -0
  71. package/dist/relationships.d.ts +79 -0
  72. package/dist/relationships.d.ts.map +1 -0
  73. package/dist/relationships.js +155 -0
  74. package/dist/relationships.js.map +1 -0
  75. package/dist/schedule.d.ts +84 -0
  76. package/dist/schedule.d.ts.map +1 -0
  77. package/dist/schedule.js +91 -0
  78. package/dist/schedule.js.map +1 -0
  79. package/dist/state.d.ts +36 -0
  80. package/dist/state.d.ts.map +1 -0
  81. package/dist/state.js +113 -0
  82. package/dist/state.js.map +1 -0
  83. package/dist/text-bodies.d.ts +71 -0
  84. package/dist/text-bodies.d.ts.map +1 -0
  85. package/dist/text-bodies.js +159 -0
  86. package/dist/text-bodies.js.map +1 -0
  87. package/dist/thread-overlay.d.ts +63 -0
  88. package/dist/thread-overlay.d.ts.map +1 -0
  89. package/dist/thread-overlay.js +110 -0
  90. package/dist/thread-overlay.js.map +1 -0
  91. package/dist/types.d.ts +51 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +7 -0
  94. package/dist/types.js.map +1 -0
  95. package/dist/visual-bindings.d.ts +95 -0
  96. package/dist/visual-bindings.d.ts.map +1 -0
  97. package/dist/visual-bindings.js +211 -0
  98. package/dist/visual-bindings.js.map +1 -0
  99. package/package.json +59 -0
@@ -0,0 +1,164 @@
1
+ /**
2
+ * bindData — make real application data participate in field behavior. Records become bodies, mapped
3
+ * metrics become state, mapped relationships become graph edges, and a recipe supplies the behavior
4
+ * (metric/feedback framework) via applyRecipe(). Updates are deterministic (diff by id); removed
5
+ * records decay before they leave rather than popping.
6
+ *
7
+ * const binding = bindData(container, records, mapper, { recipe: 'search-relevance-field' });
8
+ * binding.update(nextRecords);
9
+ * binding.destroy();
10
+ *
11
+ * The recipe frames the field (which metrics → --field-* are tracked); the per-record mapper owns the
12
+ * body tokens, metric values, and relationships — so the data drives the field, not a mock.
13
+ */
14
+ import { recipeById } from '@fundamental-engine/core';
15
+ import { applyRecipe } from "./apply-recipe.js";
16
+ /** Diff two id sets (pure). */
17
+ export function diffIds(prev, next) {
18
+ const p = new Set(prev);
19
+ const n = new Set(next);
20
+ const added = [];
21
+ const removed = [];
22
+ const kept = [];
23
+ for (const id of n)
24
+ (p.has(id) ? kept : added).push(id);
25
+ for (const id of p)
26
+ if (!n.has(id))
27
+ removed.push(id);
28
+ return { added, removed, kept };
29
+ }
30
+ const setNum = (el, k, v) => {
31
+ if (v != null && Number.isFinite(v))
32
+ el.setAttribute(k, String(v));
33
+ else
34
+ el.removeAttribute(k);
35
+ };
36
+ /** Apply a mapped record's body tokens, metric values, label/content, and relationship anchors. */
37
+ function applyMapped(el, m, contentHtml) {
38
+ const doc = el.ownerDocument;
39
+ el.setAttribute('data-body', m.body.tokens.join(' '));
40
+ setNum(el, 'data-strength', m.body.strength);
41
+ setNum(el, 'data-range', m.body.range);
42
+ setNum(el, 'data-spin', m.body.spin);
43
+ setNum(el, 'data-angle', m.body.angle);
44
+ if (m.body.feedback)
45
+ el.setAttribute('data-feedback', '');
46
+ // metric values → data-field-<metric> (the recipe + applyRecipe turn these into --field-* state)
47
+ for (const [k, v] of Object.entries(m.metrics ?? {}))
48
+ setNum(el, `data-field-${k}`, v);
49
+ // domain content (overrides label), else the plain label
50
+ if (contentHtml != null) {
51
+ let box = el.querySelector(':scope > .bd-content');
52
+ if (!box) {
53
+ box = doc.createElement('div');
54
+ box.className = 'bd-content';
55
+ el.prepend(box);
56
+ }
57
+ box.innerHTML = contentHtml;
58
+ }
59
+ else if (m.label != null) {
60
+ let lbl = el.querySelector(':scope > .bd-label');
61
+ if (!lbl) {
62
+ lbl = doc.createElement('span');
63
+ lbl.className = 'bd-label';
64
+ el.prepend(lbl);
65
+ }
66
+ lbl.textContent = m.label;
67
+ }
68
+ // relationships → child anchors the RelationshipRegistry discovers (one per edge)
69
+ el.querySelectorAll(':scope > .bd-rel').forEach((a) => a.remove());
70
+ for (const r of m.relationships ?? []) {
71
+ const a = doc.createElement('a');
72
+ a.className = 'bd-rel';
73
+ a.setAttribute('aria-hidden', 'true');
74
+ a.setAttribute('href', `#${r.to}`);
75
+ a.setAttribute('data-field-relation', r.type);
76
+ a.setAttribute('data-field-target', `#${r.to}`);
77
+ if (r.strength != null)
78
+ a.setAttribute('data-field-strength', String(r.strength));
79
+ el.appendChild(a);
80
+ }
81
+ }
82
+ /** Bind records to a field. Returns a handle with update()/destroy(). */
83
+ export function bindData(container, records, mapper, options = {}) {
84
+ const decayMs = options.decayMs ?? 400;
85
+ const tag = options.tag ?? 'div';
86
+ const doc = container.ownerDocument;
87
+ const els = new Map();
88
+ let applied = null;
89
+ const recipeArg = options.recipe;
90
+ const reapply = () => {
91
+ applied?.destroy();
92
+ applied = null;
93
+ const items = [...els.values()].filter((e) => !('bdExiting' in e.dataset));
94
+ const recipe = typeof recipeArg === 'string' ? recipeById(recipeArg) : recipeArg;
95
+ if (recipe && items.length)
96
+ applied = applyRecipe(container, recipe, { bodies: items, annotateBodies: false, reducedMotion: options.reducedMotion });
97
+ };
98
+ const render = (recs) => {
99
+ const mapped = recs.map((rec, i) => mapper(rec, i));
100
+ const nextIds = mapped.map((m) => m.id);
101
+ const { added, removed } = diffIds(els.keys(), nextIds);
102
+ mapped.forEach((m, i) => {
103
+ let el = els.get(m.id);
104
+ if (!el) {
105
+ el = doc.createElement(tag);
106
+ el.dataset.bdId = m.id;
107
+ el.id = m.id; // addressable so relationships/anchors can target a record by id
108
+ if (options.className)
109
+ el.className = options.className;
110
+ el.dataset.bdEntering = '';
111
+ container.appendChild(el);
112
+ els.set(m.id, el);
113
+ const created = el;
114
+ if (typeof requestAnimationFrame !== 'undefined')
115
+ requestAnimationFrame(() => created.removeAttribute('data-bd-entering'));
116
+ else
117
+ el.removeAttribute('data-bd-entering');
118
+ }
119
+ applyMapped(el, m, options.content?.(recs[i], m));
120
+ container.appendChild(el); // keep DOM order aligned with records
121
+ });
122
+ for (const id of removed) {
123
+ const el = els.get(id);
124
+ if (!el)
125
+ continue;
126
+ el.dataset.bdExiting = ''; // CSS fades it; zero its metric vars so feedback eases down
127
+ for (const attr of Array.from(el.attributes))
128
+ if (attr.name.startsWith('data-field-'))
129
+ el.setAttribute(attr.name, '0');
130
+ els.delete(id);
131
+ const finish = () => {
132
+ el.remove();
133
+ reapply();
134
+ };
135
+ if (typeof setTimeout !== 'undefined')
136
+ setTimeout(finish, decayMs);
137
+ else
138
+ finish();
139
+ }
140
+ if (added.length || removed.length)
141
+ reapply();
142
+ };
143
+ render(records);
144
+ return {
145
+ container,
146
+ update: render,
147
+ ids: () => [...els.keys()],
148
+ applied: () => applied,
149
+ inspect: () => {
150
+ if (!applied)
151
+ return null;
152
+ const ins = applied.inspect();
153
+ return { records: els.size, bodies: ins.measurements, relationships: ins.relationships };
154
+ },
155
+ destroy: () => {
156
+ applied?.destroy();
157
+ applied = null;
158
+ for (const el of els.values())
159
+ el.remove();
160
+ els.clear();
161
+ },
162
+ };
163
+ }
164
+ //# sourceMappingURL=bind-data.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind-data.js","sourceRoot":"","sources":["../src/bind-data.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * browserHost — the default {@link FieldHost}, binding the renderer-agnostic core engine to the
3
+ * browser (`window` / `document` / `requestAnimationFrame`). It lives in `@fundamental-engine/dom` (the DOM
4
+ * participation layer), NOT in `Fundamental` — core imports zero DOM. `createField(canvas, opts)`
5
+ * requires a host; pass `browserHost()` in the browser (or `createBrowserField` for the convenience),
6
+ * or a custom host to drive the same engine from a headless renderer / a different document / a test.
7
+ */
8
+ import type { FieldHost } from '@fundamental-engine/core';
9
+ /** Build a FieldHost backed by `window` / `document`. */
10
+ export declare function browserHost(): FieldHost;
11
+ //# sourceMappingURL=browser-host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-host.d.ts","sourceRoot":"","sources":["../src/browser-host.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAK1D,yDAAyD;AACzD,wBAAgB,WAAW,IAAI,SAAS,CAkCvC"}
@@ -0,0 +1,41 @@
1
+ import { prefersReducedMotion, pageHidden } from "./env.js";
2
+ const INPUT_EVENTS = ['pointerdown', 'wheel', 'keydown', 'touchstart'];
3
+ /** Build a FieldHost backed by `window` / `document`. */
4
+ export function browserHost() {
5
+ return {
6
+ root: document,
7
+ viewport: () => ({ width: window.innerWidth, height: window.innerHeight, dpr: window.devicePixelRatio || 1 }),
8
+ scrollY: () => window.scrollY || 0,
9
+ scrollHeight: () => document.documentElement.scrollHeight,
10
+ reducedMotion: () => prefersReducedMotion(),
11
+ hidden: () => pageHidden(),
12
+ raf: (cb) => requestAnimationFrame(cb),
13
+ cancelRaf: (id) => cancelAnimationFrame(id),
14
+ createCanvas: () => document.createElement('canvas'),
15
+ onResize: (cb) => {
16
+ window.addEventListener('resize', cb, { passive: true });
17
+ return () => window.removeEventListener('resize', cb);
18
+ },
19
+ onScroll: (cb) => {
20
+ window.addEventListener('scroll', cb, { passive: true });
21
+ return () => window.removeEventListener('scroll', cb);
22
+ },
23
+ onVisibility: (cb) => {
24
+ document.addEventListener('visibilitychange', cb);
25
+ return () => document.removeEventListener('visibilitychange', cb);
26
+ },
27
+ onInput: (cb) => {
28
+ for (const ev of INPUT_EVENTS)
29
+ window.addEventListener(ev, cb, { passive: true });
30
+ return () => {
31
+ for (const ev of INPUT_EVENTS)
32
+ window.removeEventListener(ev, cb);
33
+ };
34
+ },
35
+ onBodyEvent: (type, cb) => {
36
+ document.addEventListener(type, cb);
37
+ return () => document.removeEventListener(type, cb);
38
+ },
39
+ };
40
+ }
41
+ //# sourceMappingURL=browser-host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-host.js","sourceRoot":"","sources":["../src/browser-host.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE5D,MAAM,YAAY,GAAG,CAAC,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,YAAY,CAAU,CAAC;AAEhF,yDAAyD;AACzD,MAAM,UAAU,WAAW;IACzB,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,GAAG,EAAE,MAAM,CAAC,gBAAgB,IAAI,CAAC,EAAE,CAAC;QAC7G,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC;QAClC,YAAY,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC,YAAY;QACzD,aAAa,EAAE,GAAG,EAAE,CAAC,oBAAoB,EAAE;QAC3C,MAAM,EAAE,GAAG,EAAE,CAAC,UAAU,EAAE;QAC1B,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,qBAAqB,CAAC,EAAE,CAAC;QACtC,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,oBAAoB,CAAC,EAAE,CAAC;QAC3C,YAAY,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC;QACpD,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE;YACf,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACzD,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QACxD,CAAC;QACD,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE;YACf,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACzD,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QACxD,CAAC;QACD,YAAY,EAAE,CAAC,EAAE,EAAE,EAAE;YACnB,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;YAClD,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;QACpE,CAAC;QACD,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE;YACd,KAAK,MAAM,EAAE,IAAI,YAAY;gBAAE,MAAM,CAAC,gBAAgB,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YAClF,OAAO,GAAG,EAAE;gBACV,KAAK,MAAM,EAAE,IAAI,YAAY;oBAAE,MAAM,CAAC,mBAAmB,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;YACpE,CAAC,CAAC;QACJ,CAAC;QACD,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE;YACxB,QAAQ,CAAC,gBAAgB,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACpC,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACtD,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Contour typography (#257, #363) — glyph-outline generation for ANY font the author applies to a
3
+ * body element. The Contour Sink tier of Body Matter Interaction: the element absorbs field matter,
4
+ * the generated vector layer shows what that absorption means, the semantic text remains the source
5
+ * of meaning.
6
+ *
7
+ * Fundamental ships no font parser (the zero-dependency rule): the caller supplies the parsed font —
8
+ * any object structurally matching `ContourFont`, which opentype.js's `Font` satisfies directly:
9
+ *
10
+ * import { load } from 'opentype.js';
11
+ * import { contourSvgFor } from '@fundamental-engine/dom';
12
+ * const font = await load('/fonts/your-font.woff'); // WHATEVER face the element uses
13
+ * const handle = contourSvgFor(document.querySelector('#hero-title'), font, { rings: 3 });
14
+ * // → an aria-hidden SVG bound via data-field-visual-for, inserted after the element;
15
+ * // the platform's state mirroring carries --d / --load onto it from the body.
16
+ *
17
+ * The same primitive runs in node for build-time generation (parse the font file, call
18
+ * `contourPathData`, commit the output) — the site's gen-contours script is that usage.
19
+ *
20
+ * Layout is per-glyph with pair kerning, no shaping: correct for Latin-script display text, the
21
+ * contour use case. Complex scripts (ligatures, contextual forms) need a real shaper and are out
22
+ * of scope here.
23
+ */
24
+ /** The minimal parsed-font surface this module needs — opentype.js `Font` satisfies it. */
25
+ export interface ContourGlyph {
26
+ advanceWidth?: number;
27
+ getPath(x: number, y: number, fontSize: number): {
28
+ toPathData(decimals?: number): string;
29
+ };
30
+ }
31
+ export interface ContourFont {
32
+ unitsPerEm: number;
33
+ ascender: number;
34
+ charToGlyph(char: string): ContourGlyph;
35
+ getKerningValue(left: ContourGlyph, right: ContourGlyph): number;
36
+ }
37
+ export interface ContourPathOptions {
38
+ /** path-data decimal places (default 2). */
39
+ decimals?: number;
40
+ /** padding around the bounding viewBox so wide ring strokes don't clip (px, default 12). */
41
+ pad?: number;
42
+ }
43
+ export interface ContourPath {
44
+ text: string;
45
+ fontSize: number;
46
+ viewBox: string;
47
+ d: string;
48
+ }
49
+ /**
50
+ * Lay `text` out in `font` at `fontSize` and return the combined glyph-outline path data plus a
51
+ * padded viewBox. Pure — runs in the browser or node identically.
52
+ */
53
+ export declare function contourPathData(font: ContourFont, text: string, fontSize: number, opts?: ContourPathOptions): ContourPath;
54
+ export interface ContourSvgOptions extends ContourPathOptions {
55
+ /** text to outline — defaults to the element's textContent, trimmed. */
56
+ text?: string;
57
+ /** layout size in px — defaults to the element's computed font-size, else 96. */
58
+ fontSize?: number;
59
+ /** how many stacked-stroke rings to emit (default 3, classed ring-1…ring-N, innermost first). */
60
+ rings?: number;
61
+ /** insert the SVG into the DOM after the element (default true); false returns it unattached. */
62
+ attach?: boolean;
63
+ /** document override (tests / detached trees); defaults to the element's ownerDocument. */
64
+ doc?: Document;
65
+ }
66
+ export interface ContourSvgHandle {
67
+ svg: SVGSVGElement;
68
+ path: ContourPath;
69
+ /** remove the SVG from the DOM. */
70
+ remove(): void;
71
+ }
72
+ /**
73
+ * Generate the bound vector representation for a body element from ITS OWN font: an `aria-hidden`
74
+ * SVG of stacked-stroke contour rings, carrying `data-field-visual-for` back to the element so the
75
+ * platform's state mirroring (Bound Visual Sink) drives it from the body's live `--d` / `--load`.
76
+ * The element keeps its semantic text; if it has no id one is assigned (the binding needs a ref).
77
+ */
78
+ export declare function contourSvgFor(el: HTMLElement, font: ContourFont, opts?: ContourSvgOptions): ContourSvgHandle;
79
+ //# sourceMappingURL=contours.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contours.d.ts","sourceRoot":"","sources":["../src/contours.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,2FAA2F;AAC3F,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG;QAAE,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CAC5F;AACD,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAAC;IACxC,eAAe,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,GAAG,MAAM,CAAC;CAClE;AAED,MAAM,WAAW,kBAAkB;IACjC,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4FAA4F;IAC5F,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,CAAC,EAAE,MAAM,CAAC;CACX;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE,kBAAuB,GAAG,WAAW,CAqB7H;AAED,MAAM,WAAW,iBAAkB,SAAQ,kBAAkB;IAC3D,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iGAAiG;IACjG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iGAAiG;IACjG,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,2FAA2F;IAC3F,GAAG,CAAC,EAAE,QAAQ,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,aAAa,CAAC;IACnB,IAAI,EAAE,WAAW,CAAC;IAClB,mCAAmC;IACnC,MAAM,IAAI,IAAI,CAAC;CAChB;AAID;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,GAAE,iBAAsB,GAAG,gBAAgB,CA4BhH"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Contour typography (#257, #363) — glyph-outline generation for ANY font the author applies to a
3
+ * body element. The Contour Sink tier of Body Matter Interaction: the element absorbs field matter,
4
+ * the generated vector layer shows what that absorption means, the semantic text remains the source
5
+ * of meaning.
6
+ *
7
+ * Fundamental ships no font parser (the zero-dependency rule): the caller supplies the parsed font —
8
+ * any object structurally matching `ContourFont`, which opentype.js's `Font` satisfies directly:
9
+ *
10
+ * import { load } from 'opentype.js';
11
+ * import { contourSvgFor } from '@fundamental-engine/dom';
12
+ * const font = await load('/fonts/your-font.woff'); // WHATEVER face the element uses
13
+ * const handle = contourSvgFor(document.querySelector('#hero-title'), font, { rings: 3 });
14
+ * // → an aria-hidden SVG bound via data-field-visual-for, inserted after the element;
15
+ * // the platform's state mirroring carries --d / --load onto it from the body.
16
+ *
17
+ * The same primitive runs in node for build-time generation (parse the font file, call
18
+ * `contourPathData`, commit the output) — the site's gen-contours script is that usage.
19
+ *
20
+ * Layout is per-glyph with pair kerning, no shaping: correct for Latin-script display text, the
21
+ * contour use case. Complex scripts (ligatures, contextual forms) need a real shaper and are out
22
+ * of scope here.
23
+ */
24
+ /**
25
+ * Lay `text` out in `font` at `fontSize` and return the combined glyph-outline path data plus a
26
+ * padded viewBox. Pure — runs in the browser or node identically.
27
+ */
28
+ export function contourPathData(font, text, fontSize, opts = {}) {
29
+ const decimals = opts.decimals ?? 2;
30
+ const pad = opts.pad ?? 12;
31
+ const scale = fontSize / font.unitsPerEm;
32
+ const ascent = font.ascender * scale; // baseline at ascender height → bbox starts near y=0
33
+ const parts = [];
34
+ let x = 0;
35
+ let prev = null;
36
+ // track the bounds ourselves so we don't depend on a Path#getBoundingBox implementation —
37
+ // the em box is a safe, font-true envelope (exact ink bounds vary per glyph renderer).
38
+ for (const ch of text) {
39
+ const glyph = font.charToGlyph(ch);
40
+ if (prev)
41
+ x += font.getKerningValue(prev, glyph) * scale;
42
+ parts.push(glyph.getPath(x, ascent, fontSize).toPathData(decimals));
43
+ x += (glyph.advanceWidth ?? font.unitsPerEm / 2) * scale;
44
+ prev = glyph;
45
+ }
46
+ // the em box (height = fontSize, baseline at ascent) is the font-true envelope; exact ink
47
+ // bounds vary per glyph and aren't needed — pad absorbs over/undershoot.
48
+ const viewBox = `${(-pad).toFixed(1)} ${(-pad).toFixed(1)} ${(x + pad * 2).toFixed(1)} ${(fontSize + pad * 2).toFixed(1)}`;
49
+ return { text, fontSize, viewBox, d: parts.join('') };
50
+ }
51
+ const SVG_NS = 'http://www.w3.org/2000/svg';
52
+ /**
53
+ * Generate the bound vector representation for a body element from ITS OWN font: an `aria-hidden`
54
+ * SVG of stacked-stroke contour rings, carrying `data-field-visual-for` back to the element so the
55
+ * platform's state mirroring (Bound Visual Sink) drives it from the body's live `--d` / `--load`.
56
+ * The element keeps its semantic text; if it has no id one is assigned (the binding needs a ref).
57
+ */
58
+ export function contourSvgFor(el, font, opts = {}) {
59
+ const doc = opts.doc ?? el.ownerDocument;
60
+ const text = opts.text ?? (el.textContent ?? '').trim();
61
+ let fontSize = opts.fontSize;
62
+ if (fontSize === undefined) {
63
+ const view = doc?.defaultView;
64
+ const computed = view?.getComputedStyle ? parseFloat(view.getComputedStyle(el).fontSize) : NaN;
65
+ fontSize = Number.isFinite(computed) && computed > 0 ? computed : 96;
66
+ }
67
+ const path = contourPathData(font, text, fontSize, opts);
68
+ if (!el.id)
69
+ el.id = `contour-${Math.abs(text.split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 7))}`;
70
+ const svg = doc.createElementNS(SVG_NS, 'svg');
71
+ svg.setAttribute('viewBox', path.viewBox);
72
+ svg.setAttribute('aria-hidden', 'true');
73
+ svg.setAttribute('focusable', 'false');
74
+ svg.setAttribute('data-field-visual-for', el.id);
75
+ svg.setAttribute('data-field-visual-role', 'representation');
76
+ const rings = Math.max(1, opts.rings ?? 3);
77
+ for (let i = rings; i >= 1; i--) {
78
+ const p = doc.createElementNS(SVG_NS, 'path');
79
+ p.setAttribute('class', `ring ring-${i}`);
80
+ p.setAttribute('d', path.d);
81
+ p.setAttribute('fill', 'none');
82
+ svg.appendChild(p);
83
+ }
84
+ if (opts.attach !== false)
85
+ el.insertAdjacentElement('afterend', svg);
86
+ return { svg, path, remove: () => svg.remove() };
87
+ }
88
+ //# sourceMappingURL=contours.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contours.js","sourceRoot":"","sources":["../src/contours.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AA4BH;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAiB,EAAE,IAAY,EAAE,QAAgB,EAAE,OAA2B,EAAE;IAC9G,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC,CAAC,qDAAqD;IAC3F,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,IAAI,IAAI,GAAwB,IAAI,CAAC;IACrC,0FAA0F;IAC1F,uFAAuF;IACvF,KAAK,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC;QACtB,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACnC,IAAI,IAAI;YAAE,CAAC,IAAI,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC;QACzD,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC;QACpE,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,IAAI,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC;QACzD,IAAI,GAAG,KAAK,CAAC;IACf,CAAC;IACD,0FAA0F;IAC1F,yEAAyE;IACzE,MAAM,OAAO,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3H,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;AACxD,CAAC;AAsBD,MAAM,MAAM,GAAG,4BAA4B,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,EAAe,EAAE,IAAiB,EAAE,OAA0B,EAAE;IAC5F,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,aAAa,CAAC;IACzC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACxD,IAAI,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;IAC7B,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,GAAG,EAAE,WAAW,CAAC;QAC9B,MAAM,QAAQ,GAAG,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;QAC/F,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;IACvE,CAAC;IACD,MAAM,IAAI,GAAG,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;IAEzD,IAAI,CAAC,EAAE,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,GAAG,WAAW,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9G,MAAM,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,MAAM,EAAE,KAAK,CAAkB,CAAC;IAChE,GAAG,CAAC,YAAY,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC1C,GAAG,CAAC,YAAY,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;IACxC,GAAG,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IACvC,GAAG,CAAC,YAAY,CAAC,uBAAuB,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;IACjD,GAAG,CAAC,YAAY,CAAC,wBAAwB,EAAE,gBAAgB,CAAC,CAAC;IAC7D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC;IAC3C,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,MAAM,CAAC,GAAG,GAAG,CAAC,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC9C,CAAC,CAAC,YAAY,CAAC,OAAO,EAAE,aAAa,CAAC,EAAE,CAAC,CAAC;QAC1C,CAAC,CAAC,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QAC5B,CAAC,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC/B,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK;QAAE,EAAE,CAAC,qBAAqB,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;IACrE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC;AACnD,CAAC"}
package/dist/env.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * env — shared, SSR-safe environment probes for the platform layer.
3
+ *
4
+ * Two helpers that multiple platform modules need independently:
5
+ * - `prefersReducedMotion()` — true when the OS/browser signals reduced motion preference.
6
+ * - `pageHidden()` — true when the document visibility state is "hidden".
7
+ *
8
+ * Both are safe to call in a non-DOM environment (SSR, Node test runners, custom hosts): they
9
+ * default to `false` when `window` / `document` are absent, rather than throwing.
10
+ *
11
+ * **Overriding for tests:** call `setEnvOverrides({ reducedMotion, hidden })` before the code
12
+ * under test runs, then `clearEnvOverrides()` (or a second call with `{}`) afterward. The
13
+ * override object is shallow-merged — passing only `reducedMotion` leaves `hidden` live, and
14
+ * vice versa. This is the intended seam; tests no longer need to stub `globalThis.matchMedia` or
15
+ * `document.hidden` directly.
16
+ */
17
+ interface EnvOverrides {
18
+ reducedMotion?: boolean;
19
+ hidden?: boolean;
20
+ }
21
+ /**
22
+ * Override one or both env probes — for test use only. Shallow-merges with any existing
23
+ * overrides so callers can set just the field they need.
24
+ */
25
+ export declare function setEnvOverrides(o: EnvOverrides): void;
26
+ /** Remove all env overrides, restoring the live DOM probe behaviour. */
27
+ export declare function clearEnvOverrides(): void;
28
+ /**
29
+ * Returns `true` when `prefers-reduced-motion: reduce` is active.
30
+ * SSR-safe: returns `false` when `matchMedia` is not available.
31
+ */
32
+ export declare function prefersReducedMotion(): boolean;
33
+ /**
34
+ * Returns `true` when the document is currently hidden (background tab / minimised window).
35
+ * SSR-safe: returns `false` when `document` is not available.
36
+ */
37
+ export declare function pageHidden(): boolean;
38
+ export {};
39
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,UAAU,YAAY;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAID;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,YAAY,GAAG,IAAI,CAErD;AAED,wEAAwE;AACxE,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAG9C;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,OAAO,CAGpC"}
package/dist/env.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * env — shared, SSR-safe environment probes for the platform layer.
3
+ *
4
+ * Two helpers that multiple platform modules need independently:
5
+ * - `prefersReducedMotion()` — true when the OS/browser signals reduced motion preference.
6
+ * - `pageHidden()` — true when the document visibility state is "hidden".
7
+ *
8
+ * Both are safe to call in a non-DOM environment (SSR, Node test runners, custom hosts): they
9
+ * default to `false` when `window` / `document` are absent, rather than throwing.
10
+ *
11
+ * **Overriding for tests:** call `setEnvOverrides({ reducedMotion, hidden })` before the code
12
+ * under test runs, then `clearEnvOverrides()` (or a second call with `{}`) afterward. The
13
+ * override object is shallow-merged — passing only `reducedMotion` leaves `hidden` live, and
14
+ * vice versa. This is the intended seam; tests no longer need to stub `globalThis.matchMedia` or
15
+ * `document.hidden` directly.
16
+ */
17
+ let _overrides = {};
18
+ /**
19
+ * Override one or both env probes — for test use only. Shallow-merges with any existing
20
+ * overrides so callers can set just the field they need.
21
+ */
22
+ export function setEnvOverrides(o) {
23
+ _overrides = { ..._overrides, ...o };
24
+ }
25
+ /** Remove all env overrides, restoring the live DOM probe behaviour. */
26
+ export function clearEnvOverrides() {
27
+ _overrides = {};
28
+ }
29
+ /**
30
+ * Returns `true` when `prefers-reduced-motion: reduce` is active.
31
+ * SSR-safe: returns `false` when `matchMedia` is not available.
32
+ */
33
+ export function prefersReducedMotion() {
34
+ if (_overrides.reducedMotion !== undefined)
35
+ return _overrides.reducedMotion;
36
+ return typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
37
+ }
38
+ /**
39
+ * Returns `true` when the document is currently hidden (background tab / minimised window).
40
+ * SSR-safe: returns `false` when `document` is not available.
41
+ */
42
+ export function pageHidden() {
43
+ if (_overrides.hidden !== undefined)
44
+ return _overrides.hidden;
45
+ return typeof document !== 'undefined' && document.hidden;
46
+ }
47
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAOH,IAAI,UAAU,GAAiB,EAAE,CAAC;AAElC;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,CAAe;IAC7C,UAAU,GAAG,EAAE,GAAG,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC;AACvC,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,iBAAiB;IAC/B,UAAU,GAAG,EAAE,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,IAAI,UAAU,CAAC,aAAa,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC,aAAa,CAAC;IAC5E,OAAO,OAAO,UAAU,KAAK,WAAW,IAAI,UAAU,CAAC,kCAAkC,CAAC,CAAC,OAAO,CAAC;AACrG,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU;IACxB,IAAI,UAAU,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC,MAAM,CAAC;IAC9D,OAAO,OAAO,QAAQ,KAAK,WAAW,IAAI,QAAQ,CAAC,MAAM,CAAC;AAC5D,CAAC"}
@@ -0,0 +1,7 @@
1
+ /** Trigger a browser download of a data/blob URL. */
2
+ export declare function downloadUrl(url: string, filename: string): void;
3
+ /** Download arbitrary text (e.g. an SVG document from `segmentsToSvg`) as a file. */
4
+ export declare function downloadText(text: string, filename: string, mime?: string): void;
5
+ /** Download a canvas as a PNG file (`canvasToPng` + a download). */
6
+ export declare function downloadCanvasPng(canvas: HTMLCanvasElement, filename?: string): void;
7
+ //# sourceMappingURL=export-dom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export-dom.d.ts","sourceRoot":"","sources":["../src/export-dom.ts"],"names":[],"mappings":"AAQA,qDAAqD;AACrD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAQ/D;AAED,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,SAAkB,GAAG,IAAI,CAIzF;AAED,oEAAoE;AACpE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,EAAE,QAAQ,SAAc,GAAG,IAAI,CAEzF"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * DOM download helpers (Phase: frontier). Triggering a file download needs `document` (an anchor
3
+ * click), so these live in `@fundamental-engine/dom`, not `Fundamental`. The pure serializers stay in
4
+ * core: `segmentsToSvg` (vector) and `canvasToPng` (a canvas's own `toDataURL`). Pair them here to
5
+ * actually save a file.
6
+ */
7
+ import { canvasToPng } from '@fundamental-engine/core';
8
+ /** Trigger a browser download of a data/blob URL. */
9
+ export function downloadUrl(url, filename) {
10
+ const a = document.createElement('a');
11
+ a.href = url;
12
+ a.download = filename;
13
+ a.rel = 'noopener';
14
+ document.body.appendChild(a);
15
+ a.click();
16
+ a.remove();
17
+ }
18
+ /** Download arbitrary text (e.g. an SVG document from `segmentsToSvg`) as a file. */
19
+ export function downloadText(text, filename, mime = 'image/svg+xml') {
20
+ const url = URL.createObjectURL(new Blob([text], { type: mime }));
21
+ downloadUrl(url, filename);
22
+ URL.revokeObjectURL(url);
23
+ }
24
+ /** Download a canvas as a PNG file (`canvasToPng` + a download). */
25
+ export function downloadCanvasPng(canvas, filename = 'field.png') {
26
+ downloadUrl(canvasToPng(canvas), filename);
27
+ }
28
+ //# sourceMappingURL=export-dom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export-dom.js","sourceRoot":"","sources":["../src/export-dom.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEvD,qDAAqD;AACrD,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,QAAgB;IACvD,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;IACtC,CAAC,CAAC,IAAI,GAAG,GAAG,CAAC;IACb,CAAC,CAAC,QAAQ,GAAG,QAAQ,CAAC;IACtB,CAAC,CAAC,GAAG,GAAG,UAAU,CAAC;IACnB,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC,CAAC,KAAK,EAAE,CAAC;IACV,CAAC,CAAC,MAAM,EAAE,CAAC;AACb,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,QAAgB,EAAE,IAAI,GAAG,eAAe;IACjF,MAAM,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAClE,WAAW,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IAC3B,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,iBAAiB,CAAC,MAAyB,EAAE,QAAQ,GAAG,WAAW;IACjF,WAAW,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,57 @@
1
+ import type { StateRegistry } from './state.ts';
2
+ /** Map of state-key → CSS-var name written for an element. */
3
+ type VarBinding = Record<string, string>;
4
+ export interface ThresholdOptions {
5
+ /** the state key whose value crosses the threshold. */
6
+ metric: string;
7
+ enter: number;
8
+ exit: number;
9
+ debounce?: number;
10
+ /** event dispatched on the exit edge (e.g. `field:dim` to pair with `field:lit`). */
11
+ exitEvent?: string;
12
+ }
13
+ export declare class FeedbackRegistry {
14
+ private readonly bindings;
15
+ private readonly direct;
16
+ private readonly thresholds;
17
+ private _cssWritesLastFrame;
18
+ /** Declare which state keys map to which CSS vars on an element (e.g. `{ density: '--field-density' }`). */
19
+ bind(element: Element, map: VarBinding): void;
20
+ /**
21
+ * Remove the CSS var bound to `key` on `element` from the DOM. Use
22
+ * when a metric becomes absent — e.g. the host stops supplying `data-field-confidence` — so a value
23
+ * written on an earlier `flush()` doesn't linger. A no-op when nothing is bound for `key`; the
24
+ * binding itself is left intact, so the var is rewritten if the metric returns. `flush()` already
25
+ * skips keys with no state, so this only clears the previously written inline value.
26
+ */
27
+ clearVar(element: Element, key: string): void;
28
+ /** The declared bindings (element → the CSS-var names it writes), for lint / inspection. */
29
+ boundVars(): Array<{
30
+ element: Element;
31
+ vars: string[];
32
+ }>;
33
+ /**
34
+ * Actual `style.setProperty` calls made during the last `flush()`. Use this (not
35
+ * `boundVars().length`) to measure real per-frame DOM write cost: off-screen elements with
36
+ * active bindings still generate mutations even though they produce no visible change.
37
+ */
38
+ cssWritesLastFrame(): number;
39
+ /** Queue a direct CSS-var write (applied on the next `flush`). */
40
+ set(element: Element, vars: Record<string, number | string>): void;
41
+ /** Register a thresholded, debounced event for an element metric (hysteresis via enter/exit). */
42
+ threshold(element: Element, eventName: string, opts: ThresholdOptions): void;
43
+ /**
44
+ * Drop ALL bindings and thresholds registered for one element. Use when an element is removed from
45
+ * the DOM and you want immediate reclamation rather than waiting for the next flush() sweep.
46
+ */
47
+ unregister(element: Element): void;
48
+ /**
49
+ * Write-phase: apply bound state → CSS vars, apply queued direct writes, and run thresholders →
50
+ * fire edge events. `state` supplies the numeric values for bound vars + thresholds. Disconnected
51
+ * elements are pruned here — the natural per-frame moment — so bindings and thresholds for removed
52
+ * elements never accumulate across the lifetime of the registry.
53
+ */
54
+ flush(state: StateRegistry, now?: number): void;
55
+ }
56
+ export {};
57
+ //# sourceMappingURL=feedback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feedback.d.ts","sourceRoot":"","sources":["../src/feedback.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,8DAA8D;AAC9D,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AA6BzC,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAC3D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8C;IACrE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAwB;IACnD,OAAO,CAAC,mBAAmB,CAAK;IAEhC,4GAA4G;IAC5G,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI;IAI7C;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAK7C,4FAA4F;IAC5F,SAAS,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAIxD;;;;OAIG;IACH,kBAAkB,IAAI,MAAM;IAI5B,kEAAkE;IAClE,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,IAAI;IAMlE,iGAAiG;IACjG,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,IAAI;IAU5E;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IASlC;;;;;OAKG;IACH,KAAK,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,SAAI,GAAG,IAAI;CA0B3C"}