react-x11 1.2.0 → 2.0.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 (158) hide show
  1. package/README.md +330 -81
  2. package/package.json +132 -14
  3. package/src/ClickToComponent.js +9 -2
  4. package/src/DevToolsIntegration.js +440 -32
  5. package/src/Reconciler.js +463 -165
  6. package/src/a11y.js +1299 -0
  7. package/src/acceleratorhooks.js +86 -0
  8. package/src/accelerators.js +128 -0
  9. package/src/activate.js +168 -0
  10. package/src/anchor.js +360 -0
  11. package/src/appcontext.js +161 -0
  12. package/src/appearance.js +748 -0
  13. package/src/appearancehooks.js +96 -0
  14. package/src/apphooks.js +73 -0
  15. package/src/application.js +748 -0
  16. package/src/atspi.js +1953 -0
  17. package/src/bus.js +545 -0
  18. package/src/bushooks.js +136 -0
  19. package/src/clientmessage.js +140 -0
  20. package/src/clipboard.js +121 -0
  21. package/src/components/Button.js +148 -33
  22. package/src/components/Checkbox.js +66 -32
  23. package/src/components/Dialog.js +96 -29
  24. package/src/components/FileDialog.js +491 -0
  25. package/src/components/Icon.js +329 -0
  26. package/src/components/Menu.js +797 -133
  27. package/src/components/PasswordInput.js +434 -0
  28. package/src/components/ProgressBar.js +91 -12
  29. package/src/components/Radio.js +78 -35
  30. package/src/components/Select.js +255 -65
  31. package/src/components/Slider.js +102 -41
  32. package/src/components/SplitPane.js +206 -0
  33. package/src/components/Switch.js +100 -22
  34. package/src/components/Table.js +529 -0
  35. package/src/components/Tabs.js +211 -0
  36. package/src/components/Tooltip.js +528 -54
  37. package/src/components/anchor.js +154 -107
  38. package/src/components/change.js +34 -0
  39. package/src/components/dnd.js +103 -0
  40. package/src/components/index.js +16 -3
  41. package/src/components/keys.js +17 -20
  42. package/src/components/scribble.js +247 -0
  43. package/src/components/theme.js +350 -42
  44. package/src/components/typeahead.js +12 -2
  45. package/src/compose.js +868 -0
  46. package/src/compositing.js +224 -0
  47. package/src/dbusmenu.js +384 -0
  48. package/src/debug.d.ts +63 -0
  49. package/src/debug.js +642 -0
  50. package/src/decorations.js +486 -0
  51. package/src/desktopsettings.js +209 -0
  52. package/src/desktopsettingshooks.js +62 -0
  53. package/src/dnd.js +1725 -0
  54. package/src/editmenu.js +272 -0
  55. package/src/errors.js +98 -0
  56. package/src/events.js +1406 -162
  57. package/src/extensions.js +45 -0
  58. package/src/filedialog.js +375 -0
  59. package/src/filedialoghooks.js +132 -0
  60. package/src/fonthooks.js +64 -0
  61. package/src/fonts.js +301 -0
  62. package/src/foreignnodes.js +519 -0
  63. package/src/frame/child.js +33 -0
  64. package/src/frame/childmain.js +274 -0
  65. package/src/frame/env.js +140 -0
  66. package/src/frame/index.js +444 -0
  67. package/src/frame/lifecycle.js +67 -0
  68. package/src/frame/protocol.js +179 -0
  69. package/src/frames.js +73 -0
  70. package/src/glbackend.js +114 -0
  71. package/src/glnodes.js +125 -67
  72. package/src/globalmenu.js +683 -0
  73. package/src/host.d.ts +88 -0
  74. package/src/host.js +42 -0
  75. package/src/idle.js +490 -0
  76. package/src/idlehooks.js +100 -0
  77. package/src/imagesource.js +349 -0
  78. package/src/index.d.ts +475 -0
  79. package/src/index.js +83 -9
  80. package/src/inputtime.js +206 -0
  81. package/src/jsx-dev-runtime.d.ts +21 -0
  82. package/src/jsx-dev-runtime.js +2 -0
  83. package/src/jsx-runtime.d.ts +39 -0
  84. package/src/jsx-runtime.js +6 -0
  85. package/src/keyboard.js +256 -0
  86. package/src/keyboardstate.js +278 -0
  87. package/src/keyboardstatehooks.js +58 -0
  88. package/src/keysyms.d.ts +149 -0
  89. package/src/keysyms.js +270 -0
  90. package/src/locale.js +170 -0
  91. package/src/localehooks.js +47 -0
  92. package/src/menuitem.js +223 -0
  93. package/src/node.d.ts +597 -0
  94. package/src/node.js +44 -0
  95. package/src/nodes.js +9546 -690
  96. package/src/ntk.d.ts +44 -0
  97. package/src/ntk.js +25 -0
  98. package/src/paintcache.js +366 -0
  99. package/src/palette.js +380 -0
  100. package/src/pastestate.js +66 -0
  101. package/src/portal.js +461 -0
  102. package/src/priority.js +26 -0
  103. package/src/refresh/index.d.ts +40 -0
  104. package/src/refresh/index.js +122 -0
  105. package/src/refresh/loader.d.ts +37 -0
  106. package/src/refresh/loader.js +401 -0
  107. package/src/refresh/register.d.ts +5 -0
  108. package/src/refresh/register.js +13 -0
  109. package/src/registry.js +232 -0
  110. package/src/scale.js +626 -0
  111. package/src/scalehooks.js +27 -0
  112. package/src/screencolor.js +640 -0
  113. package/src/screencolorhooks.js +101 -0
  114. package/src/screens.js +754 -0
  115. package/src/screenshooks.js +137 -0
  116. package/src/startup.js +302 -0
  117. package/src/style.d.ts +126 -0
  118. package/src/style.js +33 -0
  119. package/src/styles.js +1482 -6
  120. package/src/svgnodes.js +306 -0
  121. package/src/testing/a11y.js +484 -0
  122. package/src/testing/components.js +414 -0
  123. package/src/testing/events.js +407 -0
  124. package/src/testing/harness.js +455 -0
  125. package/src/testing/index.d.ts +558 -0
  126. package/src/testing/index.js +85 -0
  127. package/src/testing/mock-app.js +463 -0
  128. package/src/testing/pixels.js +152 -0
  129. package/src/testing/queries.js +224 -0
  130. package/src/textrange.js +83 -0
  131. package/src/textselection.js +439 -0
  132. package/src/trace-registry.js +63 -0
  133. package/src/transfer.js +93 -0
  134. package/src/types/appearance.d.ts +84 -0
  135. package/src/types/application.d.ts +173 -0
  136. package/src/types/components.d.ts +820 -0
  137. package/src/types/dbus.d.ts +177 -0
  138. package/src/types/elements.d.ts +897 -0
  139. package/src/types/events.d.ts +549 -0
  140. package/src/types/filedialog.d.ts +260 -0
  141. package/src/types/fonts.d.ts +124 -0
  142. package/src/types/frame.d.ts +146 -0
  143. package/src/types/globalmenu.d.ts +42 -0
  144. package/src/types/nodes.d.ts +199 -0
  145. package/src/types/screencolor.d.ts +84 -0
  146. package/src/types/style.d.ts +434 -0
  147. package/src/types/system.d.ts +287 -0
  148. package/src/windowid.js +151 -0
  149. package/src/windowstate.js +393 -0
  150. package/src/xsettings.js +336 -0
  151. package/src/yoga.d.ts +55 -0
  152. package/src/yoga.js +135 -0
  153. package/src/components/Canvas3D.js +0 -28
  154. package/src/geometry3d.js +0 -223
  155. package/src/pointer3d.js +0 -158
  156. package/src/raycast3d.js +0 -146
  157. package/src/richnodes.js +0 -436
  158. package/src/scene3d.js +0 -683
package/src/palette.js ADDED
@@ -0,0 +1,380 @@
1
+ // The two built-in palettes, and which one is in force.
2
+ //
3
+ // Separate from `components/theme.js` because the palette is not a React
4
+ // concern. There are two routes into the tree — `useTheme()` through React
5
+ // context, and a `$token` resolved against the nearest `theme` **prop** by
6
+ // walking the node tree — and the second one lives in `nodes.js`, below the
7
+ // widget layer. Both have to agree on what "no theme was given" means, so the
8
+ // answer belongs underneath both of them.
9
+ //
10
+ // **The default follows the desktop.** A react-x11 app that says nothing
11
+ // about colour is dark on a dark desktop and light on a light one, the way a
12
+ // GTK or Qt app is; `<ThemeProvider>` and `colorScheme` are how an app that
13
+ // wants otherwise says so. See docs/appearance.md.
14
+
15
+ import { appearanceSnapshot } from './appearance.js';
16
+ import { readableInk, stepBeyond } from './styles.js';
17
+
18
+ /**
19
+ * The language subtags written right-to-left — CLDR's set, by the language
20
+ * rather than by the script, because a locale name is what an environment
21
+ * actually carries. Arabic and its neighbours, Hebrew, Persian and Dari,
22
+ * Pashto, Urdu, Sindhi, Kashmiri, Uyghur, Yiddish, Sorani Kurdish, Dhivehi,
23
+ * Syriac, N'Ko, and Adlam-written Fulah.
24
+ */
25
+ const RTL_LANGUAGES = new Set([
26
+ 'ae',
27
+ 'ar',
28
+ 'arc',
29
+ 'bcc',
30
+ 'bqi',
31
+ 'ckb',
32
+ 'dv',
33
+ 'fa',
34
+ 'ff',
35
+ 'glk',
36
+ 'he',
37
+ 'iw',
38
+ 'khw',
39
+ 'ks',
40
+ 'ku',
41
+ 'mzn',
42
+ 'nqo',
43
+ 'pnb',
44
+ 'prs',
45
+ 'ps',
46
+ 'sd',
47
+ 'syr',
48
+ 'ug',
49
+ 'ur',
50
+ 'yi',
51
+ ]);
52
+
53
+ /**
54
+ * Which way this desktop reads, from the environment's locale.
55
+ *
56
+ * **This is the default, and it is a default rather than a setting on
57
+ * purpose.** An app started under `LANG=ar_EG.UTF-8` is an Arabic app, and
58
+ * the person who started it should not have to have been given a language
59
+ * menu before the panels are on the right side. It is what GTK and Qt both
60
+ * do — GTK asks the translation of `"default:LTR"`, Qt asks the system
61
+ * locale — and it costs an app that never thinks about this exactly nothing,
62
+ * because every locale not in the set above answers `'ltr'`.
63
+ *
64
+ * The overrides, nearest first: a `direction` style property on any node
65
+ * mirrors that subtree, and `<ThemeProvider value={{ direction }}>` mirrors
66
+ * everything under it *including the widgets*, which is the one an app with a
67
+ * language menu wants — see docs/styling.md.
68
+ *
69
+ * There is no environment in the playground bundle, which runs this in a
70
+ * browser — hence the guard rather than a bare `process.env`. A page has no
71
+ * locale to read either way; what it has is whatever the app writes.
72
+ *
73
+ * Read once, at load: a locale does not change under a running process, and
74
+ * this is on the path that resolves the palette for every node.
75
+ */
76
+ export function localeDirection(env = ENV) {
77
+ const locale = env.LC_ALL || env.LC_MESSAGES || env.LANG || '';
78
+ // `ar_EG.UTF-8`, `he-IL`, `fa`, or `C`/`POSIX` — the language is whatever
79
+ // comes before the territory, and the separator is either spelling
80
+ const language = locale.split(/[._@-]/)[0].toLowerCase();
81
+ return RTL_LANGUAGES.has(language) ? 'rtl' : 'ltr';
82
+ }
83
+
84
+ const ENV = typeof process === 'undefined' ? {} : (process.env ?? {});
85
+
86
+ const LOCALE_DIRECTION = localeDirection();
87
+
88
+ /**
89
+ * The palette every widget reads, and the shape of the controls with it.
90
+ * A theme overrides what it cares about and inherits the rest, so the
91
+ * defaults here are the look the widgets have always had.
92
+ *
93
+ * The shape tokens are what let a theme be more than a recolour: corner
94
+ * radius, border weight, text size and the padding inside a control are
95
+ * most of what separates one platform's buttons from another's.
96
+ */
97
+ export const DefaultTheme = {
98
+ // colour
99
+ border: '#b2bec3',
100
+ borderFocus: '#2980b9',
101
+ // The ground and the things raised off it. `background` is what the window
102
+ // *is* — the fill under everything, painted by the window itself — and
103
+ // `surface` is what sits on it: a control's fill, a card, the sheet a menu
104
+ // or a dialog is drawn on. They are the same colour here because a white
105
+ // app on a white ground is what the light palette has always been; a theme
106
+ // that gives the ground its own tint (GitHub's `#f6f8fa` under white cards,
107
+ // and every macOS window) is exactly the case that had nowhere to say so.
108
+ background: 'white',
109
+ surface: 'white',
110
+ text: '#2d3436',
111
+ // Secondary ink: a placeholder, a caption, the label of a disabled
112
+ // control. Named for the text it is rather than for how it looks, so that
113
+ // `textMuted` and `text` read as one family.
114
+ textMuted: '#7f8c8d',
115
+ hoverBackground: '#2980b9',
116
+ hoverText: 'white',
117
+ accent: '#2980b9',
118
+ accentHover: '#1f6693',
119
+ accentText: 'white',
120
+ surfaceHover: '#f1f2f6',
121
+ track: '#dfe6e9',
122
+ // What a screen has to be able to *say*: this failed, this worked, look at
123
+ // this, here is a note. Without them every alert, badge and validation
124
+ // message hard-codes a hex, which is the one thing `$token` exists to
125
+ // prevent — and those are the colours most likely to be wrong on a dark
126
+ // desktop, because a red that reads on white disappears on near-black.
127
+ //
128
+ // Each is picked to work as **ink as well as fill**: 4.5:1 against this
129
+ // palette's ground, so "Password too short" under a field is legible
130
+ // without a second token for the text form of the same idea.
131
+ //
132
+ // Only `danger` has the hover and pressed steps, because it is the only
133
+ // one of the four you ever press — a destructive button. A success or a
134
+ // warning is something the app says, not something the user clicks; when
135
+ // one of them ends up on a control, the control is a `<Button>` with a
136
+ // `danger`-shaped ramp of its own.
137
+ //
138
+ // `info` is a blue of its own rather than the accent, even here where the
139
+ // accent is also blue: a theme with a green accent still gets a blue note,
140
+ // because that is what a note looks like everywhere — and this one is a
141
+ // step darker than `accent`, which is a fill first and only clears 4.3:1
142
+ // as letters.
143
+ danger: '#c0392b',
144
+ dangerHover: '#a93226',
145
+ dangerText: 'white',
146
+ success: '#1e8449',
147
+ successText: 'white',
148
+ warning: '#9a6700',
149
+ warningText: 'white',
150
+ info: '#1c6ea4',
151
+ infoText: 'white',
152
+ // The pressed step of each fill family: rest → …Hover → …Active. Every
153
+ // family that has a hover needs one, because a press is the state a
154
+ // control has to show *before* it has done anything — the activation
155
+ // itself only happens on the release, and half a second can pass in
156
+ // between.
157
+ //
158
+ // Written out here, but a theme almost never sets them: `resolveTheme`
159
+ // takes the step the palette's own hover made and takes it again. So a
160
+ // palette that names `accentHover` and stops there — which is every theme
161
+ // in `examples/themes.js` and every recipe in docs/ecosystem/theming.md —
162
+ // gets a press that matches it rather than one inherited from these.
163
+ accentActive: '#154c6d',
164
+ surfaceActive: '#e3e5ed',
165
+ textMutedActive: '#4c5a57',
166
+ dangerActive: '#922b21',
167
+ // The keyboard focus ring. Read by the renderer, not by the widgets: any
168
+ // focusable node under this palette draws it on `:focus-visible`, so a
169
+ // plain `<box focusable>` an application writes is indicated too, and a
170
+ // theme restyles every ring in the app from here.
171
+ focusRing: '#2980b9',
172
+ focusRingWidth: 2,
173
+ focusRingOffset: 1,
174
+ // shape
175
+ radius: 4,
176
+ radiusSmall: 3,
177
+ // Floating surfaces round on their own scale, and it is a wider one: a
178
+ // menu is a sheet of paper laid over the window, where a button is a
179
+ // control cut into it. Half the text size is the number every desktop
180
+ // lands near — 7px at a 14px body — and tying it to the type rather than
181
+ // to `radius` is what keeps a theme that only sets `fontSize` from
182
+ // getting a 20px menu with a 4px corner.
183
+ //
184
+ // The two inside it step down from there, because a rounded thing inside
185
+ // a rounded thing wants a *smaller* curve or the two read as concentric
186
+ // rings: the highlight on a menu row, and the tooltip bubble, which is
187
+ // the smallest floating surface there is and the one closest to text.
188
+ radiusPopup: 7,
189
+ radiusPopupItem: 5,
190
+ radiusTooltip: 4,
191
+ borderWidth: 1,
192
+ fontSize: 14,
193
+ // The two faces an app has. `fontFamily` is what a `<text>` inherits where
194
+ // nothing named one, so "this app is Inter" is a sentence said once here
195
+ // rather than on every label; `monoFamily` is the one every code surface
196
+ // reaches for — a `<Code>`, a log pane, a hex dump — and the reason it is a
197
+ // token at all is that those are written by *different* components, which
198
+ // would otherwise each grow their own prop for it and have to be set one by
199
+ // one.
200
+ //
201
+ // CSS-style family lists, the same as the `fontFamily` style property:
202
+ // `'"JetBrains Mono", monospace'` names a preference and a fallback, and
203
+ // ntk's `fonts.match` splits the list itself.
204
+ fontFamily: 'sans-serif',
205
+ monoFamily: 'monospace',
206
+ // Which way this app reads — `'ltr'` or `'rtl'`, seeded from the locale.
207
+ //
208
+ // In the palette rather than in a context of its own because both consumers
209
+ // are already here: the widgets read it through `useTheme()` to decide which
210
+ // way a slider travels or which side a submenu opens on, and the node tree
211
+ // reads it as the floor under the `direction` style property. An app with a
212
+ // language menu therefore switches the whole UI, layout and widgets
213
+ // together, with the `<ThemeProvider>` swap it was already doing for
214
+ // colours — and a `<ThemeProvider>` that names it plants the matching style
215
+ // in the tree, so the two routes cannot disagree.
216
+ direction: LOCALE_DIRECTION,
217
+ paddingX: 16,
218
+ // Measured from the **letters**, not from the font's line box: widget
219
+ // labels are trimmed to the capitals down to the baseline, so this is the
220
+ // space you actually see above and below the text — see `capTrim` in
221
+ // components/theme.js for why a line box cannot give an even one. Larger
222
+ // than it looks next to a CSS padding for that reason: 12 here is about
223
+ // what 8 came to once a typical face's ascent had been added on.
224
+ paddingY: 12,
225
+ };
226
+
227
+ // Which pressed token is derived from which pair, when the palette does not
228
+ // name it: the resting colour of the family and the hover it steps to.
229
+ //
230
+ // `textMutedActive` is measured from `border` because that is the ramp it
231
+ // belongs to: a `<Switch>` that is off has a `border`-coloured track, and
232
+ // the muted ink is the step it takes on hover. The ink and the track share a
233
+ // colour rather than a job.
234
+ const PRESSED_FROM = {
235
+ accentActive: ['accent', 'accentHover'],
236
+ surfaceActive: ['surface', 'surfaceHover'],
237
+ textMutedActive: ['border', 'textMuted'],
238
+ dangerActive: ['danger', 'dangerHover'],
239
+ };
240
+
241
+ // And which ink goes on which fill. A palette that names a fill and stops
242
+ // there gets the more legible of its own two inks — see `readableInk`.
243
+ //
244
+ // This is what makes the status family cheap to theme: naming four colours
245
+ // is a design decision, and naming the letters that go on top of each of
246
+ // them is bookkeeping that a contrast ratio can do. It is the same for
247
+ // `accent`, where it fixes a real trap — a palette whose accent is a yellow
248
+ // or a lime inherits `accentText: 'white'` and paints an invisible label.
249
+ const TEXT_FROM = {
250
+ accentText: 'accent',
251
+ hoverText: 'hoverBackground',
252
+ dangerText: 'danger',
253
+ successText: 'success',
254
+ warningText: 'warning',
255
+ infoText: 'info',
256
+ };
257
+
258
+ // And the same for the floating-surface radii, which are a function of the
259
+ // text they wrap: a palette that sets `fontSize` and nothing else still gets
260
+ // menus in proportion to it.
261
+ const RADIUS_FROM_FONT = {
262
+ radiusPopup: (size) => Math.round(size / 2),
263
+ radiusPopupItem: (size) => Math.max(0, Math.round(size / 2) - 2),
264
+ radiusTooltip: (size) => Math.max(0, Math.round(size / 2) - 3),
265
+ };
266
+
267
+ /**
268
+ * Merge a partial palette, filling in the pressed step for any family whose
269
+ * colours moved without it — and the popup radii for a palette that moved
270
+ * the text size without them.
271
+ *
272
+ * The rule per token: an explicit value wins; otherwise, if this palette
273
+ * touched either colour the step is measured between, it is re-derived; and
274
+ * otherwise whatever was already in force stands. That last clause is what
275
+ * keeps an inner `<ThemeProvider value={{ fontSize: 18 }}>` from throwing
276
+ * away a pressed colour an outer one set by hand.
277
+ *
278
+ * The alternative was three more tokens every theme has to remember, and a
279
+ * theme that forgets one does not fail loudly — it just stops answering
280
+ * presses, in the one state a control has to show while nothing else can.
281
+ *
282
+ * **`surface` follows `background` unless it is named**, which is the same
283
+ * bargain from the other end: a palette that has one ground has one ground,
284
+ * and only a design that actually raises its cards off it has to say so. Any
285
+ * other rule would leave a theme that names a `#1f1f23` background with the
286
+ * built-in dark palette's surface on its controls — a colour from a palette
287
+ * it had replaced.
288
+ */
289
+ export function resolveTheme(value, base = DefaultTheme) {
290
+ if (!value) return base;
291
+ // What this palette said, with that one implication written in, so
292
+ // everything measured from `surface` below is measured from the right
293
+ // colour and re-derived when it moved.
294
+ const named =
295
+ value.surface == null && value.background != null
296
+ ? { ...value, surface: value.background }
297
+ : value;
298
+ const merged = { ...base, ...named };
299
+ for (const [token, [rest, hover]] of Object.entries(PRESSED_FROM)) {
300
+ if (named[token] != null) continue;
301
+ if (named[rest] == null && named[hover] == null) continue;
302
+ merged[token] = stepBeyond(merged[rest], merged[hover]);
303
+ }
304
+ // The ink follows the fill it goes on, and also the two inks it is chosen
305
+ // between: a palette that moves only `background` has moved what "the
306
+ // legible one" means.
307
+ for (const [token, fill] of Object.entries(TEXT_FROM)) {
308
+ if (named[token] != null) continue;
309
+ if (named[fill] == null && named.text == null && named.background == null)
310
+ continue;
311
+ merged[token] = readableInk(merged[fill], [merged.text, merged.background]);
312
+ }
313
+ if (named.fontSize != null) {
314
+ for (const [token, from] of Object.entries(RADIUS_FROM_FONT)) {
315
+ if (named[token] == null) merged[token] = from(merged.fontSize);
316
+ }
317
+ }
318
+ return merged;
319
+ }
320
+
321
+ /**
322
+ * The dark counterpart, as the *same design* in a dark scheme rather than a
323
+ * second design: it is built by merging colour overrides over
324
+ * {@link DefaultTheme}, so every shape token — radius, border width, font
325
+ * size, the control padding — is shared by construction and cannot drift.
326
+ *
327
+ * `resolveTheme` derives the pressed steps from the hovers named here, and
328
+ * `stepBeyond` takes the direction from the colours themselves, so a press
329
+ * in dark *lightens* where the light palette's darkens. Nothing has to be
330
+ * told which scheme it is in. The status inks are derived too — this palette
331
+ * names four fills and none of the letters on them, which is what every
332
+ * theme after it gets to do.
333
+ */
334
+ export const DarkTheme = resolveTheme({
335
+ // A near-black with a little blue in it rather than #000: pure black shows
336
+ // every seam between a window and the widgets on it, and no desktop's dark
337
+ // theme uses it.
338
+ background: '#1e2228',
339
+ // Here the ground and the surface part company, which is the whole point
340
+ // of their being two tokens: a card at the ground's own colour is a card
341
+ // you cannot see, and dark designs raise by lightening because there is no
342
+ // shadow to cast on near-black.
343
+ surface: '#252a31',
344
+ surfaceHover: '#2a3038',
345
+ text: '#e6e9ed',
346
+ textMuted: '#8b939c',
347
+ border: '#454d55',
348
+ track: '#3a4149',
349
+ // Lighter and less saturated than the light palette's: the same four
350
+ // meanings, re-picked to clear 4.5:1 against near-black rather than
351
+ // against white. `#c0392b` on this ground is a bruise.
352
+ danger: '#ec6a5e',
353
+ dangerHover: '#f28d80',
354
+ success: '#2ecc71',
355
+ warning: '#f0b429',
356
+ info: '#5aa4e6',
357
+ // The accent lifts off the darker ground instead of sinking into it, so
358
+ // `accentHover` goes *up* from `accent` here and down in the light palette.
359
+ accent: '#3d8bd4',
360
+ accentHover: '#5aa4e6',
361
+ accentText: 'white',
362
+ hoverBackground: '#3d8bd4',
363
+ hoverText: 'white',
364
+ borderFocus: '#5aa4e6',
365
+ focusRing: '#5aa4e6',
366
+ });
367
+
368
+ /**
369
+ * The palette in force where nothing has been said — which is to say, the
370
+ * desktop's.
371
+ *
372
+ * Read synchronously and cheaply: `appearanceSnapshot()` is a frozen object
373
+ * seeded from disk before the first render, so this is a property lookup and
374
+ * a comparison, and it is called from the paint path.
375
+ *
376
+ * `'no-preference'` means *use your own default*, which is the light one.
377
+ */
378
+ export function baseTheme() {
379
+ return appearanceSnapshot().colorScheme === 'dark' ? DarkTheme : DefaultTheme;
380
+ }
@@ -0,0 +1,66 @@
1
+ // Is there anything to paste? — kept per app, so a menu can answer without
2
+ // a round trip.
3
+ //
4
+ // The honest answer needs the server, and asking for it costs a full
5
+ // conversion against whatever foreign client owns the selection — plus a
6
+ // two second wait when that client is wedged. A menu cannot pay that while
7
+ // it is opening, which is why the built-in edit menu shipped with Paste
8
+ // always enabled.
9
+ //
10
+ // XFixes turns it around: the server says when the selection changes hands,
11
+ // and the answer is a boolean already in hand by the time a menu opens. One
12
+ // registration per app, armed the first time anything asks.
13
+ //
14
+ // Deliberately only "is it owned", not "does it hold text". Refining it
15
+ // would mean a TARGETS round trip per change, which reintroduces the cost
16
+ // this exists to avoid; an owner offering nothing a text field can take is
17
+ // rare enough to leave the row enabled and let the paste do nothing.
18
+
19
+ const states = new WeakMap();
20
+
21
+ /**
22
+ * Start tracking `app`'s CLIPBOARD, once. Safe to call on every menu open —
23
+ * the second call and the ten thousandth do nothing.
24
+ */
25
+ export function armPasteState(app, clipboard) {
26
+ if (!app || !clipboard || states.has(app)) return;
27
+ // A clipboard without these is an older ntk, or a stand-in someone
28
+ // supplied. Tracking is the enhancement, not the feature: skip it and
29
+ // leave the row enabled rather than throwing on the way into a menu.
30
+ if (
31
+ typeof clipboard.watch !== 'function' ||
32
+ typeof clipboard.targets !== 'function'
33
+ ) {
34
+ return;
35
+ }
36
+ // Unknown means "yes": until the server has said otherwise, behave the way
37
+ // this did before there was any tracking at all.
38
+ const state = { owned: true, live: false };
39
+ states.set(app, state);
40
+
41
+ clipboard
42
+ .watch('CLIPBOARD', (ev) => {
43
+ state.live = true;
44
+ state.owned = ev.owner !== 0;
45
+ })
46
+ .catch(() => {
47
+ // No XFixes on this server. Nothing to fall back to that is worth the
48
+ // latency, so the row stays enabled — which is where it started.
49
+ });
50
+
51
+ // `watch` reports *changes*, so it says nothing about the selection that
52
+ // was already there when we armed. One question, once per app, off the
53
+ // menu's path entirely.
54
+ clipboard
55
+ .targets({ selection: 'CLIPBOARD' })
56
+ .then((offered) => {
57
+ // a change may have overtaken this round trip; it is the fresher answer
58
+ if (!state.live) state.owned = offered.length > 0;
59
+ })
60
+ .catch(() => {});
61
+ }
62
+
63
+ /** Whether a Paste row should be live. True when nothing is known yet. */
64
+ export function canPaste(app) {
65
+ return states.get(app)?.owned ?? true;
66
+ }