react-x11 2.52.0 → 2.54.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 (115) 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 +1096 -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/imagedecode-worker.js +32 -0
  76. package/src/imagedecode.js +140 -2
  77. package/src/layouts.js +1 -1
  78. package/src/nodes/animation.js +1 -1
  79. package/src/nodes/invalidate.js +1 -1
  80. package/src/nodes/layouthost.js +1 -1
  81. package/src/nodes/video.js +19 -8
  82. package/src/nodes/window/droptarget.js +1 -1
  83. package/src/ntk.d.ts +12 -0
  84. package/src/pacing.js +1 -1
  85. package/src/refresh/index.js +2 -1
  86. package/src/types/video.d.ts +5 -2
  87. package/src/videobackend.js +28 -0
  88. package/src/videoframes.js +29 -16
  89. package/src/wayland/app.js +27 -0
  90. package/src/wayland/video.js +376 -0
  91. package/src/win32/a11y.js +1 -1
  92. package/src/win32/app.js +2 -2
  93. package/src/win32/fonts.js +1 -1
  94. package/src/win32/glarea.js +4 -4
  95. package/src/win32/overlay.js +1 -1
  96. package/src/win32/panewindow.js +3 -3
  97. package/src/win32/shell.js +1 -1
  98. package/src/win32/window.js +2 -2
  99. package/src/windowid.js +1 -1
  100. package/src/xvideo.js +189 -0
  101. package/docs/architecture/accessibility.md +0 -470
  102. package/docs/architecture/animation.md +0 -707
  103. package/docs/architecture/container-queries.md +0 -525
  104. package/docs/architecture/custom-layout.md +0 -334
  105. package/docs/architecture/drag-and-drop.md +0 -941
  106. package/docs/architecture/element-layer-contents.md +0 -622
  107. package/docs/architecture/frame-pacing.md +0 -255
  108. package/docs/architecture/grid-layout.md +0 -358
  109. package/docs/architecture/protocol-efficiency.md +0 -357
  110. package/docs/architecture/video.md +0 -1149
  111. package/docs/architecture/windowed-regions.md +0 -854
  112. package/docs/glx.md +0 -137
  113. package/docs/wayland.md +0 -545
  114. package/docs/windows-embedding.md +0 -271
  115. package/docs/windows-gl.md +0 -221
@@ -11,31 +11,25 @@ wins by position rather than by clobbering a computed value:
11
11
  ```
12
12
 
13
13
  Props that a component consumes itself are never style — `ProgressBar`'s
14
- `color`, `Dialog`'s `width`/`height` (a dialog is a real popup window, and
15
- those are its window's size), `ContextMenu`'s `fontSize` (it measures labels
16
- with it). Everything else is forwarded to the host box. See
17
- [styling.md](styling.md).
14
+ `color`, `Dialog`'s `width`/`height` (its window's size), `ContextMenu`'s
15
+ `fontSize` (it measures labels with it). Everything else is forwarded to the
16
+ host box. See [styling.md](styling.md).
18
17
 
19
- Widget components are plain React built on the host elements — no
20
- reconciler support involved. They live in the package root export.
21
-
22
- All of them are screen-reader ready: roles, names, states and AT-driven
23
- control come built in, and an `aria-label` (or a label child) is the only
24
- thing a widget can need from you — [accessibility.md](accessibility.md)
25
- lists what each one announces.
18
+ The widgets are plain React over the host elements, exported from the package
19
+ root. All are screen-reader ready: roles, names, states and AT-driven control
20
+ are built in, and an `aria-label` (or a label child) is the only thing a
21
+ widget can need from you — [accessibility.md](accessibility.md) lists what
22
+ each one announces.
26
23
 
27
24
  ## Theming
28
25
 
29
26
  **Widgets follow the desktop with nothing declared** — dark on a dark desktop,
30
- light on a light one, the same way a GTK or Qt app does. See
31
- [system appearance](appearance.md) for how that is worked out and how to pin
32
- or override it.
27
+ light on a light one. See [system appearance](appearance.md) for how that is
28
+ worked out and how to pin or override it.
33
29
 
34
30
  `<ThemeProvider value={palette}>` gives every widget beneath it a palette,
35
- layered over the scheme in force — so a partial one names what your app
36
- changes and everything else keeps following. It carries **shape as well as
37
- colour** — corner radius, border weight, text size and the padding inside a
38
- control are most of what separates one platform's controls from another's:
31
+ layered over the scheme in force, so a partial one names only what your app
32
+ changes. It carries **shape as well as colour**:
39
33
 
40
34
  | token | |
41
35
  | --------------------------------------------------------------- | ------------------------------------ |
@@ -55,37 +49,24 @@ control are most of what separates one platform's controls from another's:
55
49
 
56
50
  ### The ground and what is on it
57
51
 
58
- `background` is what the **window** is — the fill under everything, painted
59
- by the window itself. `surface` is what is raised off it: a control's fill, a
60
- card, the sheet a menu or a dialog is drawn on. The light palette makes them
61
- the same colour, because a white app on a white ground is what it has always
62
- been; the dark one lifts `surface` a step, because a card at the ground's own
63
- colour is a card you cannot see.
64
-
65
- A palette that has one ground says so by naming one: **`surface` follows
66
- `background` unless you name it**, so a theme that only recolours the app
67
- keeps working and only a design that really raises its cards has a second
68
- token to fill in.
52
+ `background` is the **window**, the fill under everything. `surface` is what
53
+ is raised off it: a control's fill, a card, the sheet of a menu or dialog.
54
+ The light palette makes them the same colour; the dark one lifts `surface` a
55
+ step. **`surface` follows `background` unless you name it**, so only a design
56
+ that really raises its cards has a second token to fill in.
57
+ `surfaceHover` and `surfaceActive` are that surface's interaction steps.
69
58
 
70
59
  ```jsx
71
60
  <ThemeProvider value={{ background: '#f6f8fa', surface: '#ffffff' }}>
72
61
  ```
73
62
 
74
- `surfaceHover` and `surfaceActive` are that surface's own interaction steps,
75
- which is what they have always been — before there was a `surface` to be the
76
- steps of.
77
-
78
63
  ### What the app says with colour
79
64
 
80
- `danger`, `success`, `warning` and `info` are the four things a screen has to
81
- be able to say: this failed, this worked, look at this, here is a note. They
82
- are what an alert, a badge, a validation message and a toast are coloured
83
- from, and having them as tokens is what keeps a hard-coded `#e74c3c` — which
84
- is a bruise on a dark desktop — out of application code.
85
-
86
- Each works as **ink as well as fill**: every one clears 4.5:1 against its own
87
- palette's ground, so the message under a field and the badge beside it are one
88
- token.
65
+ `danger`, `success`, `warning` and `info` — failed, worked, look at this, a
66
+ note — colour alerts, badges, validation messages and toasts, and keep a
67
+ hard-coded `#e74c3c` (a bruise on a dark desktop) out of app code. Each
68
+ clears 4.5:1 against its palette's ground, so it works as **ink as well as
69
+ fill**:
89
70
 
90
71
  ```jsx
91
72
  <text style={{ color: '$danger' }}>Password too short</text>
@@ -94,8 +75,7 @@ token.
94
75
  </box>
95
76
  ```
96
77
 
97
- For the tinted panel an alert usually wants — a wash of the colour rather
98
- than the colour — tint the fill and leave the ink alone:
78
+ For a tinted panel, tint the fill and leave the ink alone:
99
79
 
100
80
  ```jsx
101
81
  import { tint } from 'react-x11/style';
@@ -104,59 +84,39 @@ import { tint } from 'react-x11/style';
104
84
  </box>;
105
85
  ```
106
86
 
107
- Only `danger` has `…Hover` and `…Active`, because a destructive button is the
108
- only status fill anyone presses; the other three are things the app says, not
109
- things the user clicks.
110
-
111
- The `…Text` four are the letters on a status fill, and **a palette almost
112
- never sets them**: each is derived from the fill it goes on, as the more
113
- legible of that palette's own `text` and `background`. So a theme whose
114
- `warning` is a yellow gets dark letters on it without having thought about
115
- it — and the same rule fixes `accentText`, which used to be inherited as
116
- white onto accents that could not carry white.
117
-
118
- `fontFamily` and `fontSize` are the type this app sets, and text that names
119
- neither **takes them** — a `<text>`, a `<textinput>`, a widget's own label. So
120
- `<ThemeProvider value={{ fontFamily: 'Inter', fontSize: 16 }}>` is where an app
121
- says what it is set in, once, rather than on every label, and a `Select`'s rows
122
- grow with it because a row is its label with even space all round. A `fontSize`
123
- or `fontFamily` in a style still wins, the way a style property always wins
124
- over what a node inherits.
125
-
126
- The widgets that take a size of their own keep taking it: `MenuBar` and
127
- `ContextMenu` have a `fontSize` prop because they measure their labels before
128
- they have anywhere to measure them in, and `Table` has `rowHeight` because
129
- every row has to be the same height for it to skip the ones off screen.
130
-
131
- Both are CSS-style family lists — `'"JetBrains Mono", monospace'` names a
132
- preference and a fallback.
133
-
134
- `monoFamily` is the second face, and nothing unstyled reads it: it is there so
135
- that every code surface in an app — a listing, a log pane, a hex dump, all
136
- written by different components — can say `fontFamily: '$monoFamily'` and be
137
- set from one place.
87
+ Only `danger` has `…Hover` and `…Active` — the only status fill anyone
88
+ presses. The `…Text` four (and `accentText`) are derived from their fill as
89
+ the more legible of the palette's `text` and `background`, so a palette
90
+ almost never sets them: a yellow `warning` gets dark letters unasked.
91
+
92
+ `fontFamily` and `fontSize` are the app's type, and text that names neither
93
+ **takes them** — a `<text>`, a `<textinput>`, a widget's label — so
94
+ `<ThemeProvider value={{ fontFamily: 'Inter', fontSize: 16 }}>` sets it once,
95
+ and a `Select`'s rows grow with it. A style's `fontSize`/`fontFamily` still
96
+ wins. Both are CSS-style family lists (`'"JetBrains Mono", monospace'`).
97
+ `MenuBar` and `ContextMenu` keep a `fontSize` prop (they measure labels
98
+ before they have a window to measure in), and `Table` keeps `rowHeight`
99
+ (every row must be the same height to skip the ones off screen).
100
+
101
+ `monoFamily` is read by nothing unstyled: it is there so every code surface
102
+ in an app can say `fontFamily: '$monoFamily'` and be set from one place.
138
103
 
139
104
  ### Which way it reads
140
105
 
141
- `direction` is `'ltr'` or `'rtl'`, and it is **seeded from the locale**: an
142
- app started under an RTL locale is mirrored without being configured, the
143
- way a GTK or Qt one is. Set it here to mirror a whole UI from one place:
106
+ `direction` (`'ltr'` or `'rtl'`) is **seeded from the locale**, so an app
107
+ started under an RTL locale is mirrored unconfigured. Set it here to mirror a
108
+ whole UI:
144
109
 
145
110
  ```jsx
146
111
  <ThemeProvider value={{ direction: settings.rtl ? 'rtl' : 'ltr' }}>
147
112
  ```
148
113
 
149
- The provider plants the matching `direction` style property in the node tree
150
- as it goes, so the **boxes** and the **widgets** mirror together. That is
151
- why this is the way to do it rather than a bare `<box style={{ direction:
152
- 'rtl' }}>`: yoga mirrors boxes on its own, but which way an arrow key steps,
153
- which way a chevron points and which side a submenu opens on are widget
154
- decisions, read through `useDirection()`. Under a bare style property the
155
- layout mirrors and those do not.
156
-
157
- Most of the set needs no help — `Button`, `Checkbox`, `Radio`, `Switch` and
158
- `ProgressBar` are rows and flex ratios, and come out mirrored on their own.
159
- The rest each have one thing yoga could not answer:
114
+ The provider plants the `direction` style property in the node tree, so
115
+ boxes **and** widgets mirror together. A bare
116
+ `<box style={{ direction: 'rtl' }}>` mirrors only the layout, not the widget
117
+ decisions read through `useDirection()` — which way an arrow key steps, a
118
+ chevron points or a submenu opens. `Button`, `Checkbox`, `Radio`, `Switch` and
119
+ `ProgressBar` need nothing more; the rest mirror one thing each:
160
120
 
161
121
  | widget | what mirrors |
162
122
  | ------------------------- | --------------------------------------------------- |
@@ -164,67 +124,44 @@ The rest each have one thing yoga could not answer:
164
124
  | `Tabs` | Left/Right walk the strip the way it is drawn |
165
125
  | `Table` | column resizing, the sort mark, the header's scroll |
166
126
  | `SplitPane` | which pane is first, the drag and its arrows |
167
- | `MenuBar` / `ContextMenu` | submenus open to the start side, and their arrows |
127
+ | `MenuBar` / `ContextMenu` | submenus open leftwards, and their arrows |
168
128
  | `Select`, `PasswordInput` | the field's own insets follow the text |
169
129
 
170
- `<textinput>` and `<textarea>` mirror on the inside too — the value, the
171
- placeholder and the caret are against the direction's start edge, and the
172
- value is shaped at the box's base level rather than at its own first strong
173
- character ([styling.md](styling.md#inside-an-editable-field)). What stays
174
- put: Left/Right and Home/End step through the string rather than across the
175
- screen.
130
+ `<textinput>` and `<textarea>` mirror inside too: value, placeholder and caret
131
+ sit against the start edge, and the value is shaped at the box's base level
132
+ ([styling.md](styling.md#inside-an-editable-field)); Left/Right and Home/End
133
+ still step through the string, not across the screen. The style property and
134
+ logical edges: [styling.md](styling.md#direction-and-the-logical-edges).
176
135
 
177
- See [styling.md](styling.md#direction-and-the-logical-edges) for the style
178
- property and the logical edges.
136
+ ### Size and shape
179
137
 
180
- `paddingY` is the space **you can see** — above the capitals and below the
181
- baseline — not the space plus whatever the font's ascent left over: every
182
- widget label is trimmed to its letters
138
+ `paddingY` is the space **you can see**, above the capitals and below the
139
+ baseline: every widget label is trimmed to its letters
183
140
  ([styling.md](styling.md#measuring-text-to-its-letters)), so a control is its
184
- label band plus this twice. That makes it a larger number than the same look
185
- would need in CSS, and it makes it mean the same thing in every typeface.
186
- Rows on a popup follow the same rule — a menu row and a `Select` option are
187
- their label with `padding` all round, so the space above a row's text is the
188
- space beside it. So does `<textinput>`, which is what keeps a field the same
189
- height as the `Button` and the `Select` next to it on a form; give it
190
- `paddingY` and the three agree by construction
191
- ([styling.md](styling.md#measuring-text-to-its-letters)).
192
-
193
- The `…Active` four are the colour a control takes **while it is held**, and
194
- a palette almost never sets them: each is derived from the step the palette's
195
- own hover already makes — `accent` → `accentHover` → one more of the same —
196
- so it darkens a light theme and lightens a dark one. Set one explicitly and
197
- it wins. See [The press state](#the-press-state) for why every control has
198
- one.
199
-
200
- The `radius…` three that name a popup are derived too, and from the **text
201
- size** rather than from `radius`: a menu is a sheet laid over the window
202
- where a button is a control cut into it, and half the body size is the
203
- number desktops land near — 7px at a 14px body. The other two step in from
204
- there, because a rounded thing inside a rounded thing needs the tighter
205
- curve: `radiusPopupItem` is the highlight on a menu row, `radiusTooltip` the
206
- tooltip bubble. A palette that moves `fontSize` and names none of them gets
207
- all three in proportion; naming one pins it.
208
-
209
- `radiusPopupItem` is a **ceiling**, not the radius a row gets. A rounded rect
210
- inside a rounded rect only reads as one shape when the two curves share a
211
- centre, which happens exactly when the inner radius is the outer one less the
212
- gap between them — so a row's corner is `radiusPopup` minus the sheet's
213
- hairline border minus the inset the row is padded in by, and the token caps
214
- it. A theme that wants rounder highlights rounds the sheet they sit on: that
215
- is the only change that can round both and keep them concentric.
216
-
217
- They are only ever _seen_ where the display composites — a popup gives up
218
- its corners by not painting them, and with nothing blending them those
219
- pixels are black rather than the desktop, so the widgets ask the window
220
- (`'@supports transparency'`, [styling.md](styling.md)) and stay square
221
- where the answer is no.
222
-
223
- There are two consumers of a palette, and one provider feeds both. Widgets
224
- read it as React context through `useTheme()`; a `$token` in a style
225
- resolves against the nearest `theme` **prop** in the node tree, which knows
226
- nothing about React ([styling.md](styling.md#theme-tokens)). `ThemeProvider`
227
- puts the merged palette on the context _and_ plants it in the tree, so
141
+ label band plus `paddingY` twice — a larger number than CSS would need, and
142
+ the same in every typeface. Menu rows, `Select` options and `<textinput>`
143
+ follow the rule, so a field, a `Button` and a `Select` on one row match in
144
+ height.
145
+
146
+ The `…Active` four are the colour **while held**, derived one step past the
147
+ hover (`accent` → `accentHover` → `accentActive`), so a palette rarely sets
148
+ them; one that is set wins. See [The press state](#the-press-state).
149
+
150
+ The popup radii follow the **text size**, not `radius`: `radiusPopup` is half
151
+ the body size (7px at 14px), and `radiusPopupItem` (a menu row's highlight)
152
+ and `radiusTooltip` (the bubble) step in from it. Naming one pins it.
153
+ `radiusPopupItem` is a **ceiling**: a row's corner is `radiusPopup` less the
154
+ sheet's border and the row's inset, so the curves stay concentric — round the
155
+ sheet to round the highlights. Rounded corners only show where the display
156
+ composites (`'@supports transparency'`, [styling.md](styling.md)); elsewhere
157
+ the widgets stay square.
158
+
159
+ ### `ThemeProvider` and `useTheme()`
160
+
161
+ Widgets read the palette as React context through `useTheme()`; a `$token` in
162
+ a style resolves against the nearest `theme` **prop** in the node tree
163
+ ([styling.md](styling.md#theme-tokens)). `ThemeProvider` feeds both, so this
164
+ paints the palette, and an inner provider merges over an outer one:
228
165
 
229
166
  ```jsx
230
167
  <ThemeProvider value={dark}>
@@ -232,11 +169,10 @@ puts the merged palette on the context _and_ plants it in the tree, so
232
169
  </ThemeProvider>
233
170
  ```
234
171
 
235
- paints the palette rather than nothing. Nesting works the same either way:
236
- an inner provider merges over the outer, as an inner `theme` prop does.
237
-
238
- `useTheme()` returns the palette in force — complete, whatever the provider
239
- above set — and is the same object token resolution sees:
172
+ `useTheme()` returns the complete palette in force — the object token
173
+ resolution sees. It indexes like a bag of tokens: `theme[name]` needs no cast;
174
+ named tokens keep their types, anything else is `unknown`. What a provider
175
+ _accepts_ stays closed (`Partial<Theme>`), so a palette typo is caught.
240
176
 
241
177
  ```jsx
242
178
  function Panel() {
@@ -245,20 +181,10 @@ function Panel() {
245
181
  }
246
182
  ```
247
183
 
248
- It is a bag of tokens, and indexes like one: a component that resolves a name
249
- it was handed rather than one it wrote — a code block reading its own colour
250
- map, a renderer resolving `$token` itself — writes `theme[name]` with no cast.
251
- The named tokens keep their types, anything else is `unknown` and narrows.
252
- What a provider _accepts_ stays closed (`Partial<Theme>`), because a typo in a
253
- palette has nowhere else to be caught.
254
-
255
- Where the palette reaches the tree depends on where the provider is written.
256
-
257
- **Above the windows**, at the root of the tree, it draws nothing, and every
258
- window under it takes the palette — its background and the type every
259
- unstyled `<text>` falls back to as well as the `$token`s inside. Whatever
260
- shape the windows arrive in: a component that renders one, a window that is
261
- closed for now, several at once, a provider nested in another.
184
+ **Above the windows**, at the root of the tree, a provider draws nothing and
185
+ every window under it takes the palette — background, the fallback type of
186
+ unstyled `<text>`, the `$token`s — however the windows arrive (a component
187
+ that renders one, a closed window, several, a nested provider):
262
188
 
263
189
  ```jsx
264
190
  root.render(
@@ -268,26 +194,20 @@ root.render(
268
194
  );
269
195
  ```
270
196
 
271
- **Inside a window** it is a `<box>`, styled `{ flexGrow: 1 }` so an app-level
272
- provider fills its parent; pass `style` to change that
273
- (`style={{ flexGrow: 0 }}` around a single control). A window nests only in a
274
- window, so a nested `<window>` under a provider written directly inside a
275
- window is handed on to that window, and takes the palette with it. Further
276
- down — a provider inside a `<box>` — there is no window to hand it to, and the
277
- error says to move the provider.
278
-
279
- Widgets plant the merged palette on their own root node too, so a `$token`
280
- in a style you pass one resolves even with no provider anywhere.
197
+ **Inside a window** it is a `<box>` styled `{ flexGrow: 1 }`; pass `style` to
198
+ change that (`{ flexGrow: 0 }` around a single control). A nested `<window>`
199
+ under a provider written directly in a window takes the palette with it; a
200
+ provider deeper inside a `<box>` has no window to hand it to, and the error
201
+ says to move it. Widgets plant the palette on their own root node too, so a
202
+ `$token` in a style you pass one resolves with no provider at all.
281
203
 
282
- `examples/themes.js` has three worked palettes — GitHub, macOS and Windows,
283
- each in light and dark — and `npm run examples:theming` switches between
284
- them at runtime.
204
+ `examples/themes.js` has GitHub, macOS and Windows palettes in light and dark;
205
+ `npm run examples:theming` switches between them.
285
206
 
286
207
  ## System icons
287
208
 
288
- The glyphs the widgets here are drawn with, exported so an application and a
289
- third-party widget can use the same ones — a toolbar you write and a `Select`
290
- you did not should not disagree about which way a chevron points.
209
+ The glyphs the widgets are drawn with, exported so your toolbar and a
210
+ `Select` agree about which way a chevron points.
291
211
 
292
212
  ```jsx
293
213
  import { Icon } from 'react-x11';
@@ -312,45 +232,28 @@ import { Icon } from 'react-x11';
312
232
  Everything else goes to the host `<canvas>`, so a clickable one is
313
233
  `<Icon name="close" onClick={…} focusable />`.
314
234
 
315
- **`size` is the mark, not a grid it sits in.** This is the one place the set
316
- departs from how lucide and its descendants are drawn: theirs put about 14px
317
- of ink in a 24px box, so you reach for `size={20}` next to 14px text. Here
318
- the ink runs corner to corner, so `size` is what you actually see and the
319
- number to pick is the number you want — the `Select` chevron is
320
- `capBand(fontSize)` because it should be as wide as the capitals beside it.
321
-
322
- A chevron is the one glyph whose two axes differ: the arms are at 45°, so it
323
- is as long as `size` along the way it points across and half that the other
324
- way. `<Icon name="chevronDown" size={10} />` is 10 wide and 5 tall; the same
325
- size of `chevronRight` is 5 wide and 10 tall.
235
+ **`size` is the mark**, ink corner to corner — not a 24px grid with 14px of
236
+ ink in it as in lucide-style sets — so pick the size you want to see. A
237
+ chevron is the one glyph whose axes differ (45° arms):
238
+ `<Icon name="chevronDown" size={10} />` is 10 wide and 5 tall, and
239
+ `chevronRight` at the same size is 5 wide and 10 tall.
326
240
 
327
241
  ### Affordances, not nouns
328
242
 
329
- The set holds glyphs that say something about **the control**: there is more
330
- here, this one is chosen, this closes, this is hidden. It holds no nouns —
331
- no folder, no document, no save, no printer. Those belong to an icon theme
332
- (lucide, an XDG icon theme, your own art), they are unbounded in number, and
333
- a widget set that starts shipping them has taken on a design system.
334
-
335
- That is also the answer to "why is X missing": if X names a thing rather
336
- than an action the control affords, it is not going to be here. Bringing
337
- your own set is expected and supported — a `<svg>` or a `<canvas>` goes
338
- anywhere an `<Icon>` does, and `ContextMenu`'s `icon` takes either. The
339
- platform's own set is there too:
340
- [`<image src={{ symbol }}>`](elements.md#src-symbol---the-platforms-own-icons)
341
- draws an SF Symbol on macOS and an icon from the desktop's icon theme
342
- elsewhere, in the text colour.
343
-
344
- The **drawings are not themable** for the same reason: the geometry is the
345
- widget set's vocabulary, and an application that wants a different chevron
346
- wants an icon library. Colour and size are yours; the shape is not.
243
+ The set holds glyphs about **the control** — more here, chosen, close,
244
+ hidden — and no nouns (folder, document, printer). Bring your own icon set:
245
+ a `<svg>` or `<canvas>` goes anywhere an `<Icon>` does, and `ContextMenu`'s
246
+ `icon` takes either. The platform's own set is
247
+ [`<image src={{ symbol }}>`](elements.md#src-symbol---the-platforms-own-icons):
248
+ an SF Symbol on macOS, the desktop's icon theme elsewhere, in the text colour.
249
+ The drawings are not themable: colour and size are yours, the shape is not.
347
250
 
348
251
  ### Colour inherits; size does not
349
252
 
350
- `color` travels down the tree
351
- ([styling.md](styling.md#inheritance-the-ink-the-face-and-the-size)), so an
352
- icon takes the ink of whatever it is written inside and needs nothing said
353
- at the call site:
253
+ `color` inherits ([styling.md](styling.md#inheritance-the-ink-the-face-and-the-size)),
254
+ so an icon takes the ink it is written inside, hover included; the `color`
255
+ prop is for ink the surrounding text does not have (a destructive mark, a
256
+ check on an accent fill):
354
257
 
355
258
  ```jsx
356
259
  <box style={{ color: theme.textMuted, ':hover': { color: theme.accent } }}>
@@ -359,39 +262,18 @@ at the call site:
359
262
  </box>
360
263
  ```
361
264
 
362
- That covers the hover case too. `:hover` marks the row, `color` is
363
- inherited, and the label and the icon both follow — which is why
364
- `ContextMenu`'s submenu chevron carries no colour of its own any more. The `color` prop is for saying something the surrounding text does
365
- not: a destructive action's mark, or a check drawn on an accent fill.
366
-
367
- **`size` does not inherit, and is deliberately not `fontSize`.** A glyph is
368
- a drawing rather than a letter — no baseline to sit on, no ascent to be
369
- measured against — so it takes its default from the palette's `fontSize` and
370
- stays put when a label beside it shrinks. Pass `size` for the one icon that
371
- has to be bigger.
265
+ **`size` does not inherit, and is not `fontSize`**: it defaults from the
266
+ palette's `fontSize` and stays put when a label beside it shrinks. Pass
267
+ `size` for the one icon that must be bigger.
372
268
 
373
269
  ### What it costs to draw one
374
270
 
375
- Each glyph is a drawing over `<canvas mono>`
376
- ([elements.md](elements.md#canvas)), which is a promise that everything it
377
- paints is one colour it did not choose — so one drawing serves every state a
378
- control puts it in, and the paint cache can keep one rendered copy of
379
- `chevronDown` at 12px for every chevron on the screen at once.
380
-
381
- The entry is **coverage**, so the colour is applied at composite time and
382
- stays out of the key: one rendered copy of `chevronDown` at 12px serves the
383
- resting row, the highlighted row, the disabled one and both colour schemes —
384
- the trick the glyph cache runs on text, and the one `<svg>` gets for a
385
- `fill="currentColor"` document. The size cannot leave the key, since a
386
- coverage surface is pixels at a fixed size, so one icon at two sizes is two
387
- entries.
388
-
389
- The drawings are module-level, so re-rendering a list invalidates none of
390
- its icons: `<canvas>` compares `onDraw` by identity, and a fresh closure per
391
- render is a repaint per glyph.
392
-
393
- `icons` is the map of raw drawings, for a widget that wants the glyph
394
- without the component:
271
+ Each glyph is a module-level drawing over `<canvas mono>`
272
+ ([elements.md](elements.md#canvas)), cached as coverage and coloured at
273
+ composite time, so one copy per glyph and size serves every colour and state.
274
+ `icons` is the map of raw drawings, for a widget that wants the glyph without
275
+ the component; keep `onDraw` identity-stable as these are, or every render
276
+ repaints:
395
277
 
396
278
  ```jsx
397
279
  import { icons } from 'react-x11';
@@ -404,28 +286,21 @@ import { icons } from 'react-x11';
404
286
  />;
405
287
  ```
406
288
 
407
- They are **decoration by default** — `aria-hidden`, because the meaning is
408
- already on the control, in its `role` and its `aria-expanded`. Name one
289
+ Icons are **`aria-hidden` by default** — the meaning is on the control's
290
+ `role` and `aria-expanded`. Name one
409
291
  (`aria-hidden={false} aria-label="Close"`) only when the icon _is_ the
410
- control and nothing else says so.
292
+ control.
411
293
 
412
294
  ## Basic controls
413
295
 
414
- `Button`, `Checkbox`, `Radio`/`RadioGroup`, `Switch` and `ProgressBar` share
415
- one piece of plumbing, `useControl(disabled, onActivate)`: it makes the
416
- control focusable, activates it on click — and so on Space and Enter, which
417
- are a click on anything with an `onClick` unless Ctrl, Alt or Super make
418
- them a shortcut's ([events.md](events.md#space-and-enter-are-a-click)), the
419
- widgets having no key mapping of their own any more — sets the pointer cursor,
420
- and expresses hover, press and focus
421
- feedback as `:hover`/`:active`/`:focus` style blocks rather than React state
422
- — so moving the pointer over a control repaints one node instead of
423
- rendering.
424
-
425
- `Button`, `Checkbox`, `Switch` and `ProgressBar` take `style`, merged after
426
- their own so an override wins by position, and forward any remaining props
427
- to the host box. `Radio` is the exception: it takes only the props listed
428
- below, and the group around it carries the layout.
296
+ `Button`, `Checkbox`, `Radio`/`RadioGroup`, `Switch` and `ProgressBar` are
297
+ focusable, activate on click — and so on Space and Enter, unless Ctrl, Alt or
298
+ Super make it a shortcut ([events.md](events.md#space-and-enter-are-a-click))
299
+ — set the pointer cursor, and draw hover, press and focus as
300
+ `:hover`/`:active`/`:focus` style blocks, so hovering repaints one node
301
+ rather than rendering. All but `Radio` take `style` (merged after their own)
302
+ and forward other props to the host box; `Radio` takes only its listed props,
303
+ and its group carries the layout.
429
304
 
430
305
  ```jsx
431
306
  import { Button, Checkbox, RadioGroup, Radio, Switch, ProgressBar } from 'react-x11';
@@ -444,93 +319,59 @@ import { Button, Checkbox, RadioGroup, Radio, Switch, ProgressBar } from 'react-
444
319
  ```
445
320
 
446
321
  Label text is the children (or a `label` prop); a bare string is wrapped in
447
- a `<text>` for you, so `<Button>Save</Button>` needs no `<text>`.
322
+ a `<text>` for you. All five are demoed in `examples/widgets.jsx`.
448
323
 
449
324
  ### Native bezels
450
325
 
451
- On a backend that renders the platform's own control bezels — today the
452
- **Cocoa** backend, never X11 — `Button`, `Checkbox`, `Radio`, `Switch`,
453
- `Slider` and `Select` wear AppKit's pixels by default, while interaction,
454
- focus, keyboard and accessibility stay this library's shared implementation.
455
- The mechanism is a bezel _image_ drawn through the ordinary paint path, not
456
- an embedded `NSView`: the press model and the event routing are behaviour
457
- this project considers part of its identity, and a real `NSControl` would
458
- take them over wholesale. [macos.md](macos.md#native-controls) is the
459
- design record.
460
-
461
- **Default native, seam out**, at three levels:
326
+ On the **Cocoa** backend (never X11) `Button`, `Checkbox`, `Radio`, `Switch`,
327
+ `Slider` and `Select` wear AppKit's bezels by default; on **win32** all but
328
+ `Switch` wear the system's own while the desktop is light, and are drawn in
329
+ dark mode. Interaction, focus, keyboard and accessibility stay this
330
+ library's. Opt out at three levels:
462
331
 
463
332
  ```jsx
333
+ // the app's identity
464
334
  <ThemeProvider value={{ controls: 'drawn' }}>
465
- {' '}
466
- {/* the app's identity */}
467
- <Button native={false}>Brand</Button> {/* one control */}
335
+ {/* one control */}
336
+ <Button native={false}>Brand</Button>
468
337
  </ThemeProvider>;
469
338
 
470
339
  const canGoNative = useSupports('nativeControls'); // what the backend has
471
340
  ```
472
341
 
473
- `controls` is `'auto'` (bezels where the backend has them), `'native'` (ask
474
- for them, and hear about it in development where there are none) or
475
- `'drawn'` (this library's rendering everywhere). It lives in the theme
476
- rather than in `style` because whether an app looks native is an
477
- app-identity decision, not a per-node one. `native={false}` is the escape
478
- hatch for the one custom-branded control — and a control naming its own
479
- colours in `style` should also say it, because a bezel the system draws
480
- ignores them.
481
-
482
- Under a native bezel, `style` sizes the box the control _sits in_ rather
483
- than the control itself. A `<Button>` or a `<Select>` keeps AppKit's own
484
- height and is centred in whatever footprint the layout gives it, so
485
- `style={{ height: 44 }}`, a `height: '100%'` in a taller parent, a
486
- `flexGrow: 1` or a taller neighbour in a stretching row leave a 22pt
487
- control in a 44pt slot instead of stretching a bezel the system draws at
488
- one size. The slack around it is not part of the control: a press up there
489
- does nothing, as it does in AppKit. Drawn controls stretch as they always
490
- have — their chrome is drawn to the box and their label is centred in it,
491
- so a stretched drawn control is merely roomy.
492
-
493
- The one thing that makes a native button taller is its own label. Squeezed
494
- narrower than its label, a `<Button>` wraps it at its words — native or
495
- drawn — and grows by the lines it gained, which on macOS is drawn with
496
- AppKit's flexible-height push bezel, the one it has for a title that wraps.
497
- A row that should never wrap its buttons wants `flexWrap: 'wrap'` on the
498
- row instead, so the buttons move to a second row rather than their labels
499
- to a second line.
342
+ `controls` (a theme token, since looking native is an app-wide decision) is
343
+ `'auto'` (bezels where the backend has them), `'native'` (ask, and hear about
344
+ it in development where there are none) or `'drawn'`. Give `native={false}`
345
+ to a control that names its own colours in `style` — a system bezel ignores
346
+ them.
347
+
348
+ Under a native bezel, `style` sizes the slot the control _sits in_: a
349
+ `<Button>` or `<Select>` keeps AppKit's height, centred, so
350
+ `height: 44`, `height: '100%'`, `flexGrow: 1` or a taller neighbour in a
351
+ stretching row leave a 22pt control in a 44pt slot, and a press in the slack
352
+ does nothing. Drawn controls stretch to the box. A `<Button>` squeezed
353
+ narrower than its label wraps it at its words and grows by the lines —
354
+ native or drawn (on macOS with AppKit's flexible-height bezel); give the row
355
+ `flexWrap: 'wrap'` to move buttons to a second row instead. Design notes:
356
+ [macos.md](macos.md#native-controls).
500
357
 
501
358
  ### The press state
502
359
 
503
- Every control here activates on the **release** — that is what a click is.
504
- So every one of them also has a distinct look while it is being _held_,
505
- because otherwise a click a user takes half a second over is half a second
506
- of a control that has visibly not heard them, and the change, when it comes,
507
- reads as the machine being slow rather than the hand being unhurried.
508
-
509
- Four states, all different: resting, hovered, held, and hovered again on the
510
- release. The held one is drawn on the press **even though the press itself
511
- does nothing** — it acknowledges the input, it does not promise the outcome.
512
- A press dragged off the control drops it, and picking the control back up
513
- restores it, so the way it looks always agrees with whether releasing there
514
- would activate anything.
515
-
516
- Nothing is needed to get this: it is what the widgets do. Writing a control
517
- of your own, `:active` is the state block for it —
518
- [styling.md](styling.md#inline-pseudo-states) — and the palette's
519
- `accentActive`/`surfaceActive`/`textMutedActive`/`dangerActive` are the
520
- colours.
360
+ Every control activates on the **release**, and looks distinct while
361
+ **held** — on the press, though the press does nothing yet — so a slow click
362
+ is visibly heard. Four states: resting, hovered, held, hovered again on
363
+ release. Dragging off the control drops the held look and coming back
364
+ restores it, so the look always says whether releasing there would activate.
365
+ For a control of your own, `:active`
366
+ ([styling.md](styling.md#inline-pseudo-states)) is the state block and
367
+ `accentActive`/`surfaceActive`/`textMutedActive`/`dangerActive` the colours.
521
368
 
522
369
  ### The change event, and `name`
523
370
 
524
371
  Every value control — `Checkbox`, `Switch`, `RadioGroup`, `Select`,
525
- `Slider` — calls `onChange(ev)` with a change event, the **same signature**
372
+ `Slider` — calls `onChange(ev)` with the **same change event**
526
373
  `<textinput>` and `<textarea>` use
527
- ([elements.md](elements.md#the-change-event)). The new value is `ev.value`:
528
-
529
- ```jsx
530
- <Checkbox checked={agreed} onChange={(ev) => setAgreed(ev.value)}>
531
- I agree
532
- </Checkbox>
533
- ```
374
+ ([elements.md](elements.md#the-change-event)); the new value is `ev.value`:
534
375
 
535
376
  ```js
536
377
  { type: 'change',
@@ -539,29 +380,22 @@ Every value control — `Checkbox`, `Switch`, `RadioGroup`, `Select`,
539
380
  name: 'agree', value: true }
540
381
  ```
541
382
 
542
- One signature is the point. It is what lets a form library's handler be
543
- passed straight to any control in the library, with no per-widget adapter:
383
+ So a form library's handler goes straight onto any control:
544
384
 
545
385
  ```jsx
546
386
  <textinput name="host" value={f.values.host} onChange={f.handleChange} />
547
387
  <Checkbox name="agree" checked={f.values.agree} onChange={f.handleChange} />
548
388
  ```
549
389
 
550
- `target` is a plain descriptor rather than a node, and that is the one place
551
- this differs from the host elements: a widget is several nodes with no single
552
- element holding its value, so there is nothing honest to point at. Its shape
553
- is what formik's `handleChange` and react-hook-form's event reader
554
- destructure — `target.type` is how they tell a checkbox from a text field,
555
- which is why it is set even though nothing in react-x11 reads it. There is no
556
- `preventDefault`: the value has already changed by the time the handler runs.
557
-
558
- **The line is `name`.** A widget that takes one is a form field and reports
559
- an event. `Tabs`, `Table` and the menus are not form fields — you would never
560
- register a tab strip with formik — and keep their plain callbacks (`Tabs`
561
- calls `onChange(id)`).
390
+ `target` is a plain descriptor, not a node (a widget has no single node
391
+ holding its value), shaped as formik's `handleChange` and react-hook-form
392
+ read it — `target.type` tells a checkbox from a text field. There is no
393
+ `preventDefault`: the value has already changed.
562
394
 
563
- `name` is otherwise inert; it exists so a form library has somewhere to put
564
- one. See [docs/ecosystem/forms.md](ecosystem/forms.md).
395
+ **The line is `name`**: a widget that takes one is a form field and reports
396
+ an event. `Tabs`, `Table` and the menus are not, and keep plain callbacks
397
+ (`Tabs` calls `onChange(id)`). `name` is otherwise inert. See
398
+ [docs/ecosystem/forms.md](ecosystem/forms.md).
565
399
 
566
400
  ### `Button`
567
401
 
@@ -574,31 +408,18 @@ one. See [docs/ecosystem/forms.md](ecosystem/forms.md).
574
408
  | `primary` | speaks in the accent: the fill when solid, the ink and border otherwise |
575
409
  | `disabled` | inert, dimmed, not focusable |
576
410
 
577
- Chrome and colour are two axes, and they compose. `variant` is how much box
578
- the button carries — `solid` a fill, `outline` a border on nothing, `ghost`
579
- neither, for the affordance that sits inside other content (a `✕` on a chip,
580
- a delete on a form row) and must not add a box to the row it lives in.
581
- `primary` is whose colours it speaks in. So a dialog footer is an `outline`
582
- beside a `primary` solid, and a toolbar's loudest icon is
583
- `primary variant="ghost"` — every combination keeps the widget states, which
584
- is the point of not hand-drawing the chrome-less ones. All variants keep the
585
- border _width_, so a mixed row lines up to the pixel.
586
-
587
- `size="small"` halves the control padding — the toolbar and inline-row
588
- metric. It is derived from the palette, so a theme that moves `paddingY`
589
- moves both sizes together, which a hand-made `style={{ height: 22 }}` would
590
- not.
591
-
592
- A label with less room than it needs wraps at its words, the lines centred
593
- under one another, and the button grows to hold them — a row too narrow
594
- for its buttons squeezes each to its longest word first, as it would any
595
- paragraph ([Native bezels](#native-bezels) has what that does to a native
596
- button).
597
-
598
- An **element** child inherits the resolved label colour — the ink is set on
599
- the button's box and `color` inherits ([styling.md](styling.md)) — so an
600
- icon+label button is correct by construction in every state, disabled
601
- included, with nothing passed to the icon:
411
+ `variant` and `primary` compose: `solid` is a fill, `outline` a border,
412
+ `ghost` neither (for a `✕` on a chip, or a delete inside a form row); a
413
+ dialog footer is `outline` beside `primary`, a toolbar's loudest icon
414
+ `primary variant="ghost"`. Every variant keeps the widget states and the
415
+ border _width_, so a mixed row lines up. `size="small"` halves the control
416
+ padding and follows the palette's `paddingY`, which a hand-set `height`
417
+ would not.
418
+
419
+ A label with too little room wraps at its words, centred, and the button
420
+ grows — a too-narrow row squeezes each button to its longest word first. An
421
+ **element** child inherits the label's colour in every state, disabled
422
+ included:
602
423
 
603
424
  ```jsx
604
425
  <Button primary disabled={busy} onPress={run}>
@@ -623,23 +444,18 @@ included, with nothing passed to the icon:
623
444
 
624
445
  ### `Radio` / `RadioGroup`
625
446
 
626
- `RadioGroup` takes `value`, `onChange(ev)`, `name`, `style` and any
627
- box props; each `Radio` takes the `value` it selects, plus `children`/`label`
628
- and `disabled` — and nothing else, so per-radio styling goes on the group. A
629
- `Radio` outside a `RadioGroup` throws rather than silently doing nothing.
630
- `name` lives on the group the way it does in HTML: the group is the field.
631
-
632
- Arrow keys move the selection through the group in **mount order**,
633
- wrapping — Up/Left back, Down/Right forward — which is how a native radio
634
- group behaves; click or Space selects the focused one.
447
+ `RadioGroup` takes `value`, `onChange(ev)`, `name`, `style` and any box
448
+ props; each `Radio` takes only its `value`, `children`/`label` and
449
+ `disabled`, so per-radio styling goes on the group. A `Radio` outside a
450
+ `RadioGroup` throws. Arrow keys move the selection in **mount order**,
451
+ wrapping (Up/Left back, Down/Right forward); click or Space selects the
452
+ focused one.
635
453
 
636
454
  ### `Switch`
637
455
 
638
- `checked`, `onChange(ev)`, `name` and `disabled`, the same semantics
639
- as `Checkbox` in a sliding pill. The thumb is absolutely positioned and
640
- animates on `left` (`transition: { left: 120 }`) because `justifyContent`
641
- would flip between the ends with nothing in between to animate — the worked
642
- example in [styling.md](styling.md#transitions).
456
+ `checked`, `onChange(ev)`, `name` and `disabled`, as `Checkbox`, in a
457
+ sliding pill (the thumb's `left` transition is the worked example in
458
+ [styling.md](styling.md#transitions)).
643
459
 
644
460
  ### `ProgressBar`
645
461
 
@@ -658,29 +474,17 @@ Determinate with a `value`, indeterminate without one.
658
474
  <ProgressBar indeterminate style={{ width: 200 }} />
659
475
  ```
660
476
 
661
- The determinate fill is expressed as flex ratios rather than a percentage
662
- width. A percentage child resolves against space that is still being
663
- measured, which fed back into the track's intrinsic width — a card with a
664
- fuller bar came out wider than one with an empty bar.
665
-
666
- `indeterminate` is the worked example of a
667
- [style loop](styling.md#loops): the block is absolutely positioned and
668
- travels on the logical `start` edge from `-40%` to `100%` of the track, so it
669
- mirrors with the text direction, is right at any width without measuring
670
- anything, and repaints the bar rather than the window. It reports
671
- `aria-busy` **with no value at all**, which is what tells a screen reader
672
- that a percentage is not coming.
673
-
674
- Under [reduced motion](system.md#usedesktopsettings--how-the-desktop-wants-an-app-to-feel)
675
- the loop never starts — that part is core's, and every loop in every app gets
676
- it — and the block parks in the middle of the track instead of at its
677
- off-screen starting point. A still bar that shows nothing would say the app
678
- had stopped; a full one would say it had finished.
477
+ The indeterminate block is a [style loop](styling.md#loops) on the logical
478
+ `start` edge, so it mirrors with the text direction and fits any width; it
479
+ reports `aria-busy` **with no value**, telling a screen reader no percentage
480
+ is coming. Under
481
+ [reduced motion](system.md#usedesktopsettings--how-the-desktop-wants-an-app-to-feel)
482
+ it does not move and parks mid-track.
679
483
 
680
484
  ## `Select`
681
485
 
682
- A dropdown whose menu is a real override-redirect `<popup>` window anchored
683
- below the trigger.
486
+ A dropdown whose menu is an override-redirect `<popup>` anchored below the
487
+ trigger.
684
488
 
685
489
  ```jsx
686
490
  import { Select } from 'react-x11';
@@ -709,11 +513,9 @@ import { Select } from 'react-x11';
709
513
  | `chevronStyle` | the chevron's style, over `textMuted` |
710
514
  | `native`, `nativeMenu` | the platform's bezel, and its menu |
711
515
 
712
- The two style slots restyle what is inside the trigger, and `style` restyles
713
- the trigger itself, so a select that has to wear somebody else's look — a
714
- toolbar's, or a web page's — is the widget restyled and keeps the menu, the
715
- keyboard, type-ahead and the focus ring. A borderless select that sits in a
716
- line of text:
516
+ `style` restyles the trigger and the two slots what is inside it, keeping the
517
+ menu, keyboard, type-ahead and focus ring. A borderless select in a line of
518
+ text:
717
519
 
718
520
  ```jsx
719
521
  <Select
@@ -732,96 +534,57 @@ line of text:
732
534
  />
733
535
  ```
734
536
 
735
- The caption's `fontSize`, as a number, is the one the chevron is sized
736
- against, since the chevron is as tall as the capitals beside it and an
737
- `<Icon>`'s size does not inherit. `chevronStyle={{ display: 'none' }}` leaves
738
- it out, for a trigger whose arrow is drawn by something else. A select with
739
- either slot is drawn, where `controls: 'auto'` would otherwise use the
740
- platform's popup bezel, whose pixels no style reaches. The menu is not
741
- restyled: it is the same surface a menu is.
742
-
743
- **The menu can be the platform's.** On macOS, with `@windowkit/appkit` 0.20
744
- or later, a select under the native bezel drops a real `NSMenu`: the chosen
745
- row placed over the trigger and checked, the menu at least the trigger's
746
- width, AppKit's tracking, type-select and VoiceOver. A drawn menu sized as
747
- one was the best a drawn widget could do; this is the menu itself.
748
- `nativeMenu` is that choice on its own: it follows the trigger by default,
749
- `true` asks for the platform's menu under a drawn trigger too — what a
750
- browser does with a `<select>` a page styled, and what `<Html>` does — set
751
- at the caption's size and in the family its style names when the system
752
- has it, and `false` keeps the drawn menu. Elsewhere, and on an older
753
- bridge, the menu is drawn whatever the prop says. The menu is the
754
- platform's to dismiss: Escape, a click outside, or the application losing
755
- focus, and the select hears a pick or nothing.
756
-
757
- Behavior: the menu opens on the **press** — Space and Enter toggle it too;
758
- Escape, focus loss, or picking closes it; the option list scrolls when taller
759
- than 220px; the trigger participates in Tab traversal.
760
-
761
- **A caption is one line.** The trigger is as wide as the form made it and the
762
- value in it can be any length, so a caption with no room left ends in a `…`
763
- rather than wrapping to a second line — which there is no height for: the
764
- trigger is one row of label, and under a native popup bezel it is AppKit's
765
- own fixed height, so the wrapped line drew outside the control. The menu is
766
- sized to its longest option rather than to the trigger, so its rows are
767
- whole; where the screen is narrower than that longest label and the sheet is
768
- clamped to it, they end in a `…` too.
769
-
770
- The menu is the **same surface a menu is**: an ARGB popup rounded at
771
- `radiusPopup` with a hairline border where the display composites, and the
772
- active option is the same pill at `radiusPopupItem`, inset from the sheet's
773
- edge. A dropdown and a menu are one kind of thing, and two shapes for it
774
- would only say that the widgets were written at different times.
775
-
776
- Opening on the press rather than the release is deliberate, and it is the
777
- one control whose answer to a press is more than a colour: a dropdown exists
778
- to be looked at, so waiting for the button to come back up before showing it
779
- wastes the whole time the button is down. It is what every desktop toolkit
780
- does. A press while the menu is up dismisses it through the popup's pointer
781
- grab, so the two never fight over the toggle.
782
-
783
- Keyboard, while the trigger is focused (the popup is override-redirect and
784
- never takes focus, so the trigger keeps handling keys with the menu open):
785
-
786
- | key | closed | open |
787
- | ----------------- | -------------- | --------------------------------------- |
788
- | `Down` / `Up` | opens the menu | move the active option, wrapping around |
789
- | `Home` / `End` | — | first / last option |
790
- | `Enter` / `Space` | opens the menu | pick the active option |
791
- | `Escape` | — | close without picking |
792
-
793
- The menu opens with the current value active, hovering an option makes it
794
- active (pointer and keyboard share one highlight), and the active option is
795
- scrolled into view.
796
-
797
- **PageUp/PageDown** move by a menu viewport (`MAX_MENU_HEIGHT / ITEM_HEIGHT`
798
- options), clamping at the ends rather than wrapping the way the arrows do.
799
-
800
- **Losing the window** closes it too. That is not the same event as the
801
- trigger's own blur: a window losing the window manager's focus leaves the
802
- node inside it focused and merely stops it looking active — so without this
803
- the menu would still be up over an application you have switched away from,
804
- holding the pointer grab it opened with. The menus do the same.
805
-
806
- **Type-ahead.** Typing letters jumps to the matching option: with the menu
807
- open it moves the highlight, and with it closed it changes the value
808
- outright, the way a native select does. Keystrokes within 700ms accumulate
809
- into one query (`b`,`l` finds _blueberry_, not _banana_); repeating a
810
- single letter cycles through the options starting with it.
537
+ The chevron is sized against the caption's numeric `fontSize`;
538
+ `chevronStyle={{ display: 'none' }}` drops it. A select with either slot is
539
+ drawn even under `controls: 'auto'`. The menu itself is not restyled.
540
+
541
+ **The menu can be the platform's.** On macOS with `@windowkit/appkit` 0.20 or
542
+ later, a select under the native bezel opens a real `NSMenu`: the chosen row
543
+ over the trigger and checked, at least the trigger's width, with AppKit's
544
+ tracking, type-select and VoiceOver. `nativeMenu` makes that choice alone: it
545
+ follows the trigger by default; `true` uses the platform menu under a drawn
546
+ trigger too (as a browser does for a styled `<select>`, and as `<Html>`
547
+ does), at the caption's size and family when the system has it; `false`
548
+ keeps the drawn menu. Elsewhere, or on an older bridge, the menu is drawn. A
549
+ platform menu dismisses itself (Escape, a click outside, the app losing
550
+ focus), and the select hears a pick or nothing.
551
+
552
+ **Behaviour.** The menu opens on the **press**, as in every desktop toolkit;
553
+ a press while it is up closes it. Space and Enter toggle it; Escape, focus
554
+ loss, losing the window (switching applications) or picking close it. The
555
+ list scrolls past 220px, is sized to its longest option (rows end in `…`
556
+ only on a screen narrower than that), and is drawn as a menu is. The
557
+ trigger is a Tab stop, and a caption with no room ends in `…` rather than
558
+ wrapping.
559
+
560
+ Keys go to the focused trigger, menu open or not:
561
+
562
+ | key | closed | open |
563
+ | --------------------- | -------------- | --------------------------------------- |
564
+ | `Down` / `Up` | opens the menu | move the active option, wrapping around |
565
+ | `Home` / `End` | — | first / last option |
566
+ | `PageUp` / `PageDown` | — | a menu's height of options, clamped |
567
+ | `Enter` / `Space` | opens the menu | pick the active option |
568
+ | `Escape` | — | close without picking |
569
+
570
+ The menu opens with the current value active; hovering an option makes it
571
+ active (one highlight for pointer and keyboard), and it is scrolled into view.
572
+ **Type-ahead**: letters typed within 700ms form one query (`b`,`l` finds
573
+ _blueberry_, not _banana_), and a repeated single letter cycles; with the
574
+ menu open this moves the highlight, closed it changes the value, as a native
575
+ select does.
811
576
 
812
577
  ### Theming
813
578
 
814
- `Select` has no provider of its own — it reads the palette from
815
- [`ThemeProvider`](#theming) like every other widget. The trigger is
816
- `background`/`text` in a `border` box that turns `borderFocus` on focus or
817
- while open, the chevron and the placeholder text are `textMuted`, and the menu
818
- highlight is `hoverBackground`/`hoverText`.
579
+ `Select` reads the palette from [`ThemeProvider`](#theming) like every
580
+ widget: the trigger is `background`/`text` in a `border` box that turns
581
+ `borderFocus` when focused or open; chevron and placeholder are `textMuted`;
582
+ the menu highlight is `hoverBackground`/`hoverText`.
819
583
 
820
584
  ## `Calendar` / `DatePicker`
821
585
 
822
- Moved to [`@react-x11/components`](ecosystem.md). A date picker is a locale
823
- problem before it is a widget, and the calendar systems, holiday sets and
824
- range semantics people need do not belong in a renderer.
586
+ Moved to [`@react-x11/components`](ecosystem.md): a date picker is a locale
587
+ problem before it is a widget.
825
588
 
826
589
  ## `PasswordInput`
827
590
 
@@ -849,81 +612,35 @@ A masked field whose mask is a **scribble**, not a row of bullets.
849
612
 
850
613
  ### Why a scribble
851
614
 
852
- Bullets answer the wrong question. They report **how many characters** have
853
- been typed — countably, from across the room — and they report almost nothing
854
- about the keystroke that just landed, because one more identical dot at the
855
- end of a row of identical dots is the least visible change a field could
856
- make. The feedback is weakest exactly where a password field needs it, and
857
- strongest exactly where it should not be.
858
-
859
- So the mask is a single stroke through points chosen by a generator seeded
860
- from the window id and a hash of the value. Every keystroke reseeds it, so
861
- **the whole curve moves on every character** — feedback you cannot miss — and
862
- nothing in the shape is per-character: the pen visits a number of points taken
863
- from the mask's _width_, where a character is worth about half a point, so no
864
- part of the stroke can be matched to anything typed.
865
-
866
- It is a **scribble rather than a waveform**, and that is a decision rather
867
- than a look. A stroke whose `x` only ever increases is the plot of a
868
- function, and the eye reads it as one — value against position, meaning in
869
- the peaks — however wild the `y` is. So the points are laid out one per
870
- column, which is what makes the stroke cover the width it was given, and then
871
- visited **out of order**: the pen doubles back, crosses what it has already
872
- drawn, and leaves loops. The shuffle is local rather than global, because in
873
- a box seventeen pixels tall a jump right across it is a long shallow scratch,
874
- and a maskful of those is not a scribble either.
875
-
876
- What does grow is the width, because a mask that did not would say nothing
877
- about progress. Each position contributes an advance drawn from a **second,
878
- window-seeded** stream, so the width only ever grows as you type, never
879
- twitches when a character is replaced, and is not a clean multiple of
880
- anything a glance could divide.
881
-
882
- The honest limit, since a mask that oversells itself is worse than one that
883
- does not: **this hides a glance, not a recording.** Someone watching the
884
- field grow keystroke by keystroke still counts the keystrokes, and a long
885
- password still sits in a visibly different bracket from a short one.
615
+ Bullets show how many characters were typed and barely show the keystroke
616
+ that just landed. The scribble is the reverse: **the whole stroke changes on
617
+ every character**, nothing in it is per-character, and its width only grows
618
+ (never twitching when a character is replaced, never a clean multiple of
619
+ anything). **It hides a glance, not a recording**: someone watching it grow
620
+ still counts keystrokes. Design notes:
621
+ [design/components.md](../design/components.md#why-a-scribble).
886
622
 
887
623
  ### Two states, and what holds across both
888
624
 
889
- **Masked**, editing is smaller than `<textinput>`'s, because a scribble has
890
- nowhere to put a caret: type, Backspace, Ctrl+Backspace / Ctrl+U / Delete to
891
- clear, Ctrl+V or Shift+Insert to paste, Enter to submit. No caret, no
892
- selection, no undo history — a rewindable secret is not a feature.
893
-
894
- **Revealed**, it is an ordinary text input, because that is what it looks
895
- like and anything else would be a trap. A real `<textinput>` takes the mask's
896
- place: caret, selection, arrow keys, a click into the middle of the word,
897
- undo, the edit menu. Focus follows the swap in both directions, and the
898
- reveal ends when the keyboard leaves the widget — not when it moves between
899
- the field and the input inside it.
900
-
901
- What holds in **both** states is that nothing leaves by a selection. Ctrl+C
902
- and Ctrl+X do nothing, the revealed input carries
903
- [`sensitive`](elements.md#sensitive) so its menu has no Cut or Copy, and
904
- neither state ever takes PRIMARY, so a middle click in another window cannot
905
- spend it. The line is between what is on screen and what is on the clipboard:
906
- the first stops being visible when the field is hidden, the second is
907
- readable by every client on the display until something else takes it.
908
-
909
- While the value is masked it is **never laid out and never drawn**: the mask
910
- is measured from one reference character, so the secret does not enter ntk's
911
- shaping cache and its glyphs never reach the X server. Revealing it costs
912
- what revealing it costs. The eye is a pointer affordance and not a tab stop,
913
- as GTK's peek icon is; `revealed` + `onRevealChange` put the toggle wherever
914
- your keyboard can reach it.
915
-
916
- Caps Lock is reported while it is on, from the modifier state on the keys as
917
- they arrive — the mistake that a masked field otherwise lets you make four
918
- times before telling you.
625
+ **Masked**: type, Backspace, Ctrl+Backspace / Ctrl+U / Delete to clear,
626
+ Ctrl+V or Shift+Insert to paste, Enter to submit. No caret, no selection, no
627
+ undo history. **Revealed**: a real `<textinput>` takes its place — caret,
628
+ selection, arrows, clicks, undo, the edit menu. Focus follows the swap both
629
+ ways, and the reveal ends when the keyboard leaves the widget.
630
+
631
+ In **both**, nothing leaves by a selection: Ctrl+C and Ctrl+X do nothing, the
632
+ revealed input is [`sensitive`](elements.md#sensitive) (no Cut or Copy in its
633
+ menu), and neither state takes PRIMARY. While masked the value is **never
634
+ laid out or drawn**, so its glyphs never reach the display server. The eye is
635
+ a pointer affordance, not a tab stop; `revealed` + `onRevealChange` put the
636
+ toggle where the keyboard can reach it. Caps Lock is reported while it is on.
919
637
 
920
638
  ### Drawing your own mask
921
639
 
922
- `drawMask(ctx, info)` replaces the scribble entirely; `info` is
923
- `{ width, height, seed, color, length }`, where `width` is the mask width the
924
- field worked out and `seed` is the value-and-window seed. The scribble itself
925
- is `strokeScribble` in `src/components/scribble.js`, which is worth reading
926
- before replacing it — the reasoning for each number is there.
640
+ `drawMask(ctx, info)` replaces the scribble; `info` is
641
+ `{ width, height, seed, color, length }`, with the mask width the field
642
+ worked out and the value-and-window seed. The scribble is `strokeScribble` in
643
+ `src/components/scribble.js`, with the reasoning for each number.
927
644
 
928
645
  ```jsx
929
646
  <PasswordInput
@@ -936,12 +653,10 @@ before replacing it — the reasoning for each number is there.
936
653
 
937
654
  ### Password managers
938
655
 
939
- Paste and typing are the two seams that reach a field on this desktop, and
940
- both work here: XTEST auto-type arrives as ordinary key events, and Ctrl+V
941
- takes a secret from the clipboard with control characters stripped so a
942
- manager's trailing newline stays out of it. Which manager does what, what the
943
- window title has to do with it, and the seams that are _not_ a field —
944
- Secret Service, the sandbox portal, AT-SPI — are in
656
+ XTEST auto-type arrives as ordinary key events, and Ctrl+V pastes with
657
+ control characters stripped (so a manager's trailing newline stays out).
658
+ Which manager does what, the window title's part, and Secret Service, the
659
+ portal and AT-SPI:
945
660
  [desktop.md](desktop.md#password-fields-and-password-managers).
946
661
 
947
662
  ## `Slider`
@@ -972,9 +687,8 @@ import { Slider } from 'react-x11';
972
687
  | `trackStyle` | the track's style, over `height` and `track` |
973
688
  | `fillStyle` | the filled part of the track, over `accent` |
974
689
 
975
- The three style slots restyle the drawn parts without giving up what the
976
- widget does — the drag, the keyboard, the `aria-*` values, RTL and the target
977
- size. The flat slider most design systems draw is:
690
+ The style slots restyle the parts and keep the drag, keyboard, `aria-*`
691
+ values, RTL and target size. A flat slider:
978
692
 
979
693
  ```jsx
980
694
  <Slider
@@ -992,22 +706,15 @@ size. The flat slider most design systems draw is:
992
706
  />
993
707
  ```
994
708
 
995
- The thumb's `width` and `height`, as numbers, are the sizes the widget works
996
- with: the drag's travel is the track less one thumb, and the control is as tall
997
- as the taller of thumb and track, with its target still grown to 24. A
998
- `marginStart` and `marginEnd` of half a thumb on the track are what let the
999
- thumb overhang its ends. A slider with any slot is drawn, where `controls: 'auto'` would
1000
- otherwise use the platform's bezel, whose pixels no style reaches.
1001
-
1002
- Dragging uses [pointer capture](events.md#pointer-capture): the press
1003
- captures, so the thumb keeps following a pointer that has wandered far
1004
- outside the widget, and releasing out there still ends the drag.
1005
-
1006
- Keyboard: arrows step, `Home`/`End` jump to the ends, `PageUp`/`PageDown`
1007
- move by ten steps.
709
+ The thumb is centred on its value, so the travel is the track less one thumb
710
+ (numeric `thumbStyle` `width`/`height` are what the widget uses); the control
711
+ is as tall as the taller of thumb and track, its target at least 24. Half a
712
+ thumb of `marginStart`/`marginEnd` on the track lets the thumb overhang the
713
+ ends. A slider with any slot is drawn even under `controls: 'auto'`.
1008
714
 
1009
- The thumb is centred on its value, so the usable travel is the track width
1010
- minus one thumb width — otherwise `min` and `max` would be unreachable.
715
+ Dragging uses [pointer capture](events.md#pointer-capture), so it follows
716
+ the pointer anywhere and ends wherever it is released. Keyboard: arrows
717
+ step, `Home`/`End` jump to the ends, `PageUp`/`PageDown` move ten steps.
1011
718
 
1012
719
  ## `Tooltip`
1013
720
 
@@ -1030,36 +737,20 @@ import { Tooltip } from 'react-x11';
1030
737
  | `width`, `height` | pin an axis of the bubble exactly; anything not pinned is measured |
1031
738
  | `maxWidth`, `maxHeight` | cap what an element label measures out to, inside the screen's own cap |
1032
739
 
1033
- Wraps its children in a row box carrying the hover handlers and the anchor
1034
- ref, so it composes around any element. Hides immediately on leave and on
1035
- mousedown — a tooltip lingering over the menu you just opened is the
1036
- classic annoyance.
1037
-
1038
- **One at a time**, per connection: a hint belongs to where the pointer is,
1039
- and there is one pointer. A trigger taking the hover dismisses whatever is
1040
- showing straight away rather than at the end of its own delay, so two are
1041
- never up together saying different things about the same place. This does
1042
- not fight the safe-polygon grace that lets a hint with content in it be
1043
- reached — a trigger underneath an open hint cannot be hovered, the popup
1044
- being a window above it.
1045
-
1046
- `direction` is which side of the trigger it opens on. The default, `'auto'`,
1047
- takes the first side the hint fits on, preferring above — measured against
1048
- the **screen**, because a popup is a real X window and the screen is what
1049
- bounds it. A named side is a preference rather than a promise either way:
1050
- it still flips to its opposite instead of opening off-screen. (`placement`
1051
- is the older name for the same prop and still wins where it is given.)
1052
-
1053
- A **string** `label` is measured and the popup sized around it, because a
1054
- `<popup>` is a real X window and needs its size before layout. An
1055
- **element** label is measured too — nothing to size, nothing to guess: the
1056
- popup is rendered once [`hidden`](elements.md#hidden--realized-laid-out-not-on-screen)
1057
- at its natural size (a real layout of the real content, capped at the screen
1058
- and at `maxWidth`/`maxHeight`), the size is read back, and the same
1059
- placement math runs before anything is mapped. Both commits land in the same
1060
- task and the popup maps last, so it is only ever on screen placed and at its
1061
- final size. The element gets the bubble to fill, with no padding imposed on
1062
- it:
740
+ It wraps its children in a row box carrying the hover handlers and anchor ref,
741
+ so it composes around any element, and hides at once on leave and on
742
+ mousedown. **One at a time** per connection: a trigger taking the hover
743
+ dismisses any showing hint immediately. `direction` is a preference: `'auto'`
744
+ takes the first side that fits on the **screen**, preferring above, and a
745
+ named side still flips rather than open off-screen. (`placement` is the older
746
+ name and still wins where given.)
747
+
748
+ An **element** `label` is measured like a string one: rendered
749
+ [`hidden`](elements.md#hidden--realized-laid-out-not-on-screen) at its
750
+ natural size (capped by the screen and `maxWidth`/`maxHeight`) and placed
751
+ before it maps, so it never appears misplaced. It fills the bubble with no
752
+ padding imposed; anything renders there, widgets included
753
+ (`examples/tooltips.jsx`):
1063
754
 
1064
755
  ```jsx
1065
756
  <Tooltip
@@ -1076,40 +767,25 @@ it:
1076
767
  </Tooltip>
1077
768
  ```
1078
769
 
1079
- `width`/`height` still pin an axis exactly: give both and the measuring pass
1080
- is skipped entirely (the guaranteed-fit mode element labels used to
1081
- require); give one and the other is measured for it, so `width={340}` is a
1082
- fixed column whose height fits the message. The size is settled at open —
1083
- content that changes size while the hint is up keeps the bubble it opened
1084
- with until the next open, which is a hint's lifetime away.
1085
-
1086
- Anything renders in there, widgets included — it is a real tree in a real
1087
- window, not a rich-text label. `examples/tooltips.jsx` has both kinds.
1088
-
1089
- **The palette inside the bubble is upside down.** A tooltip is drawn in the
1090
- palette's ink so that it reads as a label over the desktop rather than as
1091
- another panel of the app — so inside it, `$background` is the bubble and
1092
- `$text` is the ink on it, published through a `ThemeProvider` so that
1093
- `useTheme()` agrees. Write the content the way you would write anything
1094
- else and it is legible in both schemes; hard-code a light text colour and
1095
- it will be invisible on the light bubble a dark palette gives it. `textMuted`
1096
- is derived for the same reason — the palette's own is a grey chosen against
1097
- the app's background, not against this one — and `$surface` is the bubble
1098
- too, so a card written for the app does not light up inside a tooltip.
1099
-
1100
- Where the display composites, the popup is an ARGB window: a bubble rounded
1101
- at `radiusTooltip`, no border, and a small arrow pointing back at the middle
1102
- of the trigger with everything neither covers left transparent. Without a
1103
- compositor it is the square opaque rectangle it has always been and there is
1104
- no arrow — those pixels would be black rather than empty.
770
+ `width` and `height` together skip measuring; one alone fixes that axis and
771
+ measures the other (`width={340}` is a column whose height fits). The size is
772
+ settled at open and kept until the next.
773
+
774
+ **The palette inside is inverted**: the bubble is drawn in the palette's ink,
775
+ so inside it `$background` (and `$surface`) is the bubble and `$text` the ink,
776
+ published through a `ThemeProvider` so `useTheme()` agrees, with `textMuted`
777
+ derived to match. Write content with tokens and it reads in both schemes; a
778
+ hard-coded light text colour vanishes on the light bubble of a dark palette.
779
+ Where the display composites the bubble is rounded at `radiusTooltip`, with
780
+ no border and a small arrow at the trigger's middle; elsewhere it is a
781
+ square rectangle with no arrow.
1105
782
 
1106
783
  ## `Dialog`
1107
784
 
1108
- A dialog in a `<popup>`, centred over the owner window — and one the
1109
- **window manager knows is a dialog**: it is a managed window with
1110
- `WM_TRANSIENT_FOR` pointing at its owner, so it is framed, movable, closable
1111
- through the WM, out of the taskbar and alt-tab list, and (on a full EWMH
1112
- window manager) stacked above its owner and iconified with it.
785
+ A `<popup>` centred over its owner that the **window manager knows is a
786
+ dialog**: managed and transient for its owner (`WM_TRANSIENT_FOR` on X11) —
787
+ framed, movable, closable through the WM, out of the taskbar and alt-tab, and
788
+ on a full EWMH window manager stacked above its owner and iconified with it.
1113
789
 
1114
790
  ```jsx
1115
791
  import { Button, Dialog } from 'react-x11';
@@ -1129,100 +805,65 @@ import { Button, Dialog } from 'react-x11';
1129
805
  </Dialog>;
1130
806
  ```
1131
807
 
1132
- | prop | |
1133
- | ---------- | ---------------------------------------------------------------------------------------------------- |
1134
- | `open` | renders nothing when false; the popup exists only while true |
1135
- | `title` | names the dialog, drawn once: the frame's caption when managed, a bold in-content heading when not |
1136
- | `children` | body content; strings become `<text>` |
1137
- | `actions` | elements for the right-aligned button row |
1138
- | `onClose` | Escape, or the window manager's close button |
1139
- | `managed` | `false` for the override-redirect popup 1.x shipped (below) |
1140
- | `width` | popup width (default 360); `'auto'` for the content's |
1141
- | `height` | popup height (default `'auto'`: the content's, capped at the monitor — [below](#size-and-placement)) |
1142
-
1143
- The focus behaviour is the **renderer's**, not the component's: `trapFocus`
1144
- keeps Tab inside the dialog, stops presses elsewhere from moving focus, and
1145
- hands focus back to whatever had it — usually the button that opened the
1146
- dialog — when it closes. See
1147
- [Focus scopes](events.md#focus-scopes-modals). Put `autoFocus` on a control
1148
- inside to pick the first stop; with nothing to focus, the dialog surface
1149
- takes focus itself (`tabIndex={-1}`) so Escape and Tab work immediately.
1150
-
1151
- Escape closes because keys go to the focused node inside the popup and
1152
- bubble out through the popup's place in the JSX tree; so does the window
1153
- manager's close button, through `onCloseRequest`. **Pointer modality is not
1154
- enforced** — widgets in the owner window stay clickable behind the dialog —
1155
- so it is for confirmations, not for guarding state. (`_NET_WM_STATE_MODAL`
1156
- is the mechanism that would enforce it, and it means nothing without the
1157
- `WM_TRANSIENT_FOR` this now sets; that is why the property had to come
1158
- first.)
808
+ | prop | |
809
+ | ---------- | ------------------------------------------------------------------- |
810
+ | `open` | renders nothing when false; the popup exists only while true |
811
+ | `title` | names the dialog; drawn once ([below](#managed)) |
812
+ | `children` | body content; strings become `<text>` |
813
+ | `actions` | elements for the right-aligned button row |
814
+ | `onClose` | Escape, or the window manager's close button |
815
+ | `managed` | `false` for an unframed override-redirect popup ([below](#managed)) |
816
+ | `width` | popup width (default 360); `'auto'` for the content's |
817
+ | `height` | default `'auto'`, the content's ([below](#size-and-placement)) |
818
+
819
+ Focus is the renderer's `trapFocus` ([Focus scopes](events.md#focus-scopes-modals)):
820
+ Tab stays inside, presses elsewhere do not move focus, and focus returns to
821
+ whatever had it (usually the opening button) on close. `autoFocus` picks the
822
+ first stop; with nothing focusable the dialog surface takes focus
823
+ (`tabIndex={-1}`), so Escape and Tab work at once. **Pointer modality is not
824
+ enforced** — the owner stays clickable — so it is for confirmations, not
825
+ guarding state.
1159
826
 
1160
827
  ### `managed`
1161
828
 
1162
- | `managed` | what you get |
1163
- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
1164
- | `true` (default) | a WM-managed window: frame, titlebar, movable, WM close button, transient for its owner, out of the taskbar. A press outside does **not** close it. |
1165
- | `false` | the override-redirect popup: no frame, not movable, a pointer grab, and a press anywhere outside calls `onClose`. |
1166
-
1167
- The `title` follows the frame: managed, it is the frame's caption and the
1168
- content starts with the body; unmanaged there is no frame, so the title is
1169
- drawn as a bold heading at the top of the content instead. It is never drawn
1170
- twice, and it names the dialog for accessibility either way.
1171
-
1172
- They are one choice, not two: a client-side pointer grab over a window the
1173
- window manager is trying to let the user drag swallows the press that would
1174
- start the drag, so `grab` and the frame turn on and off together. A managed
1175
- dialog staying open when you click elsewhere is not a regression — it is what
1176
- a dialog does. `managed={false}` is the right shape for a transient
1177
- confirmation on a display with no window manager at all.
1178
-
1179
- The owner is resolved automatically: `Dialog` already keeps an out-of-flow
1180
- `<box>` inside the owner window for placement, and a ref to any drawn node
1181
- resolves to the window that owns it
829
+ | `managed` | what you get |
830
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------- |
831
+ | `true` (default) | frame, titlebar (the `title`), movable, WM close button, transient for its owner; a press outside does not close it |
832
+ | `false` | no frame, not movable, a pointer grab, the `title` as a bold heading; a press outside calls `onClose` |
833
+
834
+ The frame and the grab go together (a grab would swallow the press that
835
+ starts a window drag). `managed={false}` suits a display with no window
836
+ manager. Either way the `title` names the dialog for accessibility, and the
837
+ owner is found automatically
1182
838
  ([`transientFor`](elements.md#transientfor--a-window-that-belongs-to-another)).
1183
839
 
1184
840
  ### Size and placement
1185
841
 
1186
- **The height is the content's** by default (`height="auto"`): ten lines and
1187
- a row of buttons make a dialog as tall as ten lines and a row of buttons. The
1188
- popup measures its content, centres itself over the owner window at that
1189
- size and only then opens — it is born the right size in the right place,
1190
- never shown at a guess and corrected
842
+ **The height is the content's** (`height="auto"`): the dialog measures its
843
+ content, centres over the owner at that size, and only then opens
1191
844
  ([`placement: 'center'`](elements.md#centred-over-something)).
1192
845
 
1193
- - **Capped at the monitor.** Content taller than the usable height of the
1194
- monitor caps the dialog there; the body scrolls, and the `actions` row stays
1195
- pinned at the bottom of the window. The body is a tab stop only while it has
1196
- something to scroll. A numeric `height` scrolls the same way when the
1197
- content needs more than it, where the overflow, buttons included, used to
1198
- be cut off at the bottom edge (#940).
1199
- - **Following its content.** A dialog whose content changes while it is open
1200
- re-fits to it, growing from its **top edge**: the title and the field the
1201
- user is looking at stay where they are, and only the bottom moves. It moves
1202
- up only as far as it takes to stay on the monitor. A managed dialog the user
1203
- dragged aside grows where they put it.
1204
- - **Centred once,** over where the owner window was when the dialog opened,
1205
- and not tracked after that: moving the owner does not drag the dialog with
1206
- it, which is the window manager's call on a managed one.
1207
-
1208
- `width` stays a number by default, 360: a body of prose wraps to it, where a
1209
- natural width would be as wide as its longest sentence. `width="auto"` is
1210
- there for a dialog whose content has a width of its own (a grid of fields, an
1211
- image). A number for either axis is that size exactly.
1212
-
1213
- `centerRect(node, {width, height})` is the same centring as a function,
1214
- exported alongside `anchorRect` for window-centred popups of your own that
1215
- know their size; one that does not uses `<popup anchor={{ rect, placement:
1216
- 'center' }}>` and lets the window work it out.
846
+ - **Capped at the monitor**: taller content scrolls in the body while the
847
+ `actions` row stays pinned; the body is a tab stop only while it can
848
+ scroll. A numeric `height` scrolls the same way.
849
+ - **Following its content**: content that changes while open re-fits the
850
+ dialog from its **top edge**, moving it up only to stay on the monitor; a
851
+ managed dialog the user dragged grows where they put it.
852
+ - **Centred once**, over where the owner was at open; moving the owner does
853
+ not move it.
854
+
855
+ `width` defaults to 360 so prose wraps; `width="auto"` is for content with a
856
+ width of its own (a grid of fields, an image). A number is exact on either
857
+ axis. `centerRect(node, {width, height})` is the same centring as a function,
858
+ for popups of your own that know their size; one that does not uses
859
+ `<popup anchor={{ rect, placement: 'center' }}>`.
1217
860
 
1218
861
  ## `MenuBar` / `ContextMenu`
1219
862
 
1220
- Pull-down and right-click menus, both rendered in `<popup>` windows so they
1221
- escape the owner window, both anchored with `useAnchor`.
1222
-
1223
- On the Cocoa backend with native controls, the popups are sized as NSMenu
1224
- sizes its own — 22pt rows, the 13pt menu font — so a context menu sits
1225
- beside the system's like one of them ([macos.md](macos.md#menus-the-global-menu-finally-at-home)).
863
+ Pull-down and right-click menus in `<popup>` windows, anchored with
864
+ `useAnchor`. On the Cocoa backend with native controls they are sized as
865
+ `NSMenu` sizes its own — 22pt rows, the 13pt menu font
866
+ ([macos.md](macos.md#menus-the-global-menu-finally-at-home)).
1226
867
 
1227
868
  ```jsx
1228
869
  import { MenuBar, ContextMenu } from 'react-x11';
@@ -1253,16 +894,28 @@ import { MenuBar, ContextMenu } from 'react-x11';
1253
894
 
1254
895
  Item shape: `{ label, onSelect, type, items, enabled, visible, shortcut,
1255
896
  toggleType, toggleState, icon, iconName, disposition, key }`. Both take an
1256
- `onSelect(item)` prop as well, fired after the item's own. A bar menu opens on
1257
- the **press**, for the reason `Select` does.
897
+ `onSelect(item)` prop too, fired after the item's own. A bar menu opens on
898
+ the **press**, like `Select`.
899
+
900
+ **The vocabulary is `com.canonical.dbusmenu`'s** — `type: 'separator'`,
901
+ `enabled: false`, `toggleType`/`toggleState` (`0`, `1`, or `-1` for
902
+ indeterminate), `shortcut` as modifier tokens — because on a desktop whose
903
+ panel shows application menus, `MenuBar` hands the array over and stops
904
+ drawing, with the same JSX ([globalmenu.md](globalmenu.md), also for
905
+ `globalMenu={false}`).
906
+
907
+ **`shortcut` is a binding, not a label**: `[['Control', 'S']]` makes Ctrl+S
908
+ fire the item's `onSelect` while the menu is mounted, unopened, gated by its
909
+ `enabled` and `visible`, and still when a panel has taken the menu over. The
910
+ same array is the drawn shortcut, the dbusmenu payload and the
911
+ `aria-keyshortcuts` announcement. Chord rules, key names, `useAccelerator`
912
+ and `accelerators={false}`: [events.md](events.md#accelerators).
1258
913
 
1259
914
  **A context menu's items can come from the right-click.** The click records
1260
- only where it happened; what the menu shows, how big it is and where it fits
1261
- are taken from the `items` of the render that follows. So a row that says
1262
- which thing was right-clicked in its own `onContextMenu`, with the items
1263
- derived from that, gets a menu for that thing, sized for it, on the first
1264
- right-click. Items still empty in that render open nothing: no popup, no
1265
- pointer grab, and the focus left where the press put it.
915
+ only where it happened; the menu's content, size and placement come from the
916
+ `items` of the next render, so items derived in a row's `onContextMenu` give
917
+ a menu for that row on the first right-click. Items still empty open nothing
918
+ — no popup, no grab, focus left where the press put it.
1266
919
 
1267
920
  ```jsx
1268
921
  const [target, setTarget] = useState(null);
@@ -1276,87 +929,23 @@ const [target, setTarget] = useState(null);
1276
929
  </ContextMenu>;
1277
930
  ```
1278
931
 
1279
- **`shortcut` is a binding, not a label.** It is a list of alternatives, each a
1280
- list of modifier tokens ending in the key — `[['Control', 'S']]` — and a
1281
- mounted menu answers it: Ctrl+S fires that item's `onSelect` without the menu
1282
- being opened, gated by the item's own `enabled` and `visible`, and it keeps
1283
- working when the desktop's panel has taken the menu over. One array is then
1284
- the drawn menu, the exported dbusmenu payload, the `aria-keyshortcuts`
1285
- announcement and the binding, so there is nothing to keep in step by hand. The
1286
- chord rules, the vocabulary the key tokens come from, `useAccelerator` for a
1287
- shortcut that is not in a menu, and `accelerators={false}` for an app with its
1288
- own dispatcher are all in [events.md](events.md#accelerators).
1289
-
1290
- **The vocabulary is `com.canonical.dbusmenu`'s**, and that is not incidental.
1291
- On a desktop whose panel shows application menus, `MenuBar` hands this array
1292
- straight over and stops drawing — no configuration, no second authoring model,
1293
- the same line of JSX either way. `type: 'separator'` rather than
1294
- `separator: true`, `enabled: false` rather than `disabled: true`,
1295
- `toggleType`/`toggleState` rather than a bare `checked`, and `shortcut` as a
1296
- list of modifier tokens rather than a display string, all follow from that.
1297
- `toggleState` is `0`, `1` or `-1`, the last being indeterminate. See
1298
- [globalmenu.md](globalmenu.md), which also covers `globalMenu={false}`.
1299
-
1300
- **The sheet.** Where the display composites, a menu is an ARGB window
1301
- rounded at `radiusPopup` with a hairline border, and the highlight on a row
1302
- is a pill: inset from the popup's edge by the list's own padding and rounded
1303
- a step tighter (`radiusPopupItem`), so it reads as sitting _on_ the sheet
1304
- rather than cutting across it. The fill is `hoverBackground` — the palette's
1305
- selection colour, shared with `Select`'s active option and `Table`'s current
1306
- row, so one token moves every highlight in the app. Without a compositor
1307
- both go square, which is what those pixels can honestly be.
1308
-
1309
- **One selection at a time.** The bar item, the row under it and the row
1310
- under that are one trail, and only its deepest level wears the selection
1311
- colour; everything behind is drawn in `surfaceActive`.
1312
- They are still chosen — they are the way back — but two selection-coloured
1313
- rows in two menus claim the same thing twice, and only one of them is where
1314
- the keys are going. A submenu that is open with _nothing_ selected in it has
1315
- not taken over yet: the pointer is still on the row that opened it, so that
1316
- row stays lit — which is why a menu bar item stays coloured until you touch
1317
- a row in the menu it opened, and goes quiet then.
1318
-
1319
- The bar item wears the same pill as the rows, at `radiusPopupItem` — the same
1320
- padding around the same capitals, so a title and the first row of the menu it
1321
- opens are one shape in one size, inset into the bar by a few pixels of margin
1322
- that sit outside that padding.
1323
-
1324
- **Titles that do not fit.** A bar narrower than its menus paints as many as
1325
- fit and moves the rest behind a chevron at the end of it — the system set's
1326
- `moreVertical`, drawn rather than a `»`, so it is a mark and not a font's
1327
- opinion of one. The chevron is an ordinary bar entry whose items are the
1328
- menus that were cut, so each row opens the menu it stands for as a submenu,
1329
- and it is a real stop for Left/Right: **the keyboard walks exactly what is
1330
- painted**, never a title laid out past the window's edge. Widen the window
1331
- and the titles come back one at a time. The bar measures itself, so the
1332
- first frame of a new bar always shows everything and the cut lands on the
1333
- next — and none of this happens on a desktop whose panel has taken the menu,
1334
- where there is no bar of ours to overflow.
1335
-
1336
- **Icons.** `icon` fills the column left of the label — the same column a
1337
- `toggleType` mark uses, so an item that is both checked and iconned shows
1338
- the check: the check is state, the icon is only identity. `icon` is drawn by
1339
- react-x11 and never crosses the bus; `iconName` is the icon-theme name a
1340
- panel uses ([globalmenu.md](globalmenu.md)). One column rather than
1341
- two, because a second would indent every label in the menu to reserve room
1342
- for icons most items do not have.
1343
-
1344
- The column is there for the menus that draw in it and gone from the ones
1345
- that do not. What counts is what a row actually paints: one icon, or one
1346
- toggle that is _on_, anywhere in a menu indents every label in it — and a
1347
- menu of plain commands, or of checkboxes that all happen to be off, has no
1348
- column at all and its labels sit where a label sits.
1349
-
1350
- So ticking the first item in such a menu moves its labels sideways, once.
1351
- Toolkits that reserve the column unconditionally are buying stillness in
1352
- that one frame, and paying for it with an indent on every menu that has a
1353
- toggle anywhere in it — this one makes the other trade. A menu that already
1354
- has a mark in it keeps the column whatever happens to the rest.
1355
-
1356
- Pass a **function** for anything real. It is called with the colour the
1357
- row's label is being drawn in and the size the column allows, which is what
1358
- lets one drawing follow its row into the highlight and into the disabled
1359
- grey:
932
+ **Look.** Where the display composites, a menu is rounded (`radiusPopup`,
933
+ hairline border) and the row highlight an inset pill (`radiusPopupItem`) in
934
+ `hoverBackground`, the token shared with `Select` and `Table` highlights;
935
+ without a compositor both are square. Only the deepest open level wears
936
+ `hoverBackground`; the bar item and parent rows behind it are
937
+ `surfaceActive`. A bar too narrow for its titles moves the rest behind a
938
+ `moreVertical` entry whose rows open them as submenus; the keyboard walks
939
+ only what is painted. Design notes:
940
+ [design/components.md](../design/components.md#menubar--contextmenu).
941
+
942
+ **Icons.** `icon` fills the column left of the label, shared with the
943
+ `toggleType` mark (a checked item with an icon shows the check). It is drawn
944
+ locally and never crosses the bus; `iconName` is the icon-theme name a panel
945
+ uses. The column exists only while some row paints in it, so ticking the
946
+ first item of an unticked menu shifts its labels once. Pass a **function**:
947
+ it gets the row's ink and the column size, so one drawing follows the
948
+ highlight and the disabled grey —
1360
949
 
1361
950
  ```jsx
1362
951
  const save = ({ color, size }) => (
@@ -1364,29 +953,19 @@ const save = ({ color, size }) => (
1364
953
  );
1365
954
 
1366
955
  { label: 'Save', icon: save, shortcut: [['Control', 'S']], onSelect: onSave }
1367
- ```
1368
-
1369
- Paint the SVG in `currentColor` and the renderer caches it as coverage
1370
- rather than as pixels, so recolouring per row is a composite and every
1371
- colour of the icon shares one rendered copy
1372
- ([elements.md](elements.md#svg)). A [system icon](#system-icons) is already
1373
- that, and takes the same two arguments:
1374
-
1375
- ```jsx
1376
956
  { label: 'Close', icon: (p) => <Icon name="close" {...p} />, onSelect: onClose }
1377
957
  ```
1378
958
 
1379
- A **string** icon is drawn as text, which is a one-liner — but it is only as
1380
- good as the font, and `✂` or `⏻` is an empty box on a machine without them.
1381
- An element is rendered as-is. `examples/menu.jsx` has a worked set.
1382
-
1383
- The **toggle marks** are system icons for that reason: a checked item is a
1384
- `check`, a radio one a `dot`, and the indeterminate third state a `dash`.
1385
- They used to be `✓`, `●` and `–` set in the row's font, which is the same
1386
- gamble this paragraph tells you not to take.
959
+ — an SVG painted in `currentColor` is cached once for every colour
960
+ ([elements.md](elements.md#svg)), as a [system icon](#system-icons) is. A
961
+ **string** icon is drawn as text and is only as good as the font (`✂` or `⏻`
962
+ is an empty box without one); an element renders as-is. Toggle marks are
963
+ the `check`, `dot` and `dash` icons. `examples/menu.jsx` has a worked set.
1387
964
 
1388
- **Submenus.** Give an item its own `items` and it becomes a submenu parent,
1389
- marked with a `chevronRight` and opening to the side:
965
+ **Submenus.** An item with its own `items` is a submenu parent, marked with a
966
+ `chevronRight`. Nesting is unlimited; each level is a `<popup>` opening to the
967
+ end side, its first item level with its parent row, flipping at a screen
968
+ edge.
1390
969
 
1391
970
  ```jsx
1392
971
  { label: 'Export', items: [
@@ -1395,72 +974,35 @@ marked with a `chevronRight` and opening to the side:
1395
974
  ] }
1396
975
  ```
1397
976
 
1398
- Nesting is unlimited. Each level is its own `<popup>` with `placement:
1399
- 'right'`, so it flips to the left near a screen edge like any other anchored
1400
- popup. It hangs off the **menu's** outer edge and lines up with the row that
1401
- opened it — two different nodes, which is what `anchorRect`'s `alignTo` is
1402
- for. Anchoring both to the row would put the submenu inside its parent by
1403
- the menu's border and padding, and the two would overlap.
1404
-
1405
- Lined up on the **items**, not on the boxes: a submenu whose top edge is
1406
- level with its parent row starts its first item a border and a padding
1407
- lower, and what the eye lines up is the text. `anchorRect`'s `alignOffset`
1408
- is the shift that pays for the submenu's own chrome, so the row you came
1409
- from and the row you arrive at are on one line.
1410
-
1411
- **Keyboard.** Up/Down move the active item, **skipping separators and
1412
- disabled entries** and wrapping; Home/End jump to the ends; Right opens a
1413
- submenu (selecting its first item) and Left leaves one; Enter/Space
1414
- activate — or open a submenu, for a parent row; Escape closes **one level
1415
- at a time**. In a `MenuBar`, Left/Right walk between menus _when there is
1416
- no submenu to move through_, and with one menu open, hovering another
1417
- switches to it. Hovering a submenu parent opens it with nothing selected
1418
- inside.
1419
-
1420
- **PageUp/PageDown** step ten rows and then settle on the nearest selectable
1421
- entry in the direction of travel, so a page never lands on a separator or a
1422
- disabled row. Menus size to their content rather than scrolling, so the
1423
- stride is fixed — deriving one from the menu height would just equal
1424
- Home/End.
1425
-
1426
- **Type-ahead.** Typing letters jumps to the entry whose label starts with
1427
- them, in whichever level is deepest open. Keystrokes within 700ms
1428
- accumulate into one query, so `c`,`a` finds _Carrot_ rather than jumping to
1429
- _Apple_ first; repeating a single letter cycles through the entries
1430
- starting with it. Disabled entries and separators are never matched.
1431
-
1432
- Open state is a single path of active indices — one per open level — so
1433
- moving the selection at any level truncates the path and closes deeper
1434
- levels for free.
1435
-
1436
- Both keep focus on a node in the _owner_ window — the popup is
1437
- override-redirect and never takes focus — which is the same arrangement
1438
- `Select` uses. `ContextMenu`'s wrapper is focusable for that reason and
1439
- takes focus when the menu opens.
1440
-
1441
- Switching between `MenuBar` menus reuses the same X window and moves it
1442
- rather than destroying and recreating one, so there is no flicker.
977
+ **Keyboard.** Up/Down move, **skipping separators and disabled entries**,
978
+ wrapping; Home/End jump to the ends; PageUp/PageDown step ten rows, then to
979
+ the nearest selectable one (menus size to content, never scroll); Right opens
980
+ a submenu at its first item and Left leaves it; Enter/Space activate or open;
981
+ Escape closes **one level at a time**. In a `MenuBar`, Left/Right move between
982
+ menus when there is no submenu to move through, and with one menu open,
983
+ hovering another switches to it. Hovering a parent opens its submenu with
984
+ nothing selected. **Type-ahead** works in the deepest open level: 700ms
985
+ queries (`c`,`a` finds _Carrot_), a repeated letter cycles, disabled entries
986
+ and separators never match.
987
+
988
+ Focus stays in the _owner_ window, as with `Select`; `ContextMenu`'s wrapper
989
+ is focusable and takes focus when its menu opens, but is not a Tab stop
990
+ (`tabIndex={-1}`). Losing the window closes an open menu.
1443
991
 
1444
992
  ### Safe-polygon hover
1445
993
 
1446
- Reaching a submenu means moving the pointer _diagonally_ across the rows in
1447
- between, and reaching a tooltip means leaving the trigger it belongs to — so
1448
- naive hover handling closes both just as the user aims at them. `MenuBar`,
1449
- `ContextMenu` and `Tooltip` therefore use
994
+ `MenuBar`, `ContextMenu` and `Tooltip` keep a submenu or hint open while the
995
+ pointer crosses towards it, using
1450
996
  [floating-ui's safePolygon](https://floating-ui.com/docs/usehover#safepolygon)
1451
997
  idea: the triangle between where the pointer was and the near edge of the
1452
- open surface counts as still hovering the parent.
1453
-
1454
- While the pointer is inside that triangle, hover changes are held back —
1455
- but only for `SAFE_HOVER_DELAY` (320 ms), so a pointer that stops there
1456
- still means what it landed on. Leaving the triangle switches immediately,
1457
- and reaching the surface keeps it open for as long as the pointer stays.
998
+ open surface counts as still hovering the parent. Inside it, hover changes
999
+ wait at most `SAFE_HOVER_DELAY` (320 ms); leaving it switches at once, and
1000
+ reaching the surface keeps it open.
1458
1001
 
1459
1002
  The helpers are exported for widgets of your own:
1460
1003
  `movingToward(point, apex, rect)`, `safePolygon(apex, rect, buffer)`,
1461
- `pointInPolygon(point, polygon)` and `screenPoint(ev)`. All coordinates are
1462
- **screen** coordinates, because the trigger and the popup are different X
1463
- windows.
1004
+ `pointInPolygon(point, polygon)` and `screenPoint(ev)`, all in **screen**
1005
+ coordinates, since trigger and popup are different windows.
1464
1006
 
1465
1007
  ## `Tabs`
1466
1008
 
@@ -1484,21 +1026,18 @@ One panel visible at a time, switched by a strip of tabs.
1484
1026
  | `orientation` | `'horizontal'` (default) or `'vertical'` |
1485
1027
  | `manual` | arrows move focus only; Enter or Space commits |
1486
1028
 
1487
- `content` may be a node or a function. A function is called only while that
1488
- tab is selected, which is how to avoid building a panel nobody is looking
1489
- at. Items with no `content` at all make `Tabs` a pure navigator — useful
1490
- when the panel lives elsewhere, as in `examples/app.jsx`, where the strip is
1491
- in one half of a `SplitPane` and the panel in the other.
1492
-
1493
- The strip is a **single tab stop**. Left/Right (Up/Down when vertical) move
1494
- and wrap, Home/End jump to the ends, disabled tabs are skipped. Arrows
1495
- select as they move, the way a desktop notebook behaves; `manual` splits
1496
- focus from selection, which is what you want when a panel is expensive.
1029
+ `content` may be a node or a function, called only while its tab is
1030
+ selected. Items with no `content` make `Tabs` a pure navigator (in
1031
+ `examples/app.jsx` the strip is in one `SplitPane` half, the panel in the
1032
+ other). The strip is a **single tab stop**: Left/Right (Up/Down when
1033
+ vertical) move and wrap, Home/End jump, disabled tabs are skipped. Arrows
1034
+ select as they move; `manual` splits focus from selection, for expensive
1035
+ panels.
1497
1036
 
1498
1037
  ## `Table`
1499
1038
 
1500
- A grid with a header that stays put, resizable columns, and only the rows in
1501
- view actually built.
1039
+ A grid with a fixed header, resizable columns, and only the rows in view
1040
+ built.
1502
1041
 
1503
1042
  ```jsx
1504
1043
  <Table
@@ -1522,12 +1061,8 @@ view actually built.
1522
1061
  | `onColumnResize(id, width)` | after a header drag |
1523
1062
 
1524
1063
  `value(row)` feeds sorting and the default cell text; `render(row, { selected,
1525
- column })` replaces the cell contents entirely.
1526
-
1527
- A `render` is **told when its row is selected**, because the selection is a
1528
- filled bar and a colour picked against the resting background disappears
1529
- into it — a directory in the accent, a failure in red. Fall back to
1530
- `hoverText` there and let the glyph carry the meaning:
1064
+ column })` replaces the cell. `selected` is there because a colour picked for
1065
+ the resting row can vanish into the selection bar — fall back to `hoverText`:
1531
1066
 
1532
1067
  ```jsx
1533
1068
  render: (row, { selected }) => (
@@ -1537,35 +1072,23 @@ render: (row, { selected }) => (
1537
1072
  );
1538
1073
  ```
1539
1074
 
1540
- **Rows must all be `rowHeight` tall.** That is the price of the table only
1541
- building what is on screen: with ten thousand rows it mounts the twenty or
1542
- so in the viewport and swaps them as you scroll, and everything above and
1543
- below is a single spacer box, so the scrollbar still measures the whole
1544
- list. Sorting a hundred thousand rows is still the caller's problem — pass
1545
- `sort` and sort the data yourself when that matters.
1546
-
1547
- The **table** holds the focus, not the row: a row is unmounted as soon as it
1548
- scrolls out of view, and focus would go with it. Up/Down move the selection,
1549
- PageUp/PageDown by a viewport, Home/End to the ends, and the selection is
1550
- kept on screen without building the rows in between. A click selects, a
1551
- double click activates — the same `onActivate` Enter fires, counted from
1552
- `ev.detail` like any other multi-click.
1553
-
1554
- The header scrolls sideways with the body but never vertically. Between two
1555
- headers there is a hairline rule and, just left of it, a grab band seven
1556
- pixels wide: the separator you see and the handle you hit are not the same
1557
- size, because a boundary wants to be thin and a handle wants to be easy. The
1558
- band is invisible until the pointer is on it and takes the accent while it is
1559
- held, and it is a **sibling** of the header rather than a child of it — a
1560
- click fires on the nearest common ancestor of press and release, so a handle
1561
- inside the header would end every resize with a sort of the column that was
1562
- just dragged. The handle takes focus on the press, so Left/Right resize by
1563
- 16px from the keyboard as well.
1075
+ **Every row must be `rowHeight` tall**: only the ~20 rows in view are
1076
+ mounted, swapped as you scroll, with spacers keeping the scrollbar true.
1077
+ Sorting a hundred thousand rows is still yours — pass `sort` and sort the
1078
+ data yourself.
1079
+
1080
+ The **table** holds focus, not the row (rows unmount off screen). Up/Down
1081
+ move the selection, PageUp/PageDown by a viewport, Home/End to the ends; a
1082
+ click selects and a double click (`ev.detail`) or Enter fires `onActivate`.
1083
+ The header scrolls sideways with the body, never vertically. A seven-pixel
1084
+ grab band left of each header rule resizes the column (accent while held,
1085
+ no sort on release); it takes focus on the press, so Left/Right then resize
1086
+ by 16px.
1564
1087
 
1565
1088
  ## `Tree`
1566
1089
 
1567
- Moved to [`@react-x11/components`](ecosystem.md): a tree wants virtualisation,
1568
- drag-to-reorder and lazy children, none of which core has a reason to carry.
1090
+ Moved to [`@react-x11/components`](ecosystem.md): virtualisation,
1091
+ drag-to-reorder and lazy children do not belong in core.
1569
1092
 
1570
1093
  ## `SplitPane`
1571
1094
 
@@ -1585,26 +1108,22 @@ Two panes with a divider you can drag.
1585
1108
  | `onResize(size)` | after a drag or a key step |
1586
1109
  | `min` / `minSecond` | how small either pane may get |
1587
1110
 
1588
- Only the first pane's size is stored; the second takes what is left, so a
1589
- window resize can never leave a gap. The drag clamps against the container
1590
- as it is laid out at that moment, so the limits stay honest when the window
1591
- changes underneath it.
1592
-
1593
- The divider is focusable: arrows move it by 16px, Home/End drive it to
1594
- either limit. Dragging captures the pointer, so it keeps tracking after the
1595
- pointer leaves the six pixels it started on, and it keeps the grip where it
1596
- was taken rather than jumping to the pointer.
1111
+ Only the first pane's size is stored; the second takes the rest, so a window
1112
+ resize never leaves a gap, and the drag clamps against the container as laid
1113
+ out at that moment. The divider is focusable: arrows move it 16px, Home/End
1114
+ to either limit. A drag captures the pointer and keeps the grip where it was
1115
+ taken.
1597
1116
 
1598
1117
  ## `Canvas3D`
1599
1118
 
1600
- Moved to [`@react-x11/components/three`](ecosystem.md) as `<Canvas>`, with
1601
- the scene graph it wrapped. Core keeps the `<glarea>` surface underneath it
1602
- — see [gl.md](gl.md) and `examples/viewer3d.jsx`.
1119
+ Moved to [`@react-x11/components/three`](ecosystem.md) as `<Canvas>`. Core
1120
+ keeps the `<glarea>` surface under it — [gl.md](gl.md),
1121
+ `examples/viewer3d.jsx`.
1603
1122
 
1604
1123
  ## `useAnchor(ref)` / `anchorRect(node, options)`
1605
1124
 
1606
- The placement math behind `Select` and `Tooltip`, exported for building
1607
- your own popup-based widgets.
1125
+ The placement math behind `Select` and `Tooltip`, for popup widgets of your
1126
+ own.
1608
1127
 
1609
1128
  ```jsx
1610
1129
  const ref = useRef(null);
@@ -1623,39 +1142,29 @@ const rect = measure({ placement: 'bottom', align: 'center', width, height });
1623
1142
  | `at` | a rect **inside** the node to anchor to instead — a caret, a cell |
1624
1143
  | `width`, `height` | size of the popup you are positioning |
1625
1144
  | `alignTo` | node the alignment reads, when it is not the anchor |
1626
-
1627
- `placement` is a **preference, not a promise**: a menu near the bottom of
1628
- the screen flips above its trigger rather than opening off-screen, and the
1629
- result is clamped into the screen either way. The side actually used comes
1630
- back as `placement`. Where screen geometry is unavailable it places without
1631
- clamping.
1632
-
1633
- `'center'` is no side: the popup goes **over** the anchor, middle on
1634
- middle, and is only clamped — `align` and `offset` mean nothing to it. It is
1635
- what a dialog is placed with ([`Dialog`](#size-and-placement)).
1636
-
1637
- `'start'` and `'end'` are the **logical** sides and are what a submenu
1638
- wants: it opens away from the edge its parent's rows begin at, which is
1639
- leftwards in a mirrored menu, and still flips at the screen edge from there.
1640
- `'left'`/`'right'` stay physical. `align` mirrors the same way, but only
1641
- when the popup is above or below its trigger — with one beside it the
1642
- alignment axis is vertical, and nothing vertical mirrors. The direction is
1643
- read off the anchoring node, so a menu inside a mirrored panel needs no
1644
- argument.
1645
-
1646
- `alignTo` takes the two axes from **different nodes**: the placement edge
1647
- from the anchor, the alignment from `alignTo`. A submenu is the case that
1648
- needs it — it belongs against the outer edge of the menu it comes out of,
1649
- but level with the row that opened it, and that row is inset by the menu's
1650
- border and padding. Both nodes have to be in the same window.
1145
+ | `alignOffset` | shift along the alignment axis in px, applied before clamping |
1146
+
1147
+ `placement` is a **preference**: near a screen edge the popup flips to the
1148
+ opposite side, and it is clamped into the screen either way; the side used
1149
+ comes back as `placement`. With no screen geometry it does not clamp.
1150
+ `'center'` puts the popup **over** the anchor, middle on middle, only
1151
+ clamped (`align`/`offset` ignored) — what [`Dialog`](#size-and-placement)
1152
+ uses.
1153
+
1154
+ `'start'`/`'end'` are **logical** (a submenu opens towards `'end'`, leftwards
1155
+ when mirrored); `'left'`/`'right'` are physical. `align` mirrors too, but
1156
+ only above or below the anchor. The direction is read off the anchoring
1157
+ node. `alignTo` takes the alignment from another node in the same window — a
1158
+ submenu sits against its menu's outer edge but level with the row that
1159
+ opened it, and `alignOffset` lines its first item up with that row's text.
1651
1160
 
1652
1161
  ### `at` — anchoring to part of a node
1653
1162
 
1654
- `at` is a rect in the **anchor node's own coordinates**, and it becomes the
1655
- anchor: the side that flips, the edge that aligns, the gap `offset` leaves,
1656
- the rect tracking keeps in view. A caret is the case it exists for — a
1657
- moving point inside one element — and a table cell, a chart datapoint or a
1658
- highlighted span are the same shape.
1163
+ `at` is a rect in the **anchor node's own coordinates** that becomes the
1164
+ anchor — for a caret, a table cell, a datapoint, a highlighted span. `{x, y}`
1165
+ alone is a point; the popup `width` then defaults to the sub-rect's. Being
1166
+ node-relative, it lets `useAnchorTracking` follow a caret for free, out-of-view
1167
+ test included.
1659
1168
 
1660
1169
  ```jsx
1661
1170
  const measure = useAnchor(editorRef);
@@ -1667,51 +1176,33 @@ const rect = measure({
1667
1176
  });
1668
1177
  ```
1669
1178
 
1670
- `width` and `height` are optional (`{x, y}` alone is a point), and `width`
1671
- on the popup then defaults to the sub-rect's rather than the node's.
1672
-
1673
- Node-relative rather than screen-relative on purpose: the offset stays true
1674
- through everything that moves the node, so `useAnchorTracking` follows a
1675
- caret with no extra work — and its out-of-view test becomes the caret's,
1676
- which is what you want, since an editor's own lines scroll away long before
1677
- the editor does.
1678
-
1679
1179
  ### `anchorScreenRect(app, rect, options)` — a rect with no node behind it
1680
1180
 
1681
- The same placement against a rect on the **screen**: the item a tray click
1682
- reports, or where the pointer was. `rect` and the answer are logical screen
1683
- pixels, `{x, y}` alone is a point, and the popup is kept on the monitor the
1684
- rect is on — the one the **whole rect** overlaps most, and the nearest where
1685
- it overlaps none, because a tray item's frame starts just outside its own
1686
- display. With no node to ask, `options.scale` is the display's — it
1687
- defaults to 1 — and `options.direction` decides what `'start'` and `'end'`
1688
- mean. A `<popup>` does this itself with
1181
+ The same placement against a **screen** rect in logical pixels — a tray
1182
+ item's frame, where the pointer was; `{x, y}` alone is a point. The popup
1183
+ stays on the monitor the whole rect overlaps most (the nearest if none).
1184
+ `options.scale` is the display's (default 1) and `options.direction` decides
1185
+ `'start'`/`'end'`. A `<popup>` does this itself with
1689
1186
  [`anchor={{ rect }}`](elements.md#a-popover-under-a-tray-item).
1690
1187
 
1691
- **A popup that has to measure itself first anchors from the other side.**
1692
- Rows sized to their labels, a menu as wide as its widest item: the size is
1693
- not known until the popup's content is laid out, and by then React has
1694
- already rendered. `<popup anchor={{ to: ref, at }}>` hands the placement to
1695
- the popup, which does it in the one place the size is known —
1188
+ A popup that must **measure itself first** (rows sized to labels) cannot
1189
+ know its size at render; `<popup anchor={{ to: ref, at }}>` lets the popup
1190
+ place itself once laid out —
1696
1191
  [elements.md](elements.md#anchor--a-popup-that-places-itself).
1697
1192
 
1698
1193
  ### `anchorArea(node)`
1699
1194
 
1700
- The rect a popup anchored to `node` may be placed in: the usable part of the
1701
- monitor that node is on — per-monitor, `_NET_WORKAREA` taken off it, the
1702
- same answer [`<window width="auto">`](elements.md#natural-size) is capped
1703
- by. `null` where there is no display to ask.
1704
-
1705
- What needs it is a surface sizing itself: a menu measured to its longest
1706
- label is never usefully wider than the screen it opens on, and `anchorRect`
1707
- can slide a popup back from an edge but nothing rescues one that does not
1708
- fit. `Select` and `ContextMenu` both cap themselves this way.
1195
+ The usable part of the monitor `node` is on — panels and docks taken off, the
1196
+ cap [`<window width="auto">`](elements.md#natural-size) uses — or `null` with
1197
+ no display. A surface sizing itself caps itself to it (`Select` and
1198
+ `ContextMenu` do), since `anchorRect` can slide a popup back from an edge but
1199
+ not rescue one too big for the screen.
1709
1200
 
1710
1201
  ## `useDropTarget(options)` / `useDragSource(options)`
1711
1202
 
1712
- The render-state layer over the drag-and-drop props, for the cases a
1713
- `':drag-over'` or `':dragging'` style block cannot cover — a hint label, a
1714
- disabled sibling, a drag preview that follows the pointer.
1203
+ The render-state layer over the drag-and-drop props, for what a
1204
+ `':drag-over'` or `':dragging'` style block cannot do — a hint label, a
1205
+ disabled sibling, a preview that follows the pointer.
1715
1206
 
1716
1207
  ```jsx
1717
1208
  const { dropProps, isOver, isAccepted } = useDropTarget({
@@ -1725,25 +1216,16 @@ const { dragProps, isDragging, position } = useDragSource({
1725
1216
  });
1726
1217
  ```
1727
1218
 
1728
- Spread `dropProps` / `dragProps` on any drawn element. Both are thin — the
1729
- props they return are the same ones you can write by hand — so reach for
1730
- them when the render changes, and for the plain props when it does not.
1731
- `position` is `{ x, y, accepted }` in screen coordinates while a drag is in
1732
- flight, which is what a `<popup dragPreview>` follows. Full reference:
1733
- [drag-and-drop.md](drag-and-drop.md).
1219
+ Spread `dropProps` / `dragProps` on any drawn element; they are the same
1220
+ props you could write by hand, so use them when the render changes and the
1221
+ plain props when it does not. `position` is `{ x, y, accepted }` in screen
1222
+ coordinates during a drag — what a `<popup dragPreview>` follows. Full
1223
+ reference: [drag-and-drop.md](drag-and-drop.md).
1734
1224
 
1735
1225
  ---
1736
1226
 
1737
- The other components — `Button`, `Checkbox`, `Radio`/`RadioGroup`,
1738
- `Switch`, `ProgressBar` — are demoed together in `examples/widgets.jsx`.
1739
-
1740
- The `Select` source (`src/components/Select.js`) is the reference for building
1741
- your own: hover/focus state with `useState`, a `<popup>` for anything that
1742
- must escape the window bounds, and a ref to the trigger node for anchoring
1743
- (`node.abs` + `node.root.window.x/y`).
1744
-
1745
- The window's screen position comes from the server (`TranslateCoordinates`),
1746
- refreshed when the window is realized and whenever it is configured — not
1747
- from `window.x`/`y`. Once a reparenting window manager has put the window
1748
- inside its frame, those are relative to the _frame_, and a popup placed with
1749
- them lands near the corner of the screen instead of under its trigger.
1227
+ To build a popup widget of your own, `src/components/Select.js` is the
1228
+ reference: state in React, `:hover` style blocks, a `<popup>` for anything
1229
+ that must escape the window, `useAnchor` on the trigger. Do not place a popup
1230
+ from a window's own `x`/`y` — under a reparenting window manager they are
1231
+ frame-relative; `useAnchor` and `ev.screenX`/`screenY` come from the server.