You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
89 lines
3.2 KiB
89 lines
3.2 KiB
import type { DomApplier, DomAttrValue } from '$adom';
|
|
import type { SemanticSignal } from './signal';
|
|
|
|
/**
|
|
* Project a `SemanticSignal` onto its target as `data-event-*` attrs.
|
|
*
|
|
* These are the **semantic tokens** the cascade matches against
|
|
* (`[data-event-family="signal"]`, `[data-event-intent="threat"]`, …) and
|
|
* the same tokens that eidos CSS reads to tint borders / pulse focus
|
|
* during the hold. Same surface for sema's signal-time matching and
|
|
* eidos's CSS animation.
|
|
*
|
|
* Ownership: `VisualChannel.prepare()` projects these attrs BEFORE the
|
|
* cascade resolver, through a `SignalProjector`. The engine only invokes
|
|
* generic channel prepare hooks and cleans their handles AFTER the visual
|
|
* hold.
|
|
*
|
|
* Attrs written:
|
|
* - `data-event` — signal.name (always)
|
|
* - `data-event-id` — signal.id (always; the engine generates one
|
|
* if the caller didn't pass it)
|
|
* - `data-event-phase` — `'active'` (always)
|
|
* - `data-event-family` — signal.family (when present)
|
|
* - `data-event-intent` — signal.intent (when present)
|
|
*/
|
|
export function eventAttrs(signal: SemanticSignal): Record<string, DomAttrValue> {
|
|
const attrs: Record<string, DomAttrValue> = {
|
|
'data-event': signal.name,
|
|
'data-event-id': signal.id ?? '',
|
|
'data-event-phase': 'active',
|
|
'data-event-family': signal.family,
|
|
'data-event-intent': signal.intent
|
|
};
|
|
|
|
return attrs;
|
|
}
|
|
|
|
export function clearEventAttrs(): Record<string, DomAttrValue> {
|
|
return {
|
|
'data-event': undefined,
|
|
'data-event-id': undefined,
|
|
'data-event-phase': undefined,
|
|
'data-event-family': undefined,
|
|
'data-event-intent': undefined
|
|
};
|
|
}
|
|
|
|
export function stampEventAttrs(
|
|
target: HTMLElement,
|
|
signal: SemanticSignal,
|
|
dom: DomApplier
|
|
): void {
|
|
dom.apply({
|
|
target,
|
|
attrs: eventAttrs(signal)
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Retire the projection — but ONLY if it is still the one this signal wrote.
|
|
*
|
|
* The surface is one slot per element (`data-event`, `-id`, `-phase`,
|
|
* `-family`, `-intent`), so a second occurrence on the same node overwrites
|
|
* the first. Until 2026-08-10 the cleanup took the signal and DISCARDED it
|
|
* (`_signal`), wiping whatever it found: the older signal, finishing its hold
|
|
* first, erased the projection of the newer one mid-hold. Three independent
|
|
* audits measured it — fable S1 (2026-07-01), sema S-17 (2026-08-05, knob:
|
|
* `commit-set` erased 24 ms after birth instead of 240) and blocks A-36/A-65
|
|
* (2026-08-01/09, reproduced in the browser) — and it survived all three.
|
|
*
|
|
* The id is already in the DOM, so ownership costs one attribute read. When it
|
|
* does not match, the stamp belongs to another occurrence and the right thing
|
|
* to do is nothing: whoever owns it will retire it when ITS hold ends.
|
|
*/
|
|
export function unstampEventAttrs(
|
|
target: HTMLElement,
|
|
signal: SemanticSignal,
|
|
dom: DomApplier
|
|
): void {
|
|
const owner = target.getAttribute('data-event-id');
|
|
// `owner === null` means nothing is stamped — cleaning is a harmless no-op
|
|
// and keeps the function idempotent, which the projection handle relies on.
|
|
if (owner !== null && signal.id !== undefined && owner !== signal.id) return;
|
|
dom.apply({
|
|
target,
|
|
attrs: clearEventAttrs()
|
|
});
|
|
}
|