# 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` 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//.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-typed `MaybeActiveOrGetter` arg (it picks the `T→T` overload and returns the union). Cast the result: `toValue(x) as number` / `as T`. Do NOT modify `toValue` (684 consumers). - **Prettier per subtree:** `$libs/dom` and `arts/adom`'s `active-dom.svelte.ts` are written without semicolons and already fail `prettier --check` pre-existingly; `$libs/reactive` + `arts/adom` newer files use semicolons. Write new files with semicolons and run `prettier --write` on 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.ts` extension** 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.