react-x11 2.52.0 → 2.53.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.
Files changed (112) hide show
  1. package/README.md +10 -10
  2. package/docs/README.md +160 -314
  3. package/docs/accessibility.md +176 -229
  4. package/docs/appearance.md +77 -176
  5. package/docs/click-to-component.md +18 -51
  6. package/docs/clipboard.md +40 -59
  7. package/docs/components.md +518 -1036
  8. package/docs/dbus.md +61 -133
  9. package/docs/debugging.md +152 -245
  10. package/docs/desktop-calendar.md +51 -88
  11. package/docs/desktop.md +222 -347
  12. package/docs/devtools.md +50 -167
  13. package/docs/drag-and-drop.md +126 -204
  14. package/docs/ecosystem/animation.md +91 -125
  15. package/docs/ecosystem/data-fetching.md +38 -53
  16. package/docs/ecosystem/dev-tooling.md +159 -202
  17. package/docs/ecosystem/forms.md +61 -97
  18. package/docs/ecosystem/headless.md +64 -89
  19. package/docs/ecosystem/i18n.md +18 -21
  20. package/docs/ecosystem/icons.md +93 -126
  21. package/docs/ecosystem/layout.md +42 -52
  22. package/docs/ecosystem/routing.md +18 -29
  23. package/docs/ecosystem/state.md +71 -88
  24. package/docs/ecosystem/testing.md +131 -157
  25. package/docs/ecosystem/theming.md +119 -165
  26. package/docs/ecosystem.md +145 -179
  27. package/docs/elements.md +1091 -1774
  28. package/docs/embedding.md +83 -132
  29. package/docs/events.md +412 -695
  30. package/docs/extending.md +784 -1326
  31. package/docs/eyedropper.md +63 -122
  32. package/docs/filedialog.md +71 -107
  33. package/docs/frame.md +195 -339
  34. package/docs/gl.md +65 -62
  35. package/docs/globalmenu.md +63 -146
  36. package/docs/macos.md +531 -3234
  37. package/docs/notifications.md +33 -47
  38. package/docs/packaging.md +166 -309
  39. package/docs/permissions.md +9 -14
  40. package/docs/react-features.md +81 -106
  41. package/docs/remote.md +32 -76
  42. package/docs/scale.md +72 -186
  43. package/docs/styling.md +329 -547
  44. package/docs/system.md +200 -280
  45. package/docs/testing.md +63 -83
  46. package/docs/typescript.md +6 -48
  47. package/docs/uri-schemes.md +67 -107
  48. package/docs/wayland-backend.md +200 -459
  49. package/docs/windows-integrations.md +75 -131
  50. package/docs/windows.md +343 -1367
  51. package/package.json +3 -2
  52. package/src/Reconciler.js +2 -2
  53. package/src/a11y.js +1 -1
  54. package/src/backend/context2d.js +3 -3
  55. package/src/bootstrap.js +1 -1
  56. package/src/cocoa/app.js +6 -6
  57. package/src/cocoa/bezels.js +1 -1
  58. package/src/cocoa/fonts.js +1 -1
  59. package/src/cocoa/globalmenu.js +1 -1
  60. package/src/cocoa/keymap.js +1 -1
  61. package/src/cocoa/panewindow.js +1 -1
  62. package/src/cocoa/player.js +1 -1
  63. package/src/cocoa/presenter.js +2 -2
  64. package/src/cocoa/promotion.js +1 -1
  65. package/src/cocoa/relaunch.js +1 -1
  66. package/src/cocoa/threaded.js +1 -1
  67. package/src/cocoa/video.js +1 -1
  68. package/src/cocoa/window.js +4 -4
  69. package/src/components/Menu.js +7 -2
  70. package/src/components/Switch.js +1 -1
  71. package/src/components/native.js +1 -1
  72. package/src/dnd.js +2 -2
  73. package/src/glnodes.js +1 -1
  74. package/src/grid.js +1 -1
  75. package/src/layouts.js +1 -1
  76. package/src/nodes/animation.js +1 -1
  77. package/src/nodes/invalidate.js +1 -1
  78. package/src/nodes/layouthost.js +1 -1
  79. package/src/nodes/video.js +19 -8
  80. package/src/nodes/window/droptarget.js +1 -1
  81. package/src/pacing.js +1 -1
  82. package/src/refresh/index.js +2 -1
  83. package/src/types/video.d.ts +5 -2
  84. package/src/videobackend.js +28 -0
  85. package/src/videoframes.js +29 -16
  86. package/src/wayland/app.js +27 -0
  87. package/src/wayland/video.js +376 -0
  88. package/src/win32/a11y.js +1 -1
  89. package/src/win32/app.js +2 -2
  90. package/src/win32/fonts.js +1 -1
  91. package/src/win32/glarea.js +4 -4
  92. package/src/win32/overlay.js +1 -1
  93. package/src/win32/panewindow.js +3 -3
  94. package/src/win32/shell.js +1 -1
  95. package/src/win32/window.js +2 -2
  96. package/src/windowid.js +1 -1
  97. package/src/xvideo.js +189 -0
  98. package/docs/architecture/accessibility.md +0 -470
  99. package/docs/architecture/animation.md +0 -707
  100. package/docs/architecture/container-queries.md +0 -525
  101. package/docs/architecture/custom-layout.md +0 -334
  102. package/docs/architecture/drag-and-drop.md +0 -941
  103. package/docs/architecture/element-layer-contents.md +0 -622
  104. package/docs/architecture/frame-pacing.md +0 -255
  105. package/docs/architecture/grid-layout.md +0 -358
  106. package/docs/architecture/protocol-efficiency.md +0 -357
  107. package/docs/architecture/video.md +0 -1149
  108. package/docs/architecture/windowed-regions.md +0 -854
  109. package/docs/glx.md +0 -137
  110. package/docs/wayland.md +0 -545
  111. package/docs/windows-embedding.md +0 -271
  112. package/docs/windows-gl.md +0 -221
package/docs/styling.md CHANGED
@@ -1,8 +1,7 @@
1
1
  # Styling
2
2
 
3
3
  Style lives in one place: the `style` prop. A style property passed flat is
4
- an error in development that names the fix, rather than the silent no-op an
5
- unrecognised prop used to be.
4
+ an error in development that names the fix.
6
5
 
7
6
  ## The rule
8
7
 
@@ -18,19 +17,16 @@ One namespace per kind of thing, and no name in both:
18
17
  CSS.
19
18
 
20
19
  `style` takes an object or a nested array, flattened left-to-right with
21
- falsy entries skipped. That is what replaces the **selector** cascade:
22
- precedence is written at the call site instead of resolved by specificity.
20
+ falsy entries skipped. That replaces the **selector** cascade: precedence is
21
+ written at the call site instead of resolved by specificity.
23
22
 
24
23
  ```jsx
25
24
  <box style={[s.card, isWide && s.wide, { backgroundColor: theme.panel }]} />
26
25
  ```
27
26
 
28
- Inheritance is a different thing and it does happen — see
29
- [Inheritance](#inheritance-the-ink-the-face-and-the-size) below. The two are
30
- easy to conflate because CSS ships them together: what is gone here is
31
- _matching a rule against a tree_; what remains is a handful of properties
32
- that travel down it, which is the part that makes a theme, a caption block
33
- or a dimmed row expressible at all.
27
+ Nothing matches a rule against the tree, but inheritance does happen: a
28
+ handful of properties travel down it — see
29
+ [Inheritance](#inheritance-the-ink-the-face-and-the-size).
34
30
 
35
31
  ## Lengths and shorthands
36
32
 
@@ -40,60 +36,17 @@ value, and the display scale turns either into device pixels (see
40
36
  mean what they mean in CSS; a border's or an outline's width also takes
41
37
  `'thin'`, `'medium'` and `'thick'` (1, 3 and 5).
42
38
 
43
- ### Relative units
44
-
45
- Every length also takes CSS's relative units, resolved for the node it is
46
- written on into the same logical pixels:
47
-
48
- | unit | one of it is | resolved again when |
49
- | -------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
50
- | `em` | the node's own font size — and on `fontSize` itself, the size it inherits, so `'1.5em'` is half again its parent's | an ancestor's font size changes |
51
- | `rem` | the palette's `fontSize`: the size every label that names none is set in, 14 by default | the theme changes |
52
- | `vw`, `vh`, `vmin`, `vmax` | a hundredth of the window the node is in — the `<window>` or `<popup>`, the size a `'@width >= 600'` block asks about | the window is resized |
53
-
54
- A **percentage on `fontSize`** is of the inherited size, as `em` is. A
55
- **percentage `borderRadius`** is of the box's shorter side: a corner here is
56
- a circle, so `'50%'` is a circle on a square and a pill on anything longer,
57
- where CSS would draw each corner as an ellipse. Everywhere else a percentage
58
- is the layout's, as before.
59
-
60
- `calc()`, `min()`, `max()` and `clamp()` over any of these resolve to one
61
- number where the style is — `'calc(1rem + 2px)'`, and fluid type as
62
- `fontSize: 'clamp(14px, 1rem + 0.5vw, 20px)'`. A **percentage takes part
63
- only alone** (`'calc(100% / 3)'`): yoga takes one unit per value, and what a
64
- percentage is of is decided inside the layout, so `'calc(100% - 16px)'` is an
65
- error that says so. Pad a `'100%'` box rather than subtracting from it, or
66
- give the room to `flexGrow`.
67
-
68
- `lineHeight` takes all of CSS's forms and comes to the multiple the text
69
- engines take: `1.5`, `'150%'` and `'1.5em'` are that multiple, and a length
70
- — `'24px'`, `'1.75rem'` — is divided by the node's own font size, which is
71
- what makes a design's "16 on 24" the line it asks for.
72
-
73
- What a relative length costs is the re-resolution, and only on the nodes that
74
- wrote one: a node records what its style read — the inherited size, the
75
- palette's, the window's — and is resolved again by the walk that already
76
- carries that change (the text cascade, the theme walk, the size-query
77
- registry). A style written in pixels reads none of them. Inside an
78
- `'auto'`-sized window a viewport unit makes the content depend on the window
79
- that depends on the content; like a size query, it settles where the two
80
- agree, so size such a window or keep the viewport units out of it.
81
-
82
- Some of CSS's units are **not read**, and a value in one is an error naming
83
- the unit and why — `invalid width "2ex" in <box style>: ex is not read — it
84
- needs the font's x-height`:
85
-
86
- - `ex` and `ch`, which need font metrics nothing measures yet (`0.5em` is the
87
- usual stand-in);
88
- - the container units, `cqw` and the rest: a container's size is known only
89
- after a layout pass, and a length in it would lay the tree out again on
90
- every frame the container moves — a percentage of the parent, or an
91
- `'@container'` block, says the same without that;
92
- - the print units, `pt`, `mm`, `in`: lengths here are logical pixels, which
93
- the display scale turns into device ones.
94
-
95
- So is a second value where a property takes one. Yoga's own setters used to
96
- `parseFloat` whatever they were handed, so `'2ex'` was quietly two pixels.
39
+ `null`, like `undefined`, is unset: `width: wide ? 240 : null` leaves the
40
+ width to the layout, and `''` is an unset colour. A value a property cannot
41
+ take (`width: 'hidden'`, `padding: 'auto'`, `flexDirection: 'sideways'`) or
42
+ a colour that does not parse (`'nonsense'`, `7`, `'#12'`) is an error at the
43
+ commit in development, naming the property — in a state block, a gradient
44
+ stop, a shadow, and a query block that does not match yet too. One that
45
+ gets past that — a `$token` resolving to it, or a production build — is
46
+ dropped where it is applied and reported, the way CSS drops a declaration it
47
+ cannot parse: the rest of the block still applies, and a dropped colour
48
+ paints no fill, stroke, gradient or shadow at all rather than half of one,
49
+ and the inherited ink for text.
97
50
 
98
51
  ```jsx
99
52
  <box
@@ -131,11 +84,8 @@ A `$token` may stand for any one value inside them — `padding: '$gutter 0'`,
131
84
  **The more specific property wins, wherever it is written.** A longhand
132
85
  beats its shorthand, a side beats all four sides, and a pair beats all four
133
86
  too: `paddingTop` beside `padding: '8px 16px'` is the top padding whether it
134
- comes before or after it in the style. That is the rule `paddingLeft` beside
135
- `padding: 8` has always followed, and it is not CSS's, which goes by order.
136
- A flattened style array has no order left to go by — a longhand and its
137
- shorthand end up as two keys of one object — and "the specific one wins" is
138
- the rule you can read off a style without knowing how it was assembled.
87
+ comes before or after it in the style. CSS goes by order instead; a
88
+ flattened style array has no order left to go by.
139
89
 
140
90
  A part a shorthand leaves out is left unset rather than reset to CSS's
141
91
  initial value, since a longhand written beside it would win anyway. Three
@@ -163,15 +113,67 @@ camelCase), and a misspelt name gets the nearest real ones. `background` and
163
113
  by a shorthand reaches the longhands it expands into —
164
114
  `transition: { border: 150 }` fades the `borderColor` a `border` set.
165
115
 
166
- ## What it fixes on `<window>`
116
+ ### Relative units
117
+
118
+ Every length also takes CSS's relative units, resolved for the node it is
119
+ written on into the same logical pixels:
120
+
121
+ | unit | one of it is | resolved again when |
122
+ | -------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
123
+ | `em` | the node's own font size — and on `fontSize` itself, the size it inherits, so `'1.5em'` is half again its parent's | an ancestor's font size changes |
124
+ | `rem` | the palette's `fontSize`: the size every label that names none is set in, 14 by default | the theme changes |
125
+ | `vw`, `vh`, `vmin`, `vmax` | a hundredth of the window the node is in — the `<window>` or `<popup>`, the size a `'@width >= 600'` block asks about | the window is resized |
126
+
127
+ `rem` is read from the nearest palette, the way a `$token` is: a
128
+ `<ThemeProvider value={{ fontSize: 12 }}>` around a dense panel re-bases it,
129
+ and an app porting web numbers that assume 16 sets `fontSize: 16` on its
130
+ theme.
131
+
132
+ A **percentage on `fontSize`** is of the inherited size, as `em` is. A
133
+ **percentage `borderRadius`** is of the box's shorter side: a corner here is
134
+ a circle, so `'50%'` is a circle on a square and a pill on anything longer,
135
+ where CSS would draw each corner as an ellipse. Everywhere else a percentage
136
+ is the layout's, as before.
137
+
138
+ `calc()`, `min()`, `max()` and `clamp()` over any of these resolve to one
139
+ number where the style is — `'calc(1rem + 2px)'`, and fluid type as
140
+ `fontSize: 'clamp(14px, 1rem + 0.5vw, 20px)'`. A **percentage takes part
141
+ only alone** (`'calc(100% / 3)'`): yoga takes one unit per value, and what a
142
+ percentage is of is decided inside the layout, so `'calc(100% - 16px)'` is an
143
+ error that says so. Pad a `'100%'` box rather than subtracting from it, or
144
+ give the room to `flexGrow`.
167
145
 
168
- `width`/`height` on a `<window>` are the real X window's geometry — the user
169
- can drag them, and `flush()` reads them back — so they must never reach the
170
- root yoga node. That used to mean stripping them out of the style bag
171
- (`_yogaProps`), and it forced the size hints to hide inside a `sizeHints`
172
- object because `minWidth`/`maxWidth` were already taken by yoga.
146
+ `lineHeight` takes all of CSS's forms and comes to the multiple the text
147
+ engines take: `1.5`, `'150%'` and `'1.5em'` are that multiple, and a length
148
+ — `'24px'`, `'1.75rem'` — is divided by the node's own font size, which is
149
+ what makes a design's "16 on 24" the line it asks for.
173
150
 
174
- With the split, both workarounds go:
151
+ A relative length is re-resolved only on the nodes that wrote one, when what
152
+ it reads changes; a style written in pixels costs nothing. Inside an
153
+ `'auto'`-sized window a viewport unit makes the content depend on the window
154
+ that depends on the content; like a size query, it settles where the two
155
+ agree, so size such a window or keep the viewport units out of it.
156
+
157
+ Some of CSS's units are **not read**, and a value in one is an error naming
158
+ the unit and why — `invalid width "2ex" in <box style>: ex is not read — it
159
+ needs the font's x-height`:
160
+
161
+ - `ex` and `ch`, which need font metrics nothing measures yet (`0.5em` is the
162
+ usual stand-in);
163
+ - the container units, `cqw` and the rest: a container's size is known only
164
+ after a layout pass, and a length in it would lay the tree out again on
165
+ every frame the container moves — a percentage of the parent, or an
166
+ `'@container'` block, says the same without that;
167
+ - the print units, `pt`, `mm`, `in`: lengths here are logical pixels, which
168
+ the display scale turns into device ones.
169
+
170
+ So is a second value where a property takes one.
171
+
172
+ ## What it fixes on `<window>`
173
+
174
+ `width`/`height` on a `<window>` are the real window's geometry — the user
175
+ can drag them — so they are props and never reach the layout, and the window
176
+ manager's size hints sit beside them, flat:
175
177
 
176
178
  ```jsx
177
179
  <window
@@ -187,9 +189,8 @@ With the split, both workarounds go:
187
189
  />
188
190
  ```
189
191
 
190
- `WINDOW_HINT_PROPS` is mapped back to ntk's `sizeHints` shape in
191
- `windowAttributes()`, and `style.backgroundColor` is forwarded as the window's
192
- creation attribute — the ntk contract is unchanged, only the call site is.
192
+ `style` is the root box, and its `backgroundColor` is also the window's own
193
+ background colour.
193
194
 
194
195
  ## Inline pseudo-states
195
196
 
@@ -206,12 +207,10 @@ The thing inline CSS cannot do, and the reason people keep a stylesheet:
206
207
  ```
207
208
 
208
209
  `:hover`, `:focus-within`, `:focus`, `:focus-visible`, `:active`,
209
- `:disabled`, `:drag-over`, `:dragging`.
210
- These are **node states, not selectors** — each is something the node itself
211
- already knows, so resolving them needs no specificity and no matching. The
212
- event manager already tracks the hover path, the press chain and the focused
213
- node; a state change recomputes one node's style and repaints. **No React
214
- render.**
210
+ `:disabled`, `:drag-over`, `:dragging`. These are **node states, not
211
+ selectors**: each is something the node itself already knows, so there is no
212
+ specificity and no matching. A state change recomputes one node's style and
213
+ repaints — **no React render.**
215
214
 
216
215
  Precedence is fixed and low-to-high: `:hover` → `:focus-within` → `:focus` →
217
216
  `:focus-visible` → `:active` → `:disabled` → `:drag-over` → `:dragging`,
@@ -221,18 +220,13 @@ focus, and a drag in progress outranks all of the pointer and focus states.
221
220
  Because the hover _path_ is the ancestor chain, hovering a child lights up an
222
221
  ancestor's `:hover` block, exactly like CSS.
223
222
 
224
- `:active` is a press, and it follows the same chain for the same reason: the
225
- node under the pointer is whatever a control happens to be built out of — a
226
- button's label, a switch's thumb — and it is the control that has to draw
227
- the press. It is also **live for the whole gesture**: it drops when the
228
- pointer leaves the chain and comes back when it returns, so `:active` always
229
- means "releasing now activates this", matching the nearest-common-ancestor
230
- rule the click itself is synthesized on.
231
-
232
- This is not decoration. A control acts on the release, so `:active` is the
233
- only thing it can show while it is held, and a click held half a second is
234
- half a second of silence without it. Give every interactive thing one — see
235
- [components.md](components.md#the-press-state).
223
+ `:active` is a press, and follows the same chain, so the control draws the
224
+ press whichever of its parts — a label, a thumb — is under the pointer. It
225
+ is **live for the whole gesture**: it drops when the pointer leaves the
226
+ chain and comes back when it returns, so it always means "releasing now
227
+ activates this". A control acts on the release, so give every interactive
228
+ thing an `:active` look: it is the only thing it can show while it is held
229
+ ([components.md](components.md#the-press-state)).
236
230
 
237
231
  ```jsx
238
232
  <box
@@ -249,30 +243,25 @@ A `transition` on the same property is welcome — what matters is that the
249
243
  change _starts_ on the press frame, not that it finishes there.
250
244
 
251
245
  The keyboard has no press state: Space and Enter act on the down, so the
252
- activation is immediate, but nothing draws held-ness. X reports auto-repeat
253
- as release/press pairs and neither ntk nor node-x11 implements XKB's
254
- `DetectableAutoRepeat`, so a held key cannot be told from fast tapping.
246
+ activation is immediate, but nothing draws held-ness — on X11 a held key
247
+ cannot be told from fast tapping.
255
248
 
256
249
  `:focus-visible` is focus that came from the keyboard — Tab, an arrow inside
257
250
  a widget, `autoFocus`, `node.focus()`, a modal handing focus back as it
258
- closes. A **press** sets `:focus` and not `:focus-visible`, for the reason
259
- CSS grew the distinction: the user knows where they clicked, and a ring on
260
- every click is noise, where a ring on Tab is the only cue a keyboard user
261
- has. Put focus rings in `:focus-visible` and colour changes that are welcome
262
- either way in `:focus`.
251
+ closes. A **press** sets `:focus` and not `:focus-visible`, as in CSS: the
252
+ user knows where they clicked, and a ring on every click is noise, where a
253
+ ring on Tab is the only cue a keyboard user has. Put focus rings in
254
+ `:focus-visible` and colour changes that are welcome either way in `:focus`.
263
255
 
264
256
  The exception is CSS's too: a press on a **text control** — `<textinput>`,
265
257
  `<textarea>`, or a custom element reporting editable text — does set
266
- `:focus-visible`. What the click told the user is where the caret went, not
267
- that every keystroke from now on lands there, and that second fact is what
268
- the ring is for.
258
+ `:focus-visible`, because what the click told the user is where the caret
259
+ went, not that every keystroke from now on lands there.
269
260
 
270
261
  `:focus-within` is focus on this node **or inside it** — CSS's, and the
271
262
  answer to "the row should light up while the field in it is being typed
272
- into", which is the one thing a node's own states cannot say. It is diffed
273
- over the focused node's ancestor chain, the same walk `:hover` uses, and a
274
- `<popup>` counts as inside the node it hangs off in the JSX tree, so a
275
- `Select` with its menu open still reads as focused.
263
+ into". A `<popup>` counts as inside the node it hangs off in the JSX tree,
264
+ so a `Select` with its menu open still reads as focused.
276
265
 
277
266
  ```jsx
278
267
  <box
@@ -293,13 +282,19 @@ that. `:dragging` is set on the source node for the duration of a drag.
293
282
  See [drag-and-drop.md](drag-and-drop.md).
294
283
 
295
284
  **State blocks may only set paint properties** (`backgroundColor`,
296
- `borderColor` and the per-side `borderTopColor`/…, `borderRadius`, `zIndex`,
297
- `outlineWidth`, `outlineColor`,
298
- `outlineOffset`, `opacity`, `color`) — enforced at declaration
299
- time by `createStyles`. A `:hover` that could set `padding` would reflow the
300
- tree on pointer move: jitter, and the end of the "hover is a repaint" property
301
- that makes this worth having. Anything that changes layout or what renders
302
- stays in React state.
285
+ `backgroundImage`, `borderColor` and the per-side `borderTopColor`/…,
286
+ `borderRadius`, `boxShadow`, `zIndex`, `outlineWidth`, `outlineColor`,
287
+ `outlineOffset`, `opacity`, `color`, and the `background` and `outline`
288
+ shorthands) — enforced at declaration time by `createStyles`, so that a
289
+ pointer move never reflows the tree. Anything that changes layout or what
290
+ renders stays in React state.
291
+
292
+ There are **no relational selectors** — no `:hover > child`, no sibling
293
+ combinator, no Tailwind-style `group`. A parent's `:hover` reaches its
294
+ children because `color` is
295
+ [inherited](#inheritance-the-ink-the-face-and-the-size), a child's hover
296
+ reaches its ancestors because the hover path is the ancestor chain, and a
297
+ sibling reacting to a sibling stays in React ([In components](#in-components)).
303
298
 
304
299
  ### The focus ring
305
300
 
@@ -312,8 +307,7 @@ for the keyboard (see `:focus-visible` above).
312
307
  `outlineWidth`, `outlineColor` and `outlineOffset` override it, and they are
313
308
  paint properties like any other: animatable, legal in a state block, and
314
309
  painted **outside the border box** so switching one on cannot move the thing
315
- it surrounds. That is the whole reason CSS has `outline` as well as `border`,
316
- and the reason these are not in the layout vocabulary.
310
+ it surrounds.
317
311
 
318
312
  ```jsx
319
313
  <box focusable /> {/* ring, for free */}
@@ -342,13 +336,11 @@ it mirrors that subtree and nothing above it. Rows run the other way, `flex-star
342
336
  is the right-hand end, and every logical edge below swaps sides with it.
343
337
 
344
338
  **The default comes from the locale.** An app started under
345
- `LANG=ar_EG.UTF-8` is mirrored with no configuration, because that is what
346
- GTK and Qt both do and because the alternative is an Arabic desktop where
347
- one application's panels are on the wrong side. Every locale that is not
348
- written right to left answers `'ltr'`, so an app that never thinks about
349
- this pays nothing. The palette carries it — `theme.direction` — so an app
350
- with a language menu switches the whole UI with the `<ThemeProvider>` swap
351
- it was already doing for colours:
339
+ `LANG=ar_EG.UTF-8` is mirrored with no configuration, as GTK and Qt apps
340
+ are. Every locale that is not written right to left answers `'ltr'`, so an
341
+ app that never thinks about this pays nothing. The palette carries it —
342
+ `theme.direction` — so an app with a language menu switches the whole UI with
343
+ the `<ThemeProvider>` swap it was already doing for colours:
352
344
 
353
345
  ```jsx
354
346
  <ThemeProvider value={{ direction: settings.rtl ? 'rtl' : 'ltr' }}>
@@ -373,10 +365,8 @@ routes always agree under one.
373
365
  | `borderStartColor` / `…EndColor` | `borderLeftColor` / `…Right…` | swapped |
374
366
 
375
367
  **A logical edge wins over the physical one even in LTR**, the way CSS's
376
- `padding-inline-start` wins over `padding-left`. Yoga's full order is
377
- start/end, then the physical side, then `EDGE_HORIZONTAL`, then
378
- `EDGE_ALL` — the opposite way round from what the vertical shorthands
379
- suggest, which is why it is pinned in a test.
368
+ `padding-inline-start` wins over `padding-left`, and the physical side wins
369
+ over all four sides.
380
370
 
381
371
  Write layout in the logical pair and it is the same layout in both
382
372
  directions. Use the physical one when you mean the screen: a drop shadow, a
@@ -392,8 +382,7 @@ resize handle in a fixed corner.
392
382
  scrollbar's travel, Up/Down on every control.
393
383
  - **The caret keys in a field.** Left and Right step through the _string_,
394
384
  not across the screen, and so do Home and End — the editing model is
395
- written in logical positions, and visual-order caret motion through bidi
396
- text is its own question.
385
+ written in logical positions.
397
386
 
398
387
  What **does**, besides the box tree: a vertical scrollbar moves to the left;
399
388
  horizontal scrolling counts from the right-hand edge, so `scrollX: 0` still
@@ -407,18 +396,16 @@ screen edge; and the inside of a `<textinput>`/`<textarea>` — see below.
407
396
  A field is laid out at its box's direction, exactly as a `<text>` is, and the
408
397
  direction does two separate things there:
409
398
 
410
- - **It is the base level the value is shaped at.** UAX#9 resolves a run of
411
- neutral characters — `"(1) 12:30"`, a filename, a lone bracket — against
412
- the paragraph level, and "take it from the first strong character" is only
413
- the rule for when nobody said. A field that never said reorders an Arabic
414
- word typed into an English form as though the whole line were Arabic, in an
415
- **LTR** window. So the field says.
399
+ - **It is the base level the value is shaped at.** A run of neutral
400
+ characters — `"(1) 12:30"`, a filename, a lone bracket — resolves against
401
+ it rather than against the first strong character, so an Arabic word typed
402
+ into an English form in an **LTR** window does not reorder the line as
403
+ though it were Arabic.
416
404
  - **It is the edge the text is against.** The value, the placeholder and the
417
405
  caret start at the direction's start edge — the right-hand one under
418
406
  `direction: 'rtl'` — and an unscrolled field still shows the _beginning_ of
419
407
  its value, so a value too long for its box overflows towards the left.
420
- Clicking, the caret, the selection bands and the scroll are read from one
421
- placement, so they mirror together.
408
+ Clicking, the caret, the selection bands and the scroll mirror together.
422
409
 
423
410
  `textAlign` works on a field too: `'start'` (the default) is the base
424
411
  direction's start edge, and `'center'`/`'end'` do what they say while the
@@ -427,7 +414,7 @@ whatever the alignment says, because that is what it is scrolled from.
427
414
 
428
415
  The text's _own_ direction is not consulted: a Latin value in an RTL field is
429
416
  placed at the field's right edge, the way `<input dir="rtl">` places one, and
430
- there is no `dir="auto"` equivalent yet.
417
+ there is no `dir="auto"` equivalent.
431
418
 
432
419
  ## Per-side borders
433
420
 
@@ -449,28 +436,21 @@ widths are layout — yoga sees each edge, so the bar above insets content on
449
436
  the left only — and the colours are paint, legal in a state block like
450
437
  `borderColor` itself.
451
438
 
452
- This is what a rule or an accent edge should be built from: a blockquote's
453
- bar, a table's row separators (`borderBottomWidth: 1` on each row), a tab's
454
- underline. Composing the same line out of 1px `<box>`es works but pays a
455
- node per row into layout and paint, and scatters what is really one style
456
- value ("the table's border colour") across the tree.
439
+ Build a rule or an accent edge this way — a blockquote's bar, a table's row
440
+ separators (`borderBottomWidth: 1` on each row), a tab's underline — rather
441
+ than out of 1px `<box>`es, which pay a node per row in layout and paint.
457
442
 
458
- Two edges of the v1 shape:
443
+ Two limits:
459
444
 
460
445
  - **`borderRadius` requires uniform borders** — same width and colour on
461
446
  all four sides. A non-uniform border paints square, ignores the radius,
462
- and says so once in development; bars and rules are square, so this costs
463
- nothing real.
447
+ and says so once in development.
464
448
  - Corners between two painted sides of different colours are square and
465
449
  deterministic: top and bottom span the box's full width, left and right
466
- run between them. CSS mitres that corner diagonally; nothing built from
467
- bars and rules can tell the difference.
450
+ run between them, where CSS mitres the corner diagonally.
468
451
 
469
452
  ## Gradients and shadows
470
453
 
471
- The two decorations a UI asks for after colour and radius, and the two that
472
- used to mean giving up on `<box>` and drawing the panel by hand:
473
-
474
454
  ```jsx
475
455
  <box
476
456
  style={{
@@ -488,9 +468,8 @@ used to mean giving up on `<box>` and drawing the panel by hand:
488
468
  Both are **paint** properties: legal in a state block, and outside layout
489
469
  entirely — a gradient fills the box the layout already decided on, and a
490
470
  shadow is drawn beyond it and moves nothing. Both are written in CSS's
491
- spelling, because both are values people already know by heart, and a
492
- `$token` resolves **inside** them as well as as a whole value, which is what
493
- keeps a themed decoration in a hoisted style.
471
+ spelling, and a `$token` resolves **inside** them as well as as a whole
472
+ value, which is what keeps a themed decoration in a hoisted style.
494
473
 
495
474
  ### `backgroundImage`
496
475
 
@@ -507,64 +486,39 @@ against the node's own box so it works wherever the node lands.
507
486
 
508
487
  Only linear gradients exist. A radial or conic one is a `<canvas onDraw>`
509
488
  away (`createRadialGradient` draws on every backend but Windows', and
510
- ntk's context has `createConicalGradient`),
511
- and CSS's sizing keywords for them are most of the work for very little of
512
- the demand.
513
-
514
- One cost worth knowing, because it is not where anyone would look for it: a
515
- gradient is a server-side source picture, so a **square** one is as cheap as
516
- a colour — but a **rounded** one is not. ntk's rounded-rect fast path (cached
517
- corner glyphs plus `FillRectangles`) only takes a solid colour, so a rounded
518
- gradient falls back to a coverage mask rasterized on the client and uploaded
519
- per fill: about 4 KB per box per repaint. Damage bounding means that is only
520
- paid where something changed, but a list of rounded gradient rows is the one
521
- shape to think twice about — see the `shapes: 24 gradient+shadow cards`
522
- scenario in `npm run bench`, which prices it.
489
+ ntk's context has `createConicalGradient`).
490
+
491
+ On X11 a **square** gradient is as cheap as a colour, but a **rounded** one
492
+ is rasterized on the client and uploaded per fill — about 4 KB per box per
493
+ repaint, paid only where something changed. A list of rounded gradient rows
494
+ is the one shape to think twice about.
523
495
 
524
496
  ### `boxShadow`
525
497
 
526
498
  `<x> <y> [blur] [spread] [colour]`, comma-separated for several, painted
527
499
  first-on-top like CSS's. The colour may be left out, which means the node's
528
500
  own `color` — [`currentColor`](#currentcolor), which may be written out
529
- too. A blurred shadow is a real gaussian — RENDER's convolution over a
530
- coverage surface — cached by size, radius and blur, so a list of identical
531
- cards renders one and composites it many times.
532
-
533
- On macOS and Windows, which have no X server to bake one on, the shadow is
534
- the 2d context's own `shadowBlur`, and the context draws a rounded rect's
535
- shadow from a tile it makes once and stretches — see
536
- [macos.md](macos.md#a-shadow-from-a-tile). The rest of this section is the
537
- X11 route.
538
-
539
- The blur is **baked into the cached surface's pixels**, in two separable 1-D
540
- passes, rather than left as a filter on its picture. That is not an internal
541
- detail: XRender applies a picture's filter on every composite, so a shadow
542
- whose blur rode on the picture paid its whole kernel again every frame it was
543
- drawn — a card-sized coverage surface at a wide blur is hundreds of millions
544
- of multiply-accumulates, in the server, per shadow, per repaint. Baked, a
545
- repaint composites a plain mask however wide the blur is, and the convolution
546
- runs once per distinct geometry.
501
+ too. A blurred shadow is a real gaussian, computed once per distinct size,
502
+ radius and blur and cached, so a list of identical cards renders one and
503
+ composites it many times, and a repaint costs the same however wide the
504
+ blur. On macOS and Windows it is the 2D context's own `shadowBlur`.
547
505
 
548
506
  What it does not do, and why:
549
507
 
550
- - **`inset` throws.** An inner shadow is a different drawing and a different
551
- damage story, and painting an outer one where an inner was asked for is a
552
- bug with no visible cause. An inset border or a `<canvas>` says it today.
508
+ - **`inset` throws.** An inner shadow is a different drawing, and painting an
509
+ outer one where an inner was asked for is a bug with no visible cause. An
510
+ inset border or a `<canvas>` says it today.
553
511
  - **Ignored on `<window>` and `<popup>`** (with a warning in development). A
554
512
  shadow is painted outside the box, and a toplevel owns no pixels there —
555
- a real one needs the window to carry a translucent margin of its own,
556
- which is a feature rather than a line in the painter. Put the shadow on a
557
- `<box>` inside the window; for a floating menu, the popup's own
558
- `borderRadius` plus a border is what reads as raised today.
513
+ a real one needs the window to carry a translucent margin of its own.
514
+ Put the shadow on a `<box>` inside the window; for a floating menu, the
515
+ popup's own `borderRadius` plus a border is what reads as raised.
559
516
  - **Neither transitions.** Both are several numbers and a colour in one
560
- string, and `interpolate` moves one value; they snap. A card that wants to
561
- rise on hover animates the `backgroundColor` or the border beside them.
562
-
563
- A shadow is the first thing in this vocabulary that inks pixels the node
564
- does not own, so it also widens what the node repaints — its offset, its
565
- spread and the blur's tail — and the frame that _removes_ one claims where
566
- it was. That is the renderer's business, not the application's, but it is
567
- the reason a shadow is not free the way a colour is: prefer one shadow on
517
+ string; they snap. A card that wants to rise on hover animates the
518
+ `backgroundColor` or the border beside them.
519
+
520
+ A shadow inks pixels the node does not own, so it widens what the node
521
+ repaints — its offset, its spread and the blur's tail. Prefer one shadow on
568
522
  the card to one on every row inside it.
569
523
 
570
524
  ## Opacity
@@ -598,11 +552,6 @@ faded and released when it is opaque again. `0` draws nothing and costs
598
552
  nothing — and the node is still laid out and still hit, as in CSS. A value
599
553
  outside 0 to 1 is clamped; one that is not a number throws in development.
600
554
 
601
- On the Cocoa backend a faded box is drawn into the window's bitmap like this
602
- rather than lifted onto a layer of its own (docs/macos.md): a box and its
603
- content on separate layers would fade one over the other, which is the thing
604
- `opacity` exists not to do.
605
-
606
555
  ## Measuring text to its letters
607
556
 
608
557
  ```jsx
@@ -616,18 +565,12 @@ alphabetic`: the `<text>`'s box becomes the capitals down to the last
616
565
  baseline, so padding around a label is measured from the letters and
617
566
  centring centres what you can see. Default `'none'`.
618
567
 
619
- The problem it solves is that **a line box is not the text**. It is the
620
- font's ascent plus its descent plus its line gap, and the space over a
621
- capital differs from the space under a baseline by `(ascent - capHeight) -
622
- descent` — a property of the typeface, so a label is only ever optically
623
- centred by luck. With the KaTeX face the tests use, a 24px label in a 12px
624
- padded box sits 17px below the top and 18px above the bottom; trimmed, it is
625
- 12 and 12.
626
-
627
- **`lineHeight` is not an alternative.** It is a multiplier over the natural
628
- line box and the leading still splits evenly above and below, so it moves
629
- both edges by the same amount — it can change how much space there is, never
630
- how it is balanced.
568
+ **A line box is not the text**: the space over a capital differs from the
569
+ space under a baseline by `(ascent - capHeight) - descent`, a property of
570
+ the typeface, so an untrimmed label is only optically centred by luck.
571
+ **`lineHeight` is not an alternative**: it is a multiplier over the natural
572
+ line box, and the leading still splits evenly above and below, so it changes
573
+ how much space there is, never how it is balanced.
631
574
 
632
575
  Two things worth knowing:
633
576
 
@@ -639,11 +582,9 @@ Two things worth knowing:
639
582
 
640
583
  **The built-in widgets set it on every label**, which is why `paddingY` in a
641
584
  palette is bigger than a CSS padding for the same look — it is the space you
642
- see, not the space plus whatever the ascent left over. Which way a face is
643
- off changes with the face: at 14px, SF NS leaves 3.7px above the capitals
644
- against 2.9px below the baseline, so an untrimmed label rides low, while
645
- Helvetica leaves 0.7 against 3.2 and it rides high. A widget cannot correct
646
- for that, because it does not know the face it will be drawn in.
585
+ see, not the space plus whatever the ascent left over. Which way an untrimmed
586
+ label is off changes with the face (it rides low in SF NS and high in
587
+ Helvetica), and a widget does not know the face it will be drawn in.
647
588
 
648
589
  A glyph drawn as text — a check mark, a submenu arrow, an icon — is centred
649
590
  on its own middle rather than sitting on a baseline, so trimming its box
@@ -687,16 +628,14 @@ that a paragraph is made of, and nothing hangs out of a stack of them.
687
628
  `textWrap: 'nowrap'` is CSS's, and it is what separates a cell from a
688
629
  paragraph. A `<text>` measures height-for-width: hand it a narrow box and it
689
630
  wraps to fit, which is right for prose and wrong for a row of a fixed height —
690
- a date that wraps to two lines is not a taller row, it is a line and a half of
691
- date with the rest sliced off, top and bottom, spilling over the rows either
692
- side on the way. `'nowrap'` measures at unbounded width, so the overflow is
693
- horizontal, which `overflow: 'hidden'` on the box around it already knows what
694
- to do with. Default `'wrap'`.
631
+ a date that wraps is sliced off top and bottom and spills over the rows
632
+ either side. `'nowrap'` measures at unbounded width, so the overflow is
633
+ horizontal, which `overflow: 'hidden'` on the box around it clips. Default
634
+ `'wrap'`.
695
635
 
696
- A date is the case that wants exactly that: it is a fixed width, it always
697
- fits, and clipping it would be a bug rather than a design. A **name** is not.
698
- Clipped, a truncated file name and a short one look equally complete, and the
699
- reader has no way to tell which they are looking at:
636
+ A date always fits, so clipping it would be a bug rather than a design. A
637
+ **name** does not: clipped, a truncated file name and a short one look
638
+ equally complete.
700
639
 
701
640
  ```jsx
702
641
  <text style={{ textWrap: 'nowrap', textOverflow: 'ellipsis' }}>
@@ -717,12 +656,10 @@ a substring —
717
656
  - it cuts the **visually** last run rather than the logically last one, so a
718
657
  right-to-left line ends on its left.
719
658
 
720
- `maxLines` is the other half — CSS has no single-property spelling for it
721
- (`-webkit-line-clamp` is what everyone actually writes), so this is the name
722
- the platforms that got a clean shot at it chose. A three-line blurb that ends
723
- in a `…` is `{ maxLines: 3, textOverflow: 'ellipsis' }`; the lines past the
724
- cap are dropped before the box is measured, so the height is the kept lines'
725
- and not the whole paragraph's.
659
+ `maxLines` is the other half — CSS's `-webkit-line-clamp`. A three-line blurb
660
+ that ends in a `…` is `{ maxLines: 3, textOverflow: 'ellipsis' }`; the lines
661
+ past the cap are dropped before the box is measured, so the height is the
662
+ kept lines' and not the whole paragraph's.
726
663
 
727
664
  **`textOverflow: 'ellipsis'` on its own means one line.** Eliding happens off
728
665
  a line count — the mark stands for the lines that were dropped — so an
@@ -732,8 +669,8 @@ ask for two or three.
732
669
 
733
670
  ### What `nowrap` and `ellipsis` do together
734
671
 
735
- They are not in tension, but they change what the `<text>` reports to layout,
736
- which is worth knowing before a row moves under you.
672
+ They change what the `<text>` reports to layout, which is worth knowing
673
+ before a row moves under you.
737
674
 
738
675
  A clipping `nowrap` label is measured at unbounded width, so it tells its
739
676
  container it needs the **whole string** — and CSS's automatic minimum size
@@ -741,21 +678,15 @@ turns that into a floor. It cannot be squeezed, so a long name pushes the
741
678
  column next to it out of the row and the overflow is dealt with somewhere
742
679
  above.
743
680
 
744
- An eliding one is measured against the width on offer, because at unbounded
745
- width there is one line, one line is never over the cap, and nothing would
746
- ever be cut. So its floor is small and it **gives way**: it takes the room
747
- that is left, shows `Applicati…`, and the column beside it keeps its width.
748
- That is the behaviour a table wants — a column that cannot show a file name
749
- should say so rather than making every other column narrower to avoid it.
750
-
751
- `<Table>` sets `nowrap` and `ellipsis` on every cell and header for that
752
- reason.
681
+ An eliding one is measured against the width on offer, so its floor is small
682
+ and it **gives way**: it takes the room that is left, shows `Applicati…`,
683
+ and the column beside it keeps its width. That is the behaviour a table
684
+ wants, and `<Table>` sets `nowrap` and `ellipsis` on every cell and header
685
+ for that reason.
753
686
 
754
687
  What is truncated is a fact about the pixels and nothing else. A `<text>` that
755
688
  was elided still reports the **whole** string as its accessible name, and the
756
- caret and selection indices still index into the whole string — a screen
757
- reader that read `Application Sup…` would be reading the layout instead of the
758
- content.
689
+ caret and selection indices still index into the whole string.
759
690
 
760
691
  ## Inheritance: the ink, the face and the size
761
692
 
@@ -810,12 +741,10 @@ its icon follow.
810
741
  </box>
811
742
  ```
812
743
 
813
- Nothing about that costs a layout pass. A state block may only set paint
814
- properties and `color` (see above), so the only inherited property a pointer
815
- can ever move is the ink — which drops the memoised text layouts under it
816
- and repaints, with no re-measuring and no reflow. A `fontSize` change
817
- _does_ re-measure, and it can only come from a React commit, a size query or
818
- a theme.
744
+ Nothing about that costs a layout pass: a state block sets paint and `color`
745
+ only, so the only inherited property a pointer can move is the ink, which
746
+ repaints without re-measuring. A `fontSize` change _does_ re-measure, and it
747
+ can only come from a React commit, a size query or a theme.
819
748
 
820
749
  What does **not** inherit: `textAlign`, `lineHeight`, `textWrap`,
821
750
  `textOverflow`, `maxLines` and `textBoxTrim`. CSS inherits the first two;
@@ -829,11 +758,10 @@ drawing rather than a letter, so it takes its default from the palette's
829
758
  development, naming the property; where one gets past that — a `$token`, or
830
759
  a production build — the text sets as though it had not been written, the way
831
760
  CSS inherits past a declaration it cannot parse. Numbers may be numeric
832
- strings (`fontSize: '20'`), as they always could, and a size is a
761
+ strings (`fontSize: '20'`), and a size is a
833
762
  [length](#lengths-and-shorthands) like any other: `fontSize: '20px'` is 20,
834
763
  and `'1.5em'` or `'150%'` is half again the size inherited. A `lineHeight`
835
- in a length — `'24px'` — is that multiple of the node's own size. Before, a
836
- size in px set no text at all and the others threw from inside a frame.
764
+ in a length — `'24px'` — is that multiple of the node's own size.
837
765
 
838
766
  ### `currentColor`
839
767
 
@@ -859,9 +787,8 @@ It stays the keyword in the node's style, which is CSS's computed value, and
859
787
  becomes a colour where it is painted, so it follows the ink wherever the ink
860
788
  moves: a `:hover` on a row above, a theme swap, a `color` transition frame by
861
789
  frame. A node painting in it repaints when its ink moves, bounded to that
862
- node, and a box does too although it draws no text; one that paints nothing
863
- in it still costs nothing when the ink above it moves. A `<popup>` written
864
- under the row inherits the ink like any other child and repaints with it.
790
+ node. A `<popup>` written under the row inherits the ink like any other
791
+ child and repaints with it.
865
792
 
866
793
  Two places it cannot simply follow:
867
794
 
@@ -946,28 +873,23 @@ that is merely _previewing_ files does not — it would quietly change which
946
873
  face draws the bullets and the curly quotes in its own UI.
947
874
 
948
875
  Both verbs read a given file once per connection, however often they are
949
- called: the face is cached in the app's font manager, so `useFont` in a
950
- component that re-renders sixty times a second parses nothing, and
951
- `loadFont` after `openFont` on the same path registers the face already
952
- open. This matters more than it looks — a second copy of a font is a second
953
- glyph cache and a second server-side glyphset, and a node built against one
954
- copy cannot be painted by the other.
876
+ called: `useFont` in a component that re-renders sixty times a second parses
877
+ nothing, and `loadFont` after `openFont` on the same path registers the face
878
+ already open.
955
879
 
956
880
  `font` is ntk's `Font`, which has more on it than the four members above
957
881
  ([ntk's docs/fonts.md](https://github.com/sidorares/ntk/blob/master/docs/fonts.md)). If you need
958
882
  anything else of ntk's — `Path2D`, `Image`, `Surface` — **import it from
959
883
  `react-x11/ntk`, never from `ntk`**, and never declare `ntk` as a dependency
960
884
  of your own. Two copies in one process are two font caches and two glyph
961
- atlases, with the same consequence as above and a great deal further from
962
- its cause.
885
+ atlases, and a node built against one copy cannot be painted by the other.
963
886
 
964
887
  ## `createStyles`
965
888
 
966
- Identity is the point — a hoisted style object lets `applyProps` skip the
967
- whole update with a `===` check, the same reason RN's `StyleSheet.create`
968
- still exists now that its id registry is gone. It also validates keys, which
969
- a bare object literal cannot: an unknown style property is an error at
970
- declaration instead of a silent no-op.
889
+ A hoisted style object keeps its identity, so `applyProps` skips the whole
890
+ update with a `===` check. `createStyles` also validates keys, which a bare
891
+ object literal cannot: an unknown style property is an error at declaration
892
+ instead of a silent no-op.
971
893
 
972
894
  ## `flattenStyle`
973
895
 
@@ -996,8 +918,8 @@ node-local state block can cross that.
996
918
  A component's own props are never style. `ProgressBar` takes `color`,
997
919
  `Dialog` takes `width`/`height` — a dialog is a real popup window and needs
998
920
  its geometry up front — and `ContextMenu` takes `fontSize` because it
999
- measures labels with it. `Select` and `Slider` used to take `width` purely
1000
- to put it in their own box; that is `style={{ width }}` now.
921
+ measures labels with it. A `Select`'s or a `Slider`'s width is
922
+ `style={{ width }}`.
1001
923
 
1002
924
  ## Theme tokens
1003
925
 
@@ -1017,10 +939,9 @@ const s = createStyles({
1017
939
  </window>;
1018
940
  ```
1019
941
 
1020
- That is the point of tokens: the style is **hoisted** — declared once,
1021
- outside render, with no access to React context — and still follows the
1022
- theme. Without them a palette has to be threaded to every element that
1023
- paints (`style={[s.card, { backgroundColor: theme.panel }]}`).
942
+ A **hoisted** style — declared once, outside render, with no access to React
943
+ context — still follows the theme, with no palette threaded to every element
944
+ that paints.
1024
945
 
1025
946
  Tokens are not colour-only; `padding: '$gutter'` resolves a number just as
1026
947
  well. And they resolve wherever a style value is written: in a state block, in
@@ -1055,7 +976,7 @@ which is the desktop's `sans-serif` unless the window itself was given a
1055
976
  theme. An app that ships its own fonts then draws its headings in the face it
1056
977
  shipped and its body copy in whatever the machine calls sans-serif, which is
1057
978
  both wrong and slow: resolving a generic family costs an `fc-match` per weight
1058
- on the first paint (~110ms each here).
979
+ on the first paint (~110ms each).
1059
980
 
1060
981
  Three ways to say it. For a whole app the first says it once, for the widgets
1061
982
  and every window; the last is the one for a subtree:
@@ -1072,9 +993,8 @@ and every window; the last is the one for a subtree:
1072
993
  </box>
1073
994
  ```
1074
995
 
1075
- `monoFamily` has no such fallback — nothing is monospace unless it says so, and
1076
- that is what the `$` above is for. It is a token so that the code surfaces of
1077
- an app, which are written by components that never meet, can be set from one
996
+ `monoFamily` has no such fallback — nothing is monospace unless it says so,
997
+ with `fontFamily: '$monoFamily'`, so an app's code surfaces are set from one
1078
998
  place.
1079
999
 
1080
1000
  A `theme` prop anywhere scopes its subtree, and an inner one merges over the
@@ -1091,32 +1011,24 @@ by `<ThemeProvider>`, which plants the resolved palette in the tree for this
1091
1011
  lookup to find. Hand-writing `theme={{ accentHover: … }}` on a box gets the
1092
1012
  merge and not the derivation.
1093
1013
 
1094
- Changing the theme restyles the subtree in place. Nodes whose own props did
1095
- not change are still updated — which is why a theme change also drops the
1096
- memoised text layouts under it, or cached text would keep painting the old
1097
- colour.
1014
+ Changing the theme restyles the subtree in place, nodes whose own props did
1015
+ not change included.
1098
1016
 
1099
1017
  An unknown token is **reported and dropped**: the message names the token,
1100
1018
  the element, the component that wrote it, and every token the palette in
1101
1019
  force does have; that one property goes missing, so the widget paints
1102
1020
  visibly wrong rather than the app dying; and `process.exitCode` is set to 1,
1103
- so a test run, a CI job or a supervisor still counts the run as failed.
1104
-
1105
- That is deliberately not a throw. Resolution happens when a node's ancestry
1106
- completes — inside a commit, and also from an X event when the desktop
1107
- switches between light and dark — and on both of those paths a throw is
1108
- uncatchable: it either lands on the `<window>` fiber above every error
1109
- boundary the app wrote inside it, or it unwinds into the frame loop with no
1110
- React on the stack at all. One wrong character in a style value should not
1111
- be able to blank the screen.
1112
-
1113
- Set **`REACT_X11_STRICT_TOKENS=1`** to make it fatal again — for a kiosk, or
1114
- a build that would rather stop than paint something wrong. Strict mode also
1115
- throws in the right _place_: the error is raised from the offending node's
1116
- own `commitMount`, so an error boundary at any depth inside the window
1117
- catches it and shows a fallback for that subtree. Where React genuinely is
1118
- not on the stack — the desktop appearance switch — strict mode crashes,
1119
- which is what it was asked for.
1021
+ so a test run, a CI job or a supervisor still counts the run as failed. It
1022
+ is not a throw because resolution also happens where no error boundary can
1023
+ catch one — above every boundary inside a `<window>`, or from the desktop's
1024
+ light/dark switch with no React on the stack.
1025
+
1026
+ Set **`REACT_X11_STRICT_TOKENS=1`** to make it fatal — for a kiosk, or
1027
+ a build that would rather stop than paint something wrong. Strict mode
1028
+ throws from the offending node's own `commitMount`, so an error boundary at
1029
+ any depth inside the window catches it and shows a fallback for that
1030
+ subtree. Where React genuinely is not on the stack — the desktop appearance
1031
+ switch — strict mode crashes, which is what it was asked for.
1120
1032
 
1121
1033
  A token with **no** theme above it at all warns in dev — that one is
1122
1034
  otherwise silent, since the whole style is stripped rather than one value
@@ -1127,17 +1039,15 @@ node that wears it, not just the first.
1127
1039
 
1128
1040
  Resolution walks the node tree, not React context, so a palette has to reach
1129
1041
  the tree to be seen. `<ThemeProvider>` puts it in both places — the context
1130
- widgets read with `useTheme()`, and a `theme` prop on a node — which is what
1131
- makes provider and token one mechanism rather than two
1042
+ widgets read with `useTheme()`, and a `theme` prop on a node
1132
1043
  ([components.md](components.md#theming)). Widgets plant their own merged
1133
1044
  palette on their root node as well, so `$tokens` work inside a widget
1134
1045
  subtree — and in a style you pass one — with no provider anywhere.
1135
1046
 
1136
1047
  With no `theme` prop above it at all, a token resolves against **the
1137
1048
  desktop's palette** — `backgroundColor: '$background'` in an app that never
1138
- wrote a `<ThemeProvider>` is dark on a dark desktop, which is how that app
1139
- blends in ([appearance.md](appearance.md)). A token no palette defines is
1140
- reported the same way, naming every token the one in force does have.
1049
+ wrote a `<ThemeProvider>` is dark on a dark desktop
1050
+ ([appearance.md](appearance.md)).
1141
1051
 
1142
1052
  ## Sticky positioning
1143
1053
 
@@ -1187,11 +1097,9 @@ const s = createStyles({
1187
1097
  the rows that scroll under a header do not paint over it, and it takes the
1188
1098
  press where it covers them. `zIndex` still orders it first. Give it a
1189
1099
  background, or the rows show through.
1190
- - **It lands in the frame that scrolled.** The pane applies the offset in
1191
- the layout pass the scroll runs — never through a re-render — so a header
1192
- cannot trail its content by a frame, and a pane of headers keeps the
1193
- scroll blit: a header riding with its rows costs nothing, and a held one
1194
- repaints its own rect.
1100
+ - **It lands in the frame that scrolled**, with no re-render, so a header
1101
+ cannot trail its content by a frame; a header riding with its rows costs
1102
+ nothing, and a held one repaints its own rect.
1195
1103
  - **`onLayout` reports where layout put the box**, not where it is held,
1196
1104
  the same way a scroll does not fire it. Where it is held is
1197
1105
  `getClientRects()`.
@@ -1201,9 +1109,9 @@ Both axes at once, and one sticky box inside another, are what
1201
1109
  row of room names held under the banner, a column of times held at the left,
1202
1110
  and the corner where the two meet.
1203
1111
 
1204
- Not yet: CSS's `scroll-padding`. Scrolling a node into view — keyboard focus
1205
- reaching a row — brings it to the pane's edge, which can be under a held
1206
- header.
1112
+ Not supported: CSS's `scroll-padding`. Scrolling a node into view — keyboard
1113
+ focus reaching a row — brings it to the pane's edge, which can be under a
1114
+ held header.
1207
1115
 
1208
1116
  Sticky is the built-in [custom position](#custom-positions): the same
1209
1117
  placement, written against the same context a registered one is handed.
@@ -1234,7 +1142,9 @@ registerPosition('parallax', {
1234
1142
 
1235
1143
  - **It is a `position` value**, by name or with its options, and switches
1236
1144
  like one — from a container query block too. The box is laid out exactly
1237
- where `relative` would put it, and moved; nothing around it reflows.
1145
+ where `relative` would put it, and moved; nothing around it reflows. A box
1146
+ has one `position`, so to combine a scheme with `absolute`, wrap it in a
1147
+ box.
1238
1148
  - **The insets are the scheme's.** Yoga never sees them as offsets: sticky
1239
1149
  reads them as thresholds, and a registered position reads them as whatever
1240
1150
  it documents.
@@ -1301,19 +1211,24 @@ so a grid copied from a web codebase lays out here as it was written:
1301
1211
  </box>
1302
1212
  ```
1303
1213
 
1304
- | on the grid box | CSS | |
1305
- | ----------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1306
- | `gridTemplateColumns`, `gridTemplateRows` | `grid-template-*` | a track list — lengths in `px` (logical pixels), `%` and `fr`; `auto`, `min-content`, `max-content`; `minmax()`, `fit-content()`; `repeat()` with a count, `auto-fill` or `auto-fit` — or a number: that many equal tracks |
1307
- | `gridTemplateAreas` | `grid-template-areas` | one quoted string per row — `'"head head" "side main"'` — or an array of the rows |
1308
- | `gridAutoColumns`, `gridAutoRows` | `grid-auto-*` | the size of a track the grid adds for a child placed past its template; a number is a length |
1309
- | `gridAutoFlow` | `grid-auto-flow` | `'row'` (the default) or `'column'`, and `'dense'` to back-fill a hole |
1310
- | `gap`, `columnGap`, `rowGap` | `gap` | between the tracks |
1311
- | `justifyItems`, `alignItems` | `justify-items`, `align-items` | where each child sits in its area: `'stretch'` (the default), `'flex-start'`, `'center'`, `'flex-end'` |
1312
- | `justifyContent`, `alignContent` | `justify-content`, `align-content` | where the tracks sit when they leave room |
1313
- | **on a child** | | |
1314
- | `gridColumn`, `gridRow` | `grid-column`, `grid-row` | `2`, `'span 2'`, `'2 / 4'`, `'2 / span 3'`, `'1 / -1'` |
1315
- | `gridArea` | `grid-area` | a name from `gridTemplateAreas`, or four lines; `gridColumn` and `gridRow` beside it win for their own axis |
1316
- | `justifySelf`, `alignSelf` | `justify-self`, `align-self` | its own place in its area, over the grid's |
1214
+ | on the grid box | |
1215
+ | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
1216
+ | `gridTemplateColumns`, `gridTemplateRows` | a track list, or a number: that many equal tracks |
1217
+ | `gridTemplateAreas` | one quoted string per row — `'"head head" "side main"'` — or an array of the rows |
1218
+ | `gridAutoColumns`, `gridAutoRows` | the size of a track the grid adds for a child placed past its template; a number is a length |
1219
+ | `gridAutoFlow` | `'row'` (the default) or `'column'`, and `'dense'` to back-fill a hole |
1220
+ | `gap`, `columnGap`, `rowGap` | between the tracks |
1221
+ | `justifyItems`, `alignItems` | where each child sits in its area: `'stretch'` (the default), `'flex-start'`, `'center'`, `'flex-end'` |
1222
+ | `justifyContent`, `alignContent` | where the tracks sit when they leave room |
1223
+ | **on a child** | |
1224
+ | `gridColumn`, `gridRow` | `2`, `'span 2'`, `'2 / 4'`, `'2 / span 3'`, `'1 / -1'` |
1225
+ | `gridArea` | a name from `gridTemplateAreas`, or four lines; `gridColumn` and `gridRow` beside it win for their own axis |
1226
+ | `justifySelf`, `alignSelf` | its own place in its area, over the grid's |
1227
+
1228
+ Each property is CSS's of the same name (`gridTemplateColumns` is
1229
+ `grid-template-columns`). A track list takes lengths in `px` (logical
1230
+ pixels), `%` and `fr`; `auto`, `min-content`, `max-content`; `minmax()`,
1231
+ `fit-content()`; and `repeat()` with a count, `auto-fill` or `auto-fit`.
1317
1232
 
1318
1233
  Things to know:
1319
1234
 
@@ -1338,9 +1253,7 @@ Things to know:
1338
1253
  gap is as wide as it was written. Chrome lays a track out at a fraction of
1339
1254
  a pixel, so a row whose content fits in the fraction can wrap one line
1340
1255
  differently here.
1341
- - **It is checked against Chrome.** A corpus of grids Chrome laid out is
1342
- part of the test suite (`test/grid-conformance.test.js`) and lays out here
1343
- within a pixel of Chrome's, but for the fraction above.
1256
+ - **It matches Chrome** within a pixel, but for the fraction above.
1344
1257
  - **Not here:** subgrid (a grid's child lays its own children out), baseline
1345
1258
  alignment, named lines (`[full-start]` — name an area instead) and the
1346
1259
  `grid` and `grid-template` shorthands. A value that uses one is an error
@@ -1358,6 +1271,8 @@ Things to know:
1358
1271
  it needs anyway. What a child answered is remembered until something inside
1359
1272
  it changes, so a frame in which one card's text changed lays out that card.
1360
1273
 
1274
+ Design notes: [design/architecture/grid-layout.md](../design/architecture/grid-layout.md).
1275
+
1361
1276
  ## Custom layouts
1362
1277
 
1363
1278
  `layout` hands a box's children to a layout algorithm in place of flexbox.
@@ -1399,12 +1314,10 @@ the cells are one height too.
1399
1314
 
1400
1315
  Things to know:
1401
1316
 
1402
- - **It runs inside the layout pass.** The box's size is what the algorithm
1403
- answers and its children are where it puts them, in the same frame — on
1404
- a window resize, a live resize, a scroll or a container query flip, with
1405
- no React render involved. That is the difference from measuring in
1406
- `onLayout` and setting state, which paints the old arrangement first
1407
- ([react-features.md](react-features.md#measuring-a-node)).
1317
+ - **It runs inside the layout pass**, in the same frame — on a window
1318
+ resize, a live resize, a scroll or a container query flip, with no React
1319
+ render. Measuring in `onLayout` and setting state instead paints the old
1320
+ arrangement first ([react-features.md](react-features.md#measuring-a-node)).
1408
1321
  - **It is a style property**, so a container query block can switch it:
1409
1322
  `'@container width < 500': { layout: { name: 'masonry', columns: 1 } }`.
1410
1323
  - **Right to left is mirrored for you.** An algorithm places from the left,
@@ -1422,9 +1335,8 @@ Things to know:
1422
1335
  the box is laid out as flexbox — in the same frame.
1423
1336
 
1424
1337
  Writing one of your own is `registerLayout` from `react-x11/host`
1425
- ([extending.md](extending.md#a-layout-algorithm-of-your-own)). The design
1426
- record, with why it has to run in the pass rather than in an effect, is
1427
- [architecture/custom-layout.md](architecture/custom-layout.md).
1338
+ ([extending.md](extending.md#a-layout-algorithm-of-your-own)). Design notes:
1339
+ [design/architecture/custom-layout.md](../design/architecture/custom-layout.md).
1428
1340
 
1429
1341
  ## Transitions
1430
1342
 
@@ -1441,17 +1353,16 @@ property; an object picks them individually.
1441
1353
  />
1442
1354
  ```
1443
1355
 
1444
- Numbers lerp and colours lerp per channel through ntk's own CSS colour
1445
- parser, so anything the paint path accepts animates. Two percentages of the
1446
- same unit lerp as numbers — `'-40%'` to `'100%'` is a value travelling across
1447
- whatever contains it, with nothing measuring the container. A value with no
1448
- meaningful midpoint — an enum like `flexDirection`, `auto`, a percentage
1449
- against a pixel value — snaps instead, and `zIndex` is excluded on purpose:
1450
- restacking every frame is not an animation. A duration keyed by a
1451
- [shorthand](#lengths-and-shorthands) is its longhands', the longhand's own
1452
- entry winning: `{ border: 150 }` fades the colour `border: '1px solid …'`
1453
- set, and `{ padding: 200 }` moves each side `padding: '8px 16px'` expanded
1454
- into.
1356
+ Numbers interpolate, and colours interpolate per channel, so any colour the
1357
+ paint accepts animates. Two percentages of the same unit interpolate as
1358
+ numbers — `'-40%'` to `'100%'` is a value travelling across whatever contains
1359
+ it, with nothing measuring the container. A value with no meaningful
1360
+ midpoint — an enum like `flexDirection`, `auto`, a percentage against a
1361
+ pixel value — snaps instead, and `zIndex` never transitions. A duration
1362
+ keyed by a [shorthand](#lengths-and-shorthands) is its longhands', the
1363
+ longhand's own entry winning: `{ border: 150 }` fades the colour
1364
+ `border: '1px solid …'` set, and `{ padding: 200 }` moves each side
1365
+ `padding: '8px 16px'` expanded into.
1455
1366
 
1456
1367
  The easing is a fixed ease-out cubic. A transition starts from **what is on
1457
1368
  screen**, not from the declared value, so interrupting one reverses from
@@ -1459,18 +1370,16 @@ where it got to rather than jumping to the end first. One to or from
1459
1370
  [`currentColor`](#currentcolor) runs between the colours as they are at the
1460
1371
  change.
1461
1372
 
1462
- The animation _is_ the repaint loop: the window keeps asking its frame clock
1463
- for frames while any transition is unfinished, and stops the frame the last
1464
- one lands. Nothing polls, and there is no per-widget
1373
+ A transition runs on the window's own frame clock, which stops the frame the
1374
+ last one lands: nothing polls, and there is no per-widget
1465
1375
  `requestAnimationFrame`.
1466
1376
 
1467
- Transitions may animate layout properties, unlike state blocks. That is not
1468
- an inconsistency: a pointer move must never reflow the tree, but an author
1377
+ Transitions may animate layout properties, unlike state blocks: an author
1469
1378
  who writes `transition: { left: 200 }` has asked for animated layout and
1470
1379
  pays a layout pass per frame for it, and a repaint of whatever that pass
1471
- moves rather than of the window. `Switch` is the worked example — the
1472
- thumb is absolutely positioned and slides on `start`, because
1473
- `justifyContent` would flip between the ends with nothing in between.
1380
+ moves. `Switch` is the worked example — the thumb is absolutely positioned
1381
+ and slides on `start`, because `justifyContent` would flip between the ends
1382
+ with nothing in between.
1474
1383
 
1475
1384
  ### Loops
1476
1385
 
@@ -1544,54 +1453,36 @@ const TypingDot = ({ index }) => (
1544
1453
  A changed `delay` is a different loop and starts from the top, as a changed
1545
1454
  `duration` does.
1546
1455
 
1547
- Why this and not a repeating `transition`: a transition is defined by a
1548
- _change_, from what is on screen to a new target, and it has no cycle to
1549
- repeat. A loop has no target — it has two ends and a period. They are
1550
- different declarations because they are different things, and one of them
1551
- would otherwise have to be spelled as the other with a flag.
1552
-
1553
- The easing default differs from a transition's for the same reason. A change
1554
- that ends looks right slowing into its new value; a cycle that restarts
1555
- would stutter at the wrap, so a loop is `linear` unless you say otherwise.
1456
+ A loop's easing is `linear` unless you say otherwise, where a transition's is
1457
+ ease-out: a cycle that restarts would stutter at the wrap.
1556
1458
 
1557
- **A loop costs a repaint of what moves.** It is the same machinery
1558
- transitions run on — the window's own frame clock, a damage region claimed
1559
- per frame — so an indeterminate progress bar repaints the bar, not the
1560
- window. The alternative an app would otherwise write, `setInterval` →
1561
- `setState`, is a re-render of the component and a repaint of whatever the
1562
- damage heuristics decide, at a cadence unrelated to when the window can
1563
- present.
1459
+ **A loop costs a repaint of what moves**, on the window's frame clock: an
1460
+ indeterminate progress bar repaints the bar, not the window. Prefer it to
1461
+ `setInterval` → `setState`, which re-renders the component on a cadence
1462
+ unrelated to when the window can present.
1564
1463
 
1565
1464
  On macOS a loop — or a transition — on a plain box's colour, border width
1566
- or radius is handed to Core Animation instead and costs no frames at all:
1567
- the layer presenter runs it on the node's own layer, and the surface
1568
- presenter promotes the node to a layer of its own above the window for as
1569
- long as it animates, where nothing is painted over it
1570
- ([macos.md](macos.md#layer-promotion-the-animated-few-on-their-own-layers-above-the-surface-presenter)).
1571
- So is a loop on an inset of a box out of the flow that says no pointer
1572
- lands on it — `position: 'absolute'` and `pointerEvents: 'none'`, as the
1573
- block of `<ProgressBar indeterminate>` does — since it moves that box and
1574
- nothing else: it becomes the layer's position. Without `pointerEvents:
1575
- 'none'` it stays on the clock, because input is hit where the layout put
1576
- the box.
1577
- Anything else runs as above. `examples/animation.jsx` has both shapes side
1578
- by side, with a frame counter in the terminal that shows the difference.
1579
-
1580
- **And it stops itself**, which is the whole reason this is core's job. A
1581
- forever-loop keeping a frame clock alive is invisible when it is wrong, so
1582
- every way of going off the screen is wired to it:
1583
-
1584
- | | |
1585
- | -------------------------------------------------------------------------------------------------------------------------- | --- |
1586
- | the window is unmapped, minimized, or fully obscured under a bare WM | |
1587
- | anything above the node hides it — `display: 'none'`, `<Suspense>`, `<Activity>` | |
1588
- | the node unmounts, or the style stops declaring the loop | |
1589
- | the desktop asked for **reduced motion** ([system.md](system.md#usedesktopsettings--how-the-desktop-wants-an-app-to-feel)) | |
1590
- | all the loop can draw is out of sight — scrolled out of its pane, clipped away by a box above it, off the window | |
1465
+ or radius runs in Core Animation and costs no frames at all. So does a loop
1466
+ on an inset of a box that is `position: 'absolute'` and
1467
+ `pointerEvents: 'none'`, as the block of `<ProgressBar indeterminate>` is;
1468
+ without `pointerEvents: 'none'` it stays on the frame clock, because input is
1469
+ hit where the layout put the box. Anything else runs as above.
1470
+ `examples/animation.jsx` has both shapes side by side, with a frame counter
1471
+ in the terminal that shows the difference.
1472
+
1473
+ **And it stops itself** whenever it goes off the screen:
1474
+
1475
+ - the window is unmapped, minimized, or fully obscured under a bare WM;
1476
+ - anything above the node hides it — `display: 'none'`, `<Suspense>`,
1477
+ `<Activity>`;
1478
+ - the node unmounts, or the style stops declaring the loop;
1479
+ - the desktop asked for **reduced motion**
1480
+ ([system.md](system.md#usedesktopsettings--how-the-desktop-wants-an-app-to-feel));
1481
+ - all the loop can draw is out of sight — scrolled out of its pane, clipped
1482
+ away by a box above it, off the window.
1591
1483
 
1592
1484
  Each one leaves the frame clock idle, and every one of them runs the loop
1593
- again when it goes away — with its phase reset, since a loop that resumes
1594
- mid-cycle would have been drawing where nobody could see it.
1485
+ again when it goes away — with its phase reset.
1595
1486
 
1596
1487
  Out of sight is judged after each layout pass, by what **every** frame of
1597
1488
  the loop can reach and never by where the current one has the node: the
@@ -1613,8 +1504,7 @@ still shows something in progress.
1613
1504
 
1614
1505
  Nothing about this is a per-frame callback: `animation` describes the motion
1615
1506
  and the renderer runs it. For a `<glarea>` drawing its own frames,
1616
- [`frameLoop`](gl.md) is the per-frame seam, and a 2D one is not
1617
- built yet.
1507
+ [`frameLoop`](gl.md) is the per-frame seam; there is no 2D one.
1618
1508
 
1619
1509
  ## Window size queries
1620
1510
 
@@ -1635,12 +1525,9 @@ const s = createStyles({
1635
1525
  merged in declaration order, before state blocks — so a `:hover` inside the
1636
1526
  wide layout still wins over the wide layout.
1637
1527
 
1638
- **A size query may set layout properties, unlike a state block.** That is
1639
- not an inconsistency: a pointer state must never reflow the tree, but a size
1640
- query is only ever re-evaluated inside a layout pass that a resize has
1641
- already required, so it costs nothing extra. Nodes that declare one are
1642
- registered with their window and re-resolved just before it lays out, and
1643
- only when the size actually changed.
1528
+ **A size query may set layout properties, unlike a state block.** It is only
1529
+ re-evaluated inside the layout pass a resize already requires, and only when
1530
+ the size actually changed, so it costs nothing extra.
1644
1531
 
1645
1532
  A malformed query is an error rather than a key that silently never
1646
1533
  matches.
@@ -1672,11 +1559,14 @@ A container is declared with the **`container` style property** — CSS's
1672
1559
  **nearest ancestor that declares one**. Wrapping the card in another `<box>`
1673
1560
  for a border changes nothing: the parent is not the container, the declared
1674
1561
  ancestor is, which is what lets a component be put anywhere and still get
1675
- the answer its author meant. The declaration changes nothing about the
1676
- container itself; it is neither layout nor paint, only what the blocks under
1677
- it resolve against. Being a style property, it can be set from inside a
1678
- query block — a box that is a container only in the wide layout — and
1679
- `false` takes a declaration back from earlier in a style array.
1562
+ the answer its author meant. A scroll pane is not a container unless it
1563
+ declares one, though it is the ideal one to declare. A node's blocks never
1564
+ answer to its own size, even when it declares itself a container: its
1565
+ declaration is for the blocks under it. The declaration changes nothing
1566
+ about the container itself; it is neither layout nor paint. Being a style
1567
+ property, it can be set from inside a query block — a box that is a
1568
+ container only in the wide layout — and `false` takes a declaration back
1569
+ from earlier in a style array.
1680
1570
 
1681
1571
  `width` and `height`, with `>=`, `<=`, `>` or `<`, in the logical pixels the
1682
1572
  dependent's own lengths are written in. Blocks that match are merged in
@@ -1734,16 +1624,12 @@ would; that is the reason to write a style as a block where one can.
1734
1624
 
1735
1625
  ### A block must not move the size it asks about
1736
1626
 
1737
- CSS makes this impossible by construction: `container-type: inline-size` is
1738
- size containment, the container is sized as if it had no content, and
1739
- nothing a descendant does can move it. Yoga has no containment, so here it is
1740
- a rule, and breaking it is detected rather than prevented. A block that
1741
- grows the container past the very threshold that turned it on — and shrinks
1742
- it back when it turns off — has no size that satisfies it. The renderer
1743
- notices (the node comes back to an answer it already held this frame),
1744
- **holds the answer it has**, and says so once in development, naming the
1745
- node. The answer stays held until the container's size moves for some other
1746
- reason.
1627
+ Yoga has no size containment, so here this is a rule, detected rather than
1628
+ prevented. A block that grows the container past the very threshold that
1629
+ turned it on — and shrinks it back when it turns off — has no size that
1630
+ satisfies it. The renderer notices, **holds the answer it has**, and says so
1631
+ once in development, naming the node. The answer stays held until the
1632
+ container's size moves for some other reason.
1747
1633
 
1748
1634
  The way to stay clear of it is also what makes a good container: a size that
1749
1635
  does not come from the content below it.
@@ -1758,6 +1644,8 @@ The common responsive pattern — less content when narrow, more when wide —
1758
1644
  is monotone and settles on the first re-layout; a warning means the design
1759
1645
  genuinely contradicts itself.
1760
1646
 
1647
+ Design notes: [design/architecture/container-queries.md](../design/architecture/container-queries.md).
1648
+
1761
1649
  ## Capability queries
1762
1650
 
1763
1651
  `'@supports transparency'` is to `@supports` what the size queries are to
@@ -1781,14 +1669,14 @@ const s = createStyles({
1781
1669
 
1782
1670
  Write the design that works everywhere as the base and the enhancement in
1783
1671
  the block — the same way you would write a `@media` query for a wider
1784
- screen. That ordering is the point: get it the other way round and the
1785
- fallback is the _unusual_ case, which is the one nobody tests.
1672
+ screen. Get it the other way round and the fallback is the _unusual_ case,
1673
+ which is the one nobody tests.
1786
1674
 
1787
1675
  `transparency` is true only when **both** halves hold: the window was
1788
1676
  created on a 32-bit visual (`<window transparent>`, and the display had one
1789
1677
  to give), **and** a compositor is running to blend it. Either half missing
1790
1678
  and a transparent corner is a _black_ corner — X shows the raw pixels — so
1791
- the honest answer is no, and the block does not apply.
1679
+ the answer is no, and the block does not apply.
1792
1680
 
1793
1681
  It is answered **per window**, not per display. A component rendered inside
1794
1682
  a `<popup transparent>` gets the translucent design; the same component
@@ -1809,107 +1697,10 @@ which answers the same question about the display.
1809
1697
  An unknown feature name is an error, like a malformed size query.
1810
1698
 
1811
1699
  Running with `REACT_X11_NO_TRANSPARENCY=1` makes the answer false on any
1812
- display, so the base design — the one nobody tests, being the unusual case —
1813
- can be looked at without stopping the compositor for the whole session. See
1700
+ display, so the base design can be looked at without stopping the compositor
1701
+ for the whole session. See
1814
1702
  [debugging.md](debugging.md#react_x11_no_transparency1).
1815
1703
 
1816
- ## Decided
1817
-
1818
- - **`':hover'`, not `_hover`.** The CSS spelling costs a pair of quotes and
1819
- buys transfer from every other styling system.
1820
- - **Declared containers, not the parent.** `'@container'` answers to the
1821
- nearest ancestor that _declares_ itself one, never to whatever happens to
1822
- be the parent: a wrapper `<box>` added for a border must not change a
1823
- component's answer. CSS spent a decade on element queries before shipping
1824
- container queries with exactly this requirement. Nor is a scroller a
1825
- container without saying so — it is the ideal one to declare, but the
1826
- presence of one between a node and its container must not silently change
1827
- which box answers. And there is no self query: a node's own size deciding
1828
- its own layout is the cycle containment exists to forbid.
1829
- - **Inherited properties, no relational selectors.** `color` and the font
1830
- properties travel down the tree; nothing matches a rule against it. So
1831
- there is no `:hover > child`, no sibling combinator and no Tailwind-style
1832
- `group`. Each of those exists to move a value across the tree, and
1833
- inheritance already does it in the one direction that is cheap: a parent's
1834
- `:hover` reaches its children because `color` is inherited, and a child's
1835
- hover reaches its parents because the hover path is the ancestor chain.
1836
- What is left over — a **sibling** reacting to a sibling — stays in React,
1837
- where `useControl` already returns `hover`/`focused`/`pressed` as state
1838
- ([components.md](components.md#basic-controls)). `<Checkbox>` does exactly
1839
- that, because its well is a sibling of the label the press lands on.
1840
- - **`currentColor` resolves where it is painted, not in the style.**
1841
- Resolving it where a `$token` resolves, once per style swap, would have
1842
- made every reader of a colour simpler and every inherited change wrong: a
1843
- `:hover { color }` on a row restyles no node under it, so an ink baked
1844
- into their styles would be stale in every one. So the style keeps the
1845
- keyword, as CSS's computed value does, and every reader — the paint on
1846
- each backend, a Cocoa layer's properties, the blits deciding what a box
1847
- covers — asks `resolveColor`.
1848
- - **Sticky sticks to a pane that scrolls, not to anything that clips.** CSS
1849
- makes every `overflow` but `visible` a scroll container, so a header
1850
- inside a card with `overflow: hidden` sticks to the card — which never
1851
- scrolls — and appears to do nothing, the most asked-about sticky bug there
1852
- is. Here `'hidden'` is CSS's `clip`: it clips and scrolls nothing, and only
1853
- `overflow: 'scroll'` makes a pane (see
1854
- [Sticky positioning](#sticky-positioning)). A header inside a rounded card
1855
- sticks to the list the card scrolls in, and stays inside the card.
1856
- - **A registered position is a `position` value, not a property of its
1857
- own.** CSS's `position` already picks a positioning scheme, and sticky is
1858
- one whose insets mean something other than an offset; a scheme of your own
1859
- is the same kind of thing. The first spelling, `placement`, was already
1860
- the anchor vocabulary — which side of its trigger a popup opens on — and
1861
- threw on the first `<popup placement>` it met. What one property costs is
1862
- composing a scheme with `absolute`, which a wrapping box answers.
1863
- - **A layout is a style property, and runs inside the pass.** What a layout
1864
- decides depends on sizes the pass produces, so a React-side one —
1865
- measure in `onLayout`, set state — paints the old arrangement first, and
1866
- misses every pass React never sees: a scroll, a live resize, a
1867
- transition. A style property is also one a container query block can
1868
- switch in the same frame.
1869
- - **The renderer mirrors right to left, not the algorithm.** An algorithm
1870
- places from the left, once, and each slot is mirrored across the box when
1871
- it reads right to left — the way yoga mirrors a flex row, so no layout can
1872
- forget to.
1873
- - **`null` is unset, and a layout value the layout cannot take is an error
1874
- where it is written.** `width: wide ? 240 : null` leaves the width to the
1875
- layout, as `undefined` does — yoga's own setters threw on `null`.
1876
- `width: 'hidden'`, `padding: 'auto'` or `flexDirection: 'sideways'` is an
1877
- error at the commit in development, naming the property, and that includes
1878
- a query block that does not match yet: the block is applied when it starts
1879
- matching, which is inside a frame, and a throw from there used to take the
1880
- process down. A value that only turns out wrong in a frame — a `$token`
1881
- resolving to one — is dropped there and reported, the way CSS drops a
1882
- declaration it cannot parse, and the rest of the block still applies.
1883
- - **A colour that does not parse is dropped, not painted.**
1884
- `backgroundColor: 'nonsense'`, `7` or `'#12'` is an error at the commit in
1885
- development, naming the property — in the style, in a state block, as a
1886
- gradient stop or as a shadow's colour. `''`, like `undefined`, means
1887
- unset. Where one gets past that — a `$token` whose palette entry is not a
1888
- colour, or any value in a production build — the paint drops it the way
1889
- CSS drops a declaration it cannot parse: no fill and no stroke, no
1890
- gradient or shadow at all rather than half of one, and the inherited ink
1891
- for text. On X11 it used to throw from the frame, where no error boundary
1892
- could catch it, and the other backends painted it black.
1893
-
1894
- - **The more specific property wins, not the later one.** CSS resolves a
1895
- longhand against its shorthand by order. A flattened style has no order
1896
- left — `[base, { padding: '8px 16px' }]` and `{ paddingTop: 0, … }` are
1897
- keys of one object by the time anything reads them — so a longhand beats
1898
- its shorthand and a side beats all four, wherever each is written. It is
1899
- the rule `paddingLeft` beside `padding: 8` always followed.
1900
- - **A length is pixels, and a unit nothing reads is an error.** `'12px'` is
1901
- 12 — scaled for the display like any number, where it used to reach yoga
1902
- as a string the scale skipped and the border setter read as NaN — and
1903
- `'2ex'` names its unit rather than becoming 2.
1904
- - **`rem` is the palette's `fontSize`, not 16.** CSS's root font size is
1905
- whatever the document's root says, and here that is the theme: the size
1906
- every label nobody styled is set in. So spacing written in `rem` follows a
1907
- theme that sets a bigger type size, as the palette's own radii and
1908
- paddings already do, and an app porting web numbers that assume 16 sets
1909
- `fontSize: 16` on its theme. The nearest palette is the one read, the way
1910
- a `$token` is — a `<ThemeProvider value={{ fontSize: 12 }}>` around a
1911
- dense panel re-bases it.
1912
-
1913
1704
  ## Elements that are not styled
1914
1705
 
1915
1706
  The declarative SVG children carry a vocabulary of their own — `<circle cx
@@ -1917,12 +1708,3 @@ r fill>` is SVG's, not the style channel's — so the style channel does not
1917
1708
  apply to them, the same way it does not apply to an `<input type>` in the
1918
1709
  DOM. They report
1919
1710
  `stylable === false` and their props are passed through untouched.
1920
-
1921
- ## Next
1922
-
1923
- A
1924
- `boxShadow` that a `<popup>` can cast (the window needs a translucent margin
1925
- around itself first — see [elements.md](elements.md) on `transparent`
1926
- popups). Container queries shipped; the design record, with the targeting
1927
- forms considered and the cost measured, is
1928
- [architecture/container-queries.md](architecture/container-queries.md).