@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,134 @@
1
+ /**
2
+ * FeedbackRegistry — the write-phase. Turns held state into DOM: CSS custom properties (continuous)
3
+ * and thresholded, debounced events (discrete). The native primitive Fundamental wishes existed —
4
+ * standard feedback channels with hysteresis instead of per-frame DOM events.
5
+ *
6
+ * Writes happen only in `flush()`, after measurement + simulation reads.
7
+ */
8
+ import { Thresholder } from '@fundamental-engine/core';
9
+ /** Write a CSS custom property. Returns the count of actual DOM mutations. */
10
+ function writeVar(element, name, value) {
11
+ const style = element.style;
12
+ if (!style || typeof style.setProperty !== 'function')
13
+ return 0;
14
+ style.setProperty(name, value);
15
+ return 1;
16
+ }
17
+ function removeVar(element, name) {
18
+ const style = element.style;
19
+ if (!style || typeof style.removeProperty !== 'function')
20
+ return;
21
+ style.removeProperty(name);
22
+ }
23
+ function fire(element, type, detail) {
24
+ if (typeof element.dispatchEvent !== 'function')
25
+ return;
26
+ element.dispatchEvent(new CustomEvent(type, { bubbles: true, composed: true, detail }));
27
+ }
28
+ export class FeedbackRegistry {
29
+ bindings = new Map();
30
+ direct = new Map();
31
+ thresholds = [];
32
+ _cssWritesLastFrame = 0;
33
+ /** Declare which state keys map to which CSS vars on an element (e.g. `{ density: '--field-density' }`). */
34
+ bind(element, map) {
35
+ this.bindings.set(element, { ...this.bindings.get(element), ...map });
36
+ }
37
+ /**
38
+ * Remove the CSS var bound to `key` on `element` from the DOM. Use
39
+ * when a metric becomes absent — e.g. the host stops supplying `data-field-confidence` — so a value
40
+ * written on an earlier `flush()` doesn't linger. A no-op when nothing is bound for `key`; the
41
+ * binding itself is left intact, so the var is rewritten if the metric returns. `flush()` already
42
+ * skips keys with no state, so this only clears the previously written inline value.
43
+ */
44
+ clearVar(element, key) {
45
+ const name = this.bindings.get(element)?.[key];
46
+ if (name)
47
+ removeVar(element, name);
48
+ }
49
+ /** The declared bindings (element → the CSS-var names it writes), for lint / inspection. */
50
+ boundVars() {
51
+ return [...this.bindings].map(([element, map]) => ({ element, vars: Object.values(map) }));
52
+ }
53
+ /**
54
+ * Actual `style.setProperty` calls made during the last `flush()`. Use this (not
55
+ * `boundVars().length`) to measure real per-frame DOM write cost: off-screen elements with
56
+ * active bindings still generate mutations even though they produce no visible change.
57
+ */
58
+ cssWritesLastFrame() {
59
+ return this._cssWritesLastFrame;
60
+ }
61
+ /** Queue a direct CSS-var write (applied on the next `flush`). */
62
+ set(element, vars) {
63
+ const cur = this.direct.get(element) ?? {};
64
+ for (const k of Object.keys(vars))
65
+ cur[k] = String(vars[k]);
66
+ this.direct.set(element, cur);
67
+ }
68
+ /** Register a thresholded, debounced event for an element metric (hysteresis via enter/exit). */
69
+ threshold(element, eventName, opts) {
70
+ this.thresholds.push({
71
+ element,
72
+ eventName,
73
+ metric: opts.metric,
74
+ exitEvent: opts.exitEvent,
75
+ thresholder: new Thresholder({ enter: opts.enter, exit: opts.exit, debounceMs: opts.debounce ?? 0 }),
76
+ });
77
+ }
78
+ /**
79
+ * Drop ALL bindings and thresholds registered for one element. Use when an element is removed from
80
+ * the DOM and you want immediate reclamation rather than waiting for the next flush() sweep.
81
+ */
82
+ unregister(element) {
83
+ this.bindings.delete(element);
84
+ this.direct.delete(element);
85
+ // splice in reverse so the index arithmetic stays correct as we remove entries
86
+ for (let i = this.thresholds.length - 1; i >= 0; i--) {
87
+ if (this.thresholds[i].element === element)
88
+ this.thresholds.splice(i, 1);
89
+ }
90
+ }
91
+ /**
92
+ * Write-phase: apply bound state → CSS vars, apply queued direct writes, and run thresholders →
93
+ * fire edge events. `state` supplies the numeric values for bound vars + thresholds. Disconnected
94
+ * elements are pruned here — the natural per-frame moment — so bindings and thresholds for removed
95
+ * elements never accumulate across the lifetime of the registry.
96
+ */
97
+ flush(state, now = 0) {
98
+ this._cssWritesLastFrame = 0;
99
+ for (const [el, map] of this.bindings) {
100
+ // prune entries whose element left the DOM; writing to a disconnected element is a no-op
101
+ // layout-wise but still costs a Map lookup + style.setProperty call every frame.
102
+ if (!el.isConnected) {
103
+ this.bindings.delete(el);
104
+ continue;
105
+ }
106
+ for (const key of Object.keys(map)) {
107
+ const v = state.get(el, key);
108
+ if (!v)
109
+ continue;
110
+ this._cssWritesLastFrame += writeVar(el, map[key], v.type === 'number' ? v.value.toFixed(3) : String(v.value ?? ''));
111
+ }
112
+ }
113
+ for (const [el, vars] of this.direct) {
114
+ for (const name of Object.keys(vars))
115
+ this._cssWritesLastFrame += writeVar(el, name, vars[name]);
116
+ }
117
+ this.direct.clear();
118
+ // prune disconnected threshold entries in-place (reverse splice keeps indices stable)
119
+ for (let i = this.thresholds.length - 1; i >= 0; i--) {
120
+ if (!this.thresholds[i].element.isConnected) {
121
+ this.thresholds.splice(i, 1);
122
+ continue;
123
+ }
124
+ const t = this.thresholds[i];
125
+ const value = state.number(t.element, t.metric);
126
+ const edge = t.thresholder.update(value, now);
127
+ if (edge === 'entered')
128
+ fire(t.element, t.eventName, { metric: t.metric, value });
129
+ else if (edge === 'exited' && t.exitEvent)
130
+ fire(t.element, t.exitEvent, { metric: t.metric, value });
131
+ }
132
+ }
133
+ }
134
+ //# sourceMappingURL=feedback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feedback.js","sourceRoot":"","sources":["../src/feedback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `bindFieldNav` — the navigation-chrome idiom the Fundamental site hand-spread across a dozen surfaces
3
+ * (top nav, chapter rail, docs sidebar/outline/search, breadcrumbs, pagers, footer, filter rosters),
4
+ * lifted into the platform. It runs a recipe SIGNALS-ONLY (`render: []`) over the `<a>` links inside a
5
+ * root: every link becomes a body, the current link can be pinned as the "well" (`data-field-attention
6
+ * = 1`), and previously-visited links can be marked (`data-field-memory = 1` + a `nav-visited` class)
7
+ * from a caller-supplied predicate. The platform writes the recipe's `--field-*` lanes back onto each
8
+ * link; CSS turns them into ink, weight, glows, and marks. Nothing is drawn.
9
+ *
10
+ * Progressive enhancement is the contract: under `prefers-reduced-motion` (or when the recipe can't be
11
+ * resolved / there are no links) it returns `null` and writes nothing — the links stay plain and
12
+ * reachable. The caller owns the visit log and the recipe lookup; this helper owns the binding and its
13
+ * teardown. Unfrozen (experimental) — option names may refine before 1.0.
14
+ */
15
+ import { type FieldRecipe } from '@fundamental-engine/core';
16
+ export interface FieldNavOptions {
17
+ /** the link to pin as the current/"well" — its attention lane is held at 1. */
18
+ pin?: Element | null;
19
+ /** predicate over a link's `href`: true → mark it visited (`data-field-memory=1` + `nav-visited`). */
20
+ visited?: (href: string) => boolean;
21
+ /** extra metric lanes to bind beyond the recipe's (default `['attention','memory']`). */
22
+ extraMetrics?: string[];
23
+ /** force the reduced-motion path (defaults to the OS `prefers-reduced-motion` setting). */
24
+ reducedMotion?: boolean;
25
+ }
26
+ export interface FieldNavHandle {
27
+ destroy(): void;
28
+ }
29
+ /**
30
+ * Bind a recipe signals-only over the `<a href>` links inside `root`. `recipe` may be a `FieldRecipe`
31
+ * or a catalog id. Returns a teardown, or `null` when there's nothing to bind (reduced motion, no
32
+ * links, or an unknown recipe id).
33
+ */
34
+ export declare function bindFieldNav(root: Element, recipe: FieldRecipe | string, opts?: FieldNavOptions): FieldNavHandle | null;
35
+ //# sourceMappingURL=field-nav.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-nav.d.ts","sourceRoot":"","sources":["../src/field-nav.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAIxE,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,GAAG,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACrB,sGAAsG;IACtG,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACpC,yFAAyF;IACzF,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,2FAA2F;IAC3F,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,IAAI,IAAI,CAAC;CACjB;AAWD;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,WAAW,GAAG,MAAM,EAC5B,IAAI,GAAE,eAAoB,GACzB,cAAc,GAAG,IAAI,CAgDvB"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * `bindFieldNav` — the navigation-chrome idiom the Fundamental site hand-spread across a dozen surfaces
3
+ * (top nav, chapter rail, docs sidebar/outline/search, breadcrumbs, pagers, footer, filter rosters),
4
+ * lifted into the platform. It runs a recipe SIGNALS-ONLY (`render: []`) over the `<a>` links inside a
5
+ * root: every link becomes a body, the current link can be pinned as the "well" (`data-field-attention
6
+ * = 1`), and previously-visited links can be marked (`data-field-memory = 1` + a `nav-visited` class)
7
+ * from a caller-supplied predicate. The platform writes the recipe's `--field-*` lanes back onto each
8
+ * link; CSS turns them into ink, weight, glows, and marks. Nothing is drawn.
9
+ *
10
+ * Progressive enhancement is the contract: under `prefers-reduced-motion` (or when the recipe can't be
11
+ * resolved / there are no links) it returns `null` and writes nothing — the links stay plain and
12
+ * reachable. The caller owns the visit log and the recipe lookup; this helper owns the binding and its
13
+ * teardown. Unfrozen (experimental) — option names may refine before 1.0.
14
+ */
15
+ import { recipeById } from '@fundamental-engine/core';
16
+ import { applyRecipe } from "./apply-recipe.js";
17
+ import { prefersReducedMotion } from "./env.js";
18
+ /** The `--field-*` lanes a nav binding may write — cleared on teardown so a re-bind starts clean. */
19
+ const NAV_METRIC_VARS = [
20
+ '--field-attention',
21
+ '--field-memory',
22
+ '--field-priority',
23
+ '--field-density',
24
+ '--field-recency',
25
+ ];
26
+ /**
27
+ * Bind a recipe signals-only over the `<a href>` links inside `root`. `recipe` may be a `FieldRecipe`
28
+ * or a catalog id. Returns a teardown, or `null` when there's nothing to bind (reduced motion, no
29
+ * links, or an unknown recipe id).
30
+ */
31
+ export function bindFieldNav(root, recipe, opts = {}) {
32
+ if (opts.reducedMotion ?? prefersReducedMotion())
33
+ return null;
34
+ const resolved = typeof recipe === 'string' ? recipeById(recipe) : recipe;
35
+ if (!resolved)
36
+ return null;
37
+ const links = [...root.querySelectorAll('a[href]')];
38
+ if (!links.length)
39
+ return null;
40
+ const pinned = opts.pin ?? null;
41
+ pinned?.setAttribute('data-field-attention', '1');
42
+ const marked = [];
43
+ if (opts.visited) {
44
+ for (const a of links) {
45
+ if (opts.visited(a.getAttribute('href') ?? '')) {
46
+ a.setAttribute('data-field-memory', '1');
47
+ a.classList.add('nav-visited');
48
+ marked.push(a);
49
+ }
50
+ }
51
+ }
52
+ const undoAnnotations = () => {
53
+ pinned?.removeAttribute('data-field-attention');
54
+ for (const a of marked) {
55
+ a.removeAttribute('data-field-memory');
56
+ a.classList.remove('nav-visited');
57
+ }
58
+ };
59
+ let applied;
60
+ try {
61
+ applied = applyRecipe(root, resolved, {
62
+ bodies: links,
63
+ annotateBodies: false,
64
+ renderless: true,
65
+ extraMetrics: opts.extraMetrics ?? ['attention', 'memory'],
66
+ });
67
+ }
68
+ catch {
69
+ undoAnnotations();
70
+ return null; // the plain links stand on their own
71
+ }
72
+ return {
73
+ destroy() {
74
+ applied.destroy();
75
+ undoAnnotations();
76
+ for (const a of links)
77
+ for (const v of NAV_METRIC_VARS)
78
+ a.style.removeProperty(v);
79
+ },
80
+ };
81
+ }
82
+ //# sourceMappingURL=field-nav.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-nav.js","sourceRoot":"","sources":["../src/field-nav.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,UAAU,EAAoB,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAiBhD,qGAAqG;AACrG,MAAM,eAAe,GAAG;IACtB,mBAAmB;IACnB,gBAAgB;IAChB,kBAAkB;IAClB,iBAAiB;IACjB,iBAAiB;CAClB,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAC1B,IAAa,EACb,MAA4B,EAC5B,OAAwB,EAAE;IAE1B,IAAI,IAAI,CAAC,aAAa,IAAI,oBAAoB,EAAE;QAAE,OAAO,IAAI,CAAC;IAC9D,MAAM,QAAQ,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAC1E,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,gBAAgB,CAAoB,SAAS,CAAC,CAAC,CAAC;IACvE,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAE/B,MAAM,MAAM,GAAI,IAAI,CAAC,GAA0B,IAAI,IAAI,CAAC;IACxD,MAAM,EAAE,YAAY,CAAC,sBAAsB,EAAE,GAAG,CAAC,CAAC;IAClD,MAAM,MAAM,GAAkB,EAAE,CAAC;IACjC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;gBAC/C,CAAC,CAAC,YAAY,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAC;gBACzC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;gBAC/B,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACjB,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,eAAe,GAAG,GAAS,EAAE;QACjC,MAAM,EAAE,eAAe,CAAC,sBAAsB,CAAC,CAAC;QAChD,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;YACvB,CAAC,CAAC,eAAe,CAAC,mBAAmB,CAAC,CAAC;YACvC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QACpC,CAAC;IACH,CAAC,CAAC;IAEF,IAAI,OAA4B,CAAC;IACjC,IAAI,CAAC;QACH,OAAO,GAAG,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE;YACpC,MAAM,EAAE,KAAK;YACb,cAAc,EAAE,KAAK;YACrB,UAAU,EAAE,IAAI;YAChB,YAAY,EAAE,IAAI,CAAC,YAAY,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC;SAC3D,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,eAAe,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC,CAAC,qCAAqC;IACpD,CAAC;IAED,OAAO;QACL,OAAO;YACL,OAAO,CAAC,OAAO,EAAE,CAAC;YAClB,eAAe,EAAE,CAAC;YAClB,KAAK,MAAM,CAAC,IAAI,KAAK;gBAAE,KAAK,MAAM,CAAC,IAAI,eAAe;oBAAE,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;QACpF,CAAC;KACF,CAAC;AACJ,CAAC"}
package/dist/flip.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ /** Options for {@link withFlip}. */
2
+ export interface FlipOptions {
3
+ /** Transition duration in ms. Default 500. */
4
+ duration?: number;
5
+ /** Transition easing. Default the family's `cubic-bezier(.2, .7, .2, 1)`. */
6
+ easing?: string;
7
+ /**
8
+ * Which axes the released offset travels on. `"both"` (default) animates `translate(dx, dy)`;
9
+ * `"y"` animates `translateY(dy)` only — for single-column lists where any horizontal delta
10
+ * is noise.
11
+ */
12
+ axis?: 'y' | 'both';
13
+ /**
14
+ * Elements the mutation moved but that should NOT be translated — e.g. a tile whose size class
15
+ * changed, where a translate cannot honestly animate the reflow and the caller settles it
16
+ * another way (a fade). Evaluated after `mutate`, once per element.
17
+ */
18
+ exclude?: (el: HTMLElement) => boolean;
19
+ }
20
+ /**
21
+ * Measure `elements()`, run `mutate` (which may reorder, move, or reparent them), then translate
22
+ * each moved element from its old box to its new one and release the offset under a transition.
23
+ * The inline transition is removed on `transitionend`.
24
+ *
25
+ * `elements` is called twice — once before `mutate` (First) and once after (Last) — so a callback
26
+ * that re-queries the DOM naturally covers reparenting. Elements that appear only after `mutate`
27
+ * (no first rect) are left alone. Skips the animation entirely under
28
+ * `prefers-reduced-motion: reduce` (`mutate` still runs).
29
+ */
30
+ export declare function withFlip(elements: () => HTMLElement[], mutate: () => void, opts?: FlipOptions): void;
31
+ //# sourceMappingURL=flip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flip.d.ts","sourceRoot":"","sources":["../src/flip.ts"],"names":[],"mappings":"AAeA,oCAAoC;AACpC,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,IAAI,CAAC,EAAE,GAAG,GAAG,MAAM,CAAC;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC;CACxC;AAKD;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,WAAW,EAAE,EAAE,MAAM,EAAE,MAAM,IAAI,EAAE,IAAI,GAAE,WAAgB,GAAG,IAAI,CAuCxG"}
package/dist/flip.js ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * withFlip — the FLIP reflow primitive (First, Last, Invert, Play).
3
+ *
4
+ * Every invisible-fields example re-sorts live DOM (reweight, triage, cross-pane moves) and wants
5
+ * the reflow to read as travel, not teleportation. The recipe is always the same: measure where
6
+ * each element sits, mutate the DOM, measure again, then translate each element from its old box
7
+ * back over its new one and release the offset under a transition. This module is that recipe,
8
+ * extracted (#295) so a runtime states only WHAT changes, not how the motion works.
9
+ *
10
+ * Pure DOM helper: no registries, no engine, no module-top window access (SSR-safe — the
11
+ * reduced-motion probe runs at call time). Under `prefers-reduced-motion: reduce` the mutation
12
+ * still runs; only the animation is skipped.
13
+ */
14
+ import { prefersReducedMotion } from "./env.js";
15
+ const DEFAULT_DURATION = 500;
16
+ const DEFAULT_EASING = 'cubic-bezier(.2, .7, .2, 1)';
17
+ /**
18
+ * Measure `elements()`, run `mutate` (which may reorder, move, or reparent them), then translate
19
+ * each moved element from its old box to its new one and release the offset under a transition.
20
+ * The inline transition is removed on `transitionend`.
21
+ *
22
+ * `elements` is called twice — once before `mutate` (First) and once after (Last) — so a callback
23
+ * that re-queries the DOM naturally covers reparenting. Elements that appear only after `mutate`
24
+ * (no first rect) are left alone. Skips the animation entirely under
25
+ * `prefers-reduced-motion: reduce` (`mutate` still runs).
26
+ */
27
+ export function withFlip(elements, mutate, opts = {}) {
28
+ if (prefersReducedMotion()) {
29
+ mutate();
30
+ return;
31
+ }
32
+ const duration = opts.duration ?? DEFAULT_DURATION;
33
+ const easing = opts.easing ?? DEFAULT_EASING;
34
+ const axis = opts.axis ?? 'both';
35
+ // First: where everything sits now.
36
+ const first = new Map(elements().map((el) => {
37
+ const b = el.getBoundingClientRect();
38
+ return [el, { top: b.top, left: b.left }];
39
+ }));
40
+ mutate();
41
+ // Last + Invert + Play, per element.
42
+ for (const el of elements()) {
43
+ if (opts.exclude?.(el))
44
+ continue;
45
+ const was = first.get(el);
46
+ if (!was)
47
+ continue; // appeared during mutate — no origin to travel from
48
+ const now = el.getBoundingClientRect();
49
+ const dx = axis === 'y' ? 0 : was.left - now.left;
50
+ const dy = was.top - now.top;
51
+ if (!dx && !dy)
52
+ continue;
53
+ el.style.setProperty('transform', axis === 'y' ? `translateY(${dy}px)` : `translate(${dx}px, ${dy}px)`);
54
+ el.style.setProperty('transition', 'none');
55
+ void el.getBoundingClientRect(); // commit the inverted offset before releasing it
56
+ requestAnimationFrame(() => {
57
+ el.style.setProperty('transition', `transform ${duration}ms ${easing}`);
58
+ el.style.removeProperty('transform');
59
+ el.addEventListener('transitionend', () => el.style.removeProperty('transition'), {
60
+ once: true,
61
+ });
62
+ });
63
+ }
64
+ }
65
+ //# sourceMappingURL=flip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flip.js","sourceRoot":"","sources":["../src/flip.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAsBhD,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAC7B,MAAM,cAAc,GAAG,6BAA6B,CAAC;AAErD;;;;;;;;;GASG;AACH,MAAM,UAAU,QAAQ,CAAC,QAA6B,EAAE,MAAkB,EAAE,OAAoB,EAAE;IAChG,IAAI,oBAAoB,EAAE,EAAE,CAAC;QAC3B,MAAM,EAAE,CAAC;QACT,OAAO;IACT,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,gBAAgB,CAAC;IACnD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,cAAc,CAAC;IAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,MAAM,CAAC;IACjC,oCAAoC;IACpC,MAAM,KAAK,GAAG,IAAI,GAAG,CACnB,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QACpB,MAAM,CAAC,GAAG,EAAE,CAAC,qBAAqB,EAAE,CAAC;QACrC,OAAO,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAU,CAAC;IACrD,CAAC,CAAC,CACH,CAAC;IACF,MAAM,EAAE,CAAC;IACT,qCAAqC;IACrC,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,EAAE,CAAC;QAC5B,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;YAAE,SAAS;QACjC,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1B,IAAI,CAAC,GAAG;YAAE,SAAS,CAAC,oDAAoD;QACxE,MAAM,GAAG,GAAG,EAAE,CAAC,qBAAqB,EAAE,CAAC;QACvC,MAAM,EAAE,GAAG,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;QAClD,MAAM,EAAE,GAAG,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;QAC7B,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE;YAAE,SAAS;QACzB,EAAE,CAAC,KAAK,CAAC,WAAW,CAClB,WAAW,EACX,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC,CAAC,aAAa,EAAE,OAAO,EAAE,KAAK,CACrE,CAAC;QACF,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;QAC3C,KAAK,EAAE,CAAC,qBAAqB,EAAE,CAAC,CAAC,iDAAiD;QAClF,qBAAqB,CAAC,GAAG,EAAE;YACzB,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,YAAY,EAAE,aAAa,QAAQ,MAAM,MAAM,EAAE,CAAC,CAAC;YACxE,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;YACrC,EAAE,CAAC,gBAAgB,CAAC,eAAe,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,YAAY,CAAC,EAAE;gBAChF,IAAI,EAAE,IAAI;aACX,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * QualityGovernor — detects sustained frame-budget overruns and emits a tier signal so the
3
+ * caller can adapt render quality without a hard cutoff.
4
+ *
5
+ * Tier semantics (intentionally coarse — the governor detects, the caller responds):
6
+ * 0 = full quality (default)
7
+ * 1 = effects reduced — caller should simplify overlay, drop heatmap, reduce particle draw
8
+ * 2 = minimal — caller should switch render to 'dots', cut particle cap in half
9
+ * 3 = paused — caller should suspend the field loop entirely
10
+ *
11
+ * Shipped consumer: the `<field-root>` platform runtime throttles its own tick cadence (the
12
+ * measurement/feedback DOM work) to every 2nd frame at tier 2 and every 4th at tier 3, and
13
+ * emits `field:quality-tier` so embedders can wire engine-side responses (render simplification,
14
+ * particle caps) — those are NOT automatic yet.
15
+ *
16
+ * Feed it rAF-to-rAF spacing (or better, measured work time). Callers should skip discontinuity
17
+ * frames (tab switches, system sleep) and reset() on visibilitychange — the elements runtime does.
18
+ *
19
+ * Recovery is asymmetric: a tier escalation fires after N consecutive overrun frames;
20
+ * recovery requires a longer run of clean frames to avoid thrashing at the boundary.
21
+ */
22
+ export type QualityTier = 0 | 1 | 2 | 3;
23
+ export declare class QualityGovernor {
24
+ private _tier;
25
+ private overrunStreak;
26
+ private cleanStreak;
27
+ private readonly budget;
28
+ constructor(budgetMs?: number);
29
+ get tier(): QualityTier;
30
+ /**
31
+ * Feed one frame duration (ms). Returns the new tier when it changes, `undefined` when stable.
32
+ * Call once per rAF tick, after `platform.tick()` completes.
33
+ */
34
+ feed(durationMs: number): QualityTier | undefined;
35
+ reset(): void;
36
+ }
37
+ //# sourceMappingURL=governor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"governor.d.ts","sourceRoot":"","sources":["../src/governor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAgBxC,qBAAa,eAAe;IAC1B,OAAO,CAAC,KAAK,CAAkB;IAC/B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,QAAQ,SAAQ;IAI5B,IAAI,IAAI,IAAI,WAAW,CAAuB;IAE9C;;;OAGG;IACH,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IA0BjD,KAAK,IAAI,IAAI;CAKd"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * QualityGovernor — detects sustained frame-budget overruns and emits a tier signal so the
3
+ * caller can adapt render quality without a hard cutoff.
4
+ *
5
+ * Tier semantics (intentionally coarse — the governor detects, the caller responds):
6
+ * 0 = full quality (default)
7
+ * 1 = effects reduced — caller should simplify overlay, drop heatmap, reduce particle draw
8
+ * 2 = minimal — caller should switch render to 'dots', cut particle cap in half
9
+ * 3 = paused — caller should suspend the field loop entirely
10
+ *
11
+ * Shipped consumer: the `<field-root>` platform runtime throttles its own tick cadence (the
12
+ * measurement/feedback DOM work) to every 2nd frame at tier 2 and every 4th at tier 3, and
13
+ * emits `field:quality-tier` so embedders can wire engine-side responses (render simplification,
14
+ * particle caps) — those are NOT automatic yet.
15
+ *
16
+ * Feed it rAF-to-rAF spacing (or better, measured work time). Callers should skip discontinuity
17
+ * frames (tab switches, system sleep) and reset() on visibilitychange — the elements runtime does.
18
+ *
19
+ * Recovery is asymmetric: a tier escalation fires after N consecutive overrun frames;
20
+ * recovery requires a longer run of clean frames to avoid thrashing at the boundary.
21
+ */
22
+ const ESCALATE = [
23
+ { aboveMs: 20, streak: 10, tier: 1 },
24
+ { aboveMs: 33, streak: 5, tier: 2 },
25
+ { aboveMs: 50, streak: 3, tier: 3 },
26
+ ];
27
+ const RECOVER_STREAK = 30; // clean frames before dropping a tier
28
+ export class QualityGovernor {
29
+ _tier = 0;
30
+ overrunStreak = 0;
31
+ cleanStreak = 0;
32
+ budget;
33
+ constructor(budgetMs = 16.67) {
34
+ this.budget = budgetMs;
35
+ }
36
+ get tier() { return this._tier; }
37
+ /**
38
+ * Feed one frame duration (ms). Returns the new tier when it changes, `undefined` when stable.
39
+ * Call once per rAF tick, after `platform.tick()` completes.
40
+ */
41
+ feed(durationMs) {
42
+ const overrun = durationMs > this.budget * 1.3;
43
+ if (overrun) {
44
+ this.cleanStreak = 0;
45
+ this.overrunStreak++;
46
+ for (const rule of ESCALATE) {
47
+ if (durationMs > rule.aboveMs && this.overrunStreak >= rule.streak && this._tier < rule.tier) {
48
+ this._tier = rule.tier;
49
+ return this._tier;
50
+ }
51
+ }
52
+ }
53
+ else {
54
+ this.overrunStreak = 0;
55
+ if (this._tier > 0) {
56
+ this.cleanStreak++;
57
+ if (this.cleanStreak >= RECOVER_STREAK) {
58
+ this.cleanStreak = 0;
59
+ this._tier = Math.max(0, this._tier - 1);
60
+ return this._tier;
61
+ }
62
+ }
63
+ }
64
+ return undefined;
65
+ }
66
+ reset() {
67
+ this._tier = 0;
68
+ this.overrunStreak = 0;
69
+ this.cleanStreak = 0;
70
+ }
71
+ }
72
+ //# sourceMappingURL=governor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"governor.js","sourceRoot":"","sources":["../src/governor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAUH,MAAM,QAAQ,GAAwB;IACpC,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE;IACpC,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAG,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE;IACpC,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAG,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE;CACrC,CAAC;AAEF,MAAM,cAAc,GAAG,EAAE,CAAC,CAAC,sCAAsC;AAEjE,MAAM,OAAO,eAAe;IAClB,KAAK,GAAgB,CAAC,CAAC;IACvB,aAAa,GAAG,CAAC,CAAC;IAClB,WAAW,GAAG,CAAC,CAAC;IACP,MAAM,CAAS;IAEhC,YAAY,QAAQ,GAAG,KAAK;QAC1B,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC;IACzB,CAAC;IAED,IAAI,IAAI,KAAkB,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAE9C;;;OAGG;IACH,IAAI,CAAC,UAAkB;QACrB,MAAM,OAAO,GAAG,UAAU,GAAG,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC;QAE/C,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;YACrB,IAAI,CAAC,aAAa,EAAE,CAAC;YACrB,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;gBAC5B,IAAI,UAAU,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;oBAC7F,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC;oBACvB,OAAO,IAAI,CAAC,KAAK,CAAC;gBACpB,CAAC;YACH,CAAC;QACH,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;YACvB,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;gBACnB,IAAI,CAAC,WAAW,EAAE,CAAC;gBACnB,IAAI,IAAI,CAAC,WAAW,IAAI,cAAc,EAAE,CAAC;oBACvC,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;oBACrB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,CAAgB,CAAC;oBACxD,OAAO,IAAI,CAAC,KAAK,CAAC;gBACpB,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;QACf,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;QACvB,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;IACvB,CAAC;CACF"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @fundamental-engine/dom — the platform-adjacent layer: native-first registries that let the field
3
+ * engine treat the DOM as a connected, measurable, semantic environment. Strict dependency
4
+ * direction: this package depends on `Fundamental` (core) for contracts; core never depends on it.
5
+ *
6
+ * Registries: MeasurementRegistry · StateRegistry · FeedbackRegistry · RelationshipRegistry ·
7
+ * VisualBindingRegistry · OverlayRegistry, bound by createFieldPlatform.
8
+ *
9
+ * Also the browser environment adapter for the renderer-agnostic core engine: `browserHost()` (the
10
+ * default FieldHost), `createBrowserField()` (createField + browserHost), and the DOM download
11
+ * helpers — so core can import zero DOM.
12
+ */
13
+ import { type FieldHandle, type FieldOptions } from '@fundamental-engine/core';
14
+ export * from './types.ts';
15
+ export * from './env.ts';
16
+ export * from './schedule.ts';
17
+ export * from './measurement.ts';
18
+ export * from './state.ts';
19
+ export * from './feedback.ts';
20
+ export * from './relationships.ts';
21
+ export * from './visual-bindings.ts';
22
+ export * from './overlays.ts';
23
+ export * from './lint.ts';
24
+ export * from './platform.ts';
25
+ export * from './metrics.ts';
26
+ export * from './apply-recipe.ts';
27
+ export * from './field-nav.ts';
28
+ export * from './bind-data.ts';
29
+ export * from './browser-host.ts';
30
+ export * from './export-dom.ts';
31
+ export * from './governor.ts';
32
+ export * from './flip.ts';
33
+ export * from './text-bodies.ts';
34
+ export * from './contours.ts';
35
+ export * from './thread-overlay.ts';
36
+ export * from './perf.ts';
37
+ /** Start the core engine on a canvas with the default browser host — `createField` + `browserHost()`. */
38
+ export declare function createBrowserField(canvas: HTMLCanvasElement, opts?: Omit<FieldOptions, 'host'>): FieldHandle;
39
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAe,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAG5F,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,WAAW,CAAC;AAE1B,yGAAyG;AACzG,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,iBAAiB,EAAE,IAAI,GAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAM,GAAG,WAAW,CAEhH"}
package/dist/index.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @fundamental-engine/dom — the platform-adjacent layer: native-first registries that let the field
3
+ * engine treat the DOM as a connected, measurable, semantic environment. Strict dependency
4
+ * direction: this package depends on `Fundamental` (core) for contracts; core never depends on it.
5
+ *
6
+ * Registries: MeasurementRegistry · StateRegistry · FeedbackRegistry · RelationshipRegistry ·
7
+ * VisualBindingRegistry · OverlayRegistry, bound by createFieldPlatform.
8
+ *
9
+ * Also the browser environment adapter for the renderer-agnostic core engine: `browserHost()` (the
10
+ * default FieldHost), `createBrowserField()` (createField + browserHost), and the DOM download
11
+ * helpers — so core can import zero DOM.
12
+ */
13
+ import { createField } from '@fundamental-engine/core';
14
+ import { browserHost } from "./browser-host.js";
15
+ export * from "./types.js";
16
+ export * from "./env.js";
17
+ export * from "./schedule.js";
18
+ export * from "./measurement.js";
19
+ export * from "./state.js";
20
+ export * from "./feedback.js";
21
+ export * from "./relationships.js";
22
+ export * from "./visual-bindings.js";
23
+ export * from "./overlays.js";
24
+ export * from "./lint.js";
25
+ export * from "./platform.js";
26
+ export * from "./metrics.js";
27
+ export * from "./apply-recipe.js";
28
+ export * from "./field-nav.js";
29
+ export * from "./bind-data.js";
30
+ export * from "./browser-host.js";
31
+ export * from "./export-dom.js";
32
+ export * from "./governor.js";
33
+ export * from "./flip.js";
34
+ export * from "./text-bodies.js";
35
+ export * from "./contours.js";
36
+ export * from "./thread-overlay.js";
37
+ export * from "./perf.js";
38
+ /** Start the core engine on a canvas with the default browser host — `createField` + `browserHost()`. */
39
+ export function createBrowserField(canvas, opts = {}) {
40
+ return createField(canvas, { ...opts, host: browserHost() });
41
+ }
42
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,WAAW,EAAuC,MAAM,0BAA0B,CAAC;AAC5F,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,WAAW,CAAC;AAE1B,yGAAyG;AACzG,MAAM,UAAU,kBAAkB,CAAC,MAAyB,EAAE,OAAmC,EAAE;IACjG,OAAO,WAAW,CAAC,MAAM,EAAE,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC;AAC/D,CAAC"}