@fundamental-engine/platform 0.5.1 → 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 (98) hide show
  1. package/README.md +11 -101
  2. package/dist/index.d.ts +11 -34
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +11 -37
  5. package/dist/index.js.map +1 -1
  6. package/package.json +4 -10
  7. package/dist/apply-recipe.d.ts +0 -103
  8. package/dist/apply-recipe.d.ts.map +0 -1
  9. package/dist/apply-recipe.js +0 -271
  10. package/dist/apply-recipe.js.map +0 -1
  11. package/dist/bind-data.d.ts +0 -72
  12. package/dist/bind-data.d.ts.map +0 -1
  13. package/dist/bind-data.js +0 -164
  14. package/dist/bind-data.js.map +0 -1
  15. package/dist/browser-host.d.ts +0 -11
  16. package/dist/browser-host.d.ts.map +0 -1
  17. package/dist/browser-host.js +0 -41
  18. package/dist/browser-host.js.map +0 -1
  19. package/dist/contours.d.ts +0 -79
  20. package/dist/contours.d.ts.map +0 -1
  21. package/dist/contours.js +0 -88
  22. package/dist/contours.js.map +0 -1
  23. package/dist/env.d.ts +0 -39
  24. package/dist/env.d.ts.map +0 -1
  25. package/dist/env.js +0 -47
  26. package/dist/env.js.map +0 -1
  27. package/dist/export-dom.d.ts +0 -7
  28. package/dist/export-dom.d.ts.map +0 -1
  29. package/dist/export-dom.js +0 -28
  30. package/dist/export-dom.js.map +0 -1
  31. package/dist/feedback.d.ts +0 -57
  32. package/dist/feedback.d.ts.map +0 -1
  33. package/dist/feedback.js +0 -134
  34. package/dist/feedback.js.map +0 -1
  35. package/dist/field-nav.d.ts +0 -35
  36. package/dist/field-nav.d.ts.map +0 -1
  37. package/dist/field-nav.js +0 -82
  38. package/dist/field-nav.js.map +0 -1
  39. package/dist/flip.d.ts +0 -31
  40. package/dist/flip.d.ts.map +0 -1
  41. package/dist/flip.js +0 -65
  42. package/dist/flip.js.map +0 -1
  43. package/dist/governor.d.ts +0 -37
  44. package/dist/governor.d.ts.map +0 -1
  45. package/dist/governor.js +0 -72
  46. package/dist/governor.js.map +0 -1
  47. package/dist/lint.d.ts +0 -78
  48. package/dist/lint.d.ts.map +0 -1
  49. package/dist/lint.js +0 -153
  50. package/dist/lint.js.map +0 -1
  51. package/dist/measurement.d.ts +0 -44
  52. package/dist/measurement.d.ts.map +0 -1
  53. package/dist/measurement.js +0 -95
  54. package/dist/measurement.js.map +0 -1
  55. package/dist/metrics.d.ts +0 -70
  56. package/dist/metrics.d.ts.map +0 -1
  57. package/dist/metrics.js +0 -119
  58. package/dist/metrics.js.map +0 -1
  59. package/dist/overlays.d.ts +0 -48
  60. package/dist/overlays.d.ts.map +0 -1
  61. package/dist/overlays.js +0 -48
  62. package/dist/overlays.js.map +0 -1
  63. package/dist/perf.d.ts +0 -62
  64. package/dist/perf.d.ts.map +0 -1
  65. package/dist/perf.js +0 -94
  66. package/dist/perf.js.map +0 -1
  67. package/dist/platform.d.ts +0 -40
  68. package/dist/platform.d.ts.map +0 -1
  69. package/dist/platform.js +0 -61
  70. package/dist/platform.js.map +0 -1
  71. package/dist/relationships.d.ts +0 -79
  72. package/dist/relationships.d.ts.map +0 -1
  73. package/dist/relationships.js +0 -155
  74. package/dist/relationships.js.map +0 -1
  75. package/dist/schedule.d.ts +0 -84
  76. package/dist/schedule.d.ts.map +0 -1
  77. package/dist/schedule.js +0 -91
  78. package/dist/schedule.js.map +0 -1
  79. package/dist/state.d.ts +0 -36
  80. package/dist/state.d.ts.map +0 -1
  81. package/dist/state.js +0 -113
  82. package/dist/state.js.map +0 -1
  83. package/dist/text-bodies.d.ts +0 -71
  84. package/dist/text-bodies.d.ts.map +0 -1
  85. package/dist/text-bodies.js +0 -159
  86. package/dist/text-bodies.js.map +0 -1
  87. package/dist/thread-overlay.d.ts +0 -63
  88. package/dist/thread-overlay.d.ts.map +0 -1
  89. package/dist/thread-overlay.js +0 -110
  90. package/dist/thread-overlay.js.map +0 -1
  91. package/dist/types.d.ts +0 -51
  92. package/dist/types.d.ts.map +0 -1
  93. package/dist/types.js +0 -7
  94. package/dist/types.js.map +0 -1
  95. package/dist/visual-bindings.d.ts +0 -95
  96. package/dist/visual-bindings.d.ts.map +0 -1
  97. package/dist/visual-bindings.js +0 -211
  98. package/dist/visual-bindings.js.map +0 -1
package/README.md CHANGED
@@ -1,107 +1,17 @@
1
- # @fundamental-engine/platform
1
+ # @fundamental-engine/platform — deprecated
2
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.
3
+ **Renamed to [`@fundamental-engine/dom`](https://www.npmjs.com/package/@fundamental-engine/dom).**
8
4
 
9
- Live at **[fundamental-engine.com](https://fundamental-engine.com)**.
5
+ This package is now a thin alias that re-exports `@fundamental-engine/dom` so existing installs keep
6
+ working. The layer it names is the DOM binding for the engine — `browserHost()`, the six registries
7
+ (measurement / state / feedback / relationships / visual-bindings / overlays), the frame scheduler,
8
+ `lintPlatform`, and `bindData` — so the honest name is `dom`.
10
9
 
11
- ## Install
10
+ Switch your imports:
12
11
 
13
- ```sh
14
- npm i @fundamental-engine/platform
12
+ ```diff
13
+ - import { browserHost } from '@fundamental-engine/platform';
14
+ + import { browserHost } from '@fundamental-engine/dom';
15
15
  ```
16
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/platform';
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/platform';
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/platform';
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
17
+ The alias will be removed in a future major release.
package/dist/index.d.ts CHANGED
@@ -1,39 +1,16 @@
1
1
  /**
2
- * @fundamental-engine/platform — 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.
2
+ * @fundamental-engine/platform — DEPRECATED.
5
3
  *
6
- * Registries: MeasurementRegistry · StateRegistry · FeedbackRegistry · RelationshipRegistry ·
7
- * VisualBindingRegistry · OverlayRegistry, bound by createFieldPlatform.
4
+ * This package was renamed to `@fundamental-engine/dom` (it is the DOM-binding layer:
5
+ * `browserHost()`, the six registries, the frame scheduler, `lintPlatform`, `bindData`).
6
+ * This alias re-exports it unchanged so existing installs keep working; switch your imports:
8
7
  *
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.
8
+ * ```diff
9
+ * - import { browserHost } from '@fundamental-engine/platform';
10
+ * + import { browserHost } from '@fundamental-engine/dom';
11
+ * ```
12
+ *
13
+ * The alias will be removed in a future major. See https://fundamental-engine.com.
12
14
  */
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;
15
+ export * from '@fundamental-engine/dom';
39
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,cAAc,yBAAyB,CAAC"}
package/dist/index.js CHANGED
@@ -1,42 +1,16 @@
1
1
  /**
2
- * @fundamental-engine/platform — 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.
2
+ * @fundamental-engine/platform — DEPRECATED.
5
3
  *
6
- * Registries: MeasurementRegistry · StateRegistry · FeedbackRegistry · RelationshipRegistry ·
7
- * VisualBindingRegistry · OverlayRegistry, bound by createFieldPlatform.
4
+ * This package was renamed to `@fundamental-engine/dom` (it is the DOM-binding layer:
5
+ * `browserHost()`, the six registries, the frame scheduler, `lintPlatform`, `bindData`).
6
+ * This alias re-exports it unchanged so existing installs keep working; switch your imports:
8
7
  *
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.
8
+ * ```diff
9
+ * - import { browserHost } from '@fundamental-engine/platform';
10
+ * + import { browserHost } from '@fundamental-engine/dom';
11
+ * ```
12
+ *
13
+ * The alias will be removed in a future major. See https://fundamental-engine.com.
12
14
  */
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
- }
15
+ export * from '@fundamental-engine/dom';
42
16
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,cAAc,yBAAyB,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fundamental-engine/platform",
3
- "version": "0.5.1",
4
- "description": "The platform-adjacent layer for Fundamental — native-first registries for measurement, semantic state, feedback, relationships, visual-semantic bindings, and overlays. The browser primitives Fundamental wishes existed, built on the ones it has.",
3
+ "version": "0.7.0",
4
+ "description": "DEPRECATED renamed to @fundamental-engine/dom. A thin alias that re-exports it so existing installs keep working; switch your imports to @fundamental-engine/dom.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Zach Shallbetter <hi@zachshallbetter.com> (https://zachshallbetter.com)",
@@ -16,13 +16,7 @@
16
16
  },
17
17
  "keywords": [
18
18
  "Fundamental",
19
- "dom",
20
- "measurement",
21
- "relationships",
22
- "state",
23
- "feedback",
24
- "overlays",
25
- "web"
19
+ "deprecated"
26
20
  ],
27
21
  "sideEffects": false,
28
22
  "files": [
@@ -46,7 +40,7 @@
46
40
  "access": "public"
47
41
  },
48
42
  "dependencies": {
49
- "@fundamental-engine/core": "0.5.1"
43
+ "@fundamental-engine/dom": "0.7.0"
50
44
  },
51
45
  "devDependencies": {
52
46
  "typescript": "^5.9.3"
@@ -1,103 +0,0 @@
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
@@ -1 +0,0 @@
1
- {"version":3,"file":"apply-recipe.d.ts","sourceRoot":"","sources":["../src/apply-recipe.ts"],"names":[],"mappings":"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"}
@@ -1,271 +0,0 @@
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