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
morfo-runtime
parent
9b07cead6b
commit
d9b6521020
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* DOM attribute value helpers — pure boolean → string formatters used to
|
||||
* emit `data-*` and ARIA attributes. No reactivity, no DOM, no morfo
|
||||
* dependency. Generic enough that any consumer (soma providers, eidos
|
||||
* tooling, plain HTML emitters) can reuse them.
|
||||
*/
|
||||
|
||||
export function boolToStr(condition: boolean): 'true' | 'false' {
|
||||
return condition ? 'true' : 'false'
|
||||
}
|
||||
|
||||
export function boolToEmptyStrOrUndef(condition: boolean): '' | undefined {
|
||||
return condition ? '' : undefined
|
||||
}
|
||||
|
||||
export function boolToTrueOrUndef(condition: boolean): true | undefined {
|
||||
return condition ? true : undefined
|
||||
}
|
||||
|
||||
export function getDataOpenClosed(condition: boolean): 'open' | 'closed' {
|
||||
return condition ? 'open' : 'closed'
|
||||
}
|
||||
|
||||
export function getDataChecked(condition: boolean): 'checked' | 'unchecked' {
|
||||
return condition ? 'checked' : 'unchecked'
|
||||
}
|
||||
|
||||
export function getAriaChecked(
|
||||
checked: boolean,
|
||||
indeterminate: boolean
|
||||
): 'true' | 'false' | 'mixed' {
|
||||
if (indeterminate) return 'mixed'
|
||||
return checked ? 'true' : 'false'
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
export * from './apply'
|
||||
export * from './attrs'
|
||||
export * from './consts'
|
||||
export * from './core'
|
||||
export * from './errors'
|
||||
export * from './focus'
|
||||
export * from './locale'
|
||||
export * from './resize-observer'
|
||||
export * from './responsive'
|
||||
export * from './tabbable'
|
||||
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Svelte attachment ↔ State box bridge.
|
||||
*
|
||||
* `attachRef` returns an attachment object that, when applied to a DOM
|
||||
* node via Svelte's `{@attach}` system, stores the node reference into
|
||||
* either a reactive `State<T | null>` box (preferred) or a plain
|
||||
* `(v: T | null) => void` setter, and unsets it on unmount.
|
||||
*
|
||||
* Sits in `libs/reactive` because it bridges two cross-cutting
|
||||
* primitives — Svelte's attachment API and the `State` reactive box —
|
||||
* neither of which belongs to a specific UIX layer.
|
||||
*/
|
||||
|
||||
import { untrack } from 'svelte'
|
||||
import { createAttachmentKey } from 'svelte/attachments'
|
||||
import { isState } from './guards'
|
||||
import type { State } from './types'
|
||||
|
||||
type RefSetter<T> = (v: T) => void
|
||||
|
||||
export type RefAttachment<T extends EventTarget = HTMLElement> = {
|
||||
[key: symbol]: (node: T) => (() => void) | void
|
||||
}
|
||||
|
||||
export function attachRef<T extends EventTarget = Element>(
|
||||
ref: State<T | null> | RefSetter<T | null>,
|
||||
onChange?: (v: T | null) => void
|
||||
): RefAttachment<T> {
|
||||
return {
|
||||
[createAttachmentKey()]: (node: T) => {
|
||||
if (isState(ref)) {
|
||||
ref.current = node
|
||||
untrack(() => onChange?.(node))
|
||||
return () => {
|
||||
if ('isConnected' in node && node.isConnected) return
|
||||
ref.current = null
|
||||
onChange?.(null)
|
||||
}
|
||||
}
|
||||
ref(node)
|
||||
untrack(() => onChange?.(node))
|
||||
return () => {
|
||||
if ('isConnected' in node && node.isConnected) return
|
||||
ref(null)
|
||||
onChange?.(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
export type { Active, State, Getter, MaybeActiveOrGetter, Source, Flattened } from './types.ts';
|
||||
|
||||
// ─── Symbols (needed for advanced consumers extending the system) ─────────────
|
||||
export { ActiveSymbol, WritableSymbol } from './symbols.ts';
|
||||
|
||||
// ─── Guards ───────────────────────────────────────────────────────────────────
|
||||
export { isActive, isState } from './guards.ts';
|
||||
|
||||
// ─── Core primitives ──────────────────────────────────────────────────────────
|
||||
export { state } from './state.svelte.ts';
|
||||
export { readableActive, writableActive } from './derived.svelte.ts';
|
||||
export { readonly } from './readonly.svelte.ts';
|
||||
|
||||
// ─── Utilities ────────────────────────────────────────────────────────────────
|
||||
export { toValue, normalize, flatten, autoReset } from './utils.svelte.ts';
|
||||
|
||||
// ─── Svelte attachment ↔ State bridge ────────────────────────────────────────
|
||||
export { attachRef, type RefAttachment } from './attach-ref.svelte.ts';
|
||||
@ -1,12 +1,15 @@
|
||||
/**
|
||||
* Data contract system for soma components.
|
||||
* Data contract system. Contracts are derived from a component's morfo
|
||||
* via `registerContract(morfo)`; `assertContract` validates emitted
|
||||
* `data-*` values against the contract in development (no-op in
|
||||
* production).
|
||||
*
|
||||
* Contracts are derived from a component's morfo via `registerContract(morfo)`.
|
||||
* `assertContract` validates emitted `data-*` values against the contract
|
||||
* in development; it is a no-op in production.
|
||||
* Lives in morfo because the contract IS a derivation of the morfo
|
||||
* declaration. Any consumer (soma providers, eidos validators, future
|
||||
* tooling) imports it from `$uix/morfo`.
|
||||
*/
|
||||
|
||||
import type { Morfo, MorfoPart } from '$uix/morfo';
|
||||
import type { Morfo, MorfoPart } from './types';
|
||||
|
||||
/** Internal shape of a registered contract. Not consumer-facing. */
|
||||
interface DataAttrRecord {
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Morfo, MorfoPart } from '$uix/morfo';
|
||||
import type { Morfo, MorfoPart } from './types';
|
||||
|
||||
/**
|
||||
* Creates data-attribute names for a soma component from its morfo.
|
||||
@ -1,27 +0,0 @@
|
||||
export function boolToStr(condition: boolean): 'true' | 'false' {
|
||||
return condition ? 'true' : 'false';
|
||||
}
|
||||
|
||||
export function boolToEmptyStrOrUndef(condition: boolean): '' | undefined {
|
||||
return condition ? '' : undefined;
|
||||
}
|
||||
|
||||
export function boolToTrueOrUndef(condition: boolean): true | undefined {
|
||||
return condition ? true : undefined;
|
||||
}
|
||||
|
||||
export function getDataOpenClosed(condition: boolean): 'open' | 'closed' {
|
||||
return condition ? 'open' : 'closed';
|
||||
}
|
||||
|
||||
export function getDataChecked(condition: boolean): 'checked' | 'unchecked' {
|
||||
return condition ? 'checked' : 'unchecked';
|
||||
}
|
||||
|
||||
export function getAriaChecked(
|
||||
checked: boolean,
|
||||
indeterminate: boolean
|
||||
): 'true' | 'false' | 'mixed' {
|
||||
if (indeterminate) return 'mixed';
|
||||
return checked ? 'true' : 'false';
|
||||
}
|
||||
Loading…
Reference in new issue