@vectojs/core 1.39.1 → 1.40.1

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.
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Per-node visual projection policy (RFC4 §2, CTX-0601).
3
+ *
4
+ * This is the RFC's `projection` field under its in-tree name: the per-node
5
+ * knob lives on {@link Entity.domPolicy} (named before the RFC), so
6
+ * `ProjectionPolicy` is the value vocabulary and `Entity.domPolicy` is where
7
+ * it is stored. `a11yProjection` keeps governing the _semantic/AT_ mirror
8
+ * independently — the two knobs compose, neither silently overrides the other
9
+ * (RFC4 §2).
10
+ *
11
+ * - `'canvas'` — always canvas pixels through the scene's `IRenderer`. The
12
+ * default: existing scenes behave byte-for-byte as today.
13
+ * - `'dom'` — always materialize through the DOM projection backend. Requires
14
+ * a DOM backend and a supported node kind; otherwise falls back per §3 and
15
+ * reports the fallback, never silently.
16
+ * - `'auto'` — the engine decides per node per frame from the Capability
17
+ * Matrix below: native-interaction value vs materialization cost vs backend
18
+ * availability. The only mode the engine may flip frame to frame.
19
+ */
20
+ export type ProjectionPolicy = 'canvas' | 'dom' | 'auto';
21
+ /** Where the negotiation landed, and why it landed anywhere but the request. */
22
+ export type ProjectionFallbackReason = 'no-dom-backend' | 'unsupported-kind' | 'prohibitive-cost' | 'bulk-budget' | 'active-gesture' | 'focus-pinned' | 'hysteresis';
23
+ /** Native-interaction value of materializing this kind in the DOM (§4). */
24
+ export type ProjectionNativeValue = 'none' | 'low' | 'medium' | 'high';
25
+ /** Materialization cost in one backend (§4). */
26
+ export type ProjectionCost = 'low' | 'medium' | 'high' | 'prohibitive' | 'unsupported' | 'n/a';
27
+ /**
28
+ * One Capability Matrix row (RFC4 §4): the negotiation inputs for a `domKind`.
29
+ *
30
+ * Values are **[opinions-to-measure]** starting positions from research §§8–10
31
+ * and the measured mirror costs — P1 must replace them with numbers. The only
32
+ * cell that overrides an explicit `'dom'` request besides `unsupported-kind`
33
+ * is `domCost: 'prohibitive'`, and even then it reports rather than refusing
34
+ * silently (§3 rule 2).
35
+ */
36
+ export interface ProjectionCapabilityRow {
37
+ /** `false` means the kind has no DOM representation at all. */
38
+ readonly domSupported: boolean;
39
+ /** What native interaction (selection, IME, focus, form semantics) DOM buys. */
40
+ readonly nativeValue: ProjectionNativeValue;
41
+ /** Cost of one live element for this kind. */
42
+ readonly domCost: ProjectionCost;
43
+ /** Cost of (re-)implementing this kind in canvas pixels. */
44
+ readonly canvasCost: ProjectionCost;
45
+ /**
46
+ * The table's last column verbatim: where `'auto'` rests when no rule fires.
47
+ * Unknown kinds (absent from the matrix) default to `'canvas'`.
48
+ */
49
+ readonly autoDefault: 'canvas' | 'dom';
50
+ }
51
+ /**
52
+ * Capability Matrix (RFC4 §4) keyed by `Entity.domKind`.
53
+ *
54
+ * The matrix is keyed by backend creation hint, not entity class: `domKind`
55
+ * is the tag/content mapping the DOM backend consumes, so the row travels
56
+ * with the representation, and custom kinds start at the unknown-kind default
57
+ * (`unsupported-kind` → canvas) until a scene registers a row via
58
+ * {@link Scene.registerProjectionCapability | Scene.registerProjectionCapability}.
59
+ */
60
+ export declare const DEFAULT_PROJECTION_CAPABILITIES: Readonly<Record<string, ProjectionCapabilityRow>>;
61
+ /** Fallback row for `domKind`s absent from the matrix: canvas, reported. */
62
+ export declare const UNKNOWN_PROJECTION_CAPABILITY: ProjectionCapabilityRow;
63
+ /**
64
+ * Consecutive syncs an `'auto'` node must keep voting for the other backend
65
+ * before the sticky resolution flips (RFC4 §3 rule 3).
66
+ *
67
+ * Documented tunable: scenes override per scene via the
68
+ * `projectionHysteresisFrames` option. Each flip pays `unmount` + `mount`
69
+ * plus, for text, layout handoff — the count is P1 measurement work, not dogma.
70
+ */
71
+ export declare const PROJECTION_AUTO_HYSTERESIS_FRAMES = 3;
72
+ /**
73
+ * Maximum `'auto'`-resolved DOM residents per scene per frame (RFC4 §4
74
+ * particle-row backstop: bulk-count nodes stay canvas).
75
+ *
76
+ * Explicit `'dom'` requests are the author's choice and bypass the budget;
77
+ * only the engine's own `'auto'` placements count. Documented tunable: scenes
78
+ * override per scene via the `projectionAutoDomBudget` option.
79
+ */
80
+ export declare const PROJECTION_AUTO_DOM_BUDGET = 500;
81
+ /** Inputs to {@link resolveProjection} beyond the node's own policy. */
82
+ export interface ProjectionNegotiationContext {
83
+ /** A `'dom'`-kind backend is registered on the scene. */
84
+ readonly domBackendMounted: boolean;
85
+ /** `false` under SSR/Node — negotiation short-circuits before the matrix. */
86
+ readonly hasDOM: boolean;
87
+ }
88
+ /** Output of {@link resolveProjection}: where, and why if not as requested. */
89
+ export interface ProjectionOutcome {
90
+ readonly resolved: 'canvas' | 'dom';
91
+ /** `null` when the resolution honors the request (or _is_ the default). */
92
+ readonly reason: ProjectionFallbackReason | null;
93
+ }
94
+ /**
95
+ * Capability negotiation (RFC4 §3): resolve one node's policy to a backend.
96
+ *
97
+ * Pure: hysteresis, gesture/focus pins, and the bulk budget live scene-side
98
+ * (they need cross-frame and cross-node state); this function is the
99
+ * per-node-per-sync rule set, so it stays unit-testable without a `Scene`.
100
+ *
101
+ * Rules:
102
+ *
103
+ * 1. Explicit beats automatic; possible beats explicit. `'canvas'` and a
104
+ * satisfiable `'dom'` are never second-guessed.
105
+ * 2. Fallbacks are reported, not silent — every non-honored request carries
106
+ * its reason for the per-scene queryable surface.
107
+ * 3. No-DOM environments always resolve `'canvas'` before touching the matrix.
108
+ */
109
+ export declare function resolveProjection(want: ProjectionPolicy, cap: ProjectionCapabilityRow, ctx: ProjectionNegotiationContext): ProjectionOutcome;
110
+ /**
111
+ * One node's last negotiation, for the per-scene queryable surface (RFC4 §3
112
+ * rule 2 — precedent shape: the proposed `scene.inputCapabilities` in
113
+ * input-dispatch-contract-v2 §4). Plain data only: resolution data never
114
+ * carries an `HTMLElement`, materialization stays in `@vectojs/dom`.
115
+ */
116
+ export interface ProjectionResolution {
117
+ readonly nodeId: string;
118
+ readonly want: ProjectionPolicy;
119
+ readonly resolved: 'canvas' | 'dom';
120
+ /** `null` when the resolution honors the request. */
121
+ readonly reason: ProjectionFallbackReason | null;
122
+ }
123
+ /**
124
+ * Scene-level projection capabilities (RFC4 §3 rule 2).
125
+ *
126
+ * Follows the proposed `scene.inputCapabilities` precedent shape
127
+ * (input-dispatch-contract-v2 §4 "Feature detection story"): a plain-data
128
+ * getter apps and devtools query to adapt, never to mutate.
129
+ */
130
+ export interface SceneProjectionCapabilities {
131
+ /** A real DOM is present (false under SSR/Node). */
132
+ readonly hasDOM: boolean;
133
+ /** A `'dom'`-kind backend is registered on this scene. */
134
+ readonly domBackendMounted: boolean;
135
+ /** Kinds of all registered backends, in registration order. */
136
+ readonly backends: readonly string[];
137
+ /** Live hysteresis tunables (scene overrides of the module constants). */
138
+ readonly autoHysteresisFrames: number;
139
+ readonly autoDomBudget: number;
140
+ }
141
+ /** Per-node hysteresis + memo state owned by the scene (RFC4 §3 rule 3). */
142
+ export interface ProjectionHysteresisState {
143
+ resolved: 'canvas' | 'dom';
144
+ /** Consecutive syncs voting against {@link resolved}. */
145
+ consecutive: number;
146
+ /** Frame id of the last vote, so render + a11y sync agree within a frame. */
147
+ lastFrame: number;
148
+ }
149
+ /**
150
+ * Sticky-vote half of hysteresis, factored pure for tests.
151
+ *
152
+ * Returns the resolution to keep plus the updated consecutive count: votes
153
+ * for the current backend reset the counter, votes against accumulate until
154
+ * `hysteresisFrames` consecutive, at which point the flip commits. The first
155
+ * vote for a node (no current) always commits — stickiness pins flips, never
156
+ * first placement.
157
+ */
158
+ export declare function hysteresisVote(current: 'canvas' | 'dom' | undefined, desired: 'canvas' | 'dom', consecutive: number, hysteresisFrames: number): {
159
+ resolved: 'canvas' | 'dom';
160
+ consecutive: number;
161
+ flipped: boolean;
162
+ };
@@ -0,0 +1,119 @@
1
+ import type { Entity } from '../Entity';
2
+ /**
3
+ * Per-node semantic projection decision (RFC3 §5, CTX-0599).
4
+ *
5
+ * - `'project'` — materialize the node's semantic mirror (today's behaviour).
6
+ * - `'defer-to-browser'` — let browser recovery (canvas-a11y capture,
7
+ * `html-in-canvas`, …) own this node's platform semantics. Allow-listed per
8
+ * content class (§5): plain display text first, never controls, and always
9
+ * feature-detected at runtime with a projection fallback. Unreachable while
10
+ * {@link supportsHTMLInCanvas} is `false` — see
11
+ * {@link isDeferrableSemanticNode}.
12
+ * - `'never'` — suppress the node's semantic mirror (the policy-level
13
+ * equivalent of `Entity.a11yProjection: 'never'`).
14
+ *
15
+ * Adapters plug in _below_ the semantic tree: a future backend changes how
16
+ * text is painted, not what the entity means. The upper API (`projection`,
17
+ * `selectable`, `A11yAttributes`) stays stable across backend switches.
18
+ */
19
+ export type SemanticProjectionDecision = 'project' | 'defer-to-browser' | 'never';
20
+ /**
21
+ * Browser capabilities the policy may consult. Sourced live per decision via
22
+ * {@link getSemanticProjectionCapabilities} — capability only narrows cost,
23
+ * the framework-known default stays `'project'`.
24
+ */
25
+ export interface SemanticProjectionCapabilities {
26
+ /** A real `html-in-canvas` backend exists (cf. RFC §7 standing). */
27
+ readonly htmlInCanvas: boolean;
28
+ /**
29
+ * Stable cross-engine canvas text recovery exists. Chromium capture is
30
+ * experimental as of RFC §7, so per RFC §3 rule 2 no conformance claim may
31
+ * depend on it — honestly `false` until that changes.
32
+ */
33
+ readonly canvasTextRecovery: boolean;
34
+ }
35
+ /** Runtime environment the policy may consult. */
36
+ export interface SemanticProjectionEnvironment {
37
+ /** `false` under SSR/Node, where projection is a no-op (the `!a11yRoot` guard). */
38
+ readonly hasDOM: boolean;
39
+ }
40
+ /**
41
+ * Policy seam for the semantic tier (RFC3 §5). Wired at the single point
42
+ * where the tier decides per node (`Scene.shouldProjectA11y`); the default
43
+ * below preserves current behaviour exactly.
44
+ *
45
+ * Constraints on implementations (RFC §5):
46
+ *
47
+ * - `defer-to-browser` is allow-listed per content class via
48
+ * {@link isDeferrableSemanticNode} — plain display text first, never the
49
+ * default for controls — and always feature-detected at runtime with a
50
+ * projection fallback.
51
+ * - A throwing policy must never drop semantics; `Scene` falls back to
52
+ * `'project'` on throw.
53
+ */
54
+ export interface SemanticProjectionPolicy {
55
+ /**
56
+ * Framework-known default per node; browser capability only narrows cost.
57
+ *
58
+ * @param node - The entity the semantic tier is deciding about.
59
+ * @param capabilities - Live browser capabilities (see {@link getSemanticProjectionCapabilities}).
60
+ * @param environment - Runtime environment (DOM presence, …).
61
+ */
62
+ choose(node: Entity, capabilities: SemanticProjectionCapabilities, environment: SemanticProjectionEnvironment): SemanticProjectionDecision;
63
+ }
64
+ /**
65
+ * Framework-known default: always `'project'`. Browser capability only
66
+ * narrows cost, never widens it — so the out-of-the-box behaviour is
67
+ * identical to having no policy at all.
68
+ */
69
+ export declare const DEFAULT_SEMANTIC_PROJECTION_POLICY: SemanticProjectionPolicy;
70
+ /** Live capabilities snapshot for {@link SemanticProjectionPolicy.choose}. */
71
+ export declare function getSemanticProjectionCapabilities(): SemanticProjectionCapabilities;
72
+ /**
73
+ * Whether `node` belongs to a content class that may one day defer its
74
+ * platform semantics to the browser (RFC §5 allow-list).
75
+ *
76
+ * Allow-listed today: nodes with no control semantics — no native-control tag,
77
+ * no control role, no keyboard tab stop — i.e. plain display text first, per
78
+ * §5. Everything else (controls, tab stops, natively selectable text whose
79
+ * DOM mirror owns selection per RFC §6) always keeps its framework mirror.
80
+ *
81
+ * Structural today: `Scene` additionally requires a real backend before any
82
+ * deferral takes effect, so this predicate changes no behaviour on its own —
83
+ * it is the gate a future backend consults, and the contract custom policies
84
+ * must honour when returning `'defer-to-browser'`.
85
+ */
86
+ export declare function isDeferrableSemanticNode(node: Entity): boolean;
87
+ /**
88
+ * Honest detail behind {@link supportsHTMLInCanvas}: `supported` is `false`
89
+ * until a real backend exists, and `reason` says why.
90
+ */
91
+ export type HTMLInCanvasSupport = {
92
+ readonly supported: true;
93
+ } | {
94
+ readonly supported: false;
95
+ readonly reason: 'no-dom' | 'no-backend';
96
+ };
97
+ /**
98
+ * Feature-detection contract for a future `html-in-canvas` backend
99
+ * (RFC §5 + RFC2 §8). Honest by construction:
100
+ *
101
+ * - SSR/Node (`typeof document === 'undefined'`) → `{ supported: false }`
102
+ * without touching the DOM at all (core owns no unguarded
103
+ * `document`/`window` contact per RFC1 §4).
104
+ * - Any probe throw (e.g. jsdom's unimplemented `getContext`) → `false`.
105
+ * - No backend today → `{ supported: false, reason: 'no-backend' }`.
106
+ *
107
+ * The probe is written as a real detection — a `2d` context exposing the
108
+ * `html-in-canvas` entry point (`drawElementImage`, RFC §7) reports `true` —
109
+ * so the day a browser ships it, this flips without a framework change. Until
110
+ * then there are no fake positives: nothing claims support it cannot use.
111
+ */
112
+ export declare function describeHTMLInCanvasSupport(): HTMLInCanvasSupport;
113
+ /**
114
+ * Whether an `html-in-canvas` backend is available right now (RFC §5 seam).
115
+ * `false` until a real backend exists — detection contract per RFC §5 +
116
+ * RFC2 §8, no fake positives. See {@link describeHTMLInCanvasSupport} for
117
+ * the reasoned form.
118
+ */
119
+ export declare function supportsHTMLInCanvas(): boolean;
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/core",
3
- "version": "1.39.1",
3
+ "version": "1.40.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -74,7 +74,7 @@
74
74
  "@vitest/coverage-v8": "^4.1.11",
75
75
  "esbuild": "^0.28.1",
76
76
  "jsdom": "^30.0.1",
77
- "puppeteer-core": "^25.8.0",
77
+ "puppeteer-core": "^25.9.0",
78
78
  "tsup": "^8.3.5",
79
79
  "vitest": "^4.1.11"
80
80
  }