prop-for-that 0.6.4

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.
package/llms.txt ADDED
@@ -0,0 +1,204 @@
1
+ # prop-for-that
2
+
3
+ > Expose runtime state that JavaScript knows but CSS can't see, as batched, diffed
4
+ > CSS custom properties. CSS then computes and reacts to it via `var()` (continuous
5
+ > values) or `@container style()` (discrete values), with no JS in the animation path.
6
+
7
+ prop-for-that reads browser/runtime state (pointer, viewport, element size,
8
+ visibility, slider values, network, battery, sensors, and more) and writes it into
9
+ `--live-*` (reactive) and `--const-*` (write-once) custom properties. A single
10
+ `requestAnimationFrame` writer coalesces all updates and only calls `setProperty`
11
+ when a value actually changed. Zero runtime dependencies, tree-shakeable, TypeScript.
12
+
13
+ ## Entry points
14
+
15
+ - `prop-for-that`: the core API (`configure`, `register`, `unregister`, `propsFor`, `unbind`, `reset`).
16
+ - `prop-for-that/auto`: side-effecting. On load it attaches the default globals (`viewport`, `pointer`) to `:root`, scans the DOM for `[data-props-for="key1 key2"]` and binds those sources, and watches the DOM with a `MutationObserver`. If the root `<html>` has the boolean attribute `data-props-typed`, it calls `configure({ typed: true })` first (markup equivalent; read once on load; global, no per-key subset).
17
+ - `prop-for-that/head`: side-effecting. Synchronously writes FOUC-safe constants before first paint. Inline it in `<head>`.
18
+ - `prop-for-that/plugins`: opt-in plugin sources, plus `registerPlugins` and `allPlugins`.
19
+
20
+ Build outputs: ESM + CJS + `.d.ts`. Browser-only; entries guard `typeof document`, so importing is SSR-safe (sources attach client-side).
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm i prop-for-that
26
+ ```
27
+
28
+ ## Mental model
29
+
30
+ 1. A **source** reads one group of state. It has a `key` (used in `propsFor`/`data-props-for`), a `scope` (`global` → writes to `:root`; `element` → writes to the bound element), and a `start(ctx)` that attaches listeners/observers and returns a disposer.
31
+ 2. Sources call `ctx.write(localName, value, cadence?)`, never `setProperty` directly. The **Writer** queues writes and flushes once per `requestAnimationFrame`, diffing against the last value written.
32
+ 3. CSS consumes the resulting custom properties:
33
+ - **`var()`** for continuous values (ratios, dimensions), composed with `calc()`.
34
+ - **`@container style()`** for discrete/boolean/integer values (equality match). Every element is a style-query container by default; no `container-type` needed.
35
+
36
+ ## API
37
+
38
+ ```ts
39
+ import { configure, register, unregister, propsFor, unbind, reset, pause, resume } from 'prop-for-that'
40
+
41
+ configure(opts: Partial<Config>): void // call once, before any propsFor()
42
+ register(source: Source): void // add a custom/plugin source by key
43
+ unregister(key: SourceKey): void // remove a source from the registry
44
+ propsFor(keys: string[]): Disposer // global: attach sources to config.root (:root)
45
+ propsFor(target: Element | NodeList | Element[], keys: string[]): Disposer // per element(s)
46
+ unbind(target: HTMLElement, keys?: string[]): void // detach some/all
47
+ reset(): void // tear down every binding + shared observers/listeners
48
+ pause(): void // freeze the loop (no sampling/flushing); idempotent
49
+ resume(): void // unfreeze; flushes anything queued while paused
50
+
51
+ // propsFor is global unless the first arg is a Node, NodeList, or array of elements.
52
+ // Its disposer tears down only what that call started and removes the properties it wrote.
53
+
54
+ interface Config { livePrefix: string; constPrefix: string; root: HTMLElement; typed: boolean; defaults?: Record<string, string|number>; liveHz?: number }
55
+ // defaults: livePrefix '--live-', constPrefix '--const-', root document.documentElement, typed false, liveHz unset (every frame)
56
+ // config.defaults are typed @property initial values, keyed by a source's local name
57
+ // liveHz caps loop sampling+flushing rate (e.g. 30); fewer writes = less style recalc + calmer DevTools
58
+
59
+ interface Source {
60
+ key: SourceKey
61
+ scope: 'global' | 'element'
62
+ start(ctx: SourceContext): Disposer
63
+ }
64
+ interface SourceContext {
65
+ target: HTMLElement
66
+ config: Config
67
+ write(localName: string, value: string | number, cadence?: 'live' | 'const'): void
68
+ }
69
+ type Disposer = () => void
70
+ ```
71
+
72
+ Plugins:
73
+
74
+ ```ts
75
+ import { registerPlugins, allPlugins, battery /* …named exports */ } from 'prop-for-that/plugins'
76
+
77
+ registerPlugins() // register every plugin
78
+ registerPlugins(battery, clock) // register a subset
79
+ ```
80
+
81
+ `propsFor()` returns a disposer; re-attaching an already-active key on the same target is a no-op.
82
+
83
+ ## Variable catalog (canonical)
84
+
85
+ All names below are the full property names. Reactive values use the `--live-`
86
+ prefix; constants use `--const-`. Values are unitless numbers unless noted.
87
+
88
+ ### Core sources (in the main bundle)
89
+
90
+ | key | scope | properties |
91
+ | --- | --- | --- |
92
+ | `viewport` | global | `--live-vw`, `--live-vh` |
93
+ | `pointer` | global | `--live-pointer-x`, `--live-pointer-y`, `--live-pointer-x-ratio` (0–1), `--live-pointer-y-ratio` (0–1) |
94
+ | `size` | element | `--live-w`, `--live-h`, `--live-aspect` (w/h) |
95
+ | `visibility` | element | `--live-visible` (1/0, whole element in viewport), `--const-has-entered` (1/0, latches once entirely in viewport, never resets) |
96
+ | `range` | element | `--live-value`, `--live-value-pct` (0–1). Bind the `<input>` or a container holding one. |
97
+
98
+ ### Head constants (`prop-for-that/head`)
99
+
100
+ | property | value |
101
+ | --- | --- |
102
+ | `--const-scrollbar-w` | scrollbar width in px |
103
+ | `--const-scrollbar-thin-w` | scrollbar width in px with `scrollbar-width: thin` (= `--const-scrollbar-w` where unsupported) |
104
+ | `--const-dpr` | `devicePixelRatio` |
105
+ | `--const-cores` | `navigator.hardwareConcurrency` |
106
+ | `--const-mem` | `navigator.deviceMemory` in GiB (Chromium-only, coarse; `0` elsewhere) |
107
+
108
+ ### Plugins (`prop-for-that/plugins`)
109
+
110
+ Import name is camelCase; the key for `propsFor`/`data-props-for` is the dashed form.
111
+
112
+ | import | key | scope | properties |
113
+ | --- | --- | --- | --- |
114
+ | `scrollVelocity` | `scroll-velocity` | global | `--live-scroll-velocity` (signed px/frame), `--live-scroll-direction` (1/-1/0) |
115
+ | `online` | `online` | global | `--live-online` (1/0) |
116
+ | `pageFocused` | `page-focused` | global | `--live-page-focused` (1/0 — tab frontmost and window focused; `0` when switched away) |
117
+ | `pageVisible` | `page-visible` | global | `--live-page-visible` (1/0 — tab foreground vs hidden/backgrounded; `document.visibilityState`. A visible tab can still be unfocused) |
118
+ | `navType` | `nav-type` | global | `--const-nav-type` (write-once string — how the user arrived: `navigate` / `reload` / `back_forward` / `prerender`, from `performance.getEntriesByType('navigation')[0].type`. Pairs with `@container style(--const-nav-type: reload)`) |
119
+ | `network` | `network` | global | `--live-net-downlink`, `--live-net-rtt`, `--live-net-save-data` (1/0), `--live-net-type` (slow-2g=1…4g=4, else 0) |
120
+ | `battery` | `battery` | global | `--live-battery-level` (0–1), `--live-battery-charging` (1/0) |
121
+ | `clock` | `clock` | global | `--live-now` (epoch seconds), `--live-hours`, `--live-minutes`, `--live-seconds` |
122
+ | `fps` | `fps` | global | `--live-fps` |
123
+ | `visualViewport` | `visual-viewport` | global | `--live-vvp-scale`, `--live-vvp-offset-top`, `--live-vvp-height` |
124
+ | `orientation` | `orientation` | global | `--live-orient-alpha`, `--live-orient-beta`, `--live-orient-gamma` (deg) |
125
+ | `motion` | `motion` | global | `--live-accel-x`, `--live-accel-y`, `--live-accel-z` (m/s²) |
126
+ | `geo` | `geo` | global | `--live-geo-lat`, `--live-geo-lng`, `--live-geo-accuracy` (m) |
127
+ | `cpuPressure` | `cpu-pressure` | global | `--live-cpu-pressure` (Compute Pressure tier: nominal=0, fair=1, serious=2, critical=3) |
128
+ | `pointerLocal` | `pointer-local` | element | `--live-local-pointer-x-ratio`, `--live-local-pointer-y-ratio` (0–1 within the element), `--live-local-pointer-inside` (1/0) |
129
+ | `media` | `media` | element | `--live-current-time`, `--live-duration`, `--live-progress` (0–1), `--live-paused` (1/0), `--live-volume` (0–1) |
130
+ | `field` | `field` | element | `--live-length`, `--live-empty` (1/0), `--live-valid` (1/0); per-reason validity flags (each 1/0) `--live-value-missing`, `--live-type-mismatch`, `--live-pattern-mismatch`, `--live-too-long`, `--live-too-short`, `--live-range-underflow`, `--live-range-overflow`, `--live-step-mismatch`, `--live-bad-input`, `--live-custom-error`; and with `maxlength`: `--live-remaining`, `--live-fill-pct` (0–1) |
131
+ | `select` | `select` | element | `--live-index` (selectedIndex, -1 if none), `--live-option-count`, `--live-index-pct` (0–1), `--live-value-num` (numeric value only), `--live-selected-count`, `--live-selected-pct` (0–1, counts a `<select multiple>` CSS can't tally). Bind the `<select>` or a container. |
132
+ | `colorInput` | `color-input` | element | `--live-color` (the `<input type="color">` value as sRGB hex; typed mode registers it `<color>`). Bind the input or a container. |
133
+ | `fieldState` | `field-state` | element | form interaction history (all 1/0): `--live-dirty`/`--live-pristine` (user has edited it; latches), `--live-touched`/`--live-untouched` (blurred at least once; latches), `--live-changed` (current value ≠ mount value; un-latches), `--live-submitted` (owning `<form>` submitted). Bind a single field for its state, or a `<form>`/wrapper for the aggregate over all its fields. |
134
+ | `formState` | `form-state` | element | form-level validity/completion CSS can't count: `--live-field-count`, `--live-valid-count`, `--live-invalid-count`, `--live-all-valid` (1/0 submit gate), `--live-completion` (0–1, valid required ÷ required). Bind a `<form>`/wrapper. |
135
+ | `img` | `img` | element | `--live-natural-w`, `--live-natural-h` (intrinsic px), `--live-loaded` (1/0), `--live-broken` (1/0). Bind the `<img>` or a container holding one. |
136
+ | `imgColor` | `img-color` | element | a palette from an `<img>`, all from one 16×16 bucketing pass (createImageBitmap+OffscreenCanvas). Each swatch is a single sRGB hex colour (no channel props — use relative colour syntax / color-mix to extract): `--live-img` (dominant), `--live-img-accent` (most vibrant), `--live-img-dark` (darkest non-black), `--live-img-light` (lightest non-white), `--live-img-avg` (mean). Plus `--live-img-temp` (−1 cool…+1 warm). Accent reuses the dominant for grayscale. Cross-origin needs `crossorigin` + CORS, else no-ops. Bind the `<img>` or a container. |
137
+ | `videoColor` | `video-color` | element | live colours of a playing `<video>`, each a single sRGB hex colour: `--live-video` (dominant) + `--live-video-accent` (most vibrant; reuses the dominant for a grayscale frame). Both from one 16×16 read on `requestVideoFrameCallback` (stops when paused/offscreen/backgrounded), throttled to ~4 Hz; seeds a paused/poster frame; falls back to `timeupdate`. Cross-origin needs `crossorigin` + CORS, else no-ops. Bind the `<video>` or a container. |
138
+
139
+ ## Recipes
140
+
141
+ Zero-config (attributes):
142
+
143
+ ```html
144
+ <script type="module">import 'prop-for-that/auto'</script>
145
+ <input type="range" data-props-for="range">
146
+ <section data-props-for="size visibility">…</section>
147
+ ```
148
+
149
+ Imperative:
150
+
151
+ ```js
152
+ import { propsFor } from 'prop-for-that'
153
+ propsFor(['pointer']) // writes pointer vars to :root
154
+ propsFor(el, ['size', 'visibility']) // writes element vars to el
155
+ ```
156
+
157
+ A plugin:
158
+
159
+ ```js
160
+ import { register, propsFor } from 'prop-for-that'
161
+ import { battery } from 'prop-for-that/plugins'
162
+ register(battery)
163
+ propsFor(['battery'])
164
+ ```
165
+
166
+ Container-bound range (so siblings can read the value):
167
+
168
+ ```js
169
+ // the source finds the inner <input> but writes the vars on the wrapper
170
+ propsFor(document.querySelector('.meter'), ['range'])
171
+ ```
172
+
173
+ Continuous reaction with `var()`:
174
+
175
+ ```css
176
+ .card { transform: rotateY(calc((var(--live-pointer-x-ratio) - 0.5) * 16deg)); }
177
+ ```
178
+
179
+ Discrete reaction with a style query:
180
+
181
+ ```css
182
+ @container style(--live-online: 0) { .banner { display: block; } }
183
+ @container style(--live-value: 100) { .badge { color: var(--accent); } }
184
+ ```
185
+
186
+ ## Gotchas (read before writing CSS)
187
+
188
+ - **Custom properties inherit downward only.** An `element`-scoped source writes its vars on the bound element. To let *sibling* elements react, bind a common ancestor. `range` and `field` help here: bind them to a container and they find the inner `<input>`, writing the vars on the container.
189
+ - **Pick the right consumer.** Continuous numbers go through `var()` + `calc()`. Discrete/boolean/integer values use `@container style()`, which matches on **equality only** (range comparisons like `style(--x < 5)` are not stable yet). Emit booleans/tiers when you need thresholds.
190
+ - **`visibility` is binary + latched, not a ratio.** It writes `--live-visible` and `--const-has-entered`. There is no continuous visible-ratio and no scroll-position source: use native `animation-timeline: scroll()` / `view()` for continuous scroll-driven effects.
191
+ - **Values are unitless.** Multiply by `1px`, `1deg`, `100%`, etc. in CSS.
192
+ - **Configure prefixes before attaching.** `configure({ livePrefix, constPrefix })` must run before any `propsFor()`.
193
+ - **Typed properties are opt-in.** `configure({ typed: true })` (before attaching) registers each written `--live-*` with `@property` so it interpolates (consumers add `transition`/`@keyframes`) and resolves to `0` before a value arrives. With the `auto` entry, `<html data-props-typed>` is the markup equivalent (boolean, read once on load; typing is global per `@property` name, so no per-key subset). It is a one-way door (no unregister), uses `inherits: true`, is feature-detected, and skips names already registered. Sources can declare per-property `props: { name: { syntax, initial, inherits } }` (default `<number>` / `0` / inherited); consumers can set initials with `configure({ typed: true, defaults: { 'pointer-x-ratio': 0.5 } })` (keyed by local name).
194
+ - **Permission-gated plugins** (`orientation`, `motion`, `geo`) and unsupported APIs feature-detect and no-op; registering them is always safe. On iOS some need a user-gesture permission grant before values arrive. `cpuPressure` is Chromium-only and needs a secure context + the `compute-pressure` Permissions Policy; it seeds `--live-cpu-pressure: 0` where supported, writes nothing where not.
195
+ - **Global writes land in an adopted stylesheet, not `<html>` inline style.** Root `--live-*` go into one constructable, adopted `:root {}` rule (element-scoped writes stay inline; the `head` entry still writes `--const-*` inline for first paint). Keeps the DevTools Styles panel usable under per-frame churn; `var()`/`calc()` consumption is identical (still inherits). Reading `documentElement.style` directly won't show them — read computed style. Falls back to inline where constructable stylesheets are unsupported.
196
+ - **Freeze or throttle the loop.** `pause()` / `resume()` stop and restart sampling+flushing (values hold steady; bindings stay attached). `configure({ liveHz: 30 })` caps the rate to reduce mutations/recalc, throttling samplers (`fps`, `scroll-velocity`) too.
197
+
198
+ ## Links
199
+
200
+ - [Documentation](https://prop-for-that.netlify.app/docsite/): full guides, concepts, and API.
201
+ - [Plugin & source reference](https://prop-for-that.netlify.app/docsite/reference/plugins/): every source, key, and property.
202
+ - [Live demos](https://prop-for-that.netlify.app/): interactive examples of each source.
203
+ - [Under consideration](https://prop-for-that.netlify.app/docsite/considered/): deferred/proposed sources and voting.
204
+ - [Repository](https://github.com/argyleink/prop-for-that): source, issues, changelog, contributing.
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "prop-for-that",
3
+ "version": "0.6.4",
4
+ "description": "Expose what JavaScript knows but CSS can't see, as batched, diffed CSS custom properties.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/argyleink/prop-for-that.git"
10
+ },
11
+ "author": "Adam Argyle (https://github.com/argyleink)",
12
+ "keywords": [
13
+ "css",
14
+ "custom-properties",
15
+ "css-variables",
16
+ "reactive",
17
+ "state",
18
+ "dom",
19
+ "battery",
20
+ "network",
21
+ "geolocation",
22
+ "device-orientation",
23
+ "scroll-velocity",
24
+ "fps"
25
+ ],
26
+ "sideEffects": [
27
+ "./dist/auto.js",
28
+ "./dist/auto.cjs",
29
+ "./dist/head.js",
30
+ "./dist/head.cjs"
31
+ ],
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "import": "./dist/index.js",
36
+ "require": "./dist/index.cjs"
37
+ },
38
+ "./auto": {
39
+ "types": "./dist/auto.d.ts",
40
+ "import": "./dist/auto.js",
41
+ "require": "./dist/auto.cjs"
42
+ },
43
+ "./head": {
44
+ "types": "./dist/head.d.ts",
45
+ "import": "./dist/head.js",
46
+ "require": "./dist/head.cjs"
47
+ },
48
+ "./plugins": {
49
+ "types": "./dist/plugins.d.ts",
50
+ "import": "./dist/plugins.js",
51
+ "require": "./dist/plugins.cjs"
52
+ },
53
+ "./package.json": "./package.json"
54
+ },
55
+ "files": [
56
+ "dist",
57
+ "llms.txt",
58
+ "CHANGELOG.md"
59
+ ],
60
+ "publishConfig": {
61
+ "access": "public",
62
+ "provenance": true
63
+ },
64
+ "scripts": {
65
+ "build": "tsup",
66
+ "build:demos": "cd demos && npm install && npm run build",
67
+ "build:docs": "cd docs && npm install && DOCS_BASE=/docsite npm run build",
68
+ "build:site": "npm run build:demos && npm run build:docs && node scripts/assemble-site.mjs",
69
+ "dev": "tsup --watch",
70
+ "typecheck": "tsc --noEmit",
71
+ "test": "vitest run",
72
+ "test:watch": "vitest",
73
+ "test:e2e": "playwright test",
74
+ "prepublishOnly": "npm run build"
75
+ },
76
+ "devDependencies": {
77
+ "@playwright/test": "^1.49.1",
78
+ "jsdom": "^25.0.1",
79
+ "tsup": "^8.3.5",
80
+ "typescript": "^5.7.2",
81
+ "vitest": "^2.1.8"
82
+ }
83
+ }