seitu 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +14 -1
  2. package/package.json +11 -3
  3. package/skills/README.md +101 -0
  4. package/skills/create-computed/SKILL.md +108 -0
  5. package/skills/create-debounced/SKILL.md +94 -0
  6. package/skills/create-debounced-fn/SKILL.md +95 -0
  7. package/skills/{createIndexedDbStorage → create-indexed-db-storage}/SKILL.md +62 -4
  8. package/skills/create-is-online/SKILL.md +87 -0
  9. package/skills/create-media-query/SKILL.md +101 -0
  10. package/skills/{createReadableSubscription → create-readable-subscription}/SKILL.md +60 -4
  11. package/skills/create-schema-store/SKILL.md +98 -0
  12. package/skills/{createScrollState → create-scroll-state}/SKILL.md +60 -4
  13. package/skills/create-store/SKILL.md +109 -0
  14. package/skills/{createSubscription → create-subscription}/SKILL.md +59 -4
  15. package/skills/create-throttled/SKILL.md +93 -0
  16. package/skills/create-throttled-fn/SKILL.md +96 -0
  17. package/skills/{createWebStorage → create-web-storage}/SKILL.md +65 -4
  18. package/skills/create-web-storage-value/SKILL.md +118 -0
  19. package/skills/seitu-overview/SKILL.md +73 -4
  20. package/skills/subscription-react/SKILL.md +96 -0
  21. package/skills/{useSubscription-react → use-subscription-react}/SKILL.md +69 -4
  22. package/skills/{useSubscription-vue → use-subscription-vue}/SKILL.md +61 -4
  23. package/skills/Subscription-react/SKILL.md +0 -34
  24. package/skills/createComputed/SKILL.md +0 -50
  25. package/skills/createDebounced/SKILL.md +0 -37
  26. package/skills/createDebouncedFn/SKILL.md +0 -39
  27. package/skills/createIsOnline/SKILL.md +0 -30
  28. package/skills/createMediaQuery/SKILL.md +0 -43
  29. package/skills/createSchemaStore/SKILL.md +0 -42
  30. package/skills/createStore/SKILL.md +0 -48
  31. package/skills/createThrottled/SKILL.md +0 -37
  32. package/skills/createThrottledFn/SKILL.md +0 -40
  33. package/skills/createWebStorageValue/SKILL.md +0 -60
package/README.md CHANGED
@@ -42,7 +42,20 @@ function Counter() {
42
42
  }
43
43
  ```
44
44
 
45
- Seitu has other powerful features, so check out the [docs](https://seitu.letstri.dev/docs) or the [examples](https://github.com/letstri/seitu/tree/main/examples) directory.
45
+ Seitu has other powerful features, so check out the [docs](https://seitu.letstri.dev/docs) or the [playground](https://github.com/letstri/seitu/tree/main/playground) directory.
46
+
47
+ ## Agent skills (TanStack Intent)
48
+
49
+ Seitu ships [versioned agent skills](seitu/skills/README.md) inside the npm package. Install `seitu`, then run:
50
+
51
+ ```bash
52
+ pnpm add seitu
53
+ pnpm dlx @tanstack/intent@latest install
54
+ pnpm dlx @tanstack/intent@latest list
55
+ pnpm dlx @tanstack/intent@latest load seitu#create-store
56
+ ```
57
+
58
+ Skills are indexed on the [Agent Skills Registry](https://tanstack.com/intent/registry) and update when you update the package.
46
59
 
47
60
  ## License
48
61
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "seitu",
3
3
  "displayName": "Seitu",
4
4
  "type": "module",
5
- "version": "0.15.0",
5
+ "version": "0.15.1",
6
6
  "private": false,
7
7
  "author": "Valerii Strilets",
8
8
  "license": "MIT",
@@ -21,8 +21,13 @@
21
21
  "react",
22
22
  "vue",
23
23
  "utils",
24
- "type-safe"
24
+ "type-safe",
25
+ "tanstack-intent"
25
26
  ],
27
+ "intent": {
28
+ "repo": "letstri/seitu",
29
+ "docs": "../docs/content/docs"
30
+ },
26
31
  "exports": {
27
32
  ".": {
28
33
  "types": "./dist/core/index.d.mts",
@@ -75,6 +80,7 @@
75
80
  },
76
81
  "devDependencies": {
77
82
  "@standard-schema/spec": "^1.1.0",
83
+ "@tanstack/intent": "^0.0.42",
78
84
  "@testing-library/jest-dom": "^6.9.1",
79
85
  "@testing-library/react": "^16.3.2",
80
86
  "@types/react": "^19.2.17",
@@ -87,6 +93,7 @@
87
93
  "vite": "^6.4.3",
88
94
  "vitest": "^3.2.6",
89
95
  "vue": "^3.5.35",
96
+ "yaml": "^2.8.3",
90
97
  "zod": "^4.4.3"
91
98
  },
92
99
  "scripts": {
@@ -94,6 +101,7 @@
94
101
  "test": "vitest run",
95
102
  "check-types": "tsc",
96
103
  "scripts:copy-readme": "node ./scripts/copy-readme.ts",
97
- "scripts:copy-skills": "node ./scripts/copy-skills.ts"
104
+ "skills:validate": "intent validate skills",
105
+ "skills:stale": "intent stale skills"
98
106
  }
99
107
  }
@@ -0,0 +1,101 @@
1
+ # Seitu agent skills (TanStack Intent)
2
+
3
+ These skills teach AI assistants how to integrate [Seitu](https://seitu.letstri.dev) in **your** app — not how to work on the Seitu library monorepo.
4
+
5
+ Skills ship inside the `seitu` npm package and are versioned with each release. They include `sources` metadata pointing at docs and source files so maintainers can detect drift when documentation changes.
6
+
7
+ ## Install via npm (recommended)
8
+
9
+ After adding Seitu to your project:
10
+
11
+ ```bash
12
+ pnpm add seitu
13
+ pnpm dlx @tanstack/intent@latest install
14
+ ```
15
+
16
+ Intent discovers `seitu` in `node_modules`, reads the skills bundled with your installed version, and writes lightweight skill-loading guidance into your agent config (`AGENTS.md`, `CLAUDE.md`, `.cursorrules`, etc.).
17
+
18
+ List or load a specific skill:
19
+
20
+ ```bash
21
+ pnpm dlx @tanstack/intent@latest list
22
+ pnpm dlx @tanstack/intent@latest load seitu#create-store
23
+ ```
24
+
25
+ When you `pnpm update seitu`, skills update with the package — knowledge travels through npm, not model training cutoffs.
26
+
27
+ Start with **`seitu-overview`** — module map, mental model, and decision tree.
28
+
29
+ ## Manual install (Cursor)
30
+
31
+ Copy skill folders into `.cursor/skills/`:
32
+
33
+ ```bash
34
+ cp -r node_modules/seitu/skills/seitu-overview .cursor/skills/
35
+ cp -r node_modules/seitu/skills/create-store .cursor/skills/
36
+ ```
37
+
38
+ Restart Cursor or start a new agent chat so skills are picked up.
39
+
40
+ ## Skills
41
+
42
+ ### Core (`seitu`)
43
+
44
+ | Skill | Intent id | When to use |
45
+ |-------|-----------|-------------|
46
+ | [seitu-overview](./seitu-overview/SKILL.md) | `seitu#seitu-overview` | Read first — module map and primitive selection |
47
+ | [create-store](./create-store/SKILL.md) | `seitu#create-store` | Simple in-memory reactive state |
48
+ | [create-schema-store](./create-schema-store/SKILL.md) | `seitu#create-schema-store` | Schema-validated state (Zod, Valibot, ArkType) |
49
+ | [create-computed](./create-computed/SKILL.md) | `seitu#create-computed` | Derived read-only values |
50
+ | [create-debounced](./create-debounced/SKILL.md) | `seitu#create-debounced` | Debounce subscribable updates |
51
+ | [create-throttled](./create-throttled/SKILL.md) | `seitu#create-throttled` | Throttle subscribable updates |
52
+ | [create-debounced-fn](./create-debounced-fn/SKILL.md) | `seitu#create-debounced-fn` | Debounced function with reactive result |
53
+ | [create-throttled-fn](./create-throttled-fn/SKILL.md) | `seitu#create-throttled-fn` | Throttled function with reactive result |
54
+ | [create-subscription](./create-subscription/SKILL.md) | `seitu#create-subscription` | Low-level subscribe/notify |
55
+ | [create-readable-subscription](./create-readable-subscription/SKILL.md) | `seitu#create-readable-subscription` | Compose standard Readable & Subscribable |
56
+
57
+ ### Web (`seitu/web`)
58
+
59
+ | Skill | Intent id | When to use |
60
+ |-------|-----------|-------------|
61
+ | [create-web-storage-value](./create-web-storage-value/SKILL.md) | `seitu#create-web-storage-value` | Single-key localStorage / sessionStorage |
62
+ | [create-web-storage](./create-web-storage/SKILL.md) | `seitu#create-web-storage` | Multi-key web storage |
63
+ | [create-indexed-db-storage](./create-indexed-db-storage/SKILL.md) | `seitu#create-indexed-db-storage` | Large or async IndexedDB state |
64
+ | [create-media-query](./create-media-query/SKILL.md) | `seitu#create-media-query` | Reactive CSS media queries |
65
+ | [create-is-online](./create-is-online/SKILL.md) | `seitu#create-is-online` | Online / offline status |
66
+ | [create-scroll-state](./create-scroll-state/SKILL.md) | `seitu#create-scroll-state` | Scroll position and edges |
67
+
68
+ ### React (`seitu/react`)
69
+
70
+ | Skill | Intent id | When to use |
71
+ |-------|-----------|-------------|
72
+ | [use-subscription-react](./use-subscription-react/SKILL.md) | `seitu#use-subscription-react` | Hook for any Seitu primitive |
73
+ | [subscription-react](./subscription-react/SKILL.md) | `seitu#subscription-react` | Render-prop component |
74
+
75
+ ### Vue (`seitu/vue`)
76
+
77
+ | Skill | Intent id | When to use |
78
+ |-------|-----------|-------------|
79
+ | [use-subscription-vue](./use-subscription-vue/SKILL.md) | `seitu#use-subscription-vue` | Composable for any Seitu primitive |
80
+
81
+ ## Registry and version history
82
+
83
+ The package includes the `tanstack-intent` npm keyword. Published versions are indexed on the [Agent Skills Registry](https://tanstack.com/intent/registry) with skill history per release.
84
+
85
+ ## Without skills
86
+
87
+ - Official docs: https://seitu.letstri.dev/docs
88
+ - LLM-oriented export: https://seitu.letstri.dev/llms.txt
89
+
90
+ ## Maintainer workflow (this repo)
91
+
92
+ From `seitu/`:
93
+
94
+ ```bash
95
+ pnpm run skills:validate # structure + packaging before publish
96
+ pnpm run skills:stale # flag drift vs docs/sources
97
+ pnpm run skills:sync-state # refresh source SHAs after doc/source edits
98
+ pnpm run skills:upgrade # re-apply frontmatter + Common Mistakes from _artifacts
99
+ ```
100
+
101
+ CI runs `intent validate` on PRs and `intent stale` after releases (`.github/workflows/check-skills.yml`). Update `library_version` in SKILL frontmatter when cutting a release.
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: create-computed
3
+ description: >-
4
+ Derived read-only subscription from one or many sources.
5
+ type: core
6
+ library: seitu
7
+ library_version: "0.15.1"
8
+ requires:
9
+ - seitu-overview
10
+ sources:
11
+ - letstri/seitu:docs/content/docs/core/computed.mdx
12
+ - letstri/seitu:seitu/src/core/computed.ts
13
+ ---
14
+
15
+ # createComputed
16
+
17
+ Derived read-only subscription from one or many sources. Lazy — only subscribes to sources when it has its own subscribers.
18
+
19
+ ## Single source
20
+
21
+ ```ts
22
+ import { createComputed, createStore } from 'seitu'
23
+
24
+ const store = createStore({ a: 1, b: 2 })
25
+ const sum = createComputed(store, s => s.a + s.b)
26
+ sum.get() // 3
27
+ ```
28
+
29
+ ## Multiple sources
30
+
31
+ ```ts
32
+ import { createComputed, createStore } from 'seitu'
33
+
34
+ const a = createStore(1)
35
+ const b = createStore(2)
36
+ const total = createComputed([a, b], ([a, b]) => a + b)
37
+ total.get() // 3
38
+ ```
39
+
40
+ ## Overloads
41
+
42
+ ```ts
43
+ function createComputed<T, R>(source: Source<T>, transform: (value: T) => R): Computed<R>
44
+ function createComputed<S extends Source[], R>(sources: [...S], transform: (values: SourceValues<S>) => R): Computed<R>
45
+ ```
46
+
47
+ ## Interface
48
+
49
+ ```ts
50
+ interface Computed<T> extends Readable<T>, Subscribable<T> {}
51
+ ```
52
+
53
+ Read-only. No `set()`.
54
+ ## Common Mistakes
55
+
56
+ ### [HIGH] Recomputing manually in components
57
+
58
+ Wrong:
59
+
60
+ ```ts
61
+ const sum = a.get() + b.get()
62
+ ```
63
+
64
+ Correct:
65
+
66
+ ```ts
67
+ const sum = createComputed([a, b], ([x, y]) => x + y)
68
+ ```
69
+
70
+ createComputed subscribes to sources and caches until they notify.
71
+
72
+ ### [HIGH] Creating computed inside render
73
+
74
+ Wrong:
75
+
76
+ ```ts
77
+ function App() {
78
+ const total = createComputed(store, s => s.a + s.b)
79
+ }
80
+ ```
81
+
82
+ Correct:
83
+
84
+ ```ts
85
+ const total = createComputed(store, s => s.a + s.b)
86
+ ```
87
+
88
+ New computed each render re-subscribes and loses memoization benefits.
89
+
90
+ ### [MEDIUM] Writing to computed
91
+
92
+ Wrong:
93
+
94
+ ```ts
95
+ sum.set(10)
96
+ ```
97
+
98
+ Correct:
99
+
100
+ ```ts
101
+ store.set({ a: 5, b: 5 })
102
+ ```
103
+
104
+ Computed is read-only; use the source store's set().
105
+
106
+ ## Source
107
+
108
+ `src/core/computed.ts`
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: create-debounced
3
+ description: >-
4
+ Debounce updates from a source subscribable.
5
+ type: core
6
+ library: seitu
7
+ library_version: "0.15.1"
8
+ requires:
9
+ - seitu-overview
10
+ sources:
11
+ - letstri/seitu:docs/content/docs/core/debounced.mdx
12
+ - letstri/seitu:seitu/src/core/debounced.ts
13
+ ---
14
+
15
+ # createDebounced
16
+
17
+ Wraps a source subscribable with debounce. Emits the latest value after `wait` ms of inactivity.
18
+
19
+ ```ts
20
+ import { createStore, createDebounced } from 'seitu'
21
+
22
+ const input = createStore('')
23
+ const debounced = createDebounced(input, 300)
24
+ debounced.subscribe(value => console.log('debounced:', value))
25
+ debounced.get() // current debounced value
26
+ ```
27
+
28
+ ## Signature
29
+
30
+ ```ts
31
+ function createDebounced<T>(source: Readable<T> & Subscribable<T>, wait: number): Debounced<T>
32
+ ```
33
+
34
+ ## Interface
35
+
36
+ ```ts
37
+ interface Debounced<T> extends Readable<T>, Subscribable<T> {}
38
+ ```
39
+
40
+ Read-only. Lazy subscription — only subscribes to source when it has its own subscribers.
41
+ ## Common Mistakes
42
+
43
+ ### [HIGH] Debouncing the source instead of wrapping it
44
+
45
+ Wrong:
46
+
47
+ ```ts
48
+ const raw = createStore(0)
49
+ setTimeout(() => raw.set(v), 300)
50
+ ```
51
+
52
+ Correct:
53
+
54
+ ```ts
55
+ const debounced = createDebounced(raw, 300)
56
+ ```
57
+
58
+ createDebounced wraps a Readable & Subscribable; mutating the source bypasses debounce.
59
+
60
+ ### [MEDIUM] Expecting immediate get after source change
61
+
62
+ Wrong:
63
+
64
+ ```ts
65
+ source.set(1); debounced.get() // expects 1 immediately
66
+ ```
67
+
68
+ Correct:
69
+
70
+ ```ts
71
+ debounced.subscribe(v => console.log(v))
72
+ ```
73
+
74
+ get() returns last emitted value until wait elapses.
75
+
76
+ ### [LOW] Zero or negative wait
77
+
78
+ Wrong:
79
+
80
+ ```ts
81
+ createDebounced(source, 0)
82
+ ```
83
+
84
+ Correct:
85
+
86
+ ```ts
87
+ createDebounced(source, 300)
88
+ ```
89
+
90
+ Invalid wait breaks timing expectations.
91
+
92
+ ## Source
93
+
94
+ `src/core/debounced.ts`
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: create-debounced-fn
3
+ description: >-
4
+ Debounced callable with reactive return value.
5
+ type: core
6
+ library: seitu
7
+ library_version: "0.15.1"
8
+ requires:
9
+ - seitu-overview
10
+ sources:
11
+ - letstri/seitu:docs/content/docs/core/debounced-fn.mdx
12
+ - letstri/seitu:seitu/src/core/debounced-fn.ts
13
+ ---
14
+
15
+ # createDebouncedFn
16
+
17
+ Wraps a plain function. The return value becomes subscribable state. Each call resets the debounce timer.
18
+
19
+ ```ts
20
+ import { createDebouncedFn } from 'seitu'
21
+
22
+ const search = createDebouncedFn((q: string) => fetch(`/api?q=${q}`), 300)
23
+ search('hello') // debounced — fires after 300ms of inactivity
24
+ search.get() // latest return value (undefined until first call)
25
+ search.subscribe(result => console.log('result:', result))
26
+ ```
27
+
28
+ ## Signature
29
+
30
+ ```ts
31
+ function createDebouncedFn<F extends (...args: any[]) => any>(fn: F, wait: number): DebouncedFn<F>
32
+ ```
33
+
34
+ ## Interface
35
+
36
+ ```ts
37
+ interface DebouncedFn<F> extends Readable<ReturnType<F> | undefined>, Subscribable<ReturnType<F> | undefined> {
38
+ (...args: Parameters<F>): void
39
+ }
40
+ ```
41
+
42
+ Callable + readable + subscribable. `get()` returns `undefined` until first execution.
43
+ ## Common Mistakes
44
+
45
+ ### [HIGH] Using createDebounced on a function
46
+
47
+ Wrong:
48
+
49
+ ```ts
50
+ createDebounced(fn, 300)
51
+ ```
52
+
53
+ Correct:
54
+
55
+ ```ts
56
+ createDebouncedFn(fn, 300)
57
+ ```
58
+
59
+ createDebounced wraps subscribables; createDebouncedFn wraps callables.
60
+
61
+ ### [MEDIUM] Expecting synchronous return from call
62
+
63
+ Wrong:
64
+
65
+ ```ts
66
+ const result = debouncedFn()
67
+ ```
68
+
69
+ Correct:
70
+
71
+ ```ts
72
+ debouncedFn(); const result = useSubscription(debouncedFn)
73
+ ```
74
+
75
+ Return value updates after debounced execution via subscription.
76
+
77
+ ### [MEDIUM] Not subscribing to fn result
78
+
79
+ Wrong:
80
+
81
+ ```ts
82
+ debouncedFn(args) // never read output
83
+ ```
84
+
85
+ Correct:
86
+
87
+ ```ts
88
+ debouncedFn(args); debouncedFn.subscribe(console.log)
89
+ ```
90
+
91
+ DebouncedFn is Subscribable; subscribe or useSubscription to read latest result.
92
+
93
+ ## Source
94
+
95
+ `src/core/debounced-fn.ts`
@@ -1,8 +1,15 @@
1
1
  ---
2
- name: createIndexedDbStorage
2
+ name: create-indexed-db-storage
3
3
  description: >-
4
- Create a multi-key reactive handle for IndexedDB with schema validation.
5
- Use when persisting state to IndexedDB with async set and sync cached get.
4
+ Async IndexedDB persistence with cached sync reads.
5
+ type: core
6
+ library: seitu
7
+ library_version: "0.15.1"
8
+ requires:
9
+ - seitu-overview
10
+ sources:
11
+ - letstri/seitu:docs/content/docs/web/indexed-db-storage.mdx
12
+ - letstri/seitu:seitu/src/web/indexed-db-storage.ts
6
13
  ---
7
14
 
8
15
  # createIndexedDbStorage
@@ -57,7 +64,58 @@ interface IndexedDbStorage<O> extends Subscribable<O>, Readable<O>, Writable<Par
57
64
  clear: () => Promise<void>
58
65
  }
59
66
  ```
67
+ ## Common Mistakes
68
+
69
+ ### [HIGH] Treating set as synchronous
70
+
71
+ Wrong:
72
+
73
+ ```ts
74
+ await storage.set(data)
75
+ expect(storage.get()).toEqual(persistedFromDb)
76
+ ```
77
+
78
+ Correct:
79
+
80
+ ```ts
81
+ storage.set(data)
82
+ storage.subscribe(next => { /* react when cache updates */ })
83
+ ```
84
+
85
+ IndexedDB writes are async; get() returns cached value immediately.
86
+
87
+ ### [MEDIUM] Using in Node without fake-indexeddb
88
+
89
+ Wrong:
90
+
91
+ ```ts
92
+ createIndexedDbStorage({ ... }) // in vitest without polyfill
93
+ ```
94
+
95
+ Correct:
96
+
97
+ ```ts
98
+ import 'fake-indexeddb/auto'
99
+ ```
100
+
101
+ IndexedDB is browser-only; tests need fake-indexeddb polyfill.
102
+
103
+ ### [HIGH] Missing schemas for keys
104
+
105
+ Wrong:
106
+
107
+ ```ts
108
+ createIndexedDbStorage({ dbName: 'app' })
109
+ ```
110
+
111
+ Correct:
112
+
113
+ ```ts
114
+ createIndexedDbStorage({ dbName: 'app', schemas: { items: z.array(z.string()) }, defaultValues: { items: [] } })
115
+ ```
116
+
117
+ Same as WebStorage — keys must be declared in schemas/defaultValues.
60
118
 
61
119
  ## Source
62
120
 
63
- `seitu/src/web/indexed-db-storage.ts`
121
+ `src/web/indexed-db-storage.ts`
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: create-is-online
3
+ description: >-
4
+ Reactive navigator.onLine status.
5
+ type: core
6
+ library: seitu
7
+ library_version: "0.15.1"
8
+ requires:
9
+ - seitu-overview
10
+ sources:
11
+ - letstri/seitu:docs/content/docs/web/is-online.mdx
12
+ - letstri/seitu:seitu/src/web/is-online.ts
13
+ ---
14
+
15
+ # createIsOnline
16
+
17
+ Reactive boolean for `navigator.onLine`. Listens to `online`/`offline` window events.
18
+
19
+ ```ts
20
+ import { createIsOnline } from 'seitu/web'
21
+
22
+ const online = createIsOnline()
23
+ online.get() // true or false
24
+ online.subscribe(v => console.log(v ? 'online' : 'offline'))
25
+ ```
26
+
27
+ ## Interface
28
+
29
+ ```ts
30
+ interface IsOnline extends Subscribable<boolean>, Readable<boolean> {}
31
+ ```
32
+
33
+ No options. Returns `true` during SSR (when `navigator` is undefined). Lazy — only listens to events while subscribed.
34
+ ## Common Mistakes
35
+
36
+ ### [MEDIUM] Polling navigator.onLine manually
37
+
38
+ Wrong:
39
+
40
+ ```ts
41
+ setInterval(() => setOnline(navigator.onLine), 1000)
42
+ ```
43
+
44
+ Correct:
45
+
46
+ ```ts
47
+ const online = createIsOnline()
48
+ online.subscribe(v => setOnline(v))
49
+ ```
50
+
51
+ createIsOnline subscribes to online/offline events.
52
+
53
+ ### [MEDIUM] Assuming onLine means reachable server
54
+
55
+ Wrong:
56
+
57
+ ```ts
58
+ if (online.get()) fetch('/api') // always succeeds
59
+ ```
60
+
61
+ Correct:
62
+
63
+ ```ts
64
+ if (online.get()) fetch('/api').catch(handleOffline)
65
+ ```
66
+
67
+ onLine is browser connectivity, not server health.
68
+
69
+ ### [LOW] Guarding creation with typeof window
70
+
71
+ Wrong:
72
+
73
+ ```ts
74
+ const online = typeof window !== 'undefined' ? createIsOnline() : null
75
+ ```
76
+
77
+ Correct:
78
+
79
+ ```ts
80
+ const online = createIsOnline()
81
+ ```
82
+
83
+ Returns false when offline API unavailable; safe at module scope.
84
+
85
+ ## Source
86
+
87
+ `src/web/is-online.ts`