@vitreajs/vitrea 0.1.0 → 0.2.0

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/README.md CHANGED
@@ -25,34 +25,53 @@ selectable, focusable, IME-capable, and announced by a screen reader as a button
25
25
  ## Install
26
26
 
27
27
  ```bash
28
- npm install @vitreajs/vitrea @vitreajs/vitrea-react
28
+ npm install @vitreajs/vitrea-react # React
29
+ npm install @vitreajs/vitrea-web # plain JS, or your own adapter
29
30
  ```
30
31
 
31
- The library is called vitrea and publishes under the npm scope `@vitreajs`.
32
- `@vitreajs/vitrea` is the framework-agnostic runtime, `@vitreajs/vitrea-react`
33
- the declarative surface. These two are the only published packages: the
34
- geometry kernel, the motion kernel, the DOM host layer and the WebGPU renderer
35
- are internal and bundled into them at publish time, so an app installs two
36
- packages and gets zero transitive runtime dependencies beyond React itself.
32
+ The library is called vitrea and publishes under the npm scope `@vitreajs`, in
33
+ three packages that layer the way React's own do — a pure runtime, a DOM host
34
+ over it, framework bindings over that:
35
+
36
+ | Package | What it is | Depends on |
37
+ | --- | --- | --- |
38
+ | `@vitreajs/vitrea` | the framework-agnostic runtime: scene model, capability and tier resolution, material and accessibility policy. No DOM code at all. | nothing |
39
+ | `@vitreajs/vitrea-web` | the browser host: element registration, plane management, backdrop proxies, the CSS-tier renderer, the WebGPU lifecycle. **Mounts a root from any framework, or none.** | `@vitreajs/vitrea` |
40
+ | `@vitreajs/vitrea-react` | the declarative surface: `GlassRoot`, `GlassGroup`, `GlassSurface` and the v1 controls. | both of the above |
41
+
42
+ Installing the React bindings pulls the other two in, so a React app still runs
43
+ one `npm install` and gets no transitive runtime dependency outside this project
44
+ beyond React itself. The geometry kernel, the motion kernel and the WebGPU
45
+ renderer stay internal and are bundled in at publish time.
46
+
47
+ **TypeScript.** The published declarations resolve on their own, on any
48
+ TypeScript version, with no `types` entry and nothing extra installed —
49
+ including with `skipLibCheck: false`. The WebGPU type names the artifacts use
50
+ are declared inside them, and they merge with your own WebGPU types
51
+ (TypeScript 6's DOM lib, `@types/web`, or `@webgpu/types`) rather than
52
+ competing with them.
37
53
 
38
54
  ### Which package you actually import from
39
55
 
40
56
  | You are… | Install | Import |
41
57
  | --- | --- | --- |
42
- | writing a React app | `@vitreajs/vitrea @vitreajs/vitrea-react` | `@vitreajs/vitrea-react` for components, `@vitreajs/vitrea` for types |
43
- | reading the resolved capability state | either | `@vitreajs/vitrea` |
44
- | writing a Vue/Svelte/WC adapter | `@vitreajs/vitrea` | not yet supported — see below |
58
+ | writing a React app | `@vitreajs/vitrea-react` | `@vitreajs/vitrea-react` for components, `@vitreajs/vitrea` for types |
59
+ | writing plain JavaScript, or a Vue/Svelte/Web-Components adapter | `@vitreajs/vitrea-web` | `@vitreajs/vitrea-web` for `createGlassRoot`, `@vitreajs/vitrea` for types |
60
+ | reading the resolved capability state | any of them | `@vitreajs/vitrea` |
45
61
 
46
- **One honest limit up front.** `vitrea` contains no DOM code at all, by design
62
+ **Which one has the DOM in it.** `vitrea` contains no DOM code at all, by design
47
63
  (the purity law: the core, geometry and motion packages never touch `window`,
48
- `document` or `HTMLElement`). The browser host layer element registration,
49
- plane management, backdrop proxies, the CSS-tier renderer, the WebGPU lifecycle
50
- ships bundled *inside* `vitrea-react`. So in v1 the only way to render glass in a
51
- browser is through the React bindings. `vitrea` on its own gives you the scene
52
- model, the capability and tier resolver, the material and accessibility policy,
53
- and the frame-scheduler contract; it does not give you a mounted root. A
54
- framework-agnostic host entry point is post-v1 work, not a thing you can reach
55
- today by installing `vitrea` alone.
64
+ `document` or `HTMLElement`). So `vitrea` on its own gives you the scene model,
65
+ the capability and tier resolver, the material and accessibility policy, and the
66
+ frame-scheduler contract but not a mounted root. `vitrea-web` is the package
67
+ that mounts one, and it is a peer of the React bindings rather than something
68
+ hidden inside them: the same `createGlassRoot` the bindings use is the one a
69
+ vanilla page calls. See [`@vitreajs/vitrea-web`](../platform-web/README.md) for
70
+ the imperative quickstart.
71
+
72
+ > Until 0.2 the host layer shipped bundled *inside* `vitrea-react`, so React was
73
+ > the only way to render glass in a browser. That is what publishing this package
74
+ > retired.
56
75
 
57
76
  ---
58
77
 
@@ -93,6 +112,47 @@ sampling unit — one backdrop proxy, one blur, shared between its members:
93
112
  </GlassGroup>
94
113
  ```
95
114
 
115
+ **A surface has no intrinsic size.** vitrea declares neither position nor size
116
+ for one: it measures the box your CSS produced, once per frame, and fits the
117
+ material to it. The element itself is portalled into its plane's host layer,
118
+ which is a `position: absolute; inset: 0` overlay, so a surface places itself the
119
+ way any overlay child does — give it your own width, height and positioning, and
120
+ it will be exactly as big as you made it. This is also why press compression and
121
+ morph deformation are composed transforms rather than shape changes: a transform
122
+ cannot dirty the rect it is animating.
123
+
124
+ ### Glass is a controls-layer material
125
+
126
+ There is one placement rule, and Apple states it as a prohibition rather than as
127
+ advice: **"Don't use Liquid Glass in the content layer."** The material exists to
128
+ separate the things you can act on from the things you are reading; putting it on
129
+ both collapses that distinction. Apple names both failure modes: "including it in
130
+ the content layer can result in unnecessary complexity and a confusing visual
131
+ hierarchy", and, for stacking, "avoid applying the material to both layers.
132
+ Instead, use fills, transparency, and vibrancy for the top elements."
133
+
134
+ So: glass on the toolbar, not on the article you are reading through it. Glass on
135
+ the row's button, not on the row. And glass on the control, never on the control *and* its
136
+ container. Lists and tables are the case Apple calls out by name, and they are
137
+ the case a web glass library gets wrong most often, because `asChild` will glass
138
+ a `<tr>` as readily as a `<button>`.
139
+
140
+ vitrea now checks the two compositions that are decidable from structure, at
141
+ registration, in dev mode only:
142
+
143
+ | code | fires when | what it tells you |
144
+ | --- | --- | --- |
145
+ | `glass-inside-glass` | a registered host sits inside another registered host's subtree | which pair, and to keep the material on whichever of the two is the control while the other takes a fill, a translucency or a vibrant foreground |
146
+ | `glass-in-content-layer` | a host is registered on an element whose resolved ARIA role is a list or table structure | which element and which role, and either to move the glass onto the control the row holds or — if it really is a controls-layer container — to give it the role it means, at which point the check stands down |
147
+
148
+ Both are advisory findings on the diagnostics channel rather than throws, and
149
+ both are `devMode`-only: the check runs once per `registerHost` call and never
150
+ from a frame, so a production build pays nothing for it. What vitrea does *not*
151
+ check is the rest of the rule — whether anything is actually scrolling under a
152
+ surface, whether a non-interactive label has been glassed, whether glass and
153
+ content intersect at rest. Those need judgement or a per-frame observation, and a
154
+ diagnostic that fires on a correct page is worse than no diagnostic.
155
+
96
156
  And to see what the runtime actually resolved to, rather than what you asked for:
97
157
 
98
158
  ```tsx
@@ -139,8 +199,8 @@ interface GlassGroupState {
139
199
  analysis: "exact" | "hint" | "none";
140
200
  health: "ok" | "demoted";
141
201
  demotionReason?: "no-webgpu" | "no-backdrop-filter" | "tainted-source"
142
- | "incompatible-texture" | "device-lost" | "probe-failed"
143
- | "governor";
202
+ | "incompatible-texture" | "no-texture-supplied" | "device-lost"
203
+ | "probe-failed" | "governor";
144
204
  }
145
205
  ```
146
206
 
@@ -164,7 +224,7 @@ point.
164
224
 
165
225
  | Configuration | What you get |
166
226
  | --- | --- |
167
- | **texture + exact** — you register an image, video, canvas or procedural texture as the group's backdrop | Full refraction. Edge lensing visibly bends the backdrop; a larger surface lenses deeper than a small one over the same content. Luminance, variance and edge-density analysis run on the GPU. |
227
+ | **texture + exact** — you register an image, video or canvas as the group's backdrop | Full refraction. Edge lensing visibly bends the backdrop; a larger surface lenses deeper than a small one over the same content. Luminance, variance and edge-density analysis run on the GPU. |
168
228
  | **dom + hint** — arbitrary page content, plus a `hint` (or an estimator provider) | The browser compositor does the blur through a masked backdrop proxy; the GPU renders rim lensing, tint, glow and morphs. Adaptation comes from your hint. |
169
229
  | **dom + none** — arbitrary page content, no hint (the default) | Fixed regular material, geometry-driven rim and specular, foreground from tokens or `color-scheme`. |
170
230
 
@@ -173,6 +233,74 @@ There is a built-in best-effort estimator that reads known background colours an
173
233
  images where CORS permits, and it is documented as an estimator every place it
174
234
  appears — not as pixel analysis, because that is not what it is.
175
235
 
236
+ **The backdrop reading decides the material's own appearance too, not only the
237
+ foreground.** Apple's material stops being a lighter thing in front of a dark
238
+ enough backdrop and takes that backdrop's tone; vitrea does the same, size-gated,
239
+ so a 44 px control disappears into a near-black backdrop while a large panel over
240
+ the same one keeps most of its own appearance. Read the table above with that in
241
+ mind: **texture + exact** measures the tone from the pixels you supplied,
242
+ **dom + hint** adapts to the tone your hint declares, and **dom + none** does not
243
+ adapt at all — it will not guess a backdrop it has not been shown, on either tier.
244
+ Nothing happens above roughly a fifth of the luminance range, so an ordinary page
245
+ is untouched. Measured against the macOS 26.5 reference; the law and its numbers
246
+ are in `docs/doperpowers/specs/c9a-fidelity-claims.md` §5.8.
247
+
248
+ ### Registering a texture backdrop
249
+
250
+ The texture path is two steps, and it is two because of the purity law above:
251
+ `vitrea` may not hold an `HTMLImageElement`, so it cannot be the thing you hand
252
+ pixels to. The group **declares** the source; the root **supplies** it.
253
+
254
+ ```tsx
255
+ import { GlassGroup, GlassSurface, useGlassRoot } from "@vitreajs/vitrea-react";
256
+
257
+ function Hero() {
258
+ const root = useGlassRoot();
259
+
260
+ return (
261
+ <>
262
+ {/* 1. Declare. `configuredSource` stays "texture" through any demotion. */}
263
+ <GlassGroup id="hero" backdrop={{ kind: "texture", id: "hero" }}>
264
+ <GlassSurface radius={26} thickness={18}>…</GlassSurface>
265
+ </GlassGroup>
266
+
267
+ {/* 2. Supply. The id is what joins the two halves; the order does not
268
+ matter, and `setBackdropTexture` marks the source dirty itself, so
269
+ handing the pixels over is the whole wiring. */}
270
+ <img
271
+ src="/hero.jpg"
272
+ alt=""
273
+ onLoad={(event) =>
274
+ root?.setBackdropTexture("hero", { kind: "image", image: event.currentTarget })
275
+ }
276
+ />
277
+ </>
278
+ );
279
+ }
280
+ ```
281
+
282
+ The other two forms are `{ kind: "canvas", canvas }` and `{ kind: "video", video }`.
283
+ A video and a live canvas are re-imported on every frame that samples them; a
284
+ decoded image is imported once. Passing `undefined` withdraws a source's pixels.
285
+
286
+ Declaring a texture and never supplying one is not a silent hole: the group
287
+ resolves to `health: "demoted"` with `demotionReason: "no-texture-supplied"` and
288
+ keeps drawing tint, rim and glow. The readout names the missing half.
289
+
290
+ **Where the texture is placed.** The renderer maps the source over the **whole
291
+ viewport**, cover-fit — it fills the viewport and the overflow is cropped
292
+ symmetrically, the same geometry as `object-fit: cover` on a
293
+ `position: fixed; inset: 0` element. Not over the group, and not over the
294
+ surface.
295
+
296
+ This matters whenever your app paints the same image itself, which is the usual
297
+ case: the picture is on the page and the glass sits on top of it. The two
298
+ mappings have to agree. An `<img>` sized to a region, under a texture mapped to
299
+ the viewport, samples a different crop of the same file — and the mismatch
300
+ appears as the glass showing the wrong part of the picture, which reads
301
+ convincingly like a lensing artefact rather than a registration error. Paint your
302
+ copy viewport-sized and `object-fit: cover`, or accept that the two will differ.
303
+
176
304
  ### Tiers degrade within themselves before they switch
177
305
 
178
306
  The quality governor first reduces refraction resolution, adaptation cadence and
@@ -274,6 +402,14 @@ check.
274
402
  - **Tier coherence is a Chromium measurement of a material's level**, on one
275
403
  profile, at 1×. It does not say the two tiers are identical, and it does not
276
404
  hold on Gecko or WebKit — nothing measurable does there.
405
+ - **No fidelity claim for the author tint.** `GlassSurface`'s `tint` prop is
406
+ implemented on both tiers and visually verified on both, but every constant in
407
+ the tone curve that turns an author's colour into "a range of tones mapped to
408
+ content brightness underneath" is an advisory default. The tinted native
409
+ captures that would fit them were taken on 2026-08-30 and could not be used:
410
+ they carry the tint's strength but not its colour, so two scenes declaring
411
+ different seeds came back byte-identical (claims §5.10). Until a re-captured
412
+ bed exists the tint's appearance is designed, not calibrated.
277
413
 
278
414
  ---
279
415
 
@@ -311,6 +447,74 @@ errors. Asking for the GPU tier is not the same as getting it, and
311
447
  a support query.
312
448
  - v1 corner radii are uniform. The `radii` API keeps its four-component shape,
313
449
  but a non-uniform set is a dev-mode error; per-corner algebra is post-v1.
450
+ - **One author tint seed per group.** A tint is declared per surface and its
451
+ strength is honoured per surface, but a group is one optics pass and carries
452
+ one seed, so two *different* tint colours in one group raise a dev-mode warning
453
+ and the WebGPU tier draws them all in the first surface's colour. One coloured
454
+ control among plain ones — Apple's own guidance for tinting — is the supported
455
+ composition.
456
+
457
+ ---
458
+
459
+ ## Testing your app
460
+
461
+ ### The readout is trustworthy; the environment may not be
462
+
463
+ `useGlassCapabilities()` reports what resolved. If it says
464
+ `demotionReason: "no-webgpu"`, that session genuinely had no WebGPU — the
465
+ readout is not the thing to doubt. What is worth doubting is the environment,
466
+ because one browser-automation default removes WebGPU without removing anything
467
+ you would notice.
468
+
469
+ **Playwright's bundled headless shell hands back a software adapter.** This repo
470
+ measures three different answers on one machine depending on how Chromium is
471
+ launched: the default headless shell resolves to a SwiftShader adapter, while
472
+ `channel: "chromium"` — the full browser binary — resolves to real hardware. A
473
+ suite that never asks for the channel runs entirely green on the CSS tier, and
474
+ every readout in it honestly says `no-webgpu`, which is what makes this the worst
475
+ kind of failure: nothing is broken, it is just the other tier, and the
476
+ instrumentation agrees with the wrong answer.
477
+
478
+ ```ts
479
+ // playwright.config.ts
480
+ projects: [
481
+ {
482
+ name: "chromium-gpu",
483
+ use: {
484
+ channel: "chromium",
485
+ launchOptions: {
486
+ args: ["--enable-unsafe-webgpu", "--enable-features=Vulkan,WebGPU"],
487
+ },
488
+ },
489
+ },
490
+ ],
491
+ ```
492
+
493
+ Two further requirements are not optional. `navigator.gpu` is undefined outside a
494
+ secure context, so serve the page over `http://localhost` — on `file://` and
495
+ `data:` URLs the absence reads exactly like "no WebGPU on this machine". And a
496
+ test that means to assert the GPU tier should **fail** when no adapter answers
497
+ rather than skip, and fail rather than quietly accept a software one; otherwise
498
+ it asserts nothing.
499
+
500
+ ### What is in the DOM differs by tier
501
+
502
+ The backdrop-proxy elements are not a general debugging landmark. A
503
+ `[data-vitrea-proxy]` element exists only where the GPU tier is sampling
504
+ arbitrary DOM:
505
+
506
+ | Resolved state | What the runtime puts in the DOM |
507
+ | --- | --- |
508
+ | `activeRenderer: "webgpu"`, `samplingBackend: "css-backdrop"` | one `[data-vitrea-proxy]` per group **per plane** it has members on |
509
+ | `activeRenderer: "webgpu"`, `samplingBackend: "gpu-texture"` | no proxy — the backdrop is a GPU texture, so there is nothing in the page to filter |
510
+ | `activeRenderer: "css"` | no proxies at all: the CSS tier applies `backdrop-filter` **in place**, on each glass host element |
511
+
512
+ So "find the proxy" is an assertion about one resolved state, not about the
513
+ dom-backdrop path in general — and its absence on the CSS tier is the design
514
+ rather than a fault, which is also why `probe-failed` can demote to that tier at
515
+ all: the very thing that failed is not on its path. To assert the CSS tier, read
516
+ the host element's own computed `backdrop-filter`, `background-color` and
517
+ `border-color`. To assert which tier you are on, read `useGlassCapabilities()`.
314
518
 
315
519
  ---
316
520