astra
alpha-0.1-background
alpha-0.1-dir-prefs
alpha-0.1-sec-dom
menubar-v4-safe
active-uix
morfo-runtime
morfo-driven-soma
semantuix
glm-5
main
sium-v1.0
${ noResults }
156 Commits (morfo-runtime)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
217d6969e4 |
active-uix: uix.runtime(morfo, sources) auto-injects dom + semantic
Adds a single convenience method on `ActiveUix` so soma providers stop
duplicating the `dom: this.soma.dom, semantic: this.soma.semantic`
boilerplate at every `createMorfoRuntime` callsite.
Before (8 providers, identical lines):
this.runtime = createMorfoRuntime(dialogMorfo, {
dom: this.soma.dom, // ← duplicated everywhere
semantic: this.soma.semantic, // ← duplicated everywhere
states: { … },
props: { … },
events: { … }
})
After:
this.runtime = this.soma.uix.runtime(dialogMorfo, {
states: { … },
props: { … },
events: { … }
})
The implementation is a thin convenience — internally `uix.runtime()`
calls `createMorfoRuntime(morfo, { dom: this.dom, semantic: this.semantic,
…sources })`. No registry, no scope ids, no per-instance lifecycle to
track. Per-provider runtime ownership unchanged. The user explicitly
chose this minimal fix over a "single shared runtime + scope handles"
design after weighing the tradeoffs.
Migrated callsites: avatar, collapsible, dialog, drawer, popover,
switch, toast, toggle (8 providers, ~10 callsites including toast's
two — root + item). The `createMorfoRuntime` named import dropped from
each (only `MorfoRuntime` type kept where the field is annotated).
3 new tests on `active-uix.svelte.test.ts`:
- returns a MorfoRuntime with auto-injected services
- caller does not pass dom or semantic
- attach mode uses the external app's dom + semantic
Verified: vitest 17/17 active-uix (was 14), 211/211 UIX, smoke 68/68,
morfo-check 66/66.
|
5 months ago |
|
|
9009cbfe8c |
active-uix: standalone mode no longer wraps active-app
Earlier design mistake: `createActiveUix(options)` was internally
calling `createActiveApp(...)` to compose the UIX services. That
violated the contract — active-uix was supposed to be either a
substitute for active-app (when no app exists) or a complement (when
an external app injects services).
Fix: standalone mode now instantiates each UIX service directly via
the canonical factories — no `active-app` involved:
- core: createEngineLogger, createEngineBus, createActiveTimers,
createActivePrefs (with empty schema)
- lang: createActiveLang(schema, defaultLocale, fallbackChain)
- dom: createActiveDom
- frontend: createActiveFrontend (auto-applies to <html>)
- format: createActiveFormat (clock from timers)
- semantic: new EngineSemantic(opts)
active-uix manages every owned lifecycle: dispose() tears them down
in reverse dependency order. Attach mode is unchanged — wraps an
external app, validates UIX services, doesn't touch app.dispose().
Type changes:
- `ActiveUix.app: ActiveApp | undefined`. Present only in attach
mode (it's the underlying app); `undefined` in standalone (no
app exists).
- `ActiveUix.bus: EngineBus` (generic envelope). Standalone gets
a fresh bus, attach gets the App's bus with its event map. UIX
components don't care about typed events; consumers that do
re-narrow at the call site.
- Removed `prefs?` and `extraServices?` from `ActiveUixOptions`.
Apps that need a custom prefs schema or services beyond UIX
should use attach mode (compose `createActiveApp` themselves).
New tests cover both modes: standalone (8 tests including service
disposal verification, idempotent dispose, format opt-out,
componentLangs registration toggle), attach (6 tests including
missing-services error, identity-equal service proxying, no
disposal of external app), and cross-mode parity (1 test).
Verified: vitest 15 files / 211 tests, smoke 68/68, morfo-check 66/66.
|
5 months ago |
|
|
83b841a7ca |
active-uix: introduce UIX layer between active-app and components
Decouples the UIX layer (soma / morfo runtime / sema / eidos) from
the App composition layer (`active-app`). UIX components no longer
import `\$active-app` directly — they read services through
`getActiveUix()`. Two boot paths produce the same `ActiveUix`:
- **Standalone** — `createActiveUix({ lang, semantic, … })`
bootstraps an `ActiveApp` internally with the UIX services plus
any `extraServices`. Owns the app — `dispose()` propagates.
- **Attach** — `attachActiveUix(externalApp)` wraps an app the
application composed itself (typically when it has services
beyond UIX: cache, session, http, …). The external app must
declare the UIX services; helper `defineUixServices(options)`
returns the slice ready to spread into
`createActiveApp({ services })`. Does NOT own the app.
In both cases components see only `ActiveUix`. They have no way to
tell which boot path was used.
Module:
src/uix/active-uix/
├── index.ts barrel ($active-uix)
├── types.ts ActiveUix, ActiveUixOptions, AttachActiveUixOptions
├── services.ts defineUixServices() + UIX_REQUIRED_SERVICES
├── active-uix.svelte.ts createActiveUix() + attachActiveUix() + ActiveUixImpl
├── components/uix.svelte <Uix uix={...}> wrapper (sets context + creates Soma)
└── svelte/context.svelte.ts setActiveUix() / getActiveUix() / tryGetActiveUix()
Surface exposed by `ActiveUix`:
- UIX services: lang, frontend, format (optional), dom, semantic
- Core: logger, bus, timers, prefs
- Config: somaPortalTo
- `app` escape hatch for advanced integration code (NOT for UIX
components)
Wiring done by active-uix at boot:
- `App.lang.extend('components', componentLangs)` — soma component
translations always available without per-app boilerplate.
- Frontend service auto-configured with `applyDom: true` +
`target: () => document.documentElement` so theme/dir/density
land on `<html>` automatically.
Migration:
- `Soma.create()` reads `getActiveUix()` instead of `getActiveApp()`.
Soma class no longer imports `\$active-app`.
- 3 layouts (test/eidos, test/sium, test/soma) replace
`createActiveApp + setActiveApp + <Soma>` with
`createActiveUix + <Uix>`. Boilerplate drops ~20 lines per layout.
- 7 sium pages replace `getActiveApp() + app.lang.setLocale(…)`
with `getActiveUix() + uix.lang.setLocale(…)`.
Validation:
- `attachActiveUix(app)` throws `ActiveUixMissingServiceError` if
the external app didn't declare lang/frontend/dom/semantic.
- `getActiveUix()` throws `ActiveUixNoContextError` if no `<Uix>`
ancestor mounted.
`\$active-uix` alias wired in svelte.config.js + vite.config.ts (both
client and server projects).
Verified: vitest 197/197, smoke 68/68, morfo-check 66/66, playwright
probe (4 routes including dialog with sound) zero page/console errors.
|
5 months ago |
|
|
5898cc58c7 |
libs/reactive: export ActiveProps and StateProps types
Follow-up to
|
5 months ago |
|
|
4bf6dea420 |
remove backward-compat re-export shims
Per architectural feedback: when relocating code to its correct layer,
do the migration completely — update every consumer directly and
delete the original. No re-export barrels at the old paths.
Removed shims:
- `src/uix/soma/attrs/` — was re-exporting `createAttrs` /
`registerContract` / `assertContract` / `getContract` from
`$uix/morfo` and the bool/data-attr formatters from `$libs/dom`.
All 66 importing files updated to import from the canonical
locations directly.
- `src/uix/soma/reactive/` — `reactive.svelte.ts` was a thin
re-export of `$libs/reactive`. The 79 consumers (every soma
provider + layers + types) now import primitives directly from
`$libs/reactive`. The `opts.ts` Props↔Opts bridge moved to
`src/uix/soma/provider/opts.ts` (its natural home — provider
plumbing, not reactive primitives).
- `attachRef` / `RefAttachment` re-export from
`src/uix/soma/provider/provider.svelte.ts` and from
`src/uix/soma/index.ts`. Both removed. Consumers
(`floating.svelte.ts`, `date-field-provider.svelte.ts`) now
import directly from `$libs/reactive`.
Also:
- Moved `ActiveProps<T>` / `StateProps<T>` types to
`src/libs/reactive/types.ts` — they're trivial generic helpers
over `Active<T>` / `State<T>` and shouldn't have lived in soma.
- Stripped soma's root barrel (`src/uix/soma/index.ts`) of every
re-export of libs/morfo concerns. It now only re-exports
actually-soma surfaces (Provider base, mergeProps, css helpers,
id, types, Soma class).
Result:
- `grep -r '\$soma' src/uix/morfo` → 0 matches
- `grep -r '\$uix' src/libs` → 0 matches
- `src/uix/soma/attrs/` and `src/uix/soma/reactive/` no longer exist
- Every import statement now reflects the real architectural layer
Verified: vitest 19/20 files / 214 tests (1 pre-existing air dead-
branch fail using `bun:test`), smoke 68/68, morfo-check 66/66.
|
5 months ago |
|
|
d9b6521020 |
fix dependency direction: morfo no longer imports from soma
The previous DOM consolidation commit left a wrong-direction edge —
`src/uix/morfo/runtime.svelte.ts` imported `createAttrs` from
`$soma/attrs` and `attachRef`/`RefAttachment` from `$soma/provider`.
Morfo is the declaration layer; soma is one of its consumers; morfo
must not depend on soma.
Moves:
- `createAttrs` (`src/uix/soma/attrs/create-attrs.ts`) →
`src/uix/morfo/create-attrs.ts`. It's a pure morfo→data-attr-name
derivation; its natural home is alongside the morfo declarations it
derives from. Soma is a consumer like any other.
- `contracts.ts` (registerContract / assertContract / getContract)
→ `src/uix/morfo/contracts.ts`. Same reasoning: contracts derive
from a morfo's `data: [{ values }]` enum.
- `attachRef` + `RefAttachment` (extracted from
`src/uix/soma/provider/provider.svelte.ts`) →
`src/libs/reactive/attach-ref.svelte.ts`. It bridges Svelte's
`{@attach}` system with the `State` reactive box; both ends are in
libs/reactive, so it lives there.
- Bool/data-attr formatters from `src/uix/soma/attrs/helpers.ts`
(boolToStr, boolToEmptyStrOrUndef, boolToTrueOrUndef,
getDataOpenClosed, getDataChecked, getAriaChecked) →
`src/libs/dom/attrs.ts`. Pure DOM-attr value formatters with no
morfo dependency — they're libs/dom material.
Critical bug fix uncovered by the move:
`src/uix/soma/reactive/reactive.svelte.ts` was a parallel
implementation of the reactive primitive system, with its OWN
`ActiveSymbol` / `WritableSymbol` instances — different from the
ones in `$libs/reactive`. As soon as `attachRef` (now in libs)
tried to detect a soma-created `State` via `isState`, the guard
returned false and the function fell into the setter-fn branch,
throwing "ref is not a function" in 10+ form tests.
Fix: rewrite `soma/reactive/reactive.svelte.ts` as a thin re-export
of `$libs/reactive`. They now share the same symbol identities, so
every cross-layer guard works consistently.
Compatibility:
- `$soma/attrs` is now a backward-compat barrel that re-exports from
`$uix/morfo` (createAttrs, contracts) and `$libs/dom` (helpers).
Existing soma providers continue working unchanged.
- `$soma/provider/provider.svelte.ts` re-exports `attachRef` /
`RefAttachment` from libs for any importer that still uses the
legacy path.
Verified post-cleanup:
- `grep -r '$soma' src/uix/morfo` → 0 matches (no morfo→soma)
- `grep -r '$uix' src/libs` → 0 matches (no libs→uix)
- vitest 14 files / 197 tests
- smoke 68/68
- morfo-check 66/66
|
5 months ago |
|
|
9b07cead6b |
uix: consolidate DOM + relocate morfo runtime to its layer
Three architectural cleanups in one cut:
1. **morfo runtime relocates to its own layer.** The MorfoRuntime + the
pure morfo→attrs resolver (and their tests) move from
`src/uix/soma/morfo/` → `src/uix/morfo/`. They were never
soma-specific — they interpret a morfo declaration on behalf of any
consumer. Soma was just the only consumer at the time. The new
barrel at `$uix/morfo` re-exports `createMorfoRuntime`,
`MorfoRuntime`, `MorfoRuntimeSources`, `RegisterPartOpts`,
`SourceMap`, `SemanticEmitter`, `TriggerOptions`, plus the resolver
helpers (`findMorfoPart`, `resolvePartAttrs`, `shouldEmitMorfoEntry`,
etc.).
2. **`src/uix/adom/` is gone.** The runtime pieces (`createActiveDom`,
`BodyScrollLock`, `DOMContext`, `RovingFocusGroup`, viewport) all
live at `$adom` (`src/arts/adom/`) which is the canonical
ecosystem-level ActiveDom. The `apply.ts` types and helpers
(`DomAttrValue`, `applyChange`, `removeAttrs`, `StructuralChange`)
live in `$libs/dom` and are re-exported from `$adom`. The 4 last
`$uix/adom` import sites updated to `$adom`.
3. **`src/uix/soma/dom/` is gone.** The 14 utilities (`isBrowser`,
`getDocument`, `isHTMLElement`, focus + tabbable helpers, etc.) all
exist as a superset in `$libs/dom`. The 12 soma consumers updated
their imports from `'../dom'` / `'$soma/dom'` → `'$libs/dom'`. No
functional change — purely import-path consolidation.
`src/uix/active-uix/` is preserved as an empty placeholder for the
future cross-layer composition class that will set up morfo + soma +
sema + eidos together.
The relocated runtime now imports cleanly:
- `ActiveDom`, `DomAttrValue` ← `$adom`
- `EngineSemantic`, sema helpers ← `$uix/sema`
- `Active`, `State` ← `$libs/reactive` (was `$soma/reactive`)
- `createAttrs` ← `$soma/attrs` (cross-layer; createAttrs is morfo
logic that lives in soma for now — slated for relocation in a
follow-up)
- `attachRef`, `RefAttachment` ← `$soma/provider/...` (idem)
Verified: vitest 14 files / 197 tests, smoke 68/68, morfo-check 66/66.
|
5 months ago |
|
|
a6e2c1e443 |
sema: align vocabulary with libro_semantica (signal, shift, loss)
Updates the canonical semantic vocabulary to match the latest
formulation in src/docs/libro_semantica_completo.txt:
Families (7, was 6):
contact, commit, signal, handle, emerge, shift, sustain
- `alert` → `signal` (rename: same perceptual function — attention /
salience — but the book's name reflects the broader scope including
notify, warn, emphasize, announce).
- `shift` (NEW, transitional): frame change. Modal mount, navigation,
mode change. Differs from emerge in that it reorganizes planes
(heavier backdrop, deeper shadow) — "cruce de umbral" not just
"aparición". Per book rule, the frame doesn't carry intent on its
own; evaluation belongs to a companion `signal` event.
Intents (6, was 5):
neutral, affirm, fulfill, risk, threat, loss
- `loss` (NEW): consummated negative consequence (negative valence,
low activation, posterior). Differs from `threat` (anterior, high
activation, convokes action): loss registers, doesn't convoke. Per
book §11.5 "la amenaza no es la pérdida".
sema-map.ts perceptual values:
- `signal` family entry: kept the prior `alert` numbers verbatim
(semantics is the same — attention/salience) under the new name.
- `shift` family entry: motion 260ms ease-in-out 0.98→1, sound
pitch 500 ascending gain 0.18 (deliberately quieter than emerge's
0.20 to not compete with companion signal), presence with stronger
backdrop (0.5) + shadow (blur 32 y 12 op 0.2) emphasizing
plane reorganization.
- `loss` intent deltas: descending sound contour (-150 pitch),
contraction (scale.to -0.04, motion duration ×0.95 — brief but
grave), color toward purple-grave (hue replace 280, lower
saturation/intensity), withdrawn presence (backdrop 0.05, no glow).
ARIA `role="alert"` (W3C live region) is unrelated and untouched in
soma/announce, soma/toast and morfo announce/field/form. The toast
morfo gains `loss → role:'status' aria-live:'polite'` mapping —
loss is consummated so it shouldn't interrupt.
Verbs added per the book's worked examples: signal {warn, notify,
emphasize}, shift {enter-mode, exit-mode, navigate}, commit {save,
discard, delete}, contact {press, click, hover}, handle {pick, carry,
drop}, emerge {reveal, show, hide}.
Verified: vitest 9 files / 99 tests, smoke 68/68, morfo-check 66/66.
|
5 months ago |
|
|
3faa4473f2 |
eidos/accordion: fix trigger rotating 90deg when open
The air → eidos refactor stripped the `.air-accordion-*` class hooks but
left 5 rulesets with their selector empty (just whitespace before `{`).
The most visible damage was line 161:
[data-accordion-trigger][data-state='open'] {
transform: rotate(var(--eidos-accordion-indicator-rotation));
}
Empty selector after the trailing space → the rule matched the trigger
button itself instead of the intended `.air-accordion-trigger-indicator`
descendant. So expanding any item rotated its title 90deg.
The other four orphaned rules (trigger-main flex layout, trigger-label,
indicator size/transition, content-inner padding) were dormant noise but
also dropped — the morfo doesn't declare an Indicator part and the demo
doesn't render the inner wrapper, so there's nothing to style.
Content padding is now applied directly to `[data-accordion-content]`
since there's no inner wrapper. Animation-height includes padding, which
matches the natural scrollHeight measurement.
Verified:
- playwright probe: trigger `transform: none` in both closed and open
state (was `matrix(...)` rotating 90deg before)
- smoke 68/68
|
5 months ago |
|
|
930d4c8c16 |
soma: emit perceptual signal on open in dialog/drawer/popover
Diagnosis: clicking "open" on dialog/drawer/popover produced no sound
(or any semantic feedback). Cause: every overlay trigger was bypassing
`runtime.trigger(...)` with a direct `handleOpen()` because the morfo's
declared target part (overlay content) doesn't yet exist in the DOM
when the trigger fires — and `runtime.trigger` was throwing when the
target part had no registered ref.
Fix:
- `MorfoRuntime.trigger(name, opts?)` now accepts `{ fallbackTarget?:
HTMLElement }`. When the declared target part is unmounted, the
fallback is used as the visual signal target so the perceptual
signal still fires. Sound / vibra channels don't care about the
target; the visual channel writes its `data-event*` to the trigger
briefly, which is the natural locus of the user's action anyway.
- Dialog / Drawer / Popover triggers now go through `runtime.trigger`
with the trigger element as `fallbackTarget`. Each provider's
`events` map gains an `open` (or `present`) entry that runs
`handleOpen()` as the runtime's handler step — preserving the
prewrite → emit → handler order.
- Popover's toggle uses `close-dismiss` for the close direction so
both directions emit a perceptual signal, not just open.
Soma class: `soma.dom` and `soma.semantic` are now non-optional again
(matching pre-migration semantics — every layout declares both
services). `soma.format.*` slices stay optional.
Verified end-to-end with a Playwright probe that spies AudioContext
construction:
Open dialog → 2 oscillators (was 0)
Press Escape → +2 (was 2)
Click overlay → +2 (was 0)
Gates: vitest 7 files / 78 tests, smoke 68/68, morfo-check 66/66.
|
5 months ago |
|
|
86f01f5d8e |
sema: revive resolver + signatures + real SoundChannel
Restores the resolver-driven architecture that was deleted in the
channel-modules refactor (commit
|
5 months ago |
|
|
9338be2662 |
soma+layouts: migrate App composition to createActiveApp (Phase 4 part 3)
Replaces the legacy `$lib/ext/app` `App.create({ langs, presentation })`
composition pattern with the new ecosystem's `createActiveApp({ services
})` across all three test layouts (eidos / sium / soma) and rewires Soma
+ morfo runtime to consume the new surface.
What changed:
- Soma class (uix/soma/core/soma.svelte.ts) now reads from `getActiveApp()`
in $active-app context and adapts its historical names over the new
ecosystem: `soma.langs → app.lang`, `soma.presentation → app.frontend`,
`soma.dates/nums/money/units → app.format.{dates,numbers,currency,units}`.
All 12 soma component providers consume Soma unchanged.
- New context bridge: $active-app exports `setActiveApp(app)` /
`getActiveApp()` / `tryGetActiveApp()` (mirrors `setBus` / `getBus`
from $bus). Throws `AappNoContextError` (new code APP_ERR_NO_CONTEXT)
when no App is in scope.
- 3 layouts (test/eidos, test/sium, test/soma) replace
`App.create({ langs, presentation })` with `createActiveApp({ services:
{ lang, frontend, format, dom, semantic } })` + `setActiveApp(App)` and
call `App.lang.extend(...)` post-construction for runtime translation
modules.
- 7 sium test pages migrate `App.get()` → `getActiveApp()`,
`app.langs` → `app.lang`, `app.setLocale(x)` → `app.lang.setLocale(x)`.
- Morfo runtime exports a tiny `SemanticEmitter` type (`Pick<EngineSemantic,
'emit'>`) so consumers and tests don't have to satisfy the full
EngineSemantic surface.
- soma/core/langs.ts and sium plural import migrate `LangNode` /
`p` from `$lib/ext/lang/*` → `$libs/lang`.
After this commit no consumer outside `src/lib/` imports from
`$lib/ext/app`, `$lib/ext/lang`, or `$lib/ext/presentation`. The legacy
shims under `src/lib/ext/` remain for now (Phase 5 will delete them).
Verified:
- svelte-check: only 2 pre-existing errors (event.ts, runtime test)
- vitest: 6 files / 69 tests green (sema + morfo runtime)
- smoke: 68/68 soma routes 200 OK
- morfo-check: 66/66 morfos validate against demo DOM
- playwright: /test/sium/{login,booking,color} + /test/eidos/toggle
render full content with 0 page errors / 0 console errors
- eidos recipe check: dialog provider chain end-to-end OK
|
5 months ago |
|
|
a7adfc43b1 |
sema: add defineEngineSemantic factory for createActiveApp composition
Adapts EngineSemantic to the AppServiceFactory contract so apps composed
with `createActiveApp` can declare semantic as a service:
services: {
semantic: defineEngineSemantic({ visual: { defaultHold: 400 } })
}
The factory lives in `src/uix/sema/` (not in `arts/active-app/service-
factories/`) because semantic coordination is a UIX-layer concern, not
generic app infrastructure like cache / auth / perm.
EngineSemantic itself stays unchanged — the factory just wraps it,
declares no core/service deps, and disposes the engine on teardown.
|
5 months ago |
|
|
746ce8dbe8 |
sema: rename SemanticEngine → EngineSemantic + .destroy() → .dispose()
Aligns sema's engine class with the new ecosystem's Engine* / Active*
naming convention (per src/arts/README.md):
- Engine* — public methods over private state. No reactive surface.
- Active* — Engine* extended with $state + onChange listeners.
Sema's SemanticEngine is purely imperative: a channel registry + an
async dispatch. No $state, no listeners, no loading/lastError. So by
convention it must be Engine*, not Active*.
Renames (mechanical):
SemanticEngine → EngineSemantic
SemanticEngineOpts → EngineSemanticOptions (also aligned to
EngineSiumOptions / EngineHttpOptions name)
.destroy() → .dispose() (uniform with the rest
of the ecosystem; only one call site, in
engine.test.ts)
Touched code: engine.ts, exports.ts, emit.test.ts, engine.test.ts +
README.md (kept consistent). Also sweep src/lib/ext/app/{defaults,
app.svelte}.ts which still mention the old name in fallback wiring.
Air's docs (AIR_*.md) and the legacy CONTINUITY note kept untouched
(dead branch).
This commit is purely a rename. The next commit (Phase 4 part 2) adds
the defineEngineSemantic factory and migrates soma + the 3 layouts to
createActiveApp.
Verification: sema 44/44, smoke 68/68, morfo-check 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
c3dbd47f5b |
morfo/schema: migrate $lib/sium/core → $sium (Phase 3)
Drop-in path change. The new $sium barrel ($arts/sium) re-exports the exact same surface the legacy $lib/sium/core provided: object, array, string, boolean, literal, union, discriminated, optional Schema (type) SiumValidationError Three import statements updated. Zero behavior change. Verification: morfo tests 21/21, smoke 68/68, morfo-check 66/66. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
b532b3a1e4 |
migrate $lib/util/* → $libs/* in soma + test pages (Phase 2)
Mechanical sweep of legacy util imports to the new $libs/* equivalents.
Drop-in: surfaces are compatible (verified per-module before sed).
$lib/util/dias → $libs/days (vendored dias, exact match)
$lib/util/colors → $libs/color (same exports)
$lib/util/arrays → $libs/arrays (same)
$lib/util/strings → $libs/strings (same)
$lib/util/funcs → $libs/funcs (same)
$lib/util/objs → $libs/objs (same)
$lib/util/numbers → $libs/numbers (same)
28 files touched: 6 soma source files (color, color-field, color-picker
providers + external/{colors,dates} adapters) + 22 test pages
(date/time/range pickers, calendar, color, sium booking/color/kitchen-sink).
Skipped intentionally:
- src/uix/terra/** and src/uix/air/** (dead branches per memory).
- src/lib/** internal references (legacy folder itself).
- Documentation .md files (Phase 6 cleanup).
Verification: smoke 68/68, morfo-check 66/66.
Note on the bash sed escaping: first attempt with double-quoted
patterns produced `$lib$libs/days` because `\\$lib` inside double
quotes only matched the trailing `/util/dias` portion. Fixed by
switching to single-quoted sed expressions ('s|\$lib/util/dias|
$libs/days|g') so bash leaves both `$` literals alone for sed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
62eb00fe8a |
deprecate src/lib + wire new ecosystem aliases (Phase 0+1)
Phase 0 — mark legacy as deprecated:
- Add @deprecated JSDoc header to all 27 top-level barrels under
src/lib (ext/{app,lang,dates,money,nums,units,presentation},
{actx,ling,logr,glob,sium,util}, root index.ts).
- Add src/lib/README.md with the migration map: legacy module →
new equivalent under src/arts + src/libs + src/svrs.
- Zero behavior change. Only IDE strikethrough at usage sites.
Phase 1 — wire new ecosystem aliases in vite.config.ts and svelte.config.js:
- 18 new $arts/* aliases: $active-app, $adom, $auth, $bus, $cache,
$connection, $format, $frontend, $http, $lang, $logger, $orca,
$perm, $prefs, $session, $sium, $storage, $timer.
- $libs (src/libs), $locale (src/libs/locale), $svrs (src/svrs).
- $reactive rewired from src/lib/util/reactive to src/libs/reactive
(export surfaces verified compatible — same symbols, drop-in).
- Legacy aliases ($lib, $glob, $langs, @/ling, @/logr, @/glob,
@/actx, @/uiux) preserved for backwards compat during migration.
- SvelteKit auto-regenerates .svelte-kit/tsconfig.json paths from
svelte.config.js, so TS resolution updates without manual edits.
The migration plan (Phases 2-6) lives in src/lib/README.md:
Phase 2 — sweep src/lib/util/* imports to $libs/* (drop-in).
Phase 3 — morfo/schema.ts: 3 imports to $libs/standard-schema.
Phase 4 — soma's App.create() composition to createActiveApp with
explicit service slots (including semantic for sema). Only step
that changes architecture.
Phase 5 — sweep ~82 test pages to use new aliases.
Phase 6 — remove src/lib/{glob,ling,actx} after verifying no
consumers; investigate sium and util/{dates,times,timers}.
Verification: smoke 68/68, morfo-check 66/66. svelte-check baseline
unchanged for the alias rewire (no Cannot-find-module errors). The
2 dialog-provider possibly-undefined errors at lines 113-114 are
pre-existing (this.soma vs this.soma! after Soma.get() returning
Soma | undefined) — not introduced by this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
800e794cea |
eidos batch: Accordion + Tabs + Checkbox recipes + demos
Mechanical translation from air's CSS (--air-* → --eidos-*, --_air-* →
--_eidos-*, drop .air-* class gates):
- eidos/components/accordion.css (205 lines): variants surface/ghost,
sizes sm/md/lg, indicator-placement, motion-disabled. Selectors
align with morfo: data-accordion / data-accordion-{item,header,
trigger,content}.
- eidos/components/tabs.css (222 lines): variants line/pills/surface,
sizes xs/sm/md/lg, orientation. Selectors align: data-tabs /
data-tabs-{list,trigger,content}.
- eidos/components/checkbox.css (183 lines): states checked /
unchecked / indeterminate, color variants (primary/success/warning/
danger/info/neutral), sizes. Selectors align: data-checkbox /
data-checkbox-{indicator,group,group-label}.
Demos at /test/eidos/{accordion,tabs,checkbox}. Layout nav + index
page updated.
Verification: smoke 68/68, morfo-check 66/66, all 3 pages 200.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
b5453af9c4 |
popover: extend morfo with events + soft-migrate provider + exit tinting
Same harmonization pattern as Drawer (which mirrored Dialog):
Morfo (`morfo/components/popover.ts`):
- Added 6 events: present + 5 close-* (save/cancel/dismiss/
dismiss-outside/after-fail). present + close-cancel/dismiss/
dismiss-outside use family='emerge'; close-save uses commit/fulfill;
close-after-fail uses alert/threat.
- Added data-last-action to Content's data array.
- scope: ['soma', 'sema'].
Eidos (`eidos/components/popover.css`):
- Tint rules: combine [data-state='closed'] + [data-side] +
[data-last-action] to color the trigger-facing edge (opposite to
the popover side) during exit. saved → success, failed → danger,
cancelled/dismissed/dismissed-outside → neutral.
Soma (`soma/components/popover/popover-provider.svelte.ts`):
- PopoverProvider injects MorfoRuntime + dismissWith(action).
- Registers content part on mount.
- Wires close paths through runtime.trigger():
Close.onclick → 'close-cancel'
Escape (Dismissal) → 'close-dismiss'
InteractOutside → 'close-dismiss-outside'
- PopoverProvider class re-exported from $soma/components/popover.
Demo (`test/eidos/popover/+page.svelte`):
- Tinted-exit section with Save/Cancel/Fail buttons via local
_dismiss-button.svelte (mirror of Dialog/Drawer pattern).
Verification: smoke 68/68, morfo-check 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
106b972075 |
drawer: extend morfo with events + soft-migrate provider + exit tinting
The Drawer's "show" semantic was missing from the morfo, so close paths
went straight to handleClose() without the prewrite → emit → handler
cadence. User flagged this — drawer DOES have a present/dismiss
semantic, equivalent in surface to Dialog.
Morfo (`morfo/components/drawer.ts`):
- Added `events[]` mirroring Dialog: present, close-save,
close-cancel, close-dismiss, close-dismiss-outside, close-after-fail
- Added `data-last-action` (saved/cancelled/dismissed/dismissed-outside/
failed) to Content's data so the prewrite has a declared slot
- scope: ['soma', 'sema'] now (sema participates via signal emit)
Eidos (`eidos/components/drawer.css`):
- Reserved a 3px transparent border on the LEADING edge of Content
per side (the edge facing the user, opposite to the slide-out
direction): right→border-left, left→border-right, top→border-bottom,
bottom→border-top
- Tint rules: combine [data-state='closed'] + [data-side] +
[data-last-action] so the right edge gets colored during the close
animation (saved → success, failed → danger, cancelled/dismissed →
neutral)
Soma (`soma/components/drawer/drawer-provider.svelte.ts`):
- DrawerProvider injects a MorfoRuntime (same pattern as Dialog soft-
migration). Registers content part. Wires events.{close-save,
close-cancel, close-dismiss, close-dismiss-outside, close-after-fail}
to handleClose().
- dismissWith(action) public method — same API as Dialog.dismissWith.
- Replaced 4 close paths with runtime.trigger():
Close.onclick → 'close-cancel'
Escape (modal) → 'close-dismiss'
Escape (non-modal) → 'close-dismiss'
Overlay.onclick → 'close-dismiss-outside'
Drag-to-dismiss (gesture) keeps direct handleClose() — the visible
slide-out IS the close, no 240ms hold makes sense.
- DrawerProvider class re-exported from $soma/components/drawer.
Demo (`test/eidos/drawer/+page.svelte`):
- New "Exit tinting by causa" section with Save/Cancel/Fail buttons
that call dismissWith() through a local _dismiss-button.svelte
(same pattern as Dialog's).
Verification (playwright):
save t=83ms lastAction=saved hold=240ms (commit family)
cancel t=119ms lastAction=cancelled hold=240ms (emerge)
fail t=127ms lastAction=failed hold=600ms (alert)
Smoke 68/68, morfo-check 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
d76f4eaf11 |
test/eidos: 2x2 grid with breathing room for tooltip + popover side demos
The previous layout (`repeat(4, 1fr)` with 0.5rem gap) packed the four sides in a horizontal row. Floating's collision detection saw the tooltip on `side='right'` had no room (the next button was 8px away) and shifted the placement, making the popover/tooltip appear far from its trigger. Switch to 2x2 grid with 4-7rem gaps and 3rem padding so each trigger has enough surrounding space for the tooltip/popover to render at the requested side without the floating layer flipping or shifting. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
5f6373e200 |
eidos/tooltip: recipe + per-side fade animations + demo
Translates `air/components/tooltip/tooltip.css` (small — 31 lines, only
Content + sizes styled) and adds:
- Arrow fill (matches tooltip bg)
- Per-side entrance/exit animations keyed off [data-side] (Floating
layer) and [data-state]. Distance is smaller than Popover (2px vs
4px) — tooltip is lighter visually.
- State handling for tooltip's three-value [data-state]:
'closed' → exit animation
'delayed-open' → entrance animation (hover after openDelay)
'instant-open' → no animation (keyboard focus, deliberate
invocation; the user wants it now, not after a
slide)
Demo (test/eidos/tooltip): 4-side grid + sizes (sm/md/lg).
Verification: smoke 68/68, morfo-check 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
d31bbc23f0 |
eidos/popover: recipe + per-side fade/scale animations + demo
Translates `air/components/popover/popover.css` (small — 67 lines, only
Content + Close styled) and adds three eidos extensions:
1. Trigger envelope ([data-popover-trigger]) — air relied on the
Button recipe via .air-button class. Eidos applies a baseline
button-style envelope here, consuming Capa 2 directly.
2. Overlay fade ([data-popover-overlay][data-state='open'/'closed'])
— for modal popovers. The morfo declares Overlay as optional;
consumer renders it when modal=true.
3. Per-side entrance/exit animations keyed off [data-side]
(top/right/bottom/left) and [data-state]. Soma's Floating layer
writes data-side based on resolved placement. The popover slides
into place from the trigger edge — fade + small translate (4px)
+ scale (0.985 → 1). Air's "revelation" pattern: ~180ms entrance
/ 120ms exit.
Demo (test/eidos/popover): basic + 4-side grid. Each side button
anchors a Popover.Content with the matching `side` prop so the
animation direction matches placement.
Verification: smoke 68/68, morfo-check 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
abf197a37c |
eidos/drawer: recipe + slide-in/out per side + demo (modal & non-modal)
Translates `air/components/drawer/drawer.css` to `eidos/components/
drawer.css` (mechanical: --air-* → --eidos-*, drop .air-* gates) and
adds the entrance/exit animations air's recipe was missing — air drove
drawer transitions through `air.semantic.contextIn/Out` Svelte
transitions, which don't exist in the morfo-driven path. Without them
the drawer snapped in/out instantly.
Per-side keyframes:
right → translateX(100%)
left → translateX(-100%)
top → translateY(-100%)
bottom → translateY(100%)
Plus overlay fade keyed off data-state. All ~240ms entrance / 180ms
exit (moderate / normal). The Presence layer waits for
getAnimations().finished so the content stays mounted during exit.
Demo (test/eidos/drawer):
- Modal section: 4 side buttons, render Drawer.Overlay (backdrop
+ focus trap, page non-interactive while open).
- Non-modal section: 4 side buttons, omit Drawer.Overlay + pass
modal={false}. No backdrop. Page behind stays interactive. Soma's
Dismissal layer auto-adjusts (interactOutsideBehavior default
flips from 'ignore' to 'close').
The Drawer morfo doesn't declare `events` yet, so no runtime.trigger()
cadence + no exit-tinting like Dialog. Recipe + animations only.
Verification (playwright probe):
top t=140ms translateY(-372) → t=406ms 0
right t=99ms translateX(452) → t=389ms 0
bottom t=94ms translateY(372) → t=390ms 0
left t=111ms translateX(-349) → t=428ms 0
Smoke 68/68, morfo-check 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
b096237d3e |
soma/dialog: dismissWith(action) public API + tinted demo section
Expose a programmatic dismiss method on DialogProvider that fires the
matching close-* event through the runtime, and use it from the eidos
Dialog demo to make the exit-tinting visible.
Soma:
- DialogProvider.dismissWith('save'|'cancel'|'fail'|'dismiss'|
'dismiss-outside') maps to the corresponding morfo event
(close-save, close-cancel, close-after-fail, close-dismiss,
close-dismiss-outside) and fires runtime.trigger().
- DialogProvider class re-exported from $soma/components/dialog so
consumers can call DialogProvider.get() inside a child component.
Demo (test/eidos/dialog):
- New "Exit tinting by causa" section with three buttons that close
the same dialog via three different events:
Save → close-save → border-top tinted success (240ms hold)
Cancel → close-cancel → border-top tinted neutral (240ms hold)
Fail → close-after-fail → border-top tinted danger (600ms hold)
The 600ms fail hold is sema's family='alert' default — the dialog
stays open longer for the user to register the failure before close.
- Uses a small _dismiss-button.svelte child component that captures
DialogProvider.get() at component init time. The inline onclick
pattern (Dialog.DialogProvider.get()?.dismissWith(...)) doesn't
work from the parent route's scope because Svelte's getContext is
init-time-only — the route is the parent of Dialog.Provider, not
a child. The _dismiss-button is rendered inside Dialog.Content so
its init-time context lookup resolves correctly.
Verification (playwright probe):
save t=144ms lastAction=saved hold=240ms
cancel t=116ms lastAction=cancelled hold=240ms
fail t=117ms lastAction=failed hold=600ms (full alert family)
Smoke 68/68.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
bb8c8657e1 |
soma/dialog: soft-migrate close-* events through MorfoRuntime
Inject a MorfoRuntime side-channel in DialogProvider that fires the
prewrite → semantic.emit → handler cadence for the close-* events
declared in the dialog morfo. The legacy provider machinery
(assertProps + resolveMorfoProps + Provider class hierarchy) stays —
runtime is alongside, not replacing.
Wiring:
- DialogProvider creates the runtime in its constructor with
`events.{close-save, close-cancel, close-dismiss,
close-dismiss-outside, close-after-fail}` all delegating to
handleClose(). The runtime gets dom + semantic from soma + state
sources for `open` + prop sources for `disabled`/`modal`.
- DialogContentProvider registers the content part with the runtime
when its constructor runs, so trigger() can resolve
`event.target = partRef('content')` to a real DOM node.
- DialogTriggerProvider.onclick keeps direct handleOpen() — the
'open' event in the morfo targets content, but content isn't
in the DOM until AFTER open flips. Chicken-and-egg.
- DialogCloseProvider.onclick → trigger('close-cancel').
- DialogContentProvider's dismissal callbacks:
onEscapeKeydown → trigger('close-dismiss')
onInteractOutside → trigger('close-dismiss-outside')
What this unlocks:
Each close path now writes `data-last-action` (saved/cancelled/
dismissed/dismissed-outside/failed) BEFORE flipping data-state,
visible during the exit animation. The dialog recipe's tint rules
in eidos finally fire in vivo.
Verification (playwright check-eidos-recipes):
@8ms post-click lastAction=cancelled state=open (prewrite)
@241ms lastAction=cancelled state=open (signal hold)
@378ms lastAction=cancelled state=closed (exit)
@493ms opacity=0.017 (exiting)
The 240ms wait between click and exit is sema's family='emerge' hold.
The user can override per-event via `morfo.events[].hold` if 240ms feels
too long for a dialog cancel. Smoke 68/68, morfo-check 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
084322d3db |
eidos/dialog: add [data-dialog-trigger] envelope (button-style)
Air's dialog relied on the Button recipe (`.air-button` class on
trigger). Eidos has no class composition, so the trigger was inheriting
only the archetype's cursor + focus ring, leaving it with the
browser-default button look.
Add a baseline envelope in dialog.css consuming Capa 2 tokens directly
(--eidos-color-{surface-raised, surface-overlay, border-default,
content-primary}, --eidos-control-height-md, --eidos-radius-md,
--eidos-space-3, --eidos-duration-fast). Hover and data-state='open'
both shift to surface-overlay. Disabled fades to 0.5.
Trigger isn't a separate component with its own Capa 3 tokens — it's
a part of Dialog using the system surface. Direct Capa 2 consumption
is appropriate here (same as archetype rules).
Verification: smoke 68/68, morfo-check 66/66; visual confirmed via
playwright screenshot — Open dialog/Open small/Open large render with
proper button envelope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
a50b18d1f1 |
eidos: drop legacy hand-rolled tokens.css, consume air-translated tokens
Replace the V1.5 hand-rolled motion/easing/tint/opacity/focus-ring
tokens with the air-translated equivalents already in eidos/themes/
base/_static.css and eidos/themes/base/light.css.
Substitutions applied to archetypes.css, events.css, components/
dialog.css:
--eidos-motion-fast → --eidos-duration-fast (120ms)
--eidos-motion-base → --eidos-duration-slow (320ms)
--eidos-easing-standard → --eidos-ease-default
--eidos-easing-decelerate → --eidos-ease-out
--eidos-easing-emphasized → --eidos-ease-spring
--eidos-tint-neutral → --eidos-color-neutral-element
--eidos-tint-affirm → --eidos-color-success-element
--eidos-tint-fulfill → --eidos-color-success-element
--eidos-tint-risk → --eidos-color-warning-element
--eidos-tint-threat → --eidos-color-danger-element
var(--eidos-opacity-hover) → 0.85 (single-site, archetype-internal)
var(--eidos-opacity-disabled) → 0.5 (single-site, archetype-internal)
Focus ring tokens (--eidos-focus-ring-{color,width,offset}) unchanged
— same names already exist in air's _static.css/light.css.
`affirm` and `fulfill` collapse to the same `success-element` because
air's intent vocabulary doesn't distinguish them. The morfo's
data-intent attr still preserves the distinction at the structural
layer; the visual mapping is just the same color.
Updated scripts/check-eidos-recipes.ts to navigate to the per-component
demo URLs (/test/eidos/{toggle,switch,dialog}) since the index page
no longer renders the recipes directly.
Verification: smoke 68/68, morfo-check 66/66. Playwright entrance/exit
curves intact (opacity interpolates over the configured durations).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
fa6b01c319 |
test/eidos: split into per-component demos with shared layout
Restructure the demos to mirror /test/soma's pattern: index page plus one demo per recipe, all under a shared layout that loads the eidos CSS chain and the Soma + App scaffolding once. test/eidos/ ├── +layout.svelte Soma + App + sticky nav bar ├── +page.svelte index (links + foundation explanation) ├── toggle/+page.svelte colors / sizes / variants / states ├── switch/+page.svelte colors / sizes / disabled ├── collapsible/+page.svelte ├── toast/+page.svelte fire by intent (5 intents) └── dialog/+page.svelte default + size variants Each demo exercises the variants air's CSS exposes (data-color, data-size, data-variant) by adding the attrs directly to the Provider element — they're a-la-carte since the morfos don't declare them yet. Future work: extend the morfos to declare these attrs as first-class props. Verification: all 6 routes return 200; smoke 68/68 (only covers /test/soma — eidos routes not in scope), morfo-check 66/66. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
b8dec62b35 |
eidos phase 2 batch 2: Switch, Collapsible, Toast translated
Switch (151 lines):
- mechanical: --air-* → --eidos-*, drop .air-* gates
- selectors match morfo as-is ([data-switch], [data-switch-thumb],
[data-state='checked'], [data-disabled])
- a-la-carte variants (data-color, data-size) dormant unless consumer adds
Collapsible (decorated parts only):
- mechanical translation
- dropped air's height-keyframes: they referenced
var(--terra-collapsible-content-height) (a JS-measured value from
the dead terra layer) and soma's Collapsible uses HTML `hidden`
(display:none) which kills CSS animations anyway. Left undecorated;
needs Presence layer integration in soma to enable real transitions.
Toast (substantive remap):
- selector remap due to part naming difference between air and morfo:
air [data-toast-toaster] → eidos [data-toast-viewport]
air [data-toast] (the card) → eidos [data-toast-item]
air [data-toast-message] → DROPPED (no equivalent — soma has
Title + Description direct, not a
separate "message" element)
air .air-toast-status → DROPPED (no status icon part)
air .air-toast-main → DROPPED (no content wrapper part)
- intent vocabulary remap (air data-type ↔ morfo data-intent):
error → threat
warning → risk
success → fulfill (and affirm — same palette)
info → neutral
- data-position on viewport stays a-la-carte (not in morfo)
Verification: smoke 68/68, morfo-check 66/66, focus rings + dialog
entrance/exit working per playwright.
Note: [data-dialog-trigger] has no explicit rule (air relied on
.air-button recipe). The trigger renders with browser defaults +
archetype interactive cursor. To be addressed in a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
e48f227f89 |
eidos phase 2: Toggle + Dialog recipes translated from air
Replace the V1.5 hand-rolled recipes with the translated air originals
adapted to the morfo-driven architecture.
Mechanical translation:
- sed --air-* → --eidos-* (tokens) and --_air-* → --_eidos-* (private)
- drop `.air-{part}` class gates everywhere — eidos applies universally
to whatever data-attrs the morfo emits
Substantive adjustments per recipe:
Toggle:
- kept the existing morfo data-attrs ([data-toggle], [data-state='on'],
[data-disabled]) — match air's selectors directly
- air's a-la-carte variants ([data-color], [data-size], [data-variant],
[data-block], [data-icon-only]) stay dormant: not in toggle morfo,
only fire if a consumer adds them manually
- 229 lines of palette × variant × size variants vs 40 lines hand-rolled
Dialog:
- air's pattern: animation keyframes keyed off [data-state='open'/'closed']
(NOT Presence-layer markers); Soma's Presence waits getAnimations().
finished, so this works
- air ONLY animates the overlay — content snaps in/out instantly, sub-
perceptual. Added eidos-only @keyframes for content entrance/exit
(opacity + scale) so Presence has something to wait for and the user
can perceive the transition
- kept data-last-action exit tinting (border-top by causa during
closing animation) — air doesn't have this concept, dialog morfo
declares the prewrite that writes the attr before data-state flips
Verification:
- smoke 68/68, morfo-check 66/66
- playwright entrance: opacity 0 → 1 over ~120ms (air's overlay-in token)
- playwright exit: opacity 1 → 0 over ~280ms (air's overlay-out token)
- both interpolate, not sub-perceptual; user can tweak via tokens
Note: the existing eidos/tokens.css (legacy hand-rolled) and
eidos/components/{toast,switch,collapsible}.css (still hand-rolled)
continue to load alongside; phase 3 will translate the rest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
0ebc9a9d83 |
eidos: phase 1 — translate air's foundation (contracts, tokens, themes/base)
Translates the foundation of air's visual system into eidos:
- eidos/contracts/ public token interfaces (4 root + 64 components)
- eidos/tokens/ per-component token values (62 components + motion)
- eidos/themes/base/ light + dark + _static + motion (4 files)
Mechanical translation: `--air-*` → `--eidos-*` everywhere, relative
imports preserved. The CSS is currently NOT wired into the legacy air/
folder, which stays for reference until phase 4 cleanup.
The `--eidos-*` prefix is documented in eidos/index.css with the two
reasons it earns its keep:
1. Authorship clarity at debug time (DevTools can identify the layer)
2. Override discipline (consumers know they're targeting public API)
Phase 1 only adds the foundation without touching component recipes.
The hand-rolled recipes (toast, dialog, toggle, switch, collapsible)
and the legacy hand-rolled tokens.css continue to load alongside the
new tree. Phase 2 will replace recipes with air-translated equivalents
and reconcile the duplicate motion/tint tokens.
Architectural note: air targeted terra and gated its CSS with
`.air-{component}` class on a wrapper. Eidos targets soma+morfo and
applies universally to the data-attrs the morfo declares — no class
gates, no Svelte wrappers. The morfo layer is the one that decides
which `data-*` attrs exist, which is the substantive shift between
the two systems.
Verification: smoke 68/68, morfo-check 66/66, focus rings + dialog
entrance/exit still working per Playwright check (curve unchanged).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
e4d131b58d |
sema + morfo: per-event hold override (declarative in morfo)
The hold of a component's event is now declared by the component itself,
not buried in sema's family fallback table.
Resolution precedence (in priority order):
1. signal.hold imperative, caller passes at emit time
2. morfo events[].hold declarative, component-level (this change)
3. sema family fallback 'emerge' → 'brief', 'alert' → 'noticed', ...
Sema's family table becomes the technical fallback when neither caller
nor morfo specifies. The numbers come from a single perceptual scale in
sema/durations.ts (subliminal/glimpse/brief/noticed/insistent/persistent),
so any change to the scale propagates to the family fallback automatically.
Public surface:
sema: SEMA_DURATIONS, SemaDurationLabel, SemaDurationSpec,
resolveSemaDuration() — exported from $uix/sema.
morfo: MorfoEvent.hold?: SemaDurationSpec — number or label.
soma: MorfoRuntime.trigger() reads event.hold, resolves the label →
ms via resolveSemaDuration(), passes it as signal.hold.
Tests:
- sema/durations.test.ts (4): scale values + resolver
- soma runtime (3 new): hold-less event sends no signal.hold (sema
falls back), numeric hold propagates, label hold resolves to ms.
Verification: sema 44/44, soma runtime 25/25, smoke 68/68, morfo-check 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
57590f2d60 |
scripts/check-eidos-recipes: rAF-grained transition sampling
Replace the single-point waitForTimeout snapshots with a rAF sampler that records opacity at every frame for 500ms. Lets the report show the actual easing curve, not just one ambiguous data point. Output now includes the full entrance + exit timing per frame, so a glance at the log tells whether the configured transition duration is respected end-to-end (no premature unmount, no clipped curve). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
25dbce936b |
eidos: dialog entrance/exit transitions (perceptual anchoring)
The recipe was missing transitions on the Presence-layer markers
(data-starting-style / data-ending-style). The dialog snapped into
view and snapped out — duration 0ms is sub-perceptual.
Add fade + scale on Content (320ms = --eidos-motion-base = "brief")
and fade on Overlay. Both interpolate via the Presence lifecycle:
open → element mounts with [data-starting-style] briefly →
CSS shows opacity:0 + scale(0.96) →
marker removed next rAF →
transition runs to opacity:1 + scale(1)
close → [data-ending-style] applied →
CSS animates opacity:0 + scale(0.96) →
Presence waits getAnimations().finished →
element unmounts
Verification (Playwright):
@8ms entrance: opacity 0.028, scale 0.972 (interpolating)
@80ms exit: opacity 0.46, data-ending-style still present
Both opacity AND transform animate so getAnimations().finished has
something to wait for in either direction. Easings: standard for
opacity, decelerate for transform (pops in cleaner).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
dce320d7ff |
eidos: focus ring uses box-shadow (project kills outlines globally),
dialog overlay self-positions
Two bugs surfaced by Playwright verification at /test/eidos:
1) Focus ring: archetypes.css used `outline: ...` for the focus-visible
ring. The project's layout.css has a Tailwind layer/components rule
`*:not(body):not(.focus-override) { outline: none !important }` that
kills every outline. The eidos rule was never visible.
Switch to a two-layer box-shadow ring (inner transparent gap +
outer color). Box-shadow follows border-radius, so the ring also
looks right on pills (Switch) and rounded corners (Toggle, Dialog
buttons). Ring also stands out against accent-filled states because
it sits OUTSIDE the element, not on top of it.
2) Dialog overlay: the recipe relied on [data-archetype='overlay']
from archetypes.css for `position: fixed; inset: 0`, but Dialog isn't
migrated to MorfoRuntime yet — its provider uses legacy createAttrs
which doesn't read morfo's `archetype` field. The overlay element
had `position: static, height: 0`, so the backdrop was invisible.
Inline `position: fixed; inset: 0` directly in the recipe. Component
recipes shouldn't depend on archetype rules for foundational behavior;
archetype is a transversal extra, not a foundation.
Adds scripts/check-eidos-recipes.ts — Playwright script that opens
/test/eidos, tabs into Toggle/Switch, opens the dialog, prints
computed styles + saves screenshots to g:/tmp/eidos. Used to catch
both bugs; reproducible for future visual checks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
d682506df2 |
eidos: focus ring full opacity + darker hue for accent-bg contrast
The ring color was hsl(220 90% 55% / 0.6) — same hue as the default button accent at 60% alpha, so when the Toggle is in 'on' state or the Switch is checked (both accent-filled), the ring against the active background was almost invisible. Bump to hsl(220 95% 45%) full opacity. Slightly darker than the accent default, full alpha so it stands out against accent-filled states. Note: `:focus-visible` only fires on keyboard focus (Tab), not click — expected browser behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
375e769428 |
eidos: V1.5 — Toggle + Switch component recipes
Two trigger-family recipes. Both Provider parts carry data-archetype=
'trigger', so cursor + hover dim + focus ring come from archetypes.css
for free; recipes layer in component-specific visuals.
Toggle (`[data-toggle]`):
- button envelope (padding, border, radius, white bg)
- data-state='on' → filled accent (--eidos-toggle-accent default blue)
- data-invalid → red border (uses --eidos-tint-threat)
Switch (`[data-switch]` + `[data-switch-thumb]`):
- 44×24 pill track, padded
- data-state='checked' → accent track + thumb translateX(travel)
where travel = width - thumb-size - 4px (track padding × 2)
- thumb shadow, cursor inherited from track (overrides archetype's grab)
Eidos test page drops its :global([data-toggle]) / [data-switch]
scoped CSS — recipes now suffice.
Public CSS API:
--eidos-toggle-{bg, bg-on, color, color-on, border, border-on, radius,
padding, accent}
--eidos-switch-{track-width, track-height, track-bg, track-bg-checked,
track-border, thumb-size, thumb-bg, accent}
Verification: smoke 68/68, morfo-check 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
14233af4a7 |
eidos: drop --_eidos-exit-tint indirection, inline in dialog recipe
The custom property was added in V1 anticipating multiple recipe consumers, but only Dialog uses last-action tinting. Premature abstraction — single-use indirection that obscures what's happening. Move the 5 selectors directly into dialog.css. events.css drops the machinery (replaced with a 3-line note pointing to the recipe). If a second consumer with the same exit-tint pattern shows up later, factor out then — not before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
286191339e |
eidos: V1.5 — Dialog component recipe + last-action coverage
Second per-component recipe. Composes archetype interactive defaults
(trigger, overlay, close, content, focus-visible) with the visual
envelope (button shape, modal card, backdrop) and the asymmetric exit
tinting from events.css.
The recipe consumes `--_eidos-exit-tint` via a top accent stripe on
[data-dialog-content], so close-save tints fulfill, close-after-fail
tints threat, close-cancel/dismiss/dismissed-outside tint neutral. The
private var stays inside eidos — recipes consume it; consumers don't.
events.css extends the data-last-action coverage to dismissed and
dismissed-outside (both map to neutral tint, like cancelled).
Public CSS API: --eidos-dialog-{overlay-bg, overlay-z, content-bg,
content-radius, content-padding, content-shadow, content-max-width,
content-z, button-bg, button-bg-hover, button-border, button-border-hover}.
Default z-indices (overlay 50 / content 51) are sensible defaults the
integrator can override via tokens — Dialog without z-index would be
useless out of the box.
Test page (/test/eidos) gets a Dialog section so the recipe is visible
end-to-end.
Verification: smoke 68/68, morfo-check 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
e69e0cba80 |
sema: namespace signal attrs under data-event-* (channel never touches state)
Supersedes the save/restore patch (
|
5 months ago |
|
|
b66039adb1 |
sema: VisualChannel preserves structural data-intent on cleanup
The channel writes `data-intent` from `signal.intent` during the hold,
but `data-intent` is also the structural attr the morfo of components
like Toast.Item writes from `v.propRef('intent')`. Removing it on
cleanup erased the structural value — the toast lost its tinting after
the announce signal expired.
Fix: save the prior `data-intent` value before overwrite, restore it on
cleanup. General principle — a channel should clean only what it wrote.
Other signal attrs (`data-event*`, `data-event-family`) are signal-
specific so no collision.
Tests: structural data-intent survives the hold; mismatched signal vs
structural intent (edge case) restores the structural value.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
4f0108f839 |
eidos: V1.5 — Toast component recipe
First per-component recipe in eidos/components/. Composes tokens + archetypes + intent palette into a finished Toast visual: - viewport stack layout (no positioning — integrator's call) - card with intent-tinted left border (reuses --eidos-tint-*) - absolute close button so layout doesn't depend on a header wrapper - typography for title / description, secondary-button style for action - swipe state (data-swipe='move'/'cancel') translates the card - loading state adds an accent stripe via inset shadow Toast.Item carries archetype='item' which sets cursor:pointer + user- select:none in archetypes.css; the recipe overrides those because a notification is passive, not selectable. Possible future morfo decision: introduce a 'card' or 'notification' archetype. The eidos test page drops its :global([data-toast-*]) scoped CSS — the recipe now suffices. Stale '~13 ms / 1 rAF' text replaced with the correct per-family hold (600ms for alert). Wired via @import './components/toast.css' from eidos/index.css. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
3d9a834886 |
docs: align cross-layer docs with channel-based Sema
Cleanup of stale references to the pre-refactor Sema model across architectural docs: - EngineSemantic → SemanticEngine (registry + dispatch) + VisualChannel (built-in materializer); engine no longer "depends on Dom" - "1 rAF + 1 hold frame (~13ms)" → perceptually-anchored hold (240ms emerge/commit/handle, 600ms alert/sustain, 120ms contact) - Promise semantics: sequential strict — cleanup BEFORE resolve, so structural commit lands AFTER perceptual feedback completes - Toast cadence trace updated: prewrite folded into VisualChannel.handle; CSS uses @keyframes (animation), not transition Touches: src/uix/README.md, src/uix/active_architecture.md, src/uix/sema/README.md, src/uix/eidos/README.md, src/uix/morfo/README.md, src/uix/soma/SOMA_ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
5 months ago |
|
|
b4ceb1bb09 |
docs: SOMA_ARCHITECTURE — update sema timing reference after channel refactor
The "data-event*" bullet still mentioned the old "~13ms" timing from before the perceptual anchoring fix and the channel refactor. Updated to reflect current behavior: VisualChannel writes the attrs for a configurable hold (240ms emerge/commit/handle, 600ms alert/sustain by default per family, configurable per signal or globally). No code changes — single doc fix. |
5 months ago |
|
|
5871cbca6e |
sema: refactor into channel modules — engine becomes registry + dispatch
Reorganización estructural según el spec acordado: cada canal perceptivo
vive como módulo simétrico dentro de `chans/`. El engine queda mínimo
(registry + dispatch); la lógica de escribir `data-event*` al DOM,
mantener hold y retirar pasa al `VisualChannel`. La API pública que ven
los providers (`semantic.emit(signal)`) se mantiene idéntica.
Estructura nueva
src/uix/sema/
├── engine.ts registry + dispatch (ya no conoce DOM ni hold)
├── signal.ts SemanticSignal + nuevo campo opcional `id?`
├── exports.ts barrel actualizado (chans + drop publish + drop perception)
└── chans/
├── types.ts interfaz Channel { id, handle, dispose? }
├── visual.ts VisualChannel — escribe data-event*, hold, cleanup
├── visual.test.ts 9 tests del canal aislado
├── sound.ts SoundChannel placeholder (V1 no-op)
└── vibra.ts VibraChannel placeholder (V1 no-op)
Cambios al engine
- Constructor: `new SemanticEngine(opts?)`. `opts.visual` controla el
built-in (false / VisualChannelOptions / Channel custom). `opts.dom`
desaparece — ya no es responsabilidad del engine.
- Métodos: `register(channel)`, `getChannel(id)`, `emit(signal)`,
`destroy()`. Nada más.
- emit despacha a TODOS los canales registrados:
- Canales no-visuales (sound, vibra, futuros): fire-and-forget. Errores
se loguean pero no propagan (Sema es ornamental).
- Canal visual: el único cuya Promise se awaitea — comparte plano DOM
con el commit estructural posterior.
- emit genera el `id` de la ocurrencia y lo pasa a todos los canales,
garantizando coherencia cross-canal para tooling futuro.
Cambios al VisualChannel (lógica heredada del emit() anterior)
- 5 atributos: `data-event` + `data-event-id` + `data-event-phase`
(siempre) + `data-event-family` y `data-intent` (opcionales si vienen
en el signal). Los opcionales son la proyección al DOM de la metadata
semántica que Eidos consume.
- Hold defaults internos por familia (no exportados):
emerge/commit/handle: 240ms · alert/sustain: 600ms · contact: 120ms
Justificación técnica (rangos típicos de CSS transitions), no escala
perceptiva universal. Override per signal vía `signal.hold` o global
vía `new SemanticEngine({ visual: { defaultHold } })`.
- Semántica secuencial estricta: cleanup ANTES del resolve.
Eliminado
- `src/uix/sema/perception.ts` — escalas perceptivas no son concepto
cross-canal. La tabla de defaults por familia se movió al VisualChannel
como detalle interno con justificación técnica.
- `engine.publish()` y todo el sistema legacy:
- `SemanticEventDecl`, `SemanticComponentContract`, `SemanticPublishContext`,
`PublishedSemanticEvent`, `SemanticEventFilter`
- `onEvent()` y subscribers
- `applyPrewrites()` interno
- dependencia opcional de `ActiveDom` por construcción
- Tests de `publish()` reemplazados por tests del nuevo registry/dispatch.
Bootstrap (App + defaults)
- `src/lib/ext/app/app.svelte.ts:52` — `new SemanticEngine({ dom: this.dom })`
→ `new SemanticEngine()`
- `src/lib/ext/app/defaults.ts:45` — mismo cambio
Tests
- emit.test.ts ajustado: sin createActiveDom, sin SEMA_PERCEPTION/HOLD_DEFAULTS,
test "throws without dom" eliminado, números literales (240, 600).
Añadido test "resolves immediately when visual: false". 7 tests.
- engine.test.ts reescrito: register / getChannel / emit dispatch a múltiples
canales / id propagation / id override / fire-and-forget para non-visual /
error en canal no aborta dispatch / visual: false desactiva built-in /
custom Channel reemplaza built-in / destroy. 13 tests.
- chans/visual.test.ts nuevo: 5 attrs vs 3 / signal.hold respetado /
family default fallback / global default fallback / constructor defaultHold /
cleanup / sequential strict (resolve tras cleanup) / id field. 9 tests.
- 39 tests sema verdes en total.
Documentación
- src/uix/sema/README.md — reescrito alrededor del modelo de canales
- src/uix/eidos/events.css — comentario actualizado, referencia a chans/visual.ts
- src/uix/active_architecture.md — sección Sema reescrita
Componentes NO requieren cambios — la API `semantic.emit(signal)` es
idéntica externamente. Toast / runtime.trigger / providers funcionan igual.
Verificación
- 121/121 tests focused suite (sema 39 + adom + lib/dom + morfo + soma/morfo)
- 66/66 morfo-check
- 68/68 smoke routes
- svelte-check: 155 errors (baseline, sin nuevos)
Pendientes en doc del spec marcados como "no-objetivos":
- Implementación real de SoundChannel / VibraChannel (placeholders V1)
- Arbitraje propio del VisualChannel (deuda heredada del emit anterior)
- Cancelación si el target se desconecta durante el hold
|
5 months ago |
|
|
e664b59c96 |
sema: perceptually-anchored hold + sequential strict emit semantics
Two architectural fixes surfaced by the eidos demo:
1. The previous default hold (1 rAF + 1 hold frame, ~30ms) was a
technical timing decision — invisible to humans. Signals must be
anchored to perceptual scales so they can actually be perceived.
2. The previous lifecycle resolved emit AFTER 1 rAF (caller's structural
change ran in parallel with cleanup, ~30ms later). That's an implicit
parallelism between event and state. The author has no way to say
"wait for the announcement to be perceived before changing state".
Sequential strict semantics (NEW)
- emit writes the signal, awaits the full author-specified hold, cleans
up, then resolves. State change happens AFTER cleanup — strictly
sequential.
- `signal.hold` is in milliseconds anchored to `SEMA_PERCEPTION` scales:
subliminal 50ms below conscious threshold
glimpse 120ms minimum perceptible without effort
brief 240ms comfortable acknowledgement
noticed 600ms sustained signal
insistent 1200ms demands attention
persistent 3000ms until acknowledged
- Defaults per family in `SEMA_HOLD_DEFAULTS`:
emerge: brief sustain: noticed
contact: glimpse commit: brief
alert: noticed handle: brief
- Authors override per event via `SemanticSignal.hold` based on:
- their CSS animation duration (so it completes before cleanup)
- the perceptual scale they want
- critical attention requirements (use insistent/persistent)
Files
- src/uix/sema/perception.ts (new) — SEMA_PERCEPTION + SEMA_HOLD_DEFAULTS
+ SEMA_OBSERVE_FRAME_MS constants with rationale anchored on Bloch's
law, Material/Apple HIG motion guidelines, toast UX literature.
- src/uix/sema/signal.ts — `hold` JSDoc updated to explain ms semantics +
defaults + override pattern.
- src/uix/sema/engine.ts — emit() refactored from rAF chain to
setTimeout(holdMs); cleanup happens BEFORE resolve (sequential strict);
`nextFrame()` helper removed; `resolveHoldMs(signal)` added with the
3-tier fallback (signal.hold → family default → SEMA_PERCEPTION.brief).
- src/uix/sema/exports.ts — re-exports SEMA_PERCEPTION + SEMA_HOLD_DEFAULTS
+ SEMA_OBSERVE_FRAME_MS + SemaPerception type.
- src/uix/sema/emit.test.ts — rewritten around fakeTimers + setTimeout
(no more rAF queue); new test verifies cleanup runs BEFORE resolve.
7/7 tests green.
- src/uix/sema/README.md — new "Duración del signal" section with the
full perception scale + per-family defaults tables + override pattern.
- src/uix/eidos/events.css — comment updated to explain the new hold
windows; rule `[data-event^='dismiss']` etc. clarified.
- src/uix/active_architecture.md — replaced "~13ms" prose with explicit
perception-anchored scales; added explanation of "perceptual by design,
not framerate".
- src/uix/soma/components/toast/toast-provider.svelte.ts — new $effect
that fires `present` + `announce` events when item ref attaches
(one-shot guarded), so toasts emit signals on appear. The flash on
/test/eidos is now visible thanks to (a) the perceptual hold and (b)
the wired events.
Channel parallelism note
- Hold is the DOM channel duration only.
- Future sound / vibra / motion engines subscribe to sema events
independently; each owns its own timing relative to the event id.
- Sema does not coordinate cross-channel sync — that's an integrator
decision per app.
Verification
- 22/22 sema tests green.
- 22/22 morfo runtime tests green.
- 66/66 morfo-check + 68/68 smoke (verified before commit).
- Probe shows announce signal on toast appear lasting ~600ms (alert
family default) — visible in DevTools attribute panel.
- svelte-check unchanged at 155.
Pre-existing failure in src/uix/air/internal/canvas/layout.test.ts
(`bun:test` import) is in the dead-branch air layer and unrelated.
|
5 months ago |
|
|
44a499d052 |
eidos: V1 minimum viable — tokens + archetype rules + event reactions
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).
|
5 months ago |
|
|
760575c419 |
docs: src/uix/active_architecture.md — cross-layer vision document
Synthesizes the four-layer architecture (morfo · soma · sema · eidos) into
a single readable doc. Where the per-layer READMEs are operational
references, this is the "north star" for understanding the why and how
the pieces articulate.
Structure (14 sections):
1. The thesis in one line — four layers with explicit contracts
2. The problem solved — what conventional frameworks mix together
3. The four layers — Morfo (DNA), Soma (behavior), Sema (semantic
channel), Eidos (visual)
4. How they articulate — the transcription chain (six pieces, one
responsibility each)
5. The causal chain of an interaction — concrete walkthrough of a Toast
dismiss with timing
6. The primitives that pass between layers — DOM attrs catalog +
archetypes + verbs vocabularies
7. Hard rules — the 12 invariants that keep the system coherent,
including the 2-of-3 rule
8. The autoría / transcripción distinction — the lens for deciding what
lives where
9. What this architecture is NOT — explicit non-goals
10. Current state (2026-04-26) — what's built, designed, pending
11. Acknowledged risks — over-layering, name discipline, responsibility
invasion, lack of precedent
12. Why it may be worth it — explainability, less drift, validation,
extensibility
13. The summary phrase — "Morfo declara · MorfoRuntime transcribe ·
Provider aporta · Effects sincronizan · Semantic emite · Dom aplica
· Eidos lee"
14. For deeper reading — links to per-layer docs
Also adds a top-of-page pointer in src/uix/README.md so the vision doc is
the obvious entry point for new readers.
No code changes — pure documentation.
|
5 months ago |
|
|
28f93ab089 |
docs: cross-layer articulation — eidos README + 2-of-3 rule across all layer docs
Closes the documentation loop on the cross-layer extension pass: morfo
now articulates between soma, sema, and (future) eidos. The "2-of-3 rule"
formalizes when an extension to morfo is justified vs when it should
stay as provider logic.
src/uix/eidos/README.md (new)
- Documents eidos's role and what it consumes from morfo + sema BEFORE
any code exists, so the contract is preparedly clean when implementation
starts.
- Catalogs which morfo fields eidos reads (parts, archetype, states,
data values, events, prewrite, focus, supportsNesting) and which it
ignores (computed state, runtime internals, layers).
- Documents the DOM-as-channel pattern: sema writes data-event* on emit;
eidos reacts to selectors like `[data-event^="dismiss"]`.
- Establishes the boundary with `air` (dead branch reference, not base).
src/uix/README.md (top-level)
- §8 Reglas de dependencia: adds the 2-of-3 rule table making the
morfo-extension contract explicit, plus a list of canonical vocabularies
(archetypes, verbs).
- §10 Reading order: includes eidos README + lib/dom + clarifies which
layers are dead branches.
src/uix/morfo/README.md
- New "Archetypes" section documenting the 24-verb vocabulary, the
Provider-as-trigger vs Provider-as-container distinction, and the rule
for adding new archetypes (≥2 components share the role).
- New "The 2-of-3 rule" section with the same table as the top-level,
listing which extensions did/didn't make it past the rule and why.
- `parts[].archetype` mentioned in the "What morfo contains" list.
src/uix/sema/README.md
- New "Vocabulario canónico de verbs" section listing SEMA_VERBS by
family and the `{verb}-{variant}` composite naming convention.
- Documents `validateEventName()` as advisory tooling.
src/uix/soma/SOMA_ARCHITECTURE.md
- partProps documentation now mentions data-archetype emission.
- New "Cross-layer hooks que soma emite por la regla 2-de-3" section
listing the data-* attrs soma writes that sema and eidos consume.
- Reading-order links updated.
No code changes — all docs.
|
5 months ago |