From 44a499d052a92c776d7e72de4b6d1d059f90d7ec Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 26 Apr 2026 18:48:25 +0200 Subject: [PATCH] =?UTF-8?q?eidos:=20V1=20minimum=20viable=20=E2=80=94=20to?= =?UTF-8?q?kens=20+=20archetype=20rules=20+=20event=20reactions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First eidos code. Closes the cross-layer cycle that's been documented but unproven: an actual CSS layer that consumes the morfo + sema contracts and applies styles transversally without enumerating components. src/uix/eidos/ - tokens.css — motion durations (quick/fast/base) + easings (standard / emphasized / decelerate) + opacity scale (hover/disabled/faded) + intent tints (neutral/affirm/fulfill/risk/threat) + focus ring tokens. ~30 lines. No theming dimension yet — when it arrives, these are the base theme. - archetypes.css — transversal selectors keyed by `data-archetype`. Covers trigger, overlay, content, indicator, thumb, track, close, action, item, option. Plus a universal `:focus-visible` ring rule that applies to every archetype-classified element. Single attribute selector specificity — components can override freely. - events.css — reactions to perceptual signals during their ~13ms life. `[data-event^="dismiss"]` fades out, `[data-event^="announce"]` pulses with intent-tinted box-shadow, `[data-last-action]` tints exit animation by causa. - index.css — entrypoint that imports the above in cascade order. Consumer: `import '$uix/eidos/index.css'`. src/routes/test/eidos/+page.svelte (demo) - Sets up Soma + App context inline (independent of /test/soma layout). - Imports `$uix/eidos/index.css` and demonstrates the cross-layer story: - Toggle + Switch buttons share trigger styles via `[data-archetype="trigger"]` without per-component CSS. - Collapsible expands/collapses with state-driven attrs. - Toast intent buttons fire `data-event="announce" data-intent="risk|threat|affirm"`, which eidos's events.css briefly tints via box-shadow during the signal window. - Component-specific layout (positioning, switch thumb transform) lives in the page's ` diff --git a/src/uix/eidos/archetypes.css b/src/uix/eidos/archetypes.css new file mode 100644 index 000000000..386b4e73b --- /dev/null +++ b/src/uix/eidos/archetypes.css @@ -0,0 +1,121 @@ +/** + * Eidos — transversal selectors keyed by `data-archetype`. + * + * The whole point: a single rule applies to ALL components that share an + * archetype, regardless of their specific kebab. A `Toggle` button, a + * `Dialog.Trigger`, a `Popover.Trigger` and a `DropdownMenu.Trigger` all + * carry `data-archetype="trigger"`, so the rule below covers them all. + * + * Specificity is intentionally low (single attribute selector). Component + * tokens or scoped overrides win when present. + */ + +/* ── Trigger ─────────────────────────────────────────────────────────────── + * Applies to anything that the user clicks/taps to activate or open. + */ +[data-archetype='trigger'] { + cursor: pointer; + user-select: none; + transition: + opacity var(--eidos-motion-fast) var(--eidos-easing-standard), + background-color var(--eidos-motion-fast) var(--eidos-easing-standard); +} + +[data-archetype='trigger']:hover:not([data-disabled]):not([disabled]) { + opacity: var(--eidos-opacity-hover); +} + +[data-archetype='trigger'][data-disabled], +[data-archetype='trigger'][disabled] { + cursor: not-allowed; + opacity: var(--eidos-opacity-disabled); +} + +/* ── Overlay ────────────────────────────────────────────────────────────── + * Modal backdrops behind dialog/drawer/popover content. + */ +[data-archetype='overlay'] { + position: fixed; + inset: 0; + pointer-events: auto; +} + +/* ── Content ─────────────────────────────────────────────────────────────── */ +[data-archetype='content'] { + position: relative; +} + +/* ── Indicator ───────────────────────────────────────────────────────────── */ +[data-archetype='indicator'] { + pointer-events: none; + transition: transform var(--eidos-motion-fast) var(--eidos-easing-standard); +} + +/* ── Thumb ──────────────────────────────────────────────────────────────── + * Draggable handles (slider, switch, scroll thumb). + */ +[data-archetype='thumb'] { + cursor: grab; + user-select: none; + transition: transform var(--eidos-motion-fast) var(--eidos-easing-standard); +} + +[data-archetype='thumb']:active { + cursor: grabbing; +} + +[data-archetype='thumb'][data-disabled] { + cursor: not-allowed; + opacity: var(--eidos-opacity-disabled); +} + +/* ── Track ──────────────────────────────────────────────────────────────── */ +[data-archetype='track'] { + position: relative; +} + +/* ── Close ──────────────────────────────────────────────────────────────── + * Inherits trigger's interactive feel; overrides only what's specific. + */ +[data-archetype='close'] { + cursor: pointer; + user-select: none; + transition: opacity var(--eidos-motion-fast) var(--eidos-easing-standard); +} + +[data-archetype='close']:hover { + opacity: var(--eidos-opacity-hover); +} + +/* ── Action ─────────────────────────────────────────────────────────────── */ +[data-archetype='action'] { + cursor: pointer; + user-select: none; +} + +/* ── Focus ring — universal across archetype-classified elements ───────── + * Single rule covers triggers, closes, actions, items, options, etc. The + * `:focus-visible` pseudo respects keyboard vs. pointer focus correctly. + */ +[data-archetype]:focus-visible { + outline: var(--eidos-focus-ring-width) solid var(--eidos-focus-ring-color); + outline-offset: var(--eidos-focus-ring-offset); +} + +/* ── Item / Option ──────────────────────────────────────────────────────── */ +[data-archetype='item'], +[data-archetype='option'] { + cursor: pointer; + user-select: none; +} + +[data-archetype='item'][aria-selected='true'], +[data-archetype='option'][aria-selected='true'] { + background-color: hsl(220 90% 55% / 0.1); +} + +[data-archetype='item'][data-disabled], +[data-archetype='option'][data-disabled] { + cursor: not-allowed; + opacity: var(--eidos-opacity-disabled); +} diff --git a/src/uix/eidos/events.css b/src/uix/eidos/events.css new file mode 100644 index 000000000..df194f47b --- /dev/null +++ b/src/uix/eidos/events.css @@ -0,0 +1,77 @@ +/** + * Eidos — reactions to perceptual signals (`data-event*`). + * + * `Sema` writes `data-event="..." data-event-phase="active" data-intent="..."` + * to the target during ~13ms (1 rAF + 1 hold frame). These rules let CSS + * react to the signal during that window — typically by starting a + * transition that the structural state change (which lands ~5ms later) + * continues smoothly. + * + * Reactions here are intentionally minimal. The point is to demonstrate + * the cross-layer cycle works, not to provide a full motion system. + */ + +/* ── Base: any event signal during its active phase ────────────────────── + * Hint that something is happening. Components can override per-event. + */ +[data-event][data-event-phase='active'] { + will-change: opacity, transform; +} + +/* ── Dismiss family — fade out ─────────────────────────────────────────── + * Matches `data-event="dismiss"` and any `dismiss-*` variant + * (`dismiss-outside`, etc.) via the `^=` prefix selector. + */ +[data-event^='dismiss'][data-event-phase='active'] { + transition: opacity var(--eidos-motion-quick) var(--eidos-easing-standard); + opacity: var(--eidos-opacity-faded); +} + +/* ── Close family — same fade ─────────────────────────────────────────── */ +[data-event^='close'][data-event-phase='active'] { + transition: opacity var(--eidos-motion-quick) var(--eidos-easing-standard); + opacity: var(--eidos-opacity-faded); +} + +/* ── Present / open — fade in via transform ──────────────────────────── */ +[data-event^='present'][data-event-phase='active'], +[data-event^='open'][data-event-phase='active'] { + transition: + opacity var(--eidos-motion-fast) var(--eidos-easing-decelerate), + transform var(--eidos-motion-fast) var(--eidos-easing-emphasized); +} + +/* ── Announce — pulse via outline color (ARIA assertive surrogate) ──── */ +[data-event^='announce'][data-event-phase='active'] { + transition: box-shadow var(--eidos-motion-base) var(--eidos-easing-standard); +} + +[data-event^='announce'][data-event-phase='active'][data-intent='risk'] { + box-shadow: 0 0 0 var(--eidos-focus-ring-width) var(--eidos-tint-risk); +} + +[data-event^='announce'][data-event-phase='active'][data-intent='threat'] { + box-shadow: 0 0 0 var(--eidos-focus-ring-width) var(--eidos-tint-threat); +} + +[data-event^='announce'][data-event-phase='active'][data-intent='affirm'], +[data-event^='announce'][data-event-phase='active'][data-intent='fulfill'] { + box-shadow: 0 0 0 var(--eidos-focus-ring-width) var(--eidos-tint-affirm); +} + +/* ── Last action tinting (Dialog close-cancel vs close-save) ───────── + * `data-last-action` is written by `event.prewrite` BEFORE the structural + * change. Lives across the exit animation so CSS can tint by causa. + */ +[data-state='closed'][data-last-action='cancelled'] { + /* could tint border, animation, etc. — placeholder rule for the demo */ + --_eidos-exit-tint: var(--eidos-tint-neutral); +} + +[data-state='closed'][data-last-action='saved'] { + --_eidos-exit-tint: var(--eidos-tint-fulfill); +} + +[data-state='closed'][data-last-action='failed'] { + --_eidos-exit-tint: var(--eidos-tint-threat); +} diff --git a/src/uix/eidos/index.css b/src/uix/eidos/index.css new file mode 100644 index 000000000..a6c6d3a76 --- /dev/null +++ b/src/uix/eidos/index.css @@ -0,0 +1,29 @@ +/** + * Eidos — V1 entrypoint. + * + * Imports compose into the global cascade in the order tokens → + * archetypes → events. Components scoped CSS may override anything below + * via higher specificity or later import order. + * + * Consumer usage: + * import '$uix/eidos/index.css' + * (in app root or a route layout) + * + * What this V1 ships: + * - tokens: motion durations, easings, opacities, intent tints, focus ring + * - archetypes: transversal `[data-archetype=...]` selectors covering + * trigger / overlay / content / indicator / thumb / track / close / + * action / item / option, plus a universal focus-visible ring + * - events: reactions to `data-event*` perceptual signals (dismiss / + * close / present / open / announce) and `data-last-action` exit tinting + * + * What's deliberately NOT in V1: + * - theming dimension (no `[data-theme]` overrides yet) + * - per-component recipes (each component will get its own CSS later) + * - keyframes / @starting-style / animation primitives + * - sound / vibra (those are separate engines that subscribe to sema) + */ + +@import './tokens.css'; +@import './archetypes.css'; +@import './events.css'; diff --git a/src/uix/eidos/tokens.css b/src/uix/eidos/tokens.css new file mode 100644 index 000000000..8abac7b18 --- /dev/null +++ b/src/uix/eidos/tokens.css @@ -0,0 +1,42 @@ +/** + * Eidos — minimal motion + opacity tokens (V1). + * + * Intentionally small. Eidos V1 only ships what the cross-layer demos need: + * motion durations + easings for state transitions, a focus ring color, + * and a hover dim opacity. Full token system + theming arrives later. + * + * Naming: `--eidos-{category}-{name}` (single word per slot when possible). + * No theming dimension yet — when it arrives, these become the "base" + * theme and each `[data-theme]` overrides selectively. + */ + +:root { + /* Motion durations — used by archetype selectors + event reactions */ + --eidos-motion-quick: 120ms; + --eidos-motion-fast: 200ms; + --eidos-motion-base: 320ms; + + /* Easings */ + --eidos-easing-standard: cubic-bezier(0.4, 0, 0.2, 1); + --eidos-easing-emphasized: cubic-bezier(0.2, 0, 0, 1); + --eidos-easing-decelerate: cubic-bezier(0, 0, 0.2, 1); + + /* Opacity scale (used for hover dim, disabled state, fade-out) */ + --eidos-opacity-hover: 0.85; + --eidos-opacity-disabled: 0.5; + --eidos-opacity-faded: 0; + + /* Intent tints — V1 uses currentColor + alpha; semantic engines may + * override per-theme. Just enough to prove cross-layer reactivity. */ + --eidos-tint-neutral: hsl(220 10% 50%); + --eidos-tint-affirm: hsl(140 50% 45%); + --eidos-tint-fulfill: hsl(160 60% 40%); + --eidos-tint-risk: hsl(35 90% 50%); + --eidos-tint-threat: hsl(0 75% 50%); + + /* Focus ring — a single token for now, applies anywhere `:focus-visible` + * lands on an archetype-classified element. */ + --eidos-focus-ring-color: hsl(220 90% 55% / 0.6); + --eidos-focus-ring-width: 2px; + --eidos-focus-ring-offset: 2px; +}