react-x11 2.9.1 → 2.9.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.9.1",
3
+ "version": "2.9.2",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -7,10 +7,12 @@ import { useAppOrNull } from '../appcontext.js';
7
7
  import {
8
8
  ABS_FILL,
9
9
  Bezel,
10
+ NATIVE_BAND,
10
11
  NATIVE_RING,
11
12
  TITLE_BASELINE,
12
13
  bezelNatural,
13
14
  bezelShadow,
15
+ nativeFootprintStyle,
14
16
  nativeTitleStyle,
15
17
  pressWash,
16
18
  useNativeControls,
@@ -94,64 +96,73 @@ export function Button({
94
96
  'box',
95
97
  {
96
98
  theme,
97
- role: 'button',
98
- ...props,
99
- ...boxProps,
100
- style: [
101
- controlStyle,
102
- {
103
- flexDirection: 'row',
104
- alignItems: 'center',
105
- justifyContent: 'center',
106
- gap: small ? 6 : 8,
107
- // AppKit's metrics, not the palette's: a native bezel is
108
- // designed at its own height, and stretching it is what this
109
- // mode exists to avoid. Width still follows the label.
110
- height: nat.height,
111
- paddingLeft: small ? 10 : 14,
112
- paddingRight: small ? 10 : 14,
113
- // The natural box is the bezel's footprint, shadow included;
114
- // the body is what the title is placed against — and placed,
115
- // not centred (`TITLE_BASELINE`). A label centred by its
116
- // capitals sat 1pt low beside a native button.
117
- paddingTop: shadow.top,
118
- paddingBottom: shadow.bottom + TITLE_BASELINE[controlSize],
119
- // The keyboard ring is the renderer's, on this box — shaped by
120
- // the bezel's corners and hugging it, as AppKit's is
121
- // (`NATIVE_RING`), rather than the palette's offset rectangle.
122
- borderRadius: small ? 5 : 6,
123
- ':focus-visible': {
124
- outlineWidth: NATIVE_RING.width,
125
- outlineOffset: NATIVE_RING.offset,
126
- },
127
- color: disabled
128
- ? theme.textMuted
129
- : primary
130
- ? theme.accentText
131
- : theme.text,
132
- },
133
- style,
134
- ],
99
+ // The footprint the caller's style sizes; the button below keeps
100
+ // AppKit's height inside it, centred (`nativeFootprintStyle`).
101
+ style: [nativeFootprintStyle(nat), style],
135
102
  },
136
- h(Bezel, {
137
- kind: 'push',
138
- controlSize,
139
- enabled: !disabled,
140
- // the Return-key accent fill is AppKit's own "default button"
141
- isDefault: primary && !disabled,
142
- style: ABS_FILL,
143
- }),
144
- labelContent(children ?? label, nativeTitleStyle(controlSize)),
145
- // The press answer. Last child on purpose: `:active` marks the
146
- // pressed node and its ancestors, and the topmost child is what the
147
- // press lands on. No hover tint — AppKit buttons have none.
148
- h('box', {
149
- style: [
150
- ABS_FILL,
151
- { borderRadius: small ? 5 : 6 },
152
- !disabled && { ':active': { backgroundColor: pressWash(theme) } },
153
- ],
154
- }),
103
+ h(
104
+ 'box',
105
+ {
106
+ theme,
107
+ role: 'button',
108
+ ...props,
109
+ ...boxProps,
110
+ style: [
111
+ controlStyle,
112
+ NATIVE_BAND,
113
+ {
114
+ flexDirection: 'row',
115
+ alignItems: 'center',
116
+ justifyContent: 'center',
117
+ gap: small ? 6 : 8,
118
+ // AppKit's metrics, not the palette's: a native bezel is
119
+ // designed at its own height, and stretching it is what this
120
+ // mode exists to avoid. Width still follows the label.
121
+ height: nat.height,
122
+ paddingLeft: small ? 10 : 14,
123
+ paddingRight: small ? 10 : 14,
124
+ // The natural box is the bezel's footprint, shadow included;
125
+ // the body is what the title is placed against — and placed,
126
+ // not centred (`TITLE_BASELINE`). A label centred by its
127
+ // capitals sat 1pt low beside a native button.
128
+ paddingTop: shadow.top,
129
+ paddingBottom: shadow.bottom + TITLE_BASELINE[controlSize],
130
+ // The keyboard ring is the renderer's, on this box — shaped by
131
+ // the bezel's corners and hugging it, as AppKit's is
132
+ // (`NATIVE_RING`), rather than the palette's offset rectangle.
133
+ borderRadius: small ? 5 : 6,
134
+ ':focus-visible': {
135
+ outlineWidth: NATIVE_RING.width,
136
+ outlineOffset: NATIVE_RING.offset,
137
+ },
138
+ color: disabled
139
+ ? theme.textMuted
140
+ : primary
141
+ ? theme.accentText
142
+ : theme.text,
143
+ },
144
+ ],
145
+ },
146
+ h(Bezel, {
147
+ kind: 'push',
148
+ controlSize,
149
+ enabled: !disabled,
150
+ // the Return-key accent fill is AppKit's own "default button"
151
+ isDefault: primary && !disabled,
152
+ style: ABS_FILL,
153
+ }),
154
+ labelContent(children ?? label, nativeTitleStyle(controlSize)),
155
+ // The press answer. Last child on purpose: `:active` marks the
156
+ // pressed node and its ancestors, and the topmost child is what the
157
+ // press lands on. No hover tint — AppKit buttons have none.
158
+ h('box', {
159
+ style: [
160
+ ABS_FILL,
161
+ { borderRadius: small ? 5 : 6 },
162
+ !disabled && { ':active': { backgroundColor: pressWash(theme) } },
163
+ ],
164
+ }),
165
+ ),
155
166
  );
156
167
  }
157
168
  const background = !solid
@@ -9,11 +9,13 @@ import { Icon } from './Icon.js';
9
9
  import {
10
10
  ABS_FILL,
11
11
  Bezel,
12
+ NATIVE_BAND,
12
13
  NATIVE_MENU,
13
14
  NATIVE_RING,
14
15
  TITLE_BASELINE,
15
16
  bezelNatural,
16
17
  bezelShadow,
18
+ nativeFootprintStyle,
17
19
  nativeTitleStyle,
18
20
  pressWash,
19
21
  useNativeControls,
@@ -438,7 +440,11 @@ export function Select({
438
440
  if (open) scrollRef.current?.scrollIntoView(activeRef.current);
439
441
  }, [open, activeIndex]);
440
442
 
441
- return h(
443
+ // AppKit's own popup metrics, which the trigger below is laid out at —
444
+ // and only the trigger: the footprint it sits in is the caller's to size.
445
+ const nat = nativeControls ? bezelNatural(app, 'popup') : null;
446
+
447
+ const trigger = h(
442
448
  'box',
443
449
  {
444
450
  theme,
@@ -465,7 +471,8 @@ export function Select({
465
471
  flexDirection: 'row',
466
472
  alignItems: 'center',
467
473
  gap: 8,
468
- height: bezelNatural(app, 'popup').height,
474
+ ...NATIVE_BAND,
475
+ height: nat.height,
469
476
  paddingLeft: TRIGGER_PAD_LEFT,
470
477
  paddingRight: 26,
471
478
  // The title sits where NSPopUpButtonCell puts it — on the
@@ -515,7 +522,10 @@ export function Select({
515
522
  ':hover': { backgroundColor: theme.surfaceHover },
516
523
  ':active': { backgroundColor: theme.surfaceActive },
517
524
  },
518
- style,
525
+ // The caller's style, on the trigger where the trigger is the whole
526
+ // control. Under a native bezel it sizes the footprint below instead
527
+ // and the trigger keeps AppKit's height (`nativeFootprintStyle`).
528
+ !nativeControls && style,
519
529
  ],
520
530
  },
521
531
  // `pressed` while the menu is down: AppKit's popup answers being open
@@ -635,4 +645,11 @@ export function Select({
635
645
  ),
636
646
  ),
637
647
  );
648
+
649
+ // The bezel and its title are one unit at NSPopUpButton's height; the box
650
+ // around them is what a `height`, a `flexGrow` or a parent's align-stretch
651
+ // is free to make taller, with the control centred in it (issue #510).
652
+ return nativeControls
653
+ ? h('box', { theme, style: [nativeFootprintStyle(nat), style] }, trigger)
654
+ : trigger;
638
655
  }
@@ -101,6 +101,47 @@ export function nativeTitleStyle(controlSize = 'regular') {
101
101
  return { alignSelf: 'flex-end', fontSize: CONTROL_FONT_SIZE[controlSize] };
102
102
  }
103
103
 
104
+ /**
105
+ * The **footprint** a native control sits in: the box the caller's style
106
+ * sizes, with the control — bezel, title and press wash, one unit at
107
+ * AppKit's own metrics — centred inside it.
108
+ *
109
+ * Two boxes rather than one, because the title is *placed* and not centred:
110
+ * it rides `TITLE_BASELINE` above the bezel body's bottom edge, and that is
111
+ * only where the cell puts it while the box is exactly `bezelNatural` tall.
112
+ * The caller's style is applied last and flex stretches a box regardless, so
113
+ * it was not always: `style={{ height: 44 }}`, a `height: '100%'` in a taller
114
+ * parent and a bare `flexGrow: 1` each made the box taller, and the two
115
+ * halves — the bezel absolutely filling it, the title glued to its bottom —
116
+ * came apart, the label landing below the control it names (issue #510). The
117
+ * drawn path never had this, because it centres its label: a stretched drawn
118
+ * control is merely roomy, where a stretched native one was broken.
119
+ *
120
+ * So the height a caller can move belongs to a box the control sits in, and
121
+ * the control keeps AppKit's. The default here is exactly today's box — an
122
+ * untouched control lays out as it always did, and the explicit height still
123
+ * resists a row's align-stretch — and the caller's style, applied after it,
124
+ * moves the footprint alone. The control stretches across it (align-stretch,
125
+ * the default), so a footprint given a width is a bezel that wide.
126
+ *
127
+ * The control, not the footprint, stays the *control*: the role, the
128
+ * handlers, the focus ring and the ref go on it, so a press in the slack
129
+ * above a 22pt popup in a 46pt hole does nothing at all — as it does in
130
+ * AppKit — rather than firing a button with no visible answer to the press.
131
+ */
132
+ export function nativeFootprintStyle(nat) {
133
+ return { height: nat.height, justifyContent: 'center' };
134
+ }
135
+
136
+ /**
137
+ * The rest of what a native control's own box says about its size: nothing
138
+ * shrinks it. A squashed bezel is as wrong as a stretched one, and a
139
+ * footprint shorter than the control is a caller asking for something the
140
+ * cell's metrics cannot answer — so it overflows, visibly, rather than
141
+ * quietly drawing a title where the bezel is not.
142
+ */
143
+ export const NATIVE_BAND = Object.freeze({ flexShrink: 0 });
144
+
104
145
  /**
105
146
  * NSMenu's geometry, for the menu a native popup bezel opens — read off
106
147
  * `NSMenu.size` on macOS 15 rather than off a screenshot, so it is the