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.
- package/README.md +37 -0
- package/package.json +3 -3
- package/src/Reconciler.js +85 -22
- package/src/anchor.js +60 -18
- package/src/capabilities.js +29 -4
- package/src/cocoa/app.js +15 -9
- package/src/cocoa/context2d.js +23 -0
- package/src/cocoa/fonts.js +78 -0
- package/src/cocoa/presenter.js +17 -0
- package/src/cocoa/promotion.js +20 -0
- package/src/cocoa/relaunch.js +8 -3
- package/src/cocoa/symbols.js +64 -0
- package/src/cocoa/threaded.js +24 -4
- package/src/cocoa/window.js +362 -139
- package/src/components/ProgressBar.js +1 -1
- package/src/components/Slider.js +72 -39
- package/src/components/anchor.js +7 -2
- package/src/components/index.js +1 -0
- package/src/components/theme.js +32 -28
- package/src/desktopcapabilityhooks.js +29 -6
- package/src/filedialoghooks.js +3 -5
- package/src/frame/childmain.js +8 -20
- package/src/frame/env.js +2 -10
- package/src/icontheme.js +240 -0
- package/src/imagesource.js +83 -1
- package/src/index.js +3 -0
- package/src/node.d.ts +7 -0
- package/src/nodes/animation.js +17 -47
- package/src/nodes/cascade.js +17 -2
- package/src/nodes/image.js +63 -1
- package/src/nodes/kinds.js +12 -0
- package/src/nodes/layout.js +5 -1
- package/src/nodes/node.js +17 -3
- package/src/nodes/paint.js +117 -0
- package/src/nodes/scope.js +259 -0
- package/src/nodes/scrollable.js +53 -6
- package/src/nodes/text.js +2 -0
- package/src/nodes/textarea.js +1 -1
- package/src/nodes/textinput.js +1 -1
- package/src/nodes/window/anchoring.js +45 -18
- package/src/nodes/window/flush.js +6 -5
- package/src/nodes/window/popup.js +10 -0
- package/src/nodes/window/size.js +40 -2
- package/src/nodes/window/window.js +41 -14
- package/src/registry.js +2 -1
- package/src/settings.js +332 -0
- package/src/statusnotifier.js +164 -17
- package/src/styles.js +212 -8
- package/src/symbols.js +200 -0
- package/src/testing/mock-app.js +10 -0
- package/src/trayhooks.js +21 -5
- package/src/types/capabilities.d.ts +13 -1
- package/src/types/components.d.ts +33 -0
- package/src/types/elements.d.ts +57 -6
- package/src/types/style.d.ts +57 -0
- package/src/types/system.d.ts +104 -0
- 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
|
|
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
|
package/src/components/Slider.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 -
|
|
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 -
|
|
95
|
-
: ev.x - rect.x -
|
|
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:
|
|
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:
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
}
|
package/src/components/anchor.js
CHANGED
|
@@ -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.
|
|
181
|
-
//
|
|
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
|
}
|
package/src/components/index.js
CHANGED
package/src/components/theme.js
CHANGED
|
@@ -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
|
-
//
|
|
34
|
-
// wants; `style` is there for the ones that wrap a single
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/filedialoghooks.js
CHANGED
|
@@ -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
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// and the dialog
|
|
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
|
);
|
package/src/frame/childmain.js
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
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
|
|
82
|
-
registry.set(key, { render
|
|
73
|
+
export function registerFrameProvider(key, render) {
|
|
74
|
+
registry.set(key, { render });
|
|
83
75
|
}
|
|
84
76
|
|
|
85
77
|
/**
|