@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.
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/apply-recipe.d.ts +103 -0
- package/dist/apply-recipe.d.ts.map +1 -0
- package/dist/apply-recipe.js +271 -0
- package/dist/apply-recipe.js.map +1 -0
- package/dist/bind-data.d.ts +72 -0
- package/dist/bind-data.d.ts.map +1 -0
- package/dist/bind-data.js +164 -0
- package/dist/bind-data.js.map +1 -0
- package/dist/browser-host.d.ts +11 -0
- package/dist/browser-host.d.ts.map +1 -0
- package/dist/browser-host.js +41 -0
- package/dist/browser-host.js.map +1 -0
- package/dist/contours.d.ts +79 -0
- package/dist/contours.d.ts.map +1 -0
- package/dist/contours.js +88 -0
- package/dist/contours.js.map +1 -0
- package/dist/env.d.ts +39 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +47 -0
- package/dist/env.js.map +1 -0
- package/dist/export-dom.d.ts +7 -0
- package/dist/export-dom.d.ts.map +1 -0
- package/dist/export-dom.js +28 -0
- package/dist/export-dom.js.map +1 -0
- package/dist/feedback.d.ts +57 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.js +134 -0
- package/dist/feedback.js.map +1 -0
- package/dist/field-nav.d.ts +35 -0
- package/dist/field-nav.d.ts.map +1 -0
- package/dist/field-nav.js +82 -0
- package/dist/field-nav.js.map +1 -0
- package/dist/flip.d.ts +31 -0
- package/dist/flip.d.ts.map +1 -0
- package/dist/flip.js +65 -0
- package/dist/flip.js.map +1 -0
- package/dist/governor.d.ts +37 -0
- package/dist/governor.d.ts.map +1 -0
- package/dist/governor.js +72 -0
- package/dist/governor.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/lint.d.ts +78 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +153 -0
- package/dist/lint.js.map +1 -0
- package/dist/measurement.d.ts +44 -0
- package/dist/measurement.d.ts.map +1 -0
- package/dist/measurement.js +95 -0
- package/dist/measurement.js.map +1 -0
- package/dist/metrics.d.ts +70 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +119 -0
- package/dist/metrics.js.map +1 -0
- package/dist/overlays.d.ts +48 -0
- package/dist/overlays.d.ts.map +1 -0
- package/dist/overlays.js +48 -0
- package/dist/overlays.js.map +1 -0
- package/dist/perf.d.ts +62 -0
- package/dist/perf.d.ts.map +1 -0
- package/dist/perf.js +94 -0
- package/dist/perf.js.map +1 -0
- package/dist/platform.d.ts +40 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +61 -0
- package/dist/platform.js.map +1 -0
- package/dist/relationships.d.ts +79 -0
- package/dist/relationships.d.ts.map +1 -0
- package/dist/relationships.js +155 -0
- package/dist/relationships.js.map +1 -0
- package/dist/schedule.d.ts +84 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +91 -0
- package/dist/schedule.js.map +1 -0
- package/dist/state.d.ts +36 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +113 -0
- package/dist/state.js.map +1 -0
- package/dist/text-bodies.d.ts +71 -0
- package/dist/text-bodies.d.ts.map +1 -0
- package/dist/text-bodies.js +159 -0
- package/dist/text-bodies.js.map +1 -0
- package/dist/thread-overlay.d.ts +63 -0
- package/dist/thread-overlay.d.ts.map +1 -0
- package/dist/thread-overlay.js +110 -0
- package/dist/thread-overlay.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/visual-bindings.d.ts +95 -0
- package/dist/visual-bindings.d.ts.map +1 -0
- package/dist/visual-bindings.js +211 -0
- package/dist/visual-bindings.js.map +1 -0
- package/package.json +59 -0
package/dist/feedback.js
ADDED
|
@@ -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
|
package/dist/flip.js.map
ADDED
|
@@ -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"}
|
package/dist/governor.js
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|