react-x11 2.15.3 → 2.16.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 (57) hide show
  1. package/README.md +37 -0
  2. package/package.json +3 -3
  3. package/src/Reconciler.js +85 -22
  4. package/src/anchor.js +60 -18
  5. package/src/capabilities.js +29 -4
  6. package/src/cocoa/app.js +15 -9
  7. package/src/cocoa/context2d.js +23 -0
  8. package/src/cocoa/fonts.js +78 -0
  9. package/src/cocoa/presenter.js +17 -0
  10. package/src/cocoa/promotion.js +20 -0
  11. package/src/cocoa/relaunch.js +8 -3
  12. package/src/cocoa/symbols.js +64 -0
  13. package/src/cocoa/threaded.js +24 -4
  14. package/src/cocoa/window.js +362 -139
  15. package/src/components/ProgressBar.js +1 -1
  16. package/src/components/Slider.js +72 -39
  17. package/src/components/anchor.js +7 -2
  18. package/src/components/index.js +1 -0
  19. package/src/components/theme.js +32 -28
  20. package/src/desktopcapabilityhooks.js +29 -6
  21. package/src/filedialoghooks.js +3 -5
  22. package/src/frame/childmain.js +8 -20
  23. package/src/frame/env.js +2 -10
  24. package/src/icontheme.js +240 -0
  25. package/src/imagesource.js +83 -1
  26. package/src/index.js +3 -0
  27. package/src/node.d.ts +7 -0
  28. package/src/nodes/animation.js +17 -47
  29. package/src/nodes/cascade.js +17 -2
  30. package/src/nodes/image.js +63 -1
  31. package/src/nodes/kinds.js +12 -0
  32. package/src/nodes/layout.js +5 -1
  33. package/src/nodes/node.js +17 -3
  34. package/src/nodes/paint.js +117 -0
  35. package/src/nodes/scope.js +259 -0
  36. package/src/nodes/scrollable.js +53 -6
  37. package/src/nodes/text.js +2 -0
  38. package/src/nodes/textarea.js +1 -1
  39. package/src/nodes/textinput.js +1 -1
  40. package/src/nodes/window/anchoring.js +45 -18
  41. package/src/nodes/window/flush.js +6 -5
  42. package/src/nodes/window/popup.js +10 -0
  43. package/src/nodes/window/size.js +40 -2
  44. package/src/nodes/window/window.js +41 -14
  45. package/src/registry.js +2 -1
  46. package/src/settings.js +332 -0
  47. package/src/statusnotifier.js +164 -17
  48. package/src/styles.js +212 -8
  49. package/src/symbols.js +200 -0
  50. package/src/testing/mock-app.js +10 -0
  51. package/src/trayhooks.js +21 -5
  52. package/src/types/capabilities.d.ts +13 -1
  53. package/src/types/components.d.ts +33 -0
  54. package/src/types/elements.d.ts +57 -6
  55. package/src/types/style.d.ts +57 -0
  56. package/src/types/system.d.ts +104 -0
  57. package/src/types/tray.d.ts +14 -2
@@ -25,7 +25,7 @@ const CROSSING_MS = 1100;
25
25
  *
26
26
  * The slide is a **loop** in the style (`animation`,
27
27
  * [styling.md](../../docs/styling.md#loops)), not a timer here: it runs on
28
- * the window's own frame clock, claims the track as its damage every frame
28
+ * the window's own frame clock, claims the block as its damage every frame
29
29
  * instead of invalidating the window, and stops itself when the window is
30
30
  * unmapped, minimized or buried. A `setInterval` calling `setState` would
31
31
  * do none of those three, and would re-render this component sixty times a
@@ -4,6 +4,7 @@
4
4
 
5
5
  import React, { useRef, useState } from 'react';
6
6
  import { useAppOrNull } from '../appcontext.js';
7
+ import { flattenStyle } from '../styles.js';
7
8
  import { Bezel, bezelNatural, useNativeControls } from './native.js';
8
9
  import { useTheme } from './theme.js';
9
10
  import { changeEvent } from './change.js';
@@ -21,7 +22,10 @@ import {
21
22
  const h = React.createElement;
22
23
 
23
24
  const SLIDER_THUMB = 16;
24
- const SLIDER_SLOP = (24 - SLIDER_THUMB) / 2;
25
+
26
+ /** A length a slot's style gave, when it is a number of pixels. */
27
+ const lengthIn = (style, key) =>
28
+ typeof style[key] === 'number' ? style[key] : undefined;
25
29
 
26
30
  /**
27
31
  * <Slider value min max step onChange disabled …boxProps> — draggable
@@ -34,6 +38,15 @@ const SLIDER_SLOP = (24 - SLIDER_THUMB) / 2;
34
38
  *
35
39
  * Keyboard: arrows step, Home/End jump to the ends, PageUp/PageDown move
36
40
  * by ten steps.
41
+ *
42
+ * `thumbStyle`, `trackStyle` and `fillStyle` are style slots over the three
43
+ * drawn parts (#593), each merged over the part's own default, so a designed
44
+ * slider — a flat accent thumb with a shadow and no ring on a hairline track —
45
+ * is the widget restyled rather than rebuilt from boxes. The thumb's `width`
46
+ * and `height` are read back: the pointer's travel is the track less one
47
+ * thumb, and the control is as tall as the taller of thumb and track. Any
48
+ * slot also chooses the drawn slider over the platform's own, whose pixels
49
+ * no style reaches.
37
50
  */
38
51
  export function Slider({
39
52
  value = 0,
@@ -46,11 +59,22 @@ export function Slider({
46
59
  height = 4,
47
60
  native,
48
61
  style,
62
+ thumbStyle,
63
+ trackStyle,
64
+ fillStyle,
49
65
  ...boxProps
50
66
  }) {
51
67
  const theme = useTheme();
52
68
  const app = useAppOrNull();
53
- const nativeControls = useNativeControls(native);
69
+ const styled = Boolean(thumbStyle || trackStyle || fillStyle);
70
+ const nativeControls = useNativeControls(styled ? false : native);
71
+ // the sizes the drawing and the pointer math have to agree on
72
+ const thumb = flattenStyle(thumbStyle);
73
+ const thumbWidth = lengthIn(thumb, 'width') ?? SLIDER_THUMB;
74
+ const thumbHeight = lengthIn(thumb, 'height') ?? thumbWidth;
75
+ const trackHeight = lengthIn(flattenStyle(trackStyle), 'height') ?? height;
76
+ const controlHeight = Math.max(thumbHeight, trackHeight);
77
+ const slop = Math.max(0, (24 - controlHeight) / 2);
54
78
  const [focused, setFocused] = useState(false);
55
79
  const [dragging, setDragging] = useState(false);
56
80
  const trackRef = useRef(null);
@@ -88,11 +112,11 @@ export function Slider({
88
112
  if (!rect?.width) return value;
89
113
  // the thumb is centred on the value, so the usable travel is the track
90
114
  // minus one thumb width — otherwise min/max are unreachable at the ends
91
- const travel = Math.max(1, rect.width - SLIDER_THUMB);
115
+ const travel = Math.max(1, rect.width - thumbWidth);
92
116
  const x =
93
117
  node.direction === 'rtl'
94
- ? rect.x + rect.width - ev.x - SLIDER_THUMB / 2
95
- : ev.x - rect.x - SLIDER_THUMB / 2;
118
+ ? rect.x + rect.width - ev.x - thumbWidth / 2
119
+ : ev.x - rect.x - thumbWidth / 2;
96
120
  return quantize(min + (Math.min(travel, Math.max(0, x)) / travel) * span);
97
121
  };
98
122
 
@@ -226,7 +250,7 @@ export function Slider({
226
250
  style: [
227
251
  disabled || { cursor: 'pointer' },
228
252
  {
229
- height: SLIDER_THUMB,
253
+ height: controlHeight,
230
254
  minWidth: 0,
231
255
  justifyContent: 'center',
232
256
  // the control is as tall as its thumb and the track inside it sets
@@ -234,7 +258,7 @@ export function Slider({
234
258
  // WCAG 2.2 SC 2.5.8's 24 on the axis that is short, without moving
235
259
  // a pixel of the drawing — a taller slider would misalign every row
236
260
  // it sits in.
237
- hitSlop: { top: SLIDER_SLOP, bottom: SLIDER_SLOP },
261
+ hitSlop: { top: slop, bottom: slop },
238
262
  },
239
263
  style,
240
264
  ],
@@ -243,14 +267,16 @@ export function Slider({
243
267
  h(
244
268
  'box',
245
269
  {
246
- style: {
247
- height: height,
248
- borderRadius: height / 2,
249
- backgroundColor: theme.track,
250
- flexDirection: 'row',
251
- alignItems: 'center',
252
- pointerEvents: 'none',
253
- },
270
+ style: [
271
+ {
272
+ height: height,
273
+ borderRadius: height / 2,
274
+ backgroundColor: theme.track,
275
+ },
276
+ trackStyle,
277
+ // the parts of the track the fill and the pointer depend on
278
+ { flexDirection: 'row', alignItems: 'center', pointerEvents: 'none' },
279
+ ],
254
280
  },
255
281
  // flex ratios, not a percentage width: a percentage child resolves
256
282
  // against the space available while the track is still being
@@ -258,14 +284,15 @@ export function Slider({
258
284
  // at value = max the control grew, which moved the handle, which
259
285
  // changed the value, and a drag turned into an oscillation
260
286
  h('box', {
261
- style: {
262
- flexGrow: fraction,
263
- flexShrink: 0,
264
- flexBasis: 0,
265
- height: height,
266
- borderRadius: height / 2,
267
- backgroundColor: disabled ? theme.textMuted : theme.accent,
268
- },
287
+ style: [
288
+ {
289
+ height: trackHeight,
290
+ borderRadius: trackHeight / 2,
291
+ backgroundColor: disabled ? theme.textMuted : theme.accent,
292
+ },
293
+ fillStyle,
294
+ { flexGrow: fraction, flexShrink: 0, flexBasis: 0 },
295
+ ],
269
296
  }),
270
297
  h('box', {
271
298
  style: { flexGrow: 1 - fraction, flexShrink: 0, flexBasis: 0 },
@@ -276,22 +303,28 @@ export function Slider({
276
303
  // mirror on their own — a `row` runs the other way under `direction:
277
304
  // 'rtl'` — and these are what keep the thumb on top of the join.
278
305
  h('box', {
279
- style: {
280
- position: 'absolute',
281
- start: `${fraction * 100}%`,
282
- marginStart: -SLIDER_THUMB * fraction,
283
- width: SLIDER_THUMB,
284
- height: SLIDER_THUMB,
285
- borderRadius: SLIDER_THUMB / 2,
286
- borderWidth: 1,
287
- borderColor: disabled
288
- ? theme.border
289
- : focused || dragging
290
- ? theme.accentHover
291
- : theme.border,
292
- backgroundColor: disabled ? theme.surfaceHover : theme.surface,
293
- pointerEvents: 'none',
294
- },
306
+ style: [
307
+ {
308
+ width: thumbWidth,
309
+ height: thumbHeight,
310
+ borderRadius: Math.min(thumbWidth, thumbHeight) / 2,
311
+ borderWidth: 1,
312
+ borderColor: disabled
313
+ ? theme.border
314
+ : focused || dragging
315
+ ? theme.accentHover
316
+ : theme.border,
317
+ backgroundColor: disabled ? theme.surfaceHover : theme.surface,
318
+ },
319
+ thumbStyle,
320
+ // where it is: the value's place on the travel the math uses
321
+ {
322
+ position: 'absolute',
323
+ start: `${fraction * 100}%`,
324
+ marginStart: -thumbWidth * fraction,
325
+ pointerEvents: 'none',
326
+ },
327
+ ],
295
328
  }),
296
329
  );
297
330
  }
@@ -11,6 +11,7 @@ export {
11
11
  anchorArea,
12
12
  anchorOffscreen,
13
13
  anchorRect,
14
+ anchorScreenRect,
14
15
  centerRect,
15
16
  screenRect,
16
17
  subRect,
@@ -177,8 +178,10 @@ export function measureLabel(node, text, style) {
177
178
  // `{ opsz: 17 }` on its window — the text cut of a variable face, wider
178
179
  // than the display cut most files default to — draws every row in it and
179
180
  // used to measure none, and the popup came out narrow enough to wrap its
180
- // own labels. An explicit `style` still wins, since a caller that names a
181
- // face is measuring something it is about to draw in that face.
181
+ // own labels. `letterSpacing` and the OpenType features move advances the
182
+ // same way (a device length and a tag bag, as they cascade). An explicit
183
+ // `style` still wins, since a caller that names a face is measuring
184
+ // something it is about to draw in that face.
182
185
  const inherited = node?.inheritedTextStyle;
183
186
  const layout = fonts.layout(String(text), {
184
187
  family: style?.family ?? inherited?.family ?? 'sans-serif',
@@ -186,6 +189,8 @@ export function measureLabel(node, text, style) {
186
189
  weight: style?.weight ?? inherited?.weight ?? 'normal',
187
190
  style: style?.style ?? inherited?.style,
188
191
  variations: style?.variations ?? inherited?.variations,
192
+ letterSpacing: inherited?.letterSpacing,
193
+ features: inherited?.features,
189
194
  });
190
195
  return { width: layout.width / s, height: layout.height / s };
191
196
  }
@@ -6,6 +6,7 @@ export { ThemeProvider, useDirection, useTheme } from './theme.js';
6
6
  export {
7
7
  anchorArea,
8
8
  anchorRect,
9
+ anchorScreenRect,
9
10
  centerRect,
10
11
  screenRect,
11
12
  useAnchor,
@@ -10,6 +10,7 @@
10
10
  import React, { useContext, useMemo, useState } from 'react';
11
11
  import { useAppearanceWhen } from '../appearancehooks.js';
12
12
  import { EnvValue, registerFrameProvider } from '../frame/env.js';
13
+ import { THEME_SCOPE } from '../nodes/kinds.js';
13
14
  import {
14
15
  DarkTheme,
15
16
  DefaultTheme,
@@ -30,8 +31,9 @@ export { DarkTheme, DefaultTheme, resolveTheme };
30
31
  // has to merge again.
31
32
  const ThemeContext = React.createContext(null);
32
33
 
33
- // The provider's box fills its parent, which is what an app-level provider
34
- // wants; `style` is there for the ones that wrap a single control.
34
+ // Inside a window the provider's box fills its parent, which is what an
35
+ // app-level provider wants; `style` is there for the ones that wrap a single
36
+ // control. Above the windows there is nothing to fill, and neither applies.
35
37
  const FILL = Object.freeze({ flexGrow: 1 });
36
38
 
37
39
  /**
@@ -47,6 +49,25 @@ const FILL = Object.freeze({ flexGrow: 1 });
47
49
  * Skip the second and `<ThemeProvider value={dark}>` over
48
50
  * `<box style={{ color: '$text' }}>` silently paints nothing (#119).
49
51
  *
52
+ * ## Where it goes
53
+ *
54
+ * **Above the windows, or inside one.** At the root it draws nothing and the
55
+ * windows under it take the palette — whatever shape they arrive in: a
56
+ * component that renders one, a window that is closed for now, several at
57
+ * once (#584).
58
+ *
59
+ * ```jsx
60
+ * root.render(
61
+ * <ThemeProvider colorScheme={dark ? 'dark' : 'light'}>
62
+ * <App />
63
+ * </ThemeProvider>,
64
+ * );
65
+ * ```
66
+ *
67
+ * Inside a window it is a box that fills its parent, with `style` for the
68
+ * rest. A window nested under it is handed on to the window the provider is
69
+ * directly inside, with the palette, since a window nests only in a window.
70
+ *
50
71
  * ## What "already in force" means
51
72
  *
52
73
  * **The desktop's palette.** With no provider at all an app is dark on a dark
@@ -134,7 +155,13 @@ export function ThemeProvider({
134
155
  h(
135
156
  EnvValue,
136
157
  { k: THEME_ENV_KEY, value: theme },
137
- planted(children, theme, boxStyle),
158
+ // The node the palette is planted on (nodes/kinds.js). Written the same
159
+ // wherever the provider is: the renderer knows whether that is inside a
160
+ // window, where it is a box, or above the windows, where it is a node
161
+ // that draws nothing — which a provider could never tell from its
162
+ // children, since a component, a closed window and a fragment all look
163
+ // alike from here (#584).
164
+ h(THEME_SCOPE, { theme, style: boxStyle }, children),
138
165
  ),
139
166
  );
140
167
  }
@@ -149,33 +176,10 @@ export const THEME_ENV_KEY = 'react-x11:theme';
149
176
  // the complete merged palette, so it wins every token; a pane's own inner
150
177
  // ThemeProvider still overrides below it, which is the opt-out a pane that
151
178
  // wants its own look already has.
152
- registerFrameProvider(
153
- THEME_ENV_KEY,
154
- (value, children) => h(ThemeProvider, { value }, children),
155
- // directly around the pane's window: `planted` puts the palette on a
156
- // window among its direct children, and the window is where it must land
157
- { innermost: true },
179
+ registerFrameProvider(THEME_ENV_KEY, (value, children) =>
180
+ h(ThemeProvider, { value }, children),
158
181
  );
159
182
 
160
- /**
161
- * The node that carries the palette into the tree. Normally a box — but a
162
- * `<window>` may only be a root child or nested in another window, never
163
- * inside a box, so a provider above one plants the prop on the windows
164
- * themselves instead of coming between them. An explicit `theme` on a child
165
- * still wins, since that is what it means everywhere else.
166
- */
167
- function planted(children, theme, style) {
168
- const kids = React.Children.toArray(children);
169
- if (kids.some((k) => React.isValidElement(k) && k.type === 'window')) {
170
- return kids.map((k) =>
171
- React.isValidElement(k)
172
- ? React.cloneElement(k, { theme: k.props.theme ?? theme })
173
- : k,
174
- );
175
- }
176
- return h('box', { theme, style }, children);
177
- }
178
-
179
183
  /**
180
184
  * The palette in force here — already merged over any outer provider, and the
181
185
  * same object the provider planted in the tree, so `useTheme()` and a `$token`
@@ -22,15 +22,26 @@
22
22
 
23
23
  import { useEffect, useState } from 'react';
24
24
 
25
+ import { useAppOrNull } from './appcontext.js';
25
26
  import { sessionBus } from './bus.js';
26
- import { NO_CAPABILITY, desktopCapability } from './capabilities.js';
27
+ import {
28
+ NO_CAPABILITY,
29
+ capabilityNow,
30
+ desktopCapability,
31
+ } from './capabilities.js';
32
+
33
+ /** No answer yet: nothing available, and not settled. */
34
+ const PENDING = Object.freeze({ ...NO_CAPABILITY, settled: false });
35
+
36
+ /** A probe's answer, as the hook hands it out. */
37
+ const settledState = (result) => Object.freeze({ ...result, settled: true });
27
38
 
28
39
  /** Value equality over the two levels a capability result has. */
29
40
  function same(a, b) {
30
41
  if (a === b) return true;
31
42
  if (!a || !b) return false;
32
43
  if (a.available !== b.available || a.backend !== b.backend) return false;
33
- if (a.reason !== b.reason) return false;
44
+ if (a.reason !== b.reason || a.settled !== b.settled) return false;
34
45
  const ka = Object.keys(a.features);
35
46
  const kb = Object.keys(b.features);
36
47
  if (ka.length !== kb.length) return false;
@@ -59,7 +70,18 @@ function same(a, b) {
59
70
  * Capability names: `'notifications'`, `'tray'`, `'launcher'`.
60
71
  */
61
72
  export function useDesktopCapability(name) {
62
- const [state, setState] = useState(NO_CAPABILITY);
73
+ // The app this component is rendered into, when there is one: a process
74
+ // with several connections has several answers, and the one that matters
75
+ // here is this tree's.
76
+ const app = useAppOrNull() ?? undefined;
77
+ // Settled from the first frame where the answer needed no asking — the
78
+ // Cocoa app's tray and Dock tile — and pending everywhere else until the
79
+ // first probe answers, so an app can hold back its fallback instead of
80
+ // flashing it.
81
+ const [state, setState] = useState(() => {
82
+ const now = capabilityNow(name, { app });
83
+ return now ? settledState(now) : PENDING;
84
+ });
63
85
 
64
86
  useEffect(() => {
65
87
  let cancelled = false;
@@ -68,11 +90,12 @@ export function useDesktopCapability(name) {
68
90
  let onChanged = null;
69
91
 
70
92
  const probe = () => {
71
- desktopCapability(name)
93
+ desktopCapability(name, { app })
72
94
  .then((next) => {
73
95
  if (cancelled) return;
74
96
  // Replaced only when it differs by value: see the header.
75
- setState((prev) => (same(prev, next) ? prev : next));
97
+ const settled = settledState(next);
98
+ setState((prev) => (same(prev, settled) ? prev : settled));
76
99
  })
77
100
  .catch(() => {});
78
101
  };
@@ -131,7 +154,7 @@ export function useDesktopCapability(name) {
131
154
  await ref?.release();
132
155
  })();
133
156
  };
134
- }, [name]);
157
+ }, [name, app]);
135
158
 
136
159
  return state;
137
160
  }
@@ -40,11 +40,9 @@ async function showBuiltin(app, theme, props) {
40
40
  const root = await createRoot({ app });
41
41
  try {
42
42
  return await new Promise((resolve) => {
43
- // `<FileDialog>` renders a `<window>`, and a window may only be a root
44
- // child or nested in another window — so the palette travels as a prop
45
- // and the dialog installs the provider *inside* its own window. Wrapping
46
- // it here in `<ThemeProvider>` would put a `<box>` between the root and
47
- // the window, which throws.
43
+ // The dialog is on a root of its own, which no provider in the app's
44
+ // tree reaches — so the palette the caller resolved travels as a prop,
45
+ // and the dialog puts it on its window and in a provider inside it.
48
46
  root.render(
49
47
  React.createElement(FileDialog, { ...props, theme, onDone: resolve }),
50
48
  );
@@ -53,10 +53,7 @@ const h = React.createElement;
53
53
  /** Recreate the bridged providers around the pane's window, outermost
54
54
  * first — the parent's own nesting order, which the env Map records and
55
55
  * structured clone preserves. A key the pane never registered wraps
56
- * nothing: no module in this process reads it. Providers registered
57
- * `innermost` (the theme) wrap directly around the window, inside the
58
- * rest — see registerFrameProvider (src/frame/env.js) for why the
59
- * adjacency matters. */
56
+ * nothing: no module in this process reads it. */
60
57
  function wrapEnv(env, inner) {
61
58
  const wrap = (tree, [key, value]) => {
62
59
  const registered = registeredFrameContext(key);
@@ -69,16 +66,8 @@ function wrapEnv(env, inner) {
69
66
  tree,
70
67
  );
71
68
  };
72
- const entries = [...env];
73
- const isInnermost = ([key]) =>
74
- registeredFrameContext(key)?.innermost === true;
75
69
  let tree = inner;
76
- for (const entry of entries.filter(isInnermost).reverse()) {
77
- tree = wrap(tree, entry);
78
- }
79
- for (const entry of entries.filter((e) => !isInnermost(e)).reverse()) {
80
- tree = wrap(tree, entry);
81
- }
70
+ for (const entry of [...env].reverse()) tree = wrap(tree, entry);
82
71
  return tree;
83
72
  }
84
73
 
@@ -118,13 +107,12 @@ function Bridge({ Component, store, rect, invoke, onReady }) {
118
107
  }, [onReady]);
119
108
 
120
109
  // The bridged providers wrap the *window*, not the pane component — the
121
- // same position they held in the host. ThemeProvider plants the palette
122
- // on a window it finds among its children (theme.js, `planted`), and the
123
- // window is where it has to land: the window's own background follows the
124
- // palette, and it is the top of the node tree every `$token` beneath
125
- // resolves through. Mounted inside the window, the palette reached a box
126
- // and the window kept resolving against the pane process's own desktop —
127
- // a dark-desktop pane in a light-themed app, wrong in both directions.
110
+ // same position they held in the host. The theme has to reach the window
111
+ // itself: the window's own background follows the palette, and it is the
112
+ // top of the node tree every `$token` beneath resolves through. Mounted
113
+ // inside the window, the palette reached a box and the window kept
114
+ // resolving against the pane process's own desktop a dark-desktop pane
115
+ // in a light-themed app, wrong in both directions.
128
116
  return wrapEnv(
129
117
  snapshot.env,
130
118
  h(
package/src/frame/env.js CHANGED
@@ -69,17 +69,9 @@ export function registeredFrameContext(key) {
69
69
  * than `Context.Provider`, like `ThemeProvider`, which also plants the
70
70
  * palette on a node so `$token` styles resolve. `render(value, children)`
71
71
  * returns the wrapped element.
72
- *
73
- * `innermost: true` puts the provider directly around the pane's window,
74
- * inside every other bridged provider. ThemeProvider needs the adjacency:
75
- * it plants the palette on a window it finds among its *direct* children,
76
- * and the window is where the palette must land — the window's own
77
- * background follows it, and the node tree under it is what every `$token`
78
- * resolves through. Plain context providers have no such constraint and
79
- * keep the host's nesting order outside.
80
72
  */
81
- export function registerFrameProvider(key, render, { innermost = false } = {}) {
82
- registry.set(key, { render, innermost });
73
+ export function registerFrameProvider(key, render) {
74
+ registry.set(key, { render });
83
75
  }
84
76
 
85
77
  /**