react-x11 2.49.0 → 2.51.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 +5 -1
- package/package.json +4 -4
- package/src/ClickToComponent.js +11 -132
- package/src/DevToolsIntegration.js +126 -42
- package/src/Reconciler.js +26 -10
- package/src/cli/dev.js +162 -19
- package/src/cli/errorwindow.js +368 -0
- package/src/cli/index.js +4 -0
- package/src/cocoa/app.js +22 -0
- package/src/cocoa/presenter.js +3 -1
- package/src/cocoa/promotion.js +10 -11
- package/src/cocoa/relaunch.js +114 -15
- package/src/cocoa/window.js +98 -15
- package/src/currentcolor.js +28 -0
- package/src/decorations.js +21 -9
- package/src/editor.js +151 -0
- package/src/foreignnodes.js +8 -1
- package/src/gloverlay.js +2 -1
- package/src/node.d.ts +5 -0
- package/src/nodes/animation.js +31 -3
- package/src/nodes/boxpaint.js +34 -8
- package/src/nodes/cascade.js +50 -3
- package/src/nodes/damage.js +2 -0
- package/src/nodes/layout.js +9 -17
- package/src/nodes/moveblit.js +5 -5
- package/src/nodes/node.js +8 -0
- package/src/nodes/paint.js +4 -2
- package/src/nodes/queries.js +46 -34
- package/src/nodes/scrollable.js +4 -1
- package/src/nodes/scrollblit.js +10 -7
- package/src/nodes/styling.js +176 -21
- package/src/nodes/window/capabilities.js +1 -1
- package/src/nodes/window/floors.js +63 -18
- package/src/nodes/window/size.js +94 -19
- package/src/refresh/loader.d.ts +16 -0
- package/src/refresh/loader.js +58 -8
- package/src/shorthands.js +1266 -0
- package/src/styles.js +370 -131
- package/src/types/elements.d.ts +5 -2
- package/src/types/style.d.ts +163 -39
- package/src/types/system.d.ts +4 -1
package/src/styles.js
CHANGED
|
@@ -1,17 +1,39 @@
|
|
|
1
1
|
// Style props → yoga setters (layout) and paint metadata. Flat, ink-style
|
|
2
2
|
// props: <box flexDirection="row" padding={8} backgroundColor="#eee">.
|
|
3
|
-
//
|
|
3
|
+
// Lengths are logical pixels — a number, or a string like '12px' that the
|
|
4
|
+
// funnel reads into one — and '50%' / 'auto' pass through to yoga; CSS's
|
|
5
|
+
// shorthands expand into the longhands here (src/shorthands.js).
|
|
4
6
|
// The engine is ours (`./yoga.js`) — the enum tables below are built at
|
|
5
7
|
// module scope, which is what that module's synchronous half is for.
|
|
6
8
|
import { cssColorStraight } from 'ntk/color';
|
|
7
9
|
|
|
8
|
-
import {
|
|
10
|
+
import { isCurrentColor, mentionsCurrentColor } from './currentcolor.js';
|
|
11
|
+
import {
|
|
12
|
+
parseBoxShadow,
|
|
13
|
+
parseLinearGradient,
|
|
14
|
+
shadowSpecs,
|
|
15
|
+
} from './decorations.js';
|
|
9
16
|
import { OBJECT_FITS } from './nodes/fit.js';
|
|
10
17
|
import {
|
|
11
18
|
validateGridValue,
|
|
12
19
|
GRID_CONTAINER_PROPS,
|
|
13
20
|
GRID_ITEM_PROPS,
|
|
14
21
|
} from './grid.js';
|
|
22
|
+
import {
|
|
23
|
+
LENGTH_FORMS,
|
|
24
|
+
LINE_STYLES,
|
|
25
|
+
PROBE,
|
|
26
|
+
SHORTHAND_PROPS,
|
|
27
|
+
SHORTHANDS_OF,
|
|
28
|
+
borderProblem,
|
|
29
|
+
checkShorthand,
|
|
30
|
+
expandStyle,
|
|
31
|
+
isShorthandValue,
|
|
32
|
+
locate,
|
|
33
|
+
needsExpanding,
|
|
34
|
+
readLength,
|
|
35
|
+
readLineHeight,
|
|
36
|
+
} from './shorthands.js';
|
|
15
37
|
import { Yoga, layoutLoaded } from './yoga.js';
|
|
16
38
|
|
|
17
39
|
const FLEX_DIRECTION = {
|
|
@@ -84,28 +106,6 @@ const DISPLAY = {
|
|
|
84
106
|
none: Yoga.DISPLAY_NONE,
|
|
85
107
|
};
|
|
86
108
|
|
|
87
|
-
/**
|
|
88
|
-
* CSS's `flex` shorthand, as the three properties it sets. A number is
|
|
89
|
-
* `flex: <grow> 1 0` — "take this share of what is left, from a base size of
|
|
90
|
-
* nothing" — which is what `flex: 1` means everywhere it is written; the two
|
|
91
|
-
* keywords are CSS's own, `'auto'` for "grow and shrink from my content" and
|
|
92
|
-
* `'none'` for "do neither".
|
|
93
|
-
*
|
|
94
|
-
* It is here rather than in `LAYOUT_APPLIERS` because a shorthand is not a
|
|
95
|
-
* yoga property: it expands into three of them before the diff runs, so
|
|
96
|
-
* `{ flex: 1, flexBasis: 'auto' }` resolves the way CSS does — the longhand
|
|
97
|
-
* after the shorthand wins — and the applier for each still sees a plain
|
|
98
|
-
* value changing.
|
|
99
|
-
*/
|
|
100
|
-
const FLEX_SHORTHAND = {
|
|
101
|
-
none: { flexGrow: 0, flexShrink: 0, flexBasis: 'auto' },
|
|
102
|
-
auto: { flexGrow: 1, flexShrink: 1, flexBasis: 'auto' },
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const isFlexShorthand = (v) =>
|
|
106
|
-
(typeof v === 'number' && Number.isFinite(v) && v >= 0) ||
|
|
107
|
-
(typeof v === 'string' && v in FLEX_SHORTHAND);
|
|
108
|
-
|
|
109
109
|
const OVERFLOW = {
|
|
110
110
|
visible: Yoga.OVERFLOW_VISIBLE,
|
|
111
111
|
hidden: Yoga.OVERFLOW_HIDDEN,
|
|
@@ -219,6 +219,22 @@ const LAYOUT_APPLIERS = {
|
|
|
219
219
|
borderEndWidth: (n, v) => n.setBorder(Yoga.EDGE_END, v),
|
|
220
220
|
};
|
|
221
221
|
|
|
222
|
+
// Every length a setter is handed goes through the length grammar first, so
|
|
223
|
+
// what yoga is asked to take is what the grammar says a length is — at a
|
|
224
|
+
// commit, in a frame, and in development's check, which asks these same
|
|
225
|
+
// appliers (`layoutRefusal`). Yoga's own string handling is `parseFloat`
|
|
226
|
+
// with a `%` check: it read `'2em'` as two pixels, and its border setter
|
|
227
|
+
// coerced a string to a number, so `'2px'` was NaN. By here the funnel has
|
|
228
|
+
// read every length it could into a number (`resolveComputedStyle`), so a
|
|
229
|
+
// string is a percentage, `'auto'`, or one the grammar refuses — which this
|
|
230
|
+
// throws, naming its unit.
|
|
231
|
+
for (const key of LENGTH_FORMS.keys()) {
|
|
232
|
+
const set = LAYOUT_APPLIERS[key];
|
|
233
|
+
if (set === undefined) continue;
|
|
234
|
+
LAYOUT_APPLIERS[key] = (n, v) =>
|
|
235
|
+
set(n, typeof v === 'string' ? readLength(key, v) : v);
|
|
236
|
+
}
|
|
237
|
+
|
|
222
238
|
// Props that only affect painting, not geometry.
|
|
223
239
|
//
|
|
224
240
|
// `outline*` is here rather than beside `border*` in LAYOUT_APPLIERS for the
|
|
@@ -564,13 +580,19 @@ export const STATE_KEYS = [
|
|
|
564
580
|
// could set `padding` or `fontSize` would reflow the tree on pointer move,
|
|
565
581
|
// which is both a jitter bug and the end of the "hover is a repaint, not a
|
|
566
582
|
// React render" property that makes this worth having at all.
|
|
567
|
-
|
|
583
|
+
// The two shorthands that are paint all the way down — `background` sets a
|
|
584
|
+
// colour and a gradient, `outline` a ring's width and colour — are legal here
|
|
585
|
+
// for the same reason their longhands are. `border` is not: its width is
|
|
586
|
+
// layout.
|
|
587
|
+
const STATE_PROPS = new Set([...PAINT_PROPS, 'color', 'background', 'outline']);
|
|
568
588
|
|
|
569
589
|
const STYLE_PROPS = new Set([
|
|
570
590
|
...Object.keys(LAYOUT_APPLIERS),
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
|
|
591
|
+
// CSS's shorthands — `flex`, `border` and its sides, `inset`, `outline`,
|
|
592
|
+
// `background`, the logical pairs — which no setter and no paint reads:
|
|
593
|
+
// `resolveComputedStyle` expands them into the longhands first
|
|
594
|
+
// (src/shorthands.js)
|
|
595
|
+
...SHORTHAND_PROPS,
|
|
574
596
|
...PAINT_PROPS,
|
|
575
597
|
...TEXT_LAYOUT_PROPS,
|
|
576
598
|
...TEXT_PAINT_PROPS,
|
|
@@ -859,6 +881,29 @@ const COLOR_PROPS = new Set([
|
|
|
859
881
|
'color',
|
|
860
882
|
]);
|
|
861
883
|
|
|
884
|
+
/** …and the ones a node paints with, as against `color`, the ink itself. */
|
|
885
|
+
const PAINTED_COLOR_PROPS = [...COLOR_PROPS].filter((key) => key !== 'color');
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Whether this style paints anything in the node's ink — a colour that is
|
|
889
|
+
* `currentColor`, a gradient stop that is, or a shadow that names no colour
|
|
890
|
+
* of its own. Such a node repaints when its ink moves, which nothing in its
|
|
891
|
+
* own style says (`NodeCascade._retext`): a `:hover { color }` on the row
|
|
892
|
+
* around it is where the change was written.
|
|
893
|
+
*
|
|
894
|
+
* Asked only once the ink has moved, of a node that has resolved one, so a
|
|
895
|
+
* style swap pays nothing for it.
|
|
896
|
+
*/
|
|
897
|
+
export function paintsCurrentColor(style) {
|
|
898
|
+
for (const key of PAINTED_COLOR_PROPS) {
|
|
899
|
+
if (isCurrentColor(style[key])) return true;
|
|
900
|
+
}
|
|
901
|
+
if (mentionsCurrentColor(style.backgroundImage)) return true;
|
|
902
|
+
const shadow = style.boxShadow;
|
|
903
|
+
if (typeof shadow !== 'string' || shadow === 'none') return false;
|
|
904
|
+
return Boolean(shadowSpecs(shadow)?.some((s) => s.color === null));
|
|
905
|
+
}
|
|
906
|
+
|
|
862
907
|
/**
|
|
863
908
|
* The style values that are a small language rather than a number — a
|
|
864
909
|
* gradient, a shadow, and grid's track lists, areas and lines — and
|
|
@@ -878,6 +923,65 @@ const COLOR_PROPS = new Set([
|
|
|
878
923
|
* (`isColor`), since a theme is not a call site.
|
|
879
924
|
*/
|
|
880
925
|
function validateValue(key, value, where) {
|
|
926
|
+
// A shorthand — or a longhand written with several values, `padding:
|
|
927
|
+
// '8px 16px'` — is read the way the funnel will expand it, a `$token`
|
|
928
|
+
// standing in for one value of whatever it resolves to. A whole token is
|
|
929
|
+
// one value of a shape nobody knows yet, and is answered by the frame.
|
|
930
|
+
if (isShorthandValue(key, value)) {
|
|
931
|
+
if (isToken(value)) return;
|
|
932
|
+
try {
|
|
933
|
+
checkShorthand(key, value);
|
|
934
|
+
} catch (err) {
|
|
935
|
+
throw locate(err, where);
|
|
936
|
+
}
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
// A length written as a string is read into the number it means before
|
|
940
|
+
// anything below looks at it, so `'12px'` passes where 12 does and `'2em'`
|
|
941
|
+
// is an error naming its unit — yoga's own setters read it as 2.
|
|
942
|
+
if (
|
|
943
|
+
LENGTH_FORMS.has(key) &&
|
|
944
|
+
typeof value === 'string' &&
|
|
945
|
+
value !== '' &&
|
|
946
|
+
!isToken(value) &&
|
|
947
|
+
!mentionsToken(value)
|
|
948
|
+
) {
|
|
949
|
+
try {
|
|
950
|
+
// relative units resolve against the probe: the question here is
|
|
951
|
+
// whether the value reads, and what it comes to is the node's to say
|
|
952
|
+
value = readLength(key, value, undefined, PROBE);
|
|
953
|
+
} catch (err) {
|
|
954
|
+
throw locate(err, where);
|
|
955
|
+
}
|
|
956
|
+
// a percentage radius is of the box, and read where the corner is drawn
|
|
957
|
+
if (key === 'borderRadius') return;
|
|
958
|
+
}
|
|
959
|
+
if (key === 'lineHeight') {
|
|
960
|
+
if (
|
|
961
|
+
value == null ||
|
|
962
|
+
value === '' ||
|
|
963
|
+
isToken(value) ||
|
|
964
|
+
mentionsToken(value)
|
|
965
|
+
) {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
try {
|
|
969
|
+
readLineHeight(value, PROBE);
|
|
970
|
+
} catch (err) {
|
|
971
|
+
throw locate(err, where);
|
|
972
|
+
}
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (key === 'borderStyle') {
|
|
976
|
+
if (value == null || LINE_STYLES.has(value) || isToken(value)) return;
|
|
977
|
+
throw new Error(
|
|
978
|
+
`react-x11: invalid borderStyle ${JSON.stringify(value)} in ${where} ` +
|
|
979
|
+
`(expected ${[...LINE_STYLES].map((v) => `'${v}'`).join(' or ')}` +
|
|
980
|
+
(value === 'none' || value === 'hidden'
|
|
981
|
+
? ' — a box with no border is borderWidth: 0)'
|
|
982
|
+
: ')'),
|
|
983
|
+
);
|
|
984
|
+
}
|
|
881
985
|
const text = TEXT_VALUE_CHECKS.get(key);
|
|
882
986
|
if (text !== undefined) {
|
|
883
987
|
// unset, the way a conditional writes it
|
|
@@ -896,10 +1000,13 @@ function validateValue(key, value, where) {
|
|
|
896
1000
|
// or `''`, which has always painted nothing
|
|
897
1001
|
if (value == null || value === '') return;
|
|
898
1002
|
if (isColor(value) || isToken(value) || mentionsToken(value)) return;
|
|
1003
|
+
// the node's own ink, resolved where it is painted — and on `color`
|
|
1004
|
+
// itself, the ink it inherits, which is what CSS makes of it there
|
|
1005
|
+
if (isCurrentColor(value)) return;
|
|
899
1006
|
throw new Error(
|
|
900
1007
|
`react-x11: invalid ${key} ${JSON.stringify(value)} in ${where} ` +
|
|
901
|
-
"(expected a CSS colour — '#2980b9', 'tomato', 'rgba(0, 0, 0, 0.5)' " +
|
|
902
|
-
"or '
|
|
1008
|
+
"(expected a CSS colour — '#2980b9', 'tomato', 'rgba(0, 0, 0, 0.5)', " +
|
|
1009
|
+
"'transparent' or 'currentColor' — or a $token)",
|
|
903
1010
|
);
|
|
904
1011
|
}
|
|
905
1012
|
if (key === 'letterSpacing') {
|
|
@@ -977,14 +1084,12 @@ function validateValue(key, value, where) {
|
|
|
977
1084
|
isToken(value) || mentionsToken(value) ? null : layoutRefusal(key, value);
|
|
978
1085
|
if (refused) {
|
|
979
1086
|
// an enum's own message lists the choices; yoga's names only itself
|
|
980
|
-
if (refused.message.startsWith('react-x11:'))
|
|
981
|
-
refused.message += `\n in ${where}`;
|
|
982
|
-
throw refused;
|
|
983
|
-
}
|
|
1087
|
+
if (refused.message.startsWith('react-x11:')) throw locate(refused, where);
|
|
984
1088
|
const auto = layoutRefusal(key, 'auto') === null ? ", or 'auto'" : '';
|
|
985
1089
|
throw new Error(
|
|
986
1090
|
`react-x11: invalid ${key} ${JSON.stringify(value)} in ${where} ` +
|
|
987
|
-
|
|
1091
|
+
"(expected a number of logical pixels, like 12 or '12px', or a " +
|
|
1092
|
+
`percentage like '50%'${auto})`,
|
|
988
1093
|
);
|
|
989
1094
|
}
|
|
990
1095
|
const grid = GRID_VALUES.has(key);
|
|
@@ -1017,7 +1122,97 @@ const isAlgorithm = (v) =>
|
|
|
1017
1122
|
(typeof v === 'string' && ALGORITHM_NAME.test(v)) ||
|
|
1018
1123
|
(isOptionBag(v) && typeof v.name === 'string' && ALGORITHM_NAME.test(v.name));
|
|
1019
1124
|
|
|
1020
|
-
|
|
1125
|
+
const CORNERS =
|
|
1126
|
+
'corners are not rounded one at a time yet; borderRadius rounds all four';
|
|
1127
|
+
const PICTURE =
|
|
1128
|
+
'a box paints a colour and a gradient; a picture is an <image>, fitted ' +
|
|
1129
|
+
'with objectFit';
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* The CSS and React Native names an app reaches for that are not properties
|
|
1133
|
+
* here, and what to write instead. Each is the property somebody coming from
|
|
1134
|
+
* the web or from React Native writes first — the ones a typo's distance
|
|
1135
|
+
* cannot find, because the right answer is a different word.
|
|
1136
|
+
*/
|
|
1137
|
+
const INSTEAD = {
|
|
1138
|
+
font:
|
|
1139
|
+
'the font shorthand is not supported; write fontSize, fontFamily, ' +
|
|
1140
|
+
'fontWeight, fontStyle and lineHeight',
|
|
1141
|
+
paddingHorizontal:
|
|
1142
|
+
"that is React Native's name; write paddingInline (start and end), or " +
|
|
1143
|
+
'paddingLeft and paddingRight',
|
|
1144
|
+
paddingVertical: "that is React Native's name; write paddingBlock",
|
|
1145
|
+
marginHorizontal:
|
|
1146
|
+
"that is React Native's name; write marginInline (start and end), or " +
|
|
1147
|
+
'marginLeft and marginRight',
|
|
1148
|
+
marginVertical: "that is React Native's name; write marginBlock",
|
|
1149
|
+
insetInline: 'write start and end',
|
|
1150
|
+
insetBlock: 'write top and bottom',
|
|
1151
|
+
borderTopLeftRadius: CORNERS,
|
|
1152
|
+
borderTopRightRadius: CORNERS,
|
|
1153
|
+
borderBottomLeftRadius: CORNERS,
|
|
1154
|
+
borderBottomRightRadius: CORNERS,
|
|
1155
|
+
boxSizing: 'every box is border-box here, so there is nothing to set',
|
|
1156
|
+
whiteSpace: "write textWrap: 'nowrap' to keep text on one line",
|
|
1157
|
+
placeItems: 'write alignItems and justifyItems',
|
|
1158
|
+
placeContent: 'write alignContent and justifyContent',
|
|
1159
|
+
placeSelf: 'write alignSelf and justifySelf',
|
|
1160
|
+
backgroundSize: PICTURE,
|
|
1161
|
+
backgroundPosition: PICTURE,
|
|
1162
|
+
backgroundRepeat: PICTURE,
|
|
1163
|
+
textDecoration: 'text decorations are not supported',
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
/** Edit distance, for "did you mean": two names one or two keystrokes
|
|
1167
|
+
* apart, which is what a typo is. */
|
|
1168
|
+
function editDistance(a, b) {
|
|
1169
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
1170
|
+
for (let i = 1; i <= a.length; i++) {
|
|
1171
|
+
const row = [i];
|
|
1172
|
+
for (let j = 1; j <= b.length; j++) {
|
|
1173
|
+
row[j] = Math.min(
|
|
1174
|
+
prev[j] + 1,
|
|
1175
|
+
row[j - 1] + 1,
|
|
1176
|
+
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
prev = row;
|
|
1180
|
+
}
|
|
1181
|
+
return prev[b.length];
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* What to say after "unknown style property": the camelCase spelling of a
|
|
1186
|
+
* kebab-case name, what to write instead of a name with no property here,
|
|
1187
|
+
* or the nearest property names. Empty when none of those finds anything —
|
|
1188
|
+
* a name nothing is near is better left unanswered than guessed at.
|
|
1189
|
+
*/
|
|
1190
|
+
function unknownPropertyHint(key) {
|
|
1191
|
+
const camel = key.includes('-')
|
|
1192
|
+
? key.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
|
|
1193
|
+
: key;
|
|
1194
|
+
if (camel !== key && STYLE_PROPS.has(camel)) {
|
|
1195
|
+
return ` — style names are camelCase: ${camel}`;
|
|
1196
|
+
}
|
|
1197
|
+
if (Object.hasOwn(INSTEAD, camel)) return ` — ${INSTEAD[camel]}`;
|
|
1198
|
+
const lower = camel.toLowerCase();
|
|
1199
|
+
const reach = camel.length <= 5 ? 1 : 2;
|
|
1200
|
+
const near = [];
|
|
1201
|
+
for (const name of STYLE_PROPS) {
|
|
1202
|
+
if (Math.abs(name.length - camel.length) > reach) continue;
|
|
1203
|
+
const d =
|
|
1204
|
+
name.toLowerCase() === lower
|
|
1205
|
+
? 0
|
|
1206
|
+
: editDistance(lower, name.toLowerCase());
|
|
1207
|
+
if (d <= reach) near.push([d, name]);
|
|
1208
|
+
}
|
|
1209
|
+
if (near.length === 0) return '';
|
|
1210
|
+
near.sort((a, b) => a[0] - b[0] || a[1].localeCompare(b[1]));
|
|
1211
|
+
const names = near.slice(0, 3).map(([, name]) => name);
|
|
1212
|
+
return ` — did you mean ${names.join(' or ')}?`;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function validateStyle(style, where, nested = false) {
|
|
1021
1216
|
for (const key of Object.keys(style)) {
|
|
1022
1217
|
if (isQuery(key)) {
|
|
1023
1218
|
if (!parseQuery(key)) {
|
|
@@ -1029,7 +1224,7 @@ function validateStyle(style, where) {
|
|
|
1029
1224
|
`"@supports ${SUPPORTS_FEATURES.join('" / "@supports ')}")`,
|
|
1030
1225
|
);
|
|
1031
1226
|
}
|
|
1032
|
-
validateStyle(style[key] ?? {}, `${where} ${key}
|
|
1227
|
+
validateStyle(style[key] ?? {}, `${where} ${key}`, true);
|
|
1033
1228
|
continue;
|
|
1034
1229
|
}
|
|
1035
1230
|
if (isState(key)) {
|
|
@@ -1054,12 +1249,15 @@ function validateStyle(style, where) {
|
|
|
1054
1249
|
continue;
|
|
1055
1250
|
}
|
|
1056
1251
|
if (!STYLE_PROPS.has(key)) {
|
|
1057
|
-
throw new Error(
|
|
1252
|
+
throw new Error(
|
|
1253
|
+
`react-x11: unknown style property "${key}" in ${where}` +
|
|
1254
|
+
unknownPropertyHint(key),
|
|
1255
|
+
);
|
|
1058
1256
|
}
|
|
1059
1257
|
// The one style value that describes motion rather than a state, so the
|
|
1060
1258
|
// one whose mistakes are invisible: a loop with a bad `to` does not draw
|
|
1061
1259
|
// anything wrong, it simply never moves.
|
|
1062
|
-
if (key === 'animation') animationsOf(style, where);
|
|
1260
|
+
if (key === 'animation') animationsOf(style, where, 1, PROBE);
|
|
1063
1261
|
validateValue(key, style[key], where);
|
|
1064
1262
|
if (key === 'container' && !isContainerDeclaration(style[key])) {
|
|
1065
1263
|
throw new Error(
|
|
@@ -1090,13 +1288,12 @@ function validateStyle(style, where) {
|
|
|
1090
1288
|
'this node reads, like { span: 2 })',
|
|
1091
1289
|
);
|
|
1092
1290
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
}
|
|
1291
|
+
}
|
|
1292
|
+
// what no single value can be wrong about on its own: two line styles on
|
|
1293
|
+
// one box. Once, on the whole style, which is where its blocks are.
|
|
1294
|
+
if (!nested) {
|
|
1295
|
+
const problem = borderProblem(style);
|
|
1296
|
+
if (problem) throw new Error(`${problem}\n in ${where}`);
|
|
1100
1297
|
}
|
|
1101
1298
|
}
|
|
1102
1299
|
|
|
@@ -1216,6 +1413,11 @@ const NOT_ANIMATABLE = new Set([
|
|
|
1216
1413
|
// 2.5, is no grid at all, and a track list is a string
|
|
1217
1414
|
...GRID_CONTAINER_PROPS,
|
|
1218
1415
|
...GRID_ITEM_PROPS,
|
|
1416
|
+
// A shorthand never reaches a running style — the funnel has expanded it
|
|
1417
|
+
// into longhands, which are what animate — so a loop on one would run a
|
|
1418
|
+
// property nobody reads. A `transition` keyed by one still reaches its
|
|
1419
|
+
// longhands (`transitionFor`).
|
|
1420
|
+
...SHORTHAND_PROPS,
|
|
1219
1421
|
]);
|
|
1220
1422
|
|
|
1221
1423
|
export const isAnimatableProp = (name) =>
|
|
@@ -1224,13 +1426,22 @@ export const isAnimatableProp = (name) =>
|
|
|
1224
1426
|
/**
|
|
1225
1427
|
* `transition: 150` — every animatable property that changes, over 150ms.
|
|
1226
1428
|
* `transition: { backgroundColor: 150 }` — only these, with their own
|
|
1227
|
-
* durations. Returns
|
|
1429
|
+
* durations. Returns the duration in ms, or 0.
|
|
1430
|
+
*
|
|
1431
|
+
* A duration keyed by a shorthand is its longhands', the nearest shorthand
|
|
1432
|
+
* first, as in CSS: `transition: { border: 150 }` fades a `borderColor`
|
|
1433
|
+
* that `border: '1px solid …'` set, and `{ padding: 200 }` moves each side a
|
|
1434
|
+
* `padding: '8px 16px'` expanded into. The longhand's own entry wins.
|
|
1228
1435
|
*/
|
|
1229
1436
|
export function transitionFor(style, prop) {
|
|
1230
1437
|
const t = style.transition;
|
|
1231
1438
|
if (t == null || !isAnimatableProp(prop)) return 0;
|
|
1232
1439
|
if (typeof t === 'number') return t;
|
|
1233
|
-
|
|
1440
|
+
if (t[prop] != null) return t[prop];
|
|
1441
|
+
for (const shorthand of SHORTHANDS_OF.get(prop) ?? []) {
|
|
1442
|
+
if (t[shorthand] != null) return t[shorthand];
|
|
1443
|
+
}
|
|
1444
|
+
return 0;
|
|
1234
1445
|
}
|
|
1235
1446
|
|
|
1236
1447
|
/**
|
|
@@ -1305,6 +1516,22 @@ const parsedAnimations = new WeakMap();
|
|
|
1305
1516
|
/** What one property's loop may say. */
|
|
1306
1517
|
const LOOP_OPTIONS = ['from', 'to', 'duration', 'easing', 'alternate', 'delay'];
|
|
1307
1518
|
|
|
1519
|
+
/**
|
|
1520
|
+
* One end of a loop on a length, read the way the style's own value is:
|
|
1521
|
+
* `'12px'` travels as 12. A percentage stays one, since two of them have a
|
|
1522
|
+
* midpoint of their own; a value the grammar refuses is left as written, for
|
|
1523
|
+
* the midpoint check to name.
|
|
1524
|
+
*/
|
|
1525
|
+
function loopEnd(prop, value, units) {
|
|
1526
|
+
if (!LENGTH_FORMS.has(prop) || typeof value !== 'string') return value;
|
|
1527
|
+
if (isToken(value) || mentionsToken(value)) return value;
|
|
1528
|
+
try {
|
|
1529
|
+
return readLength(prop, value, undefined, units);
|
|
1530
|
+
} catch {
|
|
1531
|
+
return value;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1308
1535
|
function parseAnimation(spec, where) {
|
|
1309
1536
|
if (typeof spec !== 'object' || spec === null || Array.isArray(spec)) {
|
|
1310
1537
|
throw new Error(
|
|
@@ -1385,7 +1612,12 @@ function parseAnimation(spec, where) {
|
|
|
1385
1612
|
* and both ends checked for a midpoint. Null when there are none, so the
|
|
1386
1613
|
* common case allocates nothing.
|
|
1387
1614
|
*/
|
|
1388
|
-
export function animationsOf(
|
|
1615
|
+
export function animationsOf(
|
|
1616
|
+
style,
|
|
1617
|
+
where = 'a style',
|
|
1618
|
+
scale = 1,
|
|
1619
|
+
units = null,
|
|
1620
|
+
) {
|
|
1389
1621
|
const spec = style.animation;
|
|
1390
1622
|
if (spec == null) return null;
|
|
1391
1623
|
let parsed = parsedAnimations.get(spec);
|
|
@@ -1395,6 +1627,16 @@ export function animationsOf(style, where = 'a style', scale = 1) {
|
|
|
1395
1627
|
}
|
|
1396
1628
|
if (parsed.length === 0) return null;
|
|
1397
1629
|
return parsed.map((entry) => {
|
|
1630
|
+
// The declared ends, read the way the style's own value is: `'12px'`
|
|
1631
|
+
// travels as 12, and a relative length — `'2em'`, `'50vw'` — as what
|
|
1632
|
+
// it comes to for this node, which is why it is read here rather than
|
|
1633
|
+
// in the parse the declaration is cached by.
|
|
1634
|
+
if (LENGTH_FORMS.has(entry.prop)) {
|
|
1635
|
+
const from = loopEnd(entry.prop, entry.from, units);
|
|
1636
|
+
const to = loopEnd(entry.prop, entry.to, units);
|
|
1637
|
+
if (from !== entry.from || to !== entry.to)
|
|
1638
|
+
entry = { ...entry, from, to };
|
|
1639
|
+
}
|
|
1398
1640
|
// The display scale, only where the caller says the style is already in
|
|
1399
1641
|
// device pixels (`_syncLoops` passes the node's). The declared ends are
|
|
1400
1642
|
// logical like everything an app writes; a `from` *defaulted* from the
|
|
@@ -1407,7 +1649,10 @@ export function animationsOf(style, where = 'a style', scale = 1) {
|
|
|
1407
1649
|
...(typeof entry.to === 'number' && { to: entry.to * scale }),
|
|
1408
1650
|
};
|
|
1409
1651
|
}
|
|
1410
|
-
const from =
|
|
1652
|
+
const from =
|
|
1653
|
+
entry.from === undefined
|
|
1654
|
+
? loopEnd(entry.prop, style[entry.prop], units)
|
|
1655
|
+
: entry.from;
|
|
1411
1656
|
if (from === undefined) {
|
|
1412
1657
|
throw new Error(
|
|
1413
1658
|
`react-x11: animation for "${entry.prop}" in ${where} has no "from" ` +
|
|
@@ -1420,6 +1665,15 @@ export function animationsOf(style, where = 'a style', scale = 1) {
|
|
|
1420
1665
|
// resolves the tokens and comes back through here with real values.
|
|
1421
1666
|
const resolved = from === entry.from ? entry : { ...entry, from };
|
|
1422
1667
|
if (unresolvedValue(from) || unresolvedValue(entry.to)) return resolved;
|
|
1668
|
+
if (isCurrentColor(from) || isCurrentColor(entry.to)) {
|
|
1669
|
+
throw new Error(
|
|
1670
|
+
`react-x11: animation for "${entry.prop}" in ${where} runs to or ` +
|
|
1671
|
+
'from currentColor, which a loop cannot hold still: its ends are ' +
|
|
1672
|
+
'fixed for as long as it runs, and currentColor is whatever the ink ' +
|
|
1673
|
+
'is. Name the colour — or loop `color` instead, and the ' +
|
|
1674
|
+
`${entry.prop} written as currentColor follows it every frame.`,
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1423
1677
|
if (interpolate(from, entry.to, 0.5) === null) {
|
|
1424
1678
|
throw new Error(
|
|
1425
1679
|
`react-x11: animation for "${entry.prop}" in ${where} has no ` +
|
|
@@ -1661,14 +1915,23 @@ export const TRANSITION_CONTROL_POINTS = EASING_CONTROL_POINTS['ease-out'];
|
|
|
1661
1915
|
* hoisted style. It is the same substitution either way, so the value that
|
|
1662
1916
|
* *is* a token keeps its fast path and the value that *mentions* one goes
|
|
1663
1917
|
* through the regexp.
|
|
1918
|
+
*
|
|
1919
|
+
* A whole token is `$name` and nothing else. A value that only *starts* with
|
|
1920
|
+
* one is a mention — `padding: '$gutter 0'`, `border: '$hairline solid
|
|
1921
|
+
* $border'` — which the shorthands made ordinary; reading it as one token
|
|
1922
|
+
* looked up a theme entry called `gutter 0`.
|
|
1664
1923
|
*/
|
|
1665
|
-
const
|
|
1924
|
+
const WHOLE_TOKEN = /^\$[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
1925
|
+
const isToken = (v) =>
|
|
1926
|
+
typeof v === 'string' &&
|
|
1927
|
+
v.charCodeAt(0) === 36 /* $ */ &&
|
|
1928
|
+
WHOLE_TOKEN.test(v);
|
|
1666
1929
|
/** `$name`, anywhere in a string. Deliberately the same grammar as a whole
|
|
1667
1930
|
* token, so `'$accent'` and `'linear-gradient($accent, #000)'` cannot
|
|
1668
1931
|
* disagree about what a name is. */
|
|
1669
1932
|
const TOKEN_IN_VALUE = /\$[A-Za-z_][A-Za-z0-9_-]*/g;
|
|
1670
1933
|
const mentionsToken = (v) =>
|
|
1671
|
-
typeof v === 'string' && v.
|
|
1934
|
+
typeof v === 'string' && v.includes('$') && !isToken(v);
|
|
1672
1935
|
|
|
1673
1936
|
/** The nested blocks a token can hide in, one level down: a state block or
|
|
1674
1937
|
* a query block, whose values are style values, and `animation`, whose
|
|
@@ -1847,11 +2110,16 @@ function unknownToken(token, theme, where) {
|
|
|
1847
2110
|
export { validateStyle };
|
|
1848
2111
|
|
|
1849
2112
|
/**
|
|
1850
|
-
* The style the layout is actually run from: the
|
|
1851
|
-
*
|
|
2113
|
+
* The style the layout is actually run from: the shorthands expanded into
|
|
2114
|
+
* their longhands, every length read into a number (src/shorthands.js) — a
|
|
2115
|
+
* relative one against `units`, the node's font sizes and window, which
|
|
2116
|
+
* records what it read so the node can resolve it again when one moves
|
|
2117
|
+
* (nodes/styling.js) — and the defaults `overflow: 'scroll'` implies folded
|
|
2118
|
+
* in. All three are things a
|
|
1852
2119
|
* style *means* rather than things it says, and doing them here — once, on
|
|
1853
|
-
* the resolved object
|
|
1854
|
-
* of yoga
|
|
2120
|
+
* the resolved object, after the state blocks and the queries have merged
|
|
2121
|
+
* into it — is what lets everything downstream stay a flat diff of yoga
|
|
2122
|
+
* properties, and the display scale multiply numbers only.
|
|
1855
2123
|
*
|
|
1856
2124
|
* Returns `style` itself when there is nothing to add, so identity comparisons
|
|
1857
2125
|
* upstream keep meaning "the style did not change".
|
|
@@ -1870,28 +2138,25 @@ export { validateStyle };
|
|
|
1870
2138
|
* the window got. Zeroing the basis fixes the whole ancestor chain at once,
|
|
1871
2139
|
* since the content stops counting towards any of their heights.
|
|
1872
2140
|
*/
|
|
1873
|
-
export function resolveComputedStyle(style) {
|
|
2141
|
+
export function resolveComputedStyle(style, units = null) {
|
|
2142
|
+
const expands = needsExpanding(style);
|
|
1874
2143
|
const scrolls = style.overflow === 'scroll';
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
// after the expansion, so a longhand written beside the shorthand wins
|
|
1892
|
-
for (const key of Object.keys(style)) {
|
|
1893
|
-
if (key !== 'flex') out[key] = style[key];
|
|
1894
|
-
}
|
|
2144
|
+
if (!expands && !scrolls && style.animation == null) return style;
|
|
2145
|
+
// `units` may be a function that makes the context, which is then made
|
|
2146
|
+
// only for a style that has a length to read or a loop to resolve: a
|
|
2147
|
+
// style written in pixels — nearly every one — allocates none
|
|
2148
|
+
const ctx =
|
|
2149
|
+
typeof units === 'function'
|
|
2150
|
+
? expands || style.animation != null
|
|
2151
|
+
? units(style)
|
|
2152
|
+
: null
|
|
2153
|
+
: units;
|
|
2154
|
+
// the loops read the expanded style, so a `from` they default to is the
|
|
2155
|
+
// longhand's value, in pixels, whatever the style wrote it as
|
|
2156
|
+
const out = expands ? expandStyle(style, ctx) : { ...style };
|
|
2157
|
+
const loops =
|
|
2158
|
+
out.animation == null ? null : animationsOf(out, 'a style', 1, ctx);
|
|
2159
|
+
if (!expands && !scrolls && !loops) return style;
|
|
1895
2160
|
if (scrolls) {
|
|
1896
2161
|
if (out.minWidth === undefined) out.minWidth = 0;
|
|
1897
2162
|
if (out.minHeight === undefined) out.minHeight = 0;
|
|
@@ -1923,51 +2188,12 @@ export function resolveComputedStyle(style) {
|
|
|
1923
2188
|
* factors are ratios, `opacity` and `zIndex` are not lengths, and
|
|
1924
2189
|
* `boxShadow` is a string whose lengths are scaled where it is parsed
|
|
1925
2190
|
* (src/decorations.js), because its parse is memoized on the raw string.
|
|
2191
|
+
*
|
|
2192
|
+
* The keys of `LENGTH_FORMS` (src/shorthands.js): a property is scaled
|
|
2193
|
+
* exactly when it is one the length grammar reads, because those are the
|
|
2194
|
+
* same question asked twice — and two lists of it drifted.
|
|
1926
2195
|
*/
|
|
1927
|
-
export const SCALED_LENGTH_PROPS = [
|
|
1928
|
-
'width',
|
|
1929
|
-
'height',
|
|
1930
|
-
'minWidth',
|
|
1931
|
-
'minHeight',
|
|
1932
|
-
'maxWidth',
|
|
1933
|
-
'maxHeight',
|
|
1934
|
-
'flexBasis',
|
|
1935
|
-
'top',
|
|
1936
|
-
'right',
|
|
1937
|
-
'bottom',
|
|
1938
|
-
'left',
|
|
1939
|
-
'start',
|
|
1940
|
-
'end',
|
|
1941
|
-
'margin',
|
|
1942
|
-
'marginTop',
|
|
1943
|
-
'marginRight',
|
|
1944
|
-
'marginBottom',
|
|
1945
|
-
'marginLeft',
|
|
1946
|
-
'marginStart',
|
|
1947
|
-
'marginEnd',
|
|
1948
|
-
'padding',
|
|
1949
|
-
'paddingTop',
|
|
1950
|
-
'paddingRight',
|
|
1951
|
-
'paddingBottom',
|
|
1952
|
-
'paddingLeft',
|
|
1953
|
-
'paddingStart',
|
|
1954
|
-
'paddingEnd',
|
|
1955
|
-
'gap',
|
|
1956
|
-
'rowGap',
|
|
1957
|
-
'columnGap',
|
|
1958
|
-
'borderWidth',
|
|
1959
|
-
'borderTopWidth',
|
|
1960
|
-
'borderRightWidth',
|
|
1961
|
-
'borderBottomWidth',
|
|
1962
|
-
'borderLeftWidth',
|
|
1963
|
-
'borderStartWidth',
|
|
1964
|
-
'borderEndWidth',
|
|
1965
|
-
'borderRadius',
|
|
1966
|
-
'outlineWidth',
|
|
1967
|
-
'outlineOffset',
|
|
1968
|
-
'fontSize',
|
|
1969
|
-
'letterSpacing',
|
|
1970
|
-
];
|
|
2196
|
+
export const SCALED_LENGTH_PROPS = Object.freeze([...LENGTH_FORMS.keys()]);
|
|
1971
2197
|
|
|
1972
2198
|
const SCALED_LENGTHS = new Set(SCALED_LENGTH_PROPS);
|
|
1973
2199
|
|
|
@@ -1979,9 +2205,11 @@ const SCALED_LENGTHS = new Set(SCALED_LENGTH_PROPS);
|
|
|
1979
2205
|
* (`_syncStyle` → `resolveComputedStyle` → here → `_retarget`), so yoga,
|
|
1980
2206
|
* every `this.style.borderRadius ?? 0` at a paint site, and the font size
|
|
1981
2207
|
* the text stack shapes at are all *already* device pixels and none of them
|
|
1982
|
-
* ever multiplies again. Numbers scale
|
|
1983
|
-
*
|
|
1984
|
-
*
|
|
2208
|
+
* ever multiplies again. Numbers scale — and every length is a number by
|
|
2209
|
+
* now, since `resolveComputedStyle` read `'12px'` into 12, which is what
|
|
2210
|
+
* keeps a px string the size it says at 2x; `'50%'` and `'auto'` mean the
|
|
2211
|
+
* same thing at any density and pass through; `hitSlop` scales inside its
|
|
2212
|
+
* number-or-per-side shape.
|
|
1985
2213
|
*
|
|
1986
2214
|
* Never mutates: `flattenStyle` hands back the app's own hoisted object
|
|
1987
2215
|
* when it can, and scaling it in place would corrupt the next render.
|
|
@@ -2344,13 +2572,6 @@ const TEXT_VALUE_CHECKS = new Map([
|
|
|
2344
2572
|
'fontFamily',
|
|
2345
2573
|
[isFontFamily, "a family's name, like 'Inter' or 'monospace'"],
|
|
2346
2574
|
],
|
|
2347
|
-
[
|
|
2348
|
-
'lineHeight',
|
|
2349
|
-
[
|
|
2350
|
-
(v) => lineHeightOf(v) !== undefined,
|
|
2351
|
-
"a multiple of the font's own line height, like lineHeight: 1.5",
|
|
2352
|
-
],
|
|
2353
|
-
],
|
|
2354
2575
|
]);
|
|
2355
2576
|
|
|
2356
2577
|
/** Resolved text style (TextLayout base style) from props + inherited. */
|
|
@@ -2488,6 +2709,24 @@ export function resolveBorderColors(style, direction) {
|
|
|
2488
2709
|
return sides;
|
|
2489
2710
|
}
|
|
2490
2711
|
|
|
2712
|
+
/**
|
|
2713
|
+
* A box's corner radius in the pixels its rect is in: a number as it is, and
|
|
2714
|
+
* a percentage of the box's **shorter side** — the one circle a corner here
|
|
2715
|
+
* can be, where CSS's would be an ellipse taking each side's share — so
|
|
2716
|
+
* `'50%'` is a circle on a square and a pill on anything longer. Read where
|
|
2717
|
+
* the corner is drawn and where its damage is measured, from the rect the
|
|
2718
|
+
* box has then; every reader of a radius goes through here, since a
|
|
2719
|
+
* percentage reaching a `> 0` test is a square box.
|
|
2720
|
+
*/
|
|
2721
|
+
export function radiusOf(style, rect) {
|
|
2722
|
+
const r = style?.borderRadius;
|
|
2723
|
+
if (typeof r === 'number') return r;
|
|
2724
|
+
if (typeof r !== 'string' || !rect || !r.trimEnd().endsWith('%')) return 0;
|
|
2725
|
+
const share = Number.parseFloat(r);
|
|
2726
|
+
if (!Number.isFinite(share)) return 0;
|
|
2727
|
+
return (Math.min(rect.width, rect.height) * share) / 100;
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2491
2730
|
export function resolveHitSlop(value) {
|
|
2492
2731
|
if (value == null) return null;
|
|
2493
2732
|
if (typeof value === 'number') {
|