You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/continue-runed-tabbable-por...

114 lines
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-typed
`MaybeActiveOrGetter<T>` 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.

Powered by TurnKey Linux.