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.
202 lines
6.5 KiB
202 lines
6.5 KiB
/**
|
|
* Eidos — reactions to perceptual signals (`data-event*`).
|
|
*
|
|
* The VisualChannel of Sema writes signal attrs under the `data-event-*`
|
|
* namespace during a configurable hold window:
|
|
* - data-event (event name, e.g. 'announce', 'dismiss')
|
|
* - data-event-id (occurrence id from the engine)
|
|
* - data-event-phase ('active')
|
|
* - data-event-family (optional, e.g. 'signal')
|
|
* - data-event-intent (optional, e.g. 'loss')
|
|
*
|
|
* The channel never touches state attrs (`data-state`, `data-intent`,
|
|
* `data-disabled`, ...) — those are owned by the runtime/morfo. Read
|
|
* `data-event-intent` for signal-bound intent reactions; read
|
|
* `data-intent` for the persistent state of a component.
|
|
*
|
|
* Hold defaults per family (internal to chans/visual.ts):
|
|
* - emerge / commit / handle: 240ms
|
|
* - signal / shift / sustain: 600ms
|
|
* - contact: 120ms
|
|
* Authors override per signal via `signal.hold`, or globally via
|
|
* `new EngineSemantic({ visual: { defaultHold } })`.
|
|
*
|
|
* Even with the configured hold, prefer `animation: @keyframes` over
|
|
* `transition` for the visible response: the animation runs to completion
|
|
* independent of the signal's lifetime, so it works correctly with custom
|
|
* holds and won't reverse mid-flight if the signal is cleared early.
|
|
*
|
|
* Rule of thumb:
|
|
* - Use `animation: @keyframes` for visible reactions to signals.
|
|
* - Use `transition` for ongoing state attrs (data-state, data-disabled).
|
|
*/
|
|
|
|
/* ── Keyframes ──────────────────────────────────────────────────────────── */
|
|
|
|
@keyframes eidos-announce-pulse-neutral {
|
|
0%,
|
|
100% {
|
|
box-shadow: 0 0 0 0 transparent;
|
|
}
|
|
20% {
|
|
box-shadow: 0 0 0 var(--focus-ring-width) var(--color-neutral-element);
|
|
}
|
|
}
|
|
|
|
@keyframes eidos-announce-pulse-affirm {
|
|
0%,
|
|
100% {
|
|
box-shadow: 0 0 0 0 transparent;
|
|
}
|
|
20% {
|
|
box-shadow: 0 0 0 var(--focus-ring-width) var(--color-fulfill-element);
|
|
}
|
|
}
|
|
|
|
@keyframes eidos-announce-pulse-fulfill {
|
|
0%,
|
|
100% {
|
|
box-shadow: 0 0 0 0 transparent;
|
|
}
|
|
20% {
|
|
box-shadow: 0 0 0 var(--focus-ring-width) var(--color-fulfill-element);
|
|
}
|
|
}
|
|
|
|
@keyframes eidos-announce-pulse-risk {
|
|
0%,
|
|
100% {
|
|
box-shadow: 0 0 0 0 transparent;
|
|
}
|
|
15% {
|
|
box-shadow: 0 0 0 4px var(--color-risk-element);
|
|
}
|
|
30% {
|
|
box-shadow: 0 0 0 2px var(--color-risk-element);
|
|
}
|
|
}
|
|
|
|
@keyframes eidos-announce-pulse-threat {
|
|
0%,
|
|
100% {
|
|
box-shadow: 0 0 0 0 transparent;
|
|
}
|
|
10% {
|
|
box-shadow: 0 0 0 6px var(--color-threat-element);
|
|
}
|
|
35% {
|
|
box-shadow: 0 0 0 3px var(--color-threat-element);
|
|
}
|
|
}
|
|
|
|
@keyframes eidos-dismiss-fade {
|
|
to {
|
|
opacity: 0;
|
|
transform: translateX(8%);
|
|
}
|
|
}
|
|
|
|
@keyframes eidos-present-rise {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(8px);
|
|
}
|
|
}
|
|
|
|
/* contact family — brief squeeze on press, ~120ms */
|
|
@keyframes eidos-press-squeeze {
|
|
0% {
|
|
transform: scale(1);
|
|
}
|
|
40% {
|
|
transform: scale(0.96);
|
|
filter: brightness(0.96);
|
|
}
|
|
100% {
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
|
|
/* commit family — small settle bump as the new state lands, ~240ms */
|
|
@keyframes eidos-commit-settle {
|
|
0% {
|
|
box-shadow: 0 0 0 0 rgba(99, 102, 241, 0);
|
|
}
|
|
30% {
|
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.32);
|
|
}
|
|
100% {
|
|
box-shadow: 0 0 0 0 rgba(99, 102, 241, 0);
|
|
}
|
|
}
|
|
|
|
/* ── Base: any event signal during its active phase ──────────────────────
|
|
* Hint to the compositor; cleared automatically when the signal disappears.
|
|
*/
|
|
[data-event][data-event-phase='active'] {
|
|
will-change: opacity, transform, box-shadow;
|
|
}
|
|
|
|
/* ── Dismiss family — fade + slide out (animation runs 320ms) ──────── */
|
|
[data-event^='dismiss'][data-event-phase='active'],
|
|
[data-event^='close'][data-event-phase='active'] {
|
|
animation: eidos-dismiss-fade var(--duration-slow) var(--ease-default) forwards;
|
|
}
|
|
|
|
/* ── Present / open — rise into place ─────────────────────────────── */
|
|
[data-event^='present'][data-event-phase='active'],
|
|
[data-event^='open'][data-event-phase='active'] {
|
|
animation: eidos-present-rise var(--duration-slow) var(--ease-out);
|
|
}
|
|
|
|
/* ── Announce — intent-tinted pulse (visible 600-1000ms) ────────────
|
|
* Reads data-event-intent (signal-bound), NOT data-intent (state) —
|
|
* the channel writes only its own namespace.
|
|
*/
|
|
[data-event^='announce'][data-event-phase='active'][data-event-intent='neutral'] {
|
|
animation: eidos-announce-pulse-neutral 600ms var(--ease-default);
|
|
}
|
|
[data-event^='announce'][data-event-phase='active'][data-event-intent='affirm'] {
|
|
animation: eidos-announce-pulse-affirm 600ms var(--ease-default);
|
|
}
|
|
[data-event^='announce'][data-event-phase='active'][data-event-intent='fulfill'] {
|
|
animation: eidos-announce-pulse-fulfill 600ms var(--ease-default);
|
|
}
|
|
[data-event^='announce'][data-event-phase='active'][data-event-intent='risk'] {
|
|
animation: eidos-announce-pulse-risk 800ms var(--ease-spring);
|
|
}
|
|
[data-event^='announce'][data-event-phase='active'][data-event-intent='threat'] {
|
|
animation: eidos-announce-pulse-threat 1000ms var(--ease-spring);
|
|
}
|
|
|
|
/* ── Press / contact — quick squeeze acknowledgement ─────────────────
|
|
* Fired on any control whose morfo declares a `contact` event (toggle,
|
|
* switch, button…). Brief (~120ms), non-valenced. The sema visual
|
|
* channel writes data-event-family='contact' during the hold.
|
|
*/
|
|
[data-event-family='contact'][data-event-phase='active'] {
|
|
animation: eidos-press-squeeze 140ms var(--ease-default);
|
|
}
|
|
|
|
/* ── Commit — consolidation pulse on the landing of a new state ──────
|
|
* Fired by morfos that declare a `commit` event (toggle press, switch
|
|
* change, dialog close-save, etc.). The pulse uses the indigo accent;
|
|
* intent variants below tint by valence.
|
|
*/
|
|
[data-event-family='commit'][data-event-phase='active'] {
|
|
animation: eidos-commit-settle 260ms var(--ease-out);
|
|
}
|
|
[data-event-family='commit'][data-event-intent='fulfill'][data-event-phase='active'] {
|
|
animation: eidos-announce-pulse-fulfill 320ms var(--ease-out);
|
|
}
|
|
[data-event-family='commit'][data-event-intent='affirm'][data-event-phase='active'] {
|
|
animation: eidos-announce-pulse-affirm 320ms var(--ease-out);
|
|
}
|
|
[data-event-family='commit'][data-event-intent='threat'][data-event-phase='active'] {
|
|
animation: eidos-announce-pulse-threat 400ms var(--ease-spring);
|
|
}
|
|
|
|
/* `data-last-action` tinting lives in component recipes (e.g.
|
|
* dialog.css), not here. If a future second consumer appears with the
|
|
* same exit-tint pattern, factor out then — not before. */
|