11 KiB
Continue — runed + tabbable → own libraries (0-dependency)
Status 2026-07-03: F1 + F2 + F3 DONE and verified. The port is complete —
runed + tabbable are fully removed and reimplemented in-house. (F1+F2 committed as
d0f41fb2; F3 commit follows.)
Goal: remove the two runtime deps that break the "framework depends on nobody"
doctrine — runed@0.37.1 and tabbable@^6.2.0 — by porting them into our own
code (same move as float → $ethereal). Decision: port the whole runed
library, not just what we use, but fold into existing homes (no parallel API)
and rebuild DOM-reactive utilities on ActiveDom (window-correct, never
defaultWindow).
Locked architecture (where each piece lives)
| Family | Home | Notes |
|---|---|---|
| Pure reactive runes (no DOM) | $libs/reactive |
grows the existing alias |
| DOM-reactive runes | $adom (src/arts/adom) |
rebuilt on ActiveDom, pattern { dom } like FocusScope |
Utilities that already exist in $adom |
reuse, don't duplicate | listen=useEventListener, observeResize=useResizeObserver, observeMutation=useMutationObserver, observeIntersection=useIntersectionObserver, activeElement, requestFrame=rAF |
tabbable focus engine |
$libs/dom/tabbable-core.ts |
consumed by existing tabbable.ts wrapper, surfaced via $adom |
PersistedState |
$storage (src/arts/storage) |
localStorage/sessionStorage |
useSearchParams + kit subexport |
DROP | app is adapter-static, SvelteKit-coupled |
boolAttr |
already exists as boolToEmptyStrOrUndef in $libs/dom/attrs.ts — do NOT re-port |
|
extract / MaybeGetter / get |
reuse framework toValue / MaybeActiveOrGetter / Getter |
DONE — F1 (dropped the deps) + F2 (pure runes)
F1 — created: src/libs/dom/tabbable-core.ts, src/libs/reactive/watch.svelte.ts
(watch/watch.pre/watchOnce), src/libs/reactive/context.ts (Context),
src/arts/adom/element-size.svelte.ts (ElementSize, rebuilt window-correct).
Rewired ~33 watch + 3 Context imports (runed → $libs/reactive), floating
(ElementSize → $adom), focus-scope (tabbable → $adom). Removed both from
package.json.
F2 — created in $libs/reactive: previous, is-mounted, on-cleanup,
interval (was useInterval), debounce+Debounced (was useDebounce),
throttle+Throttled (was useThrottle), state-history,
finite-state-machine, resource. Renamed the use* prefix away (user OK'd).
Added Setter<T> to types.ts. Barrels updated: $libs/dom/index,
$libs/reactive/index, $adom/index.
DONE — F3 (DOM-reactive runes → $adom)
Refined the approach from the original { dom } idea to follow the committed
ElementSize precedent: each utility is window-correct by resolving the window
from its node via getWindow(node) (node-bound) or an optional window/document
option (global-scope ones) — no threaded dom param, no runed defaultWindow. Raw
observers/listeners live in the arts/adom implementation layer (allowed there,
like active-dom.svelte.ts itself). All 11 ported, type-clean (check=59), server
tests green (barrel loads). PersistedState NOT ported — the $storage art
already replaces it (its README names runed/PersistedState as the thing it
supersedes), same call as boolAttr→boolToEmptyStrOrUndef.
| runed utility | target file | maps onto | notes |
|---|---|---|---|
ElementRect |
$adom/element-rect.svelte.ts |
observeResize + observeMutation | like ElementSize; getBoundingClientRect via dom |
ActiveElement |
$adom/active-element.svelte.ts |
listen(focusin/focusout) + getActiveElement (already in $libs/dom/core, shadow-piercing) |
createSubscriber |
IsFocusWithin |
$adom/is-focus-within.svelte.ts |
ActiveElement + node.contains | |
IsIdle |
$adom/is-idle.svelte.ts |
listen + debounce (F2) |
events on window/document |
IsDocumentVisible |
$adom/is-document-visible.svelte.ts |
listen(visibilitychange) | |
IsInViewport |
$adom/is-in-viewport.svelte.ts |
observeIntersection | |
onClickOutside |
$adom/on-click-outside.svelte.ts |
listen(pointerdown/click/blur) + debounce + watch |
biggest; iframe detection; getActiveElement/isOrContainsTarget already in $libs/dom |
PressedKeys |
$adom/pressed-keys.svelte.ts |
listen(keydown/keyup/blur/visibilitychange) | createSubscriber |
ScrollState |
$adom/scroll-state.svelte.ts |
listen(scroll/scrollend) + AnimationFrames + debounce | large; getComputedStyle via dom.getWindow |
TextareaAutosize |
$adom/textarea-autosize.svelte.ts |
observeResize + watch + tick | creates hidden textarea; use dom for document |
AnimationFrames |
$adom/animation-frames.svelte.ts |
dom.requestFrame/cancelFrame loop | fps-limit, pause/resume, reactive fps |
PersistedState |
SKIPPED | already in $storage |
createActiveStorage+localAdapter/sessionAdapter/broadcast supersede it |
Still open (optional, not blocking): fold the useful runed extras into
$adom's existing observer methods where they add value (not new API):
once/pause/resume + Firefox threshold fix in observeIntersection;
takeRecords() in observeMutation.
How to re-obtain runed source (pruned from node_modules)
runed is gone from node_modules. To read source again:
npm install runed@0.37.1 --no-save (does NOT touch package.json), read
node_modules/runed/dist/utilities/<name>/<name>.svelte.js (+ .svelte.d.ts for
types) and internal/utils/*. Gotcha: the --no-save reinstall + the other
chat's running vite dev server caused an EBUSY lock and a transient
svelte-check crash; fix with npm install then
rm -rf node_modules/runed node_modules/.runed-*. GitHub mirror:
github.com/svecosystem/runed/tree/main/packages/runed/src/lib.
(A full F3 source dump may still exist at the previous session's
scratchpad/runed-f3-source.txt, but scratchpad is session-scoped — don't rely on it.)
Verification baselines
npm run check= 59 errors — all pre-existing (connection, nav-menu, time-picker, file-upload, editable/grid-list tests, web/routes demos, temas). 0 in ported files. Any of MY files appearing = regression.npm run test= 3920 pass / 24 fail — all 24 pre-existing WIP: contracts (13, menu-dial half-built), range-calendar (7, "incidencia 2026-05-20"), morfo compile (1), chronos (1), calendar (1), soma-attr-audit (1, 5s timeout flaky).
Gotchas (carry into F3)
toValue+ union types:toValue's overloads don't unwrap a union-typedMaybeActiveOrGetter<T>arg (it picks theT→Toverload and returns the union). Cast the result:toValue(x) as number/as T. Do NOT modifytoValue(684 consumers).- Prettier per subtree:
$libs/domandarts/adom'sactive-dom.svelte.tsare written without semicolons and already failprettier --checkpre-existingly;$libs/reactive+arts/adomnewer files use semicolons. Write new files with semicolons and runprettier --writeon them (they'll be compliant even if some neighbors aren't). - Layer rule: soma/components import DOM only via
$adom, never raw npm or$libs/dom. Reactive utilities via$libs/reactive. .svelte.tsextension required for any file using runes ($state/$derived/$effect).
Memory pointer
memory/project_runed_tabbable_port_2026-07-03.md has the full running log.