@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zach Shallbetter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # @fundamental-engine/dom
2
+
3
+ **The platform layer for [Fundamental](../core)** — the native browser primitives the engine wishes
4
+ existed, built on the ones it has. The core is renderer-agnostic; this package owns DOM participation:
5
+ it supplies the browser host, the six registries that let the engine treat the DOM as a connected,
6
+ measurable, semantic environment, the frame scheduler that keeps reads and writes from thrashing, and
7
+ the runtime that runs recipes and binds data.
8
+
9
+ → Live at **[fundamental-engine.com](https://fundamental-engine.com)**.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm i @fundamental-engine/dom
15
+ ```
16
+
17
+ The public surface is frozen for `0.x` (see
18
+ [API stability](../../docs/canonical/api-stability.md)).
19
+
20
+ ## The browser host
21
+
22
+ The core's `createField` is renderer-agnostic and requires a `FieldHost`. `browserHost()` is the
23
+ canonical DOM implementation, and `createBrowserField()` is the host-bundled shortcut:
24
+
25
+ ```ts
26
+ import { createField } from '@fundamental-engine/core';
27
+ import { browserHost, createBrowserField } from '@fundamental-engine/dom';
28
+
29
+ const canvas = document.querySelector('canvas')!;
30
+ const field = createField(canvas, { host: browserHost() });
31
+ // or, equivalently:
32
+ const same = createBrowserField(canvas, {});
33
+ ```
34
+
35
+ ## The platform
36
+
37
+ `createFieldPlatform(root)` wires the six native-first registries on a root element and a frame
38
+ scheduler that runs them in order: **discover → read → compute → state → write → render**.
39
+
40
+ | Registry | Role |
41
+ |---|---|
42
+ | `MeasurementRegistry` | frame-stable geometry snapshots (read-phase only) |
43
+ | `StateRegistry` | typed numeric / boolean / vector2 element state (not ARIA) |
44
+ | `FeedbackRegistry` | write-phase CSS vars + thresholded, debounced events |
45
+ | `RelationshipRegistry` | normalize native links (`href#id`, `aria-controls`, `for`, …) into one graph |
46
+ | `VisualBindingRegistry` | bind a Canvas / SVG / WebGL visual layer to its semantic source |
47
+ | `OverlayRegistry` | relationship / field-line / debug render layers |
48
+
49
+ ```ts
50
+ import { createFieldPlatform } from '@fundamental-engine/dom';
51
+
52
+ const platform = createFieldPlatform(document.documentElement);
53
+ platform.measure.register(card, { role: 'body' });
54
+ platform.state.set(card, 'density', 0.72);
55
+ platform.feedback.bind(card, { density: '--field-density' });
56
+ platform.feedback.threshold(card, 'field:lit', { metric: 'density', enter: 0.7, exit: 0.45 });
57
+ platform.tick(); // run one frame through the scheduler
58
+ ```
59
+
60
+ `createFieldPlatform` returns a `FieldPlatform` — the surface the inspector and tools read.
61
+
62
+ ## Recipes and data
63
+
64
+ The platform runs recipes and binds application data to the field. `compileRecipe()` (the pure
65
+ compiler) lives in [the core](../core); application lives here:
66
+
67
+ ```ts
68
+ import { applyRecipe, bindData } from '@fundamental-engine/dom';
69
+ import { recipeById } from '@fundamental-engine/core';
70
+
71
+ // Run a recipe over a region; inspect the live run; tear it down.
72
+ const applied = applyRecipe(root, recipeById('reading-field')!);
73
+ applied.inspect(); // { frame, measurements, relationships, lint }
74
+
75
+ // Bind records → bodies. Updates diff by id; removed records decay out.
76
+ const binding = bindData(listEl, tasks, (t) => ({
77
+ id: t.id,
78
+ body: { tokens: ['attract'], strength: 0.4 + t.priority },
79
+ metrics: { priority: t.priority },
80
+ label: t.title,
81
+ }), { recipe: 'priority-well' });
82
+ binding.update(nextTasks);
83
+ ```
84
+
85
+ `computeMetrics()` (pure) turns measurements and relationships into the metric values recipes track.
86
+
87
+ ## Lint
88
+
89
+ `lintPlatform(platform)` runs pure rules over the live registries and the markup (missing
90
+ relationship targets, sinks that capture without `data-feedback`, styles reading feedback
91
+ variables no body writes, unregistered state, overlays without links, off-phase measurement,
92
+ orphan visuals, …) and returns structured diagnostics. The inspector reads it each frame.
93
+
94
+ ## Dependency direction
95
+
96
+ Strict and one-way: **`platform → core`**. The core stays renderer-agnostic and never imports this
97
+ package. During the migration window, writes mirror `--field-*` to `--forces-*` and `field:*` events to
98
+ `forces:*`. See [`docs/canonical/platform-architecture.md`](../../docs/canonical/platform-architecture.md).
99
+
100
+ ## Related
101
+
102
+ [`Fundamental`](../core) · [`@fundamental-engine/elements`](../elements) · [`@fundamental-engine/react`](../react) ·
103
+ [`@fundamental-engine/vanilla`](../vanilla) · the [documentation map](../../docs/README.md).
104
+
105
+ ## License
106
+
107
+ MIT © Zach Shallbetter
@@ -0,0 +1,103 @@
1
+ /**
2
+ * applyRecipe — the DOM counterpart to core's `compileRecipe`. Turns a `FieldRecipe` from a record
3
+ * into a running field program: it registers the recipe's bodies (TOKEN lane only — concepts are never
4
+ * executed), binds its metrics to feedback variables, discovers its relationships, computes metric
5
+ * state each frame, installs a reduced-motion output when motion is reduced, and returns a handle that
6
+ * can be inspected and destroyed.
7
+ *
8
+ * It runs on a scoped `createFieldPlatform(root)` — the registry/feedback layer, not a particle canvas
9
+ * — so it works on ordinary content the way the Reading Field studies do.
10
+ */
11
+ import { type FieldPlatform } from './platform.ts';
12
+ import { type FieldRecipe, type CompiledRecipe } from '@fundamental-engine/core';
13
+ export interface ApplyRecipeOptions {
14
+ /** existing elements (or a selector within root) to annotate as the recipe's bodies; if omitted, demo elements are created. */
15
+ bodies?: Element[] | string;
16
+ /** when bodies are provided, whether to overwrite their data-body attributes with the recipe's (default true). Set false to keep caller-owned tokens (e.g. bindData) while still binding the recipe's metrics. */
17
+ annotateBodies?: boolean;
18
+ /** force the reduced-motion output (defaults to the OS prefers-reduced-motion setting). */
19
+ reducedMotion?: boolean;
20
+ /** compute + bind metrics (default true). */
21
+ metrics?: boolean;
22
+ /** run the rAF loop (default true). Set false to drive `tick()` yourself. */
23
+ drive?: boolean;
24
+ /** text for created demo bodies (default: the body's tokens). */
25
+ label?: (recipe: FieldRecipe, index: number) => string;
26
+ /**
27
+ * Apply the recipe with its render stack stripped (`render: []`) — the scoped invisible-field
28
+ * idiom the example family runs: the field exists purely as metrics/feedback signals on real
29
+ * content, with no drawn layers. The caller's recipe object is NEVER mutated (it may be the
30
+ * shared catalog object); `applyRecipe` derives an effective copy, which is what the returned
31
+ * handle's `recipe`/`compiled` reflect.
32
+ */
33
+ renderless?: boolean;
34
+ /**
35
+ * A live field to DRIVE with the recipe's render plan (#370) — the missing execution half of
36
+ * `recipe.render`. Structural: a `FieldHandle` or a `<field-root>` element both satisfy it.
37
+ * When provided (and not `renderless`/reduced-motion), the compiled plan executes — underlay
38
+ * matter mode via setRender, the additive overlay reading stack via setOverlay, the heatmap
39
+ * toggle — and `destroy()` resets the surfaces it drove (dots / off / false). Omitted → the
40
+ * recipe stays signals-only, exactly as before (fully additive).
41
+ */
42
+ field?: RecipeFieldTarget;
43
+ /**
44
+ * Extra metric lanes appended to the recipe's `metrics` (deduped, original order preserved) —
45
+ * e.g. `['attention', 'recency']`. Each appended metric gains the standard feedback binding
46
+ * (`attention` → `--field-attention`) and flows through the per-frame metric pipeline exactly
47
+ * like a recipe-declared one. Same no-mutation guarantee as `renderless`.
48
+ */
49
+ extraMetrics?: string[];
50
+ }
51
+ export interface AppliedRecipeInspection {
52
+ id: string;
53
+ frame: number;
54
+ measurements: number;
55
+ /** resolved relationships (both endpoints known). */
56
+ relationships: number;
57
+ /** declared relationships whose target id-ref resolves to no element. */
58
+ relationshipsUnresolved: number;
59
+ /** resolved / (resolved + unresolved); undefined when no relationships are declared. */
60
+ relationshipResolution?: number;
61
+ /** the unresolved declarations, so inspection can name each missing endpoint. */
62
+ unresolvedRelationships: Array<{
63
+ from: string;
64
+ type: string;
65
+ target: string;
66
+ }>;
67
+ /** elementKey → metric → value (the compiled recipe's lanes, live). */
68
+ metrics: Record<string, Record<string, number>>;
69
+ lint: number;
70
+ reducedMotion: boolean;
71
+ }
72
+ export interface AppliedRecipe {
73
+ id: string;
74
+ recipe: FieldRecipe;
75
+ compiled: CompiledRecipe;
76
+ platform: FieldPlatform;
77
+ root: Element;
78
+ elements: Element[];
79
+ reducedMotion: boolean;
80
+ inspect(): AppliedRecipeInspection;
81
+ tick(now?: number): void;
82
+ destroy(): void;
83
+ }
84
+ /**
85
+ * Apply a recipe to a root element. Validates, compiles, registers bodies, binds metrics, discovers
86
+ * relationships, installs the reduced-motion output, and returns a destroyable, inspectable handle.
87
+ */
88
+ /** The slice of a live field a recipe can drive — FieldHandle and <field-root> both fit. */
89
+ export interface RecipeFieldTarget {
90
+ setRender?(mode: string): void;
91
+ setOverlay?(mode: string | string[]): void;
92
+ setHeatmap?(on: boolean): void;
93
+ }
94
+ /** Execute a compiled render plan on a field target. Exported for tests and custom hosts. */
95
+ export declare function driveRenderPlan(field: RecipeFieldTarget, plan: {
96
+ underlay: string | null;
97
+ overlay: string[];
98
+ heatmap: boolean;
99
+ }): void;
100
+ export declare function applyRecipe(root: Element, recipe: FieldRecipe, options?: ApplyRecipeOptions): AppliedRecipe;
101
+ /** Tear down an applied recipe (alias of `applied.destroy()`). */
102
+ export declare function destroyRecipe(applied: AppliedRecipe): void;
103
+ //# sourceMappingURL=apply-recipe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-recipe.d.ts","sourceRoot":"","sources":["../src/apply-recipe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAIxE,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,cAAc,EACpB,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,kBAAkB;IACjC,+HAA+H;IAC/H,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;IAC5B,kNAAkN;IAClN,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,2FAA2F;IAC3F,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iEAAiE;IACjE,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACvD;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,aAAa,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,uBAAuB,EAAE,MAAM,CAAC;IAChC,wFAAwF;IACxF,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,iFAAiF;IACjF,uBAAuB,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/E,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,cAAc,CAAC;IACzB,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,aAAa,EAAE,OAAO,CAAC;IACvB,OAAO,IAAI,uBAAuB,CAAC;IACnC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,IAAI,IAAI,CAAC;CACjB;AAUD;;;GAGG;AACH,4FAA4F;AAC5F,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,UAAU,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;IAC3C,UAAU,CAAC,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAC;CAChC;AAED,6FAA6F;AAC7F,wBAAgB,eAAe,CAAC,KAAK,EAAE,iBAAiB,EAAE,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAItI;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,kBAAuB,GAAG,aAAa,CAsO/G;AAED,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAE1D"}
@@ -0,0 +1,271 @@
1
+ /**
2
+ * applyRecipe — the DOM counterpart to core's `compileRecipe`. Turns a `FieldRecipe` from a record
3
+ * into a running field program: it registers the recipe's bodies (TOKEN lane only — concepts are never
4
+ * executed), binds its metrics to feedback variables, discovers its relationships, computes metric
5
+ * state each frame, installs a reduced-motion output when motion is reduced, and returns a handle that
6
+ * can be inspected and destroyed.
7
+ *
8
+ * It runs on a scoped `createFieldPlatform(root)` — the registry/feedback layer, not a particle canvas
9
+ * — so it works on ordinary content the way the Reading Field studies do.
10
+ */
11
+ import { createFieldPlatform } from "./platform.js";
12
+ import { lintPlatform } from "./lint.js";
13
+ import { prefersReducedMotion } from "./env.js";
14
+ import { computeMetrics, groundedRecency, METRIC_KINDS } from "./metrics.js";
15
+ import { validateRecipe, compileRecipe, } from '@fundamental-engine/core';
16
+ const isMetricKind = (m) => METRIC_KINDS.includes(m);
17
+ const num = (v) => {
18
+ if (v == null)
19
+ return undefined;
20
+ const n = Number(v);
21
+ return Number.isFinite(n) ? n : undefined;
22
+ };
23
+ const elementKey = (el, i) => el.id || `${el.tagName.toLowerCase()}#${i}`;
24
+ /** Execute a compiled render plan on a field target. Exported for tests and custom hosts. */
25
+ export function driveRenderPlan(field, plan) {
26
+ if (plan.underlay && field.setRender)
27
+ field.setRender(plan.underlay);
28
+ if (field.setOverlay)
29
+ field.setOverlay(plan.overlay.length ? plan.overlay : 'off');
30
+ if (field.setHeatmap)
31
+ field.setHeatmap(plan.heatmap);
32
+ }
33
+ export function applyRecipe(root, recipe, options = {}) {
34
+ // Derive the effective recipe from the scoped-field options — a copy, so a shared catalog
35
+ // recipe object is never mutated by an applied run (`renderless` strips the render stack;
36
+ // `extraMetrics` appends + dedupes metric lanes). No options → the input recipe, untouched.
37
+ const extra = options.extraMetrics ?? [];
38
+ if (options.renderless || extra.length) {
39
+ recipe = {
40
+ ...recipe,
41
+ ...(options.renderless ? { render: [] } : {}),
42
+ ...(extra.length ? { metrics: [...new Set([...(recipe.metrics ?? []), ...extra])] } : {}),
43
+ };
44
+ }
45
+ const problems = validateRecipe(recipe);
46
+ if (problems.length)
47
+ throw new Error(`applyRecipe: invalid recipe "${recipe.id}": ${problems.map((p) => `${p.path} (${p.issue})`).join('; ')}`);
48
+ const compiled = compileRecipe(recipe);
49
+ const wantMetrics = options.metrics !== false;
50
+ // the execution half of recipe.render (#370): drive the supplied field with the compiled plan.
51
+ // Reduced motion skips the drive entirely — the recipe's static plan is the equivalent, and a
52
+ // field left at its resting surfaces (dots / off) IS the static reading.
53
+ let droveField = null;
54
+ const reducedMotion = options.reducedMotion ?? prefersReducedMotion();
55
+ const platform = createFieldPlatform(root);
56
+ if (options.field && !options.renderless && !reducedMotion) {
57
+ driveRenderPlan(options.field, compiled.render);
58
+ droveField = options.field;
59
+ }
60
+ // ── resolve body elements: annotate provided ones, or create demo bodies ──────────────
61
+ const created = [];
62
+ const restore = [];
63
+ let elements;
64
+ if (options.bodies) {
65
+ elements = typeof options.bodies === 'string' ? Array.from(root.querySelectorAll(options.bodies)) : options.bodies.slice();
66
+ if (options.annotateBodies !== false) {
67
+ elements.forEach((el, i) => {
68
+ const body = compiled.bodies[i % compiled.bodies.length];
69
+ const snap = {};
70
+ for (const [k, v] of Object.entries(body.attributes)) {
71
+ snap[k] = el.getAttribute(k);
72
+ el.setAttribute(k, v);
73
+ }
74
+ restore.push({ el, attrs: snap });
75
+ });
76
+ }
77
+ }
78
+ else {
79
+ elements = compiled.bodies.map((body, i) => {
80
+ const el = (root.ownerDocument ?? document).createElement('span');
81
+ for (const [k, v] of Object.entries(body.attributes))
82
+ el.setAttribute(k, v);
83
+ el.setAttribute('data-recipe-body', recipe.id);
84
+ el.textContent = options.label ? options.label(recipe, i) : body.tokens.join(' ');
85
+ root.appendChild(el);
86
+ created.push(el);
87
+ return el;
88
+ });
89
+ }
90
+ // ── register for measurement + bind the metric lane to feedback variables ──────────────
91
+ const varMap = {};
92
+ for (const f of compiled.feedback)
93
+ varMap[f.metric] = f.var;
94
+ for (const el of elements) {
95
+ platform.measure.register(el, { role: 'recipe-body' });
96
+ if (wantMetrics && compiled.feedback.length)
97
+ platform.feedback.bind(el, varMap);
98
+ }
99
+ // ── discover relationships once ──────────────────────────────────────────────────────
100
+ let discovered = false;
101
+ platform.on('discover', () => {
102
+ if (!discovered) {
103
+ platform.relationships.discover(root);
104
+ discovered = true;
105
+ }
106
+ });
107
+ // ── metric computation (compute → state); write phase flushes state → --field-* vars ───
108
+ const prev = new Map();
109
+ if (wantMetrics) {
110
+ const pending = new Map();
111
+ platform.on('compute', (ctx) => {
112
+ const vh = ctx.viewport?.height ?? (typeof window !== 'undefined' ? window.innerHeight : 800);
113
+ const centre = vh / 2;
114
+ // WORLD TIME, once per frame: ctx.now is the scheduler's rAF timebase, not an epoch, so
115
+ // the declared-timestamp derivation samples the wall clock HERE — one instant shared by
116
+ // every element in the frame, never a per-element Date.now().
117
+ const worldNow = Date.now();
118
+ const rels = platform.relationships.all();
119
+ const unresolved = platform.relationships.unresolvedAll();
120
+ for (const m of platform.measure.last()) {
121
+ const el = m.element;
122
+ const proximity = Math.max(0, 1 - Math.abs(m.rect.cy - centre) / (vh * 0.55));
123
+ const engaged = el.matches(':hover, :focus, :focus-within') || el.hasAttribute('data-active');
124
+ // a body touches a relationship if either endpoint is the body or sits inside it (child anchors)
125
+ const touching = rels.filter((r) => el.contains(r.from) || el.contains(r.to));
126
+ // declared-but-unresolved edges originating in this body count toward the total but not the
127
+ // resolved set, so resolution is real: a citation pointing at nothing lowers it (raising entropy).
128
+ const touchingUnresolved = unresolved.filter((u) => el.contains(u.from));
129
+ const relResolved = touching.length;
130
+ const relTotal = touching.length + touchingUnresolved.length;
131
+ const relConflict = touching.filter((r) => r.type === 'contradicts' || r.type === 'opposes' || r.type === 'conflicts-with').length;
132
+ const supplied = {};
133
+ for (const k of METRIC_KINDS) {
134
+ const s = num(el.getAttribute(`data-field-${k}`));
135
+ if (s != null)
136
+ supplied[k] = s;
137
+ }
138
+ // A declared world timestamp (data-field-at) GROUNDS the recency lane: recency becomes
139
+ // freshness(at, now, halfLife) — data time, not interaction time. An explicit
140
+ // data-field-recency still wins; without either, computeMetrics infers recency from
141
+ // interaction (the existing eased behavior, unchanged).
142
+ if (supplied.recency == null) {
143
+ const r = groundedRecency(el, worldNow);
144
+ if (r != null)
145
+ supplied.recency = r;
146
+ }
147
+ pending.set(el, computeMetrics({
148
+ proximity,
149
+ visible: m.visibilityRatio,
150
+ engaged,
151
+ dtFrames: 1,
152
+ relResolved,
153
+ relTotal,
154
+ relConflict,
155
+ supplied,
156
+ prev: prev.get(el) ?? {},
157
+ }));
158
+ }
159
+ });
160
+ platform.on('state', () => {
161
+ for (const [el, computed] of pending) {
162
+ prev.set(el, computed);
163
+ for (const metric of compiled.metrics) {
164
+ const value = isMetricKind(metric)
165
+ ? computed[metric]
166
+ : (num(el.getAttribute(`data-field-${metric}`)) ?? 0);
167
+ if (value == null) {
168
+ // The metric is absent this frame — e.g. the host supplied data-field-confidence on an
169
+ // earlier frame and has since removed it. Drop any stale state AND clear the bound CSS
170
+ // var, so the write phase neither re-emits a value nor leaves one written on a previous
171
+ // flush lingering on the element. Absent must read as absent, not last-known. Both calls
172
+ // are no-ops when nothing was ever set, so the common "never supplied" case stays cheap.
173
+ platform.state.delete(el, metric);
174
+ platform.feedback.clearVar(el, metric);
175
+ continue;
176
+ }
177
+ platform.state.set(el, metric, value);
178
+ }
179
+ }
180
+ });
181
+ }
182
+ // ── reduced-motion output: a real static surface, not just prose ──────────────────────
183
+ let staticNode = null;
184
+ if (reducedMotion) {
185
+ root.dataset.recipeReduced = 'on';
186
+ const doc = root.ownerDocument ?? document;
187
+ staticNode = doc.createElement('aside');
188
+ staticNode.className = 'recipe-static';
189
+ staticNode.setAttribute('data-recipe-static', recipe.id);
190
+ staticNode.innerHTML =
191
+ `<p class="rs-note">${escapeHtml(compiled.reducedMotion.meaningWithoutMotion)}</p>` +
192
+ (compiled.metrics.length ? `<p class="rs-metrics">Metrics: ${compiled.metrics.map((m) => `<span>${escapeHtml(m)}</span>`).join(' ')}</p>` : '') +
193
+ (compiled.relationships.length ? `<p class="rs-rels">Relationships: ${compiled.relationships.map((r) => `${escapeHtml(r.from)}→${escapeHtml(r.to)}`).join(', ')}</p>` : '');
194
+ root.appendChild(staticNode);
195
+ }
196
+ // ── drive ─────────────────────────────────────────────────────────────────────────────
197
+ let raf = 0;
198
+ const viewport = () => (typeof window !== 'undefined' ? { width: window.innerWidth, height: window.innerHeight } : undefined);
199
+ const tick = (now = 0) => {
200
+ platform.tick(now, viewport());
201
+ };
202
+ if (options.drive !== false && typeof requestAnimationFrame !== 'undefined') {
203
+ const loop = (now) => {
204
+ tick(now);
205
+ raf = requestAnimationFrame(loop);
206
+ };
207
+ raf = requestAnimationFrame(loop);
208
+ }
209
+ const inspect = () => {
210
+ const metrics = {};
211
+ elements.forEach((el, i) => {
212
+ const row = {};
213
+ for (const metric of compiled.metrics)
214
+ row[metric] = platform.state.number(el, metric);
215
+ metrics[elementKey(el, i)] = row;
216
+ });
217
+ const unresolved = platform.relationships.unresolvedAll();
218
+ const resolvedCount = platform.relationships.all().length;
219
+ const declaredTotal = resolvedCount + unresolved.length;
220
+ return {
221
+ id: recipe.id,
222
+ frame: platform.scheduler.frame,
223
+ measurements: platform.measure.size,
224
+ relationships: resolvedCount,
225
+ relationshipsUnresolved: unresolved.length,
226
+ relationshipResolution: declaredTotal > 0 ? resolvedCount / declaredTotal : undefined,
227
+ unresolvedRelationships: unresolved.map((u) => ({
228
+ from: u.from.id || u.from.tagName.toLowerCase(),
229
+ type: u.type,
230
+ target: u.target,
231
+ })),
232
+ metrics,
233
+ lint: lintPlatform(platform).length,
234
+ reducedMotion,
235
+ };
236
+ };
237
+ const destroy = () => {
238
+ if (raf)
239
+ cancelAnimationFrame(raf);
240
+ raf = 0;
241
+ // release the surfaces this recipe drove — the field returns to its resting reading
242
+ if (droveField) {
243
+ driveRenderPlan(droveField, { underlay: 'dots', overlay: [], heatmap: false });
244
+ droveField = null;
245
+ }
246
+ // clear the feedback variables this recipe wrote, so a torn-down recipe leaves the DOM plain
247
+ // (typeof-guarded like every other global here, so destroy() is safe off-DOM too)
248
+ if (typeof HTMLElement !== 'undefined')
249
+ for (const el of elements)
250
+ if (el instanceof HTMLElement)
251
+ for (const f of compiled.feedback)
252
+ el.style.removeProperty(f.var);
253
+ for (const el of created)
254
+ el.remove();
255
+ for (const { el, attrs } of restore)
256
+ for (const [k, v] of Object.entries(attrs))
257
+ v == null ? el.removeAttribute(k) : el.setAttribute(k, v);
258
+ staticNode?.remove();
259
+ if (reducedMotion)
260
+ delete root.dataset.recipeReduced;
261
+ };
262
+ return { id: recipe.id, recipe, compiled, platform, root, elements, reducedMotion, inspect, tick, destroy };
263
+ }
264
+ /** Tear down an applied recipe (alias of `applied.destroy()`). */
265
+ export function destroyRecipe(applied) {
266
+ applied.destroy();
267
+ }
268
+ function escapeHtml(s) {
269
+ return s.replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c] ?? c);
270
+ }
271
+ //# sourceMappingURL=apply-recipe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-recipe.js","sourceRoot":"","sources":["../src/apply-recipe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
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 { type FieldRecipe } from '@fundamental-engine/core';
15
+ import { type AppliedRecipe } from './apply-recipe.ts';
16
+ export interface MappedBody {
17
+ tokens: string[];
18
+ strength?: number;
19
+ range?: number;
20
+ spin?: number;
21
+ angle?: number;
22
+ feedback?: boolean;
23
+ }
24
+ export interface MappedRelationship {
25
+ to: string;
26
+ type: string;
27
+ strength?: number;
28
+ }
29
+ export interface MappedRecord {
30
+ id: string;
31
+ body: MappedBody;
32
+ metrics?: Record<string, number>;
33
+ relationships?: MappedRelationship[];
34
+ label?: string;
35
+ }
36
+ export type RecordMapper<T> = (record: T, index: number) => MappedRecord;
37
+ export interface BindDataOptions<T = unknown> {
38
+ /** the recipe whose metric/feedback framework drives the bound bodies (id or object). */
39
+ recipe?: string | FieldRecipe;
40
+ /** decay duration (ms) before a removed record leaves the DOM (default 400). */
41
+ decayMs?: number;
42
+ /** element tag for each record (default 'div'). */
43
+ tag?: string;
44
+ /** class added to each record element. */
45
+ className?: string;
46
+ /** install the recipe's reduced-motion output instead of motion (passed to applyRecipe). */
47
+ reducedMotion?: boolean;
48
+ /** custom inner HTML per record (domain markup); overrides the default label. Relationship anchors are kept. */
49
+ content?: (record: T, mapped: MappedRecord) => string;
50
+ }
51
+ export interface DataBindingInspection {
52
+ records: number;
53
+ bodies: number;
54
+ relationships: number;
55
+ }
56
+ export interface DataBinding<T> {
57
+ container: HTMLElement;
58
+ update(records: T[]): void;
59
+ ids(): string[];
60
+ applied(): AppliedRecipe | null;
61
+ inspect(): DataBindingInspection | null;
62
+ destroy(): void;
63
+ }
64
+ /** Diff two id sets (pure). */
65
+ export declare function diffIds(prev: Iterable<string>, next: Iterable<string>): {
66
+ added: string[];
67
+ removed: string[];
68
+ kept: string[];
69
+ };
70
+ /** Bind records to a field. Returns a handle with update()/destroy(). */
71
+ export declare function bindData<T>(container: HTMLElement, records: T[], mapper: RecordMapper<T>, options?: BindDataOptions<T>): DataBinding<T>;
72
+ //# sourceMappingURL=bind-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bind-data.d.ts","sourceRoot":"","sources":["../src/bind-data.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAe,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEpE,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AACD,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AACD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,aAAa,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AACD,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,YAAY,CAAC;AAEzE,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,OAAO;IAC1C,yFAAyF;IACzF,MAAM,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IAC9B,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4FAA4F;IAC5F,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gHAAgH;IAChH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,KAAK,MAAM,CAAC;CACvD;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;CACvB;AACD,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,SAAS,EAAE,WAAW,CAAC;IACvB,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC;IAC3B,GAAG,IAAI,MAAM,EAAE,CAAC;IAChB,OAAO,IAAI,aAAa,GAAG,IAAI,CAAC;IAChC,OAAO,IAAI,qBAAqB,GAAG,IAAI,CAAC;IACxC,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,+BAA+B;AAC/B,wBAAgB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAS9H;AAkDD,yEAAyE;AACzE,wBAAgB,QAAQ,CAAC,CAAC,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,eAAe,CAAC,CAAC,CAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CA2E3I"}