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 `<style>` block — eidos provides only the cross-component
baseline, NOT per-component recipes.
What this proves
- The runtime emits `data-archetype` correctly in browser (not just in
unit tests).
- A single CSS rule can target ALL triggers across all components
without enumeration. Add a new component with `archetype: 'trigger'`
and it inherits the styling automatically.
- Sema's `data-event*` signals live in the DOM long enough for CSS to
react (the ~13ms window measured by `scripts/probe-toast-dismiss.ts`
is observable as a brief box-shadow flash on toasts).
- The cross-layer cycle Morfo declara → Soma transcribe → Sema emite →
Dom aplica → Eidos lee runs end-to-end.
What V1 deliberately excludes
- 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).
Verification
- /test/eidos returns 200; manual browser check shows triggers share
hover dim + focus ring across components, toasts flash intent tint on
appear.
- 68/68 smoke routes (smoke only scans /test/soma — eidos route is
outside that scope, manually verified).
- svelte-check unchanged at 155 (pure CSS additions).
morfo-runtime
parent
760575c419
commit
44a499d052
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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';
|
||||
@ -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;
|
||||
}
|
||||
Loading…
Reference in new issue