Imported from KyleMit/Splotch (
web/src/AGENTS.md). Install upstream withnpx skills add KyleMit/Splotch --skill src. Copyright stays with the author.
src/ orientation
This directory's
CLAUDE.mdandAGENTS.mdare generated from the.ruler/AGENTS.mdbeside them — edit that source, then runnpm run ruler:applyat the repo root (ADR-0058).
Where things live (full file-by-file map: architecture skill):
lib/drawing/— imperative canvas engine.engine.tsis the facade + orchestrator (canvas, pointer tracking, public API; callbacks out, direct function calls in — ADR-0004); ops/undo/export live in sibling modules (strokeOps,undoHistory,exportDrawing— map in thearchitectureskill).lib/state/— shared Svelte 5 rune state (*.svelte.ts) plus its plain TypeScript helpers. A shared reactive store exposes onecreateX()factory over private$state, returning read-only getters plus named mutators, and a shared instance when it owns app-wide state (issue #1920). Plain.tshelpers and per-component factories are not singletons. A listening/side-effecting store exposesinstall()/dispose()on its instance and still self-initializes at module load behind a client-only guard —browserfrom$app/environment, or atypeofprobe of the exact global the module is about to touch (appearance.svelte.tsprobesmatchMedia/document); both spellings are in deliberate use (docs/audit-deferred/decisions/ssr-guard-idioms.md) — never behind an exportedinitX()a route must remember to call (seelayout.svelte.ts,appearance.svelte.ts,network.svelte.ts,fullscreen.svelte.ts).install.svelte.tsis the one exception: its one-shotbeforeinstallpromptlistener must be eager (a deferred listener could miss an event that fires before hydration), but its state seeding stays behindinitInstallPrompt(), called fromlib/boot/webOnlyServices.ts— kept split for now to avoid touching its well-tested surface, not because the seeding itself needs to be deferred. Shared derived values are exposed as plain getter functions that recompute per call (resolvedTheme()inappearance.svelte.ts,activeStrokeSize()instrokeWidth.svelte.ts), never module-level$derived— the getter reads reactive state so a caller opts into reactivity locally by wrapping it in its own$derivedwhen a template needs it (e.g.ColorPalette.svelte), yet stays callable as a plain function from a unit test with no reactive context. A module's exported reactive singleton is named after the module basename plus a kind suffix — a$state(...)object orcreateX()instance is<basename>State(settingsState,aiProgressState), a modal controller ends inModal(settingsModal) — mechanically enough thattools/tests/state-export-names.test.mjsenforces one<basename>Statename plus any*Modalcontroller names. Tests build fresh instances from the factories rather thanvi.resetModules(). Multi-phase async state is a tagged union, and every late async result checks that it still belongs to the request/visit that started it; a reset detaches side-effectful work that must finish without allowing its result to mutate the new visit. An in-memory mirror of persisted state lives no longer than the storage fact it mirrors: failed or superseded reads do not prove absence, and writes/hydrations carry ownership checks.lib/boot/— the drawing route's boot steps as named helpers, called in order fromroutes/+page.svelte'sonMount:hydrateSettings(), thenmountBootHiddenOverlays()(the idle overlay pump, ADR-0049),installContextMenuGuard(),installWakeLock(),initWebOnlyServices()(PWA updates + install prompt), andinstallUndoShortcut()(window-level Ctrl/Cmd+Z, so it keeps working even whileActionsPanelisn't the one mounting it) — the last five return the teardowns the route collects and runs on unmount. This is page-lifecycle-scoped imperative wiring — the counterpart to the self-initializing stores above, not an exception to them: it needs mount/unmount teardown, which is exactly what the route'sonMountprovides.lib/components/— UI components with scoped styles.lib/actions/— Svelte actions for gestures and dialog wiring.lib/server/— server-only modules (tokens, admin, rate limiting). Never imported client-side; excluded from the native bundle.lib/server/ai/— the provider-agnostic AI seam (ADR-0047): routes importaiProviderfromai/provider.ts; theopenaiSDK is only touched inside that directory.lib/storage.ts— dual-layer persistence (localStorage + Capacitor Preferences mirror on native, ADR-0005).lib/secureStorage.ts— client-held secrets.lib/platform/index.ts— native detection without importing@capacitor/core(ADR-0013).lib/nativePlugin.ts—lazyPluginModule(): lazy-loads a Capacitor plugin as its module namespace, never the plugin proxy (a proxy resolves.thento a native call and hangs the awaiting promise). Every plugin load — this or an inlineimport()in a component — must sit behind the literal__IS_CAPACITOR__so Rollup drops the chunk from the web bundle (isNative()alone can't tree-shake); see themobileskill's plugin-loading section.routes/api/*— serverless endpoints (seeapiskill).routes/admin— token console.routes/dev/*— test harnesses, unlocked byPUBLIC_ENABLE_DEV_HARNESS=true.
Compile-time constants from Vite (ADR-0010): __APP_VERSION__, __BUILD_TIME__,
__NATIVE_API_BASE__, __IS_CAPACITOR__ (true in the native build — prefer it over a runtime
isNative() for build-time platform branches), plus instrumentation-only __PERF_MARKS__ and
__DEV_HARNESS__. Normal builds set both instrumentation literals false; the release post-build
scan rejects retained profiling property names or engine marks.
