@ng-native/fabric 0.7.0 → 0.9.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/dist/css.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * framework-agnostic and knows nothing about Angular.
7
7
  */
8
8
  import { mixChannels, mixColours, } from "./color-mix.js";
9
+ import { backgroundLayers } from "./background-layers.js";
9
10
  import { ELEMENT_STYLES } from "./element-styles.js";
10
11
  import { relativeColour } from "./relative-colour.js";
11
12
  /** A run of CSS whitespace: spaces, tabs, newlines, carriage returns and form feeds, and no other. */
@@ -221,30 +222,41 @@ const attributeText = (actual, test) => test.insensitive ? String(actual).toLowe
221
222
  *
222
223
  * A prop set to `false` or `null` counts as absent, matching the web, where an attribute is either
223
224
  * present or not and a `disabled` binding that evaluates false removes it. Everything else is
224
- * compared as a string, so `[tabIndex="0"]` works against a numeric prop.
225
+ * compared as a string, so `[tabIndex="0"]` works against a numeric prop. `class` is no prop: it
226
+ * is the element's class list, as the attribute would spell it.
225
227
  */
226
228
  function matchesAttribute(node, test) {
227
- const actual = node.props[test.name];
229
+ const actual = test.name === 'class' ? classAttribute(node) : node.props[test.name];
228
230
  if (actual === undefined || actual === null || actual === false)
229
231
  return false;
230
232
  if (test.operator === undefined)
231
233
  return true;
232
- const value = attributeText(actual, test);
233
- switch (test.operator) {
234
+ // `^=`, `$=`, `*=` and `~=` with an empty value match nothing, as Selectors 4 says.
235
+ if (test.value === '' && !EMPTY_MATCHES.has(test.operator))
236
+ return false;
237
+ return matchesValue(attributeText(actual, test), test.operator, test.value);
238
+ }
239
+ /** The classes of a node in the order they were set, one space between each; none is absent. */
240
+ const classAttribute = (node) => node.classes?.size ? [...node.classes].join(' ') : undefined;
241
+ /** The operators an empty value can still match by: `=` and `|=`. */
242
+ const EMPTY_MATCHES = new Set(['equal', 'dash-match']);
243
+ /** Whether an attribute's value, as `attributeText` gives it, passes an operator's test. */
244
+ function matchesValue(value, operator, expected) {
245
+ switch (operator) {
234
246
  case 'equal':
235
- return value === test.value;
247
+ return value === expected;
236
248
  case 'prefix':
237
- return value.startsWith(test.value);
249
+ return value.startsWith(expected);
238
250
  case 'suffix':
239
- return value.endsWith(test.value);
251
+ return value.endsWith(expected);
240
252
  case 'substring':
241
- return value.includes(test.value);
253
+ return value.includes(expected);
242
254
  case 'includes':
243
255
  // `~=` matches one whole word of a space-separated list.
244
- return value.split(/\s+/).includes(test.value);
256
+ return value.split(/\s+/).includes(expected);
245
257
  case 'dash-match':
246
258
  // `|=` matches the value itself or the value followed by a hyphen, as `lang` uses.
247
- return value === test.value || value.startsWith(`${test.value}-`);
259
+ return value === expected || value.startsWith(`${expected}-`);
248
260
  }
249
261
  }
250
262
  /*
@@ -327,13 +339,15 @@ function matchesCompound(node, compound, sheet) {
327
339
  }
328
340
  }
329
341
  if (compound.hostContext) {
330
- // The host itself counts, as it does on the web.
331
- let found = false;
332
- for (let n = node; n && !found; n = n.parent) {
333
- found = compound.hostContext.some((option) => matchesCompound(n, option, sheet));
342
+ for (const context of compound.hostContext) {
343
+ // The host itself counts, as it does on the web.
344
+ let found = false;
345
+ for (let n = node; n && !found; n = n.parent) {
346
+ found = matchesCompound(n, context, sheet);
347
+ }
348
+ if (!found)
349
+ return false;
334
350
  }
335
- if (!found)
336
- return false;
337
351
  }
338
352
  return true;
339
353
  }
@@ -905,7 +919,34 @@ export function ruleKeys(rule) {
905
919
  const held = parentClass(rule, key);
906
920
  if (held !== undefined)
907
921
  return [`in:${held}`];
908
- return alternativeKeys(key) ?? EVERY_NODE;
922
+ return alternativeKeys(key) ?? attributeKey(key) ?? ancestorKeys(rule, key) ?? EVERY_NODE;
923
+ }
924
+ /**
925
+ * The buckets of a rule for anything inside an element with a class, `.item:focus *`: under the
926
+ * class, or each class the element may have instead. Offered to what has such an element
927
+ * somewhere over it, where the universal bucket has every element walk to the root to find none.
928
+ */
929
+ function ancestorKeys(rule, key) {
930
+ const joined = rule.combinators[rule.combinators.length - 1];
931
+ const above = key.ancestors?.[0] ??
932
+ (joined === 'descendant' ? rule.compounds[rule.compounds.length - 2] : undefined);
933
+ if (!above)
934
+ return undefined;
935
+ if (above.classes.length)
936
+ return [`under:${above.classes[0]}`];
937
+ const any = above.is?.[0];
938
+ if (!any?.length || any.some((one) => !one.classes.length))
939
+ return undefined;
940
+ return any.map((one) => `under:${one.classes[0]}`);
941
+ }
942
+ /**
943
+ * The bucket of a rule that names its element by an attribute and nothing else, `[ngpButton]`:
944
+ * a prop the element has to have, which is how a headless library names every element it
945
+ * styles. Not `class`, which is no prop of the element's to find the bucket by.
946
+ */
947
+ function attributeKey(key) {
948
+ const named = key.attributes?.find((test) => test.name !== 'class');
949
+ return named ? [`attr:${named.name}`] : undefined;
909
950
  }
910
951
  /** A class the node's parent has to have for the rule to match, where its selector says one. */
911
952
  function parentClass(rule, key) {
@@ -958,6 +999,10 @@ export function indexRules(entries, into = { buckets: new Map(), universal: [] }
958
999
  into.universal.push(indexed);
959
1000
  continue;
960
1001
  }
1002
+ if (key.startsWith('attr:'))
1003
+ into.attributes = true;
1004
+ else if (key.startsWith('under:'))
1005
+ into.under = true;
961
1006
  const bucket = into.buckets.get(key);
962
1007
  if (bucket)
963
1008
  bucket.push(indexed);
@@ -990,6 +1035,27 @@ function reach(node, index, found) {
990
1035
  if (around)
991
1036
  for (const name of around)
992
1037
  take(`in:${name}`);
1038
+ if (index.attributes)
1039
+ reachByAttribute(node, take);
1040
+ if (index.under)
1041
+ reachUnder(node, take);
1042
+ }
1043
+ /** The buckets of the classes of every element over a node. */
1044
+ function reachUnder(node, take) {
1045
+ for (let above = node.parent; above; above = above.parent) {
1046
+ if (above.classes)
1047
+ for (const name of above.classes)
1048
+ take(`under:${name}`);
1049
+ }
1050
+ }
1051
+ /** The buckets of the attributes a node has: a prop of its own that is set. */
1052
+ function reachByAttribute(node, take) {
1053
+ for (const name in node.props) {
1054
+ const value = node.props[name];
1055
+ // As `matchesAttribute` has it: a prop that is false is an attribute that is not there.
1056
+ if (value !== undefined && value !== null && value !== false)
1057
+ take(`attr:${name}`);
1058
+ }
993
1059
  }
994
1060
  /**
995
1061
  * The rules worth trying against a node, in cascade order: `order` says which of two comes
@@ -1033,6 +1099,13 @@ const DEFAULT_FONT_SIZE = 16;
1033
1099
  const EMPTY = {};
1034
1100
  /** Stands in for the parent of a root node, so the comparison needs no null case. */
1035
1101
  const ROOT_CONTEXT = {};
1102
+ /** The `scope` over a node with no parent. */
1103
+ const ROOT_SCOPE = {};
1104
+ /**
1105
+ * The `scope` of the one cache every node with nothing to style shares. It is no one node's, so
1106
+ * nothing styled under it, or holding it, is taken to match what it did.
1107
+ */
1108
+ const SHARED_SCOPE = {};
1036
1109
  /** Shared empty token map, for the overwhelmingly common case of a subtree defining none. */
1037
1110
  const NO_TOKENS = {};
1038
1111
  /**
@@ -1082,20 +1155,33 @@ function classReach(sheet) {
1082
1155
  return found;
1083
1156
  const classes = new Map();
1084
1157
  const reads = [];
1158
+ const named = new Set();
1085
1159
  for (const rule of sheet.rules) {
1086
1160
  const every = new Set();
1087
- collectClasses(rule, every, reads);
1088
- const inside = classesInside(rule);
1161
+ collectClasses(rule, every, reads, named);
1162
+ const { own, inside } = classPlaces(rule);
1089
1163
  for (const name of every) {
1090
- if (!inside.has(name))
1164
+ if (!own.has(name) && !inside.has(name))
1091
1165
  classes.set(name, true);
1092
- else
1166
+ if (own.has(name))
1167
+ noteOwn(classes, name);
1168
+ if (inside.has(name))
1093
1169
  noteInside(classes, name, rule);
1094
1170
  }
1095
1171
  }
1096
- reaches.set(sheet, (found = { classes, reads }));
1172
+ reaches.set(sheet, (found = { classes, reads, named }));
1097
1173
  return found;
1098
1174
  }
1175
+ /** Note a rule as one for the element that has a class. */
1176
+ function noteOwn(found, name) {
1177
+ const subjects = found.get(name);
1178
+ if (subjects === true)
1179
+ return;
1180
+ if (subjects)
1181
+ subjects.own = true;
1182
+ else
1183
+ found.set(name, { ...noSubjects(), own: true });
1184
+ }
1099
1185
  /** Note a rule as one that names a class as what its element is inside. */
1100
1186
  function noteInside(found, name, rule) {
1101
1187
  let subjects = found.get(name);
@@ -1119,6 +1205,8 @@ const noSubjects = () => ({
1119
1205
  inside: new Set(),
1120
1206
  any: false,
1121
1207
  });
1208
+ /** The reach of classes that rules are for the element of, and for nothing under it. */
1209
+ const OWN_ALONE = { ...noSubjects(), own: true };
1122
1210
  /** Add to `into` what `from` is for. */
1123
1211
  function addSubjects(into, from) {
1124
1212
  for (const name of from.classes)
@@ -1128,51 +1216,94 @@ function addSubjects(into, from) {
1128
1216
  for (const name of from.inside)
1129
1217
  into.inside.add(name);
1130
1218
  into.any ||= from.any;
1219
+ if (from.own)
1220
+ into.own = true;
1131
1221
  }
1132
1222
  /**
1133
- * The classes a rule names only as what its element is inside: on a compound before the last,
1134
- * with nothing but `>` and a space between there and the element, and nowhere else in the rule.
1223
+ * Where a rule names each class it names, for the classes it names in no other place than these:
1224
+ *
1225
+ * - `own`, on the element the rule is for: its own classes, and those in `:is()` and `:not()` of
1226
+ * it, which are asked of that element;
1227
+ * - `inside`, as what that element is inside: the classes of a compound before the last, with
1228
+ * nothing but `>` and a space between there and the element, and those of what `:is(.box *)`
1229
+ * and `:is(.box > *)` put over it.
1230
+ *
1231
+ * A class named anywhere else in the rule is in neither, whatever else names it: beside the
1232
+ * element, inside `:has()` or `:host-context()`, or in `:not()` of a box the element is in.
1135
1233
  */
1136
- function classesInside(rule) {
1234
+ function classPlaces(rule) {
1235
+ const own = new Set();
1137
1236
  const inside = new Set();
1138
1237
  const elsewhere = new Set();
1139
1238
  const last = rule.compounds.length - 1;
1140
1239
  let under = true;
1141
1240
  for (let index = last; index >= 0; index--) {
1142
1241
  const compound = rule.compounds[index];
1143
- if (index < last) {
1144
- const joined = rule.combinators[index];
1145
- under &&= joined === 'child' || joined === 'descendant';
1242
+ if (index === last) {
1243
+ placeClasses(compound, own, inside, elsewhere);
1244
+ continue;
1146
1245
  }
1246
+ const joined = rule.combinators[index];
1247
+ under &&= joined === 'child' || joined === 'descendant';
1147
1248
  const { classes, ...rest } = compound;
1148
1249
  collectClasses(rest, elsewhere, []);
1149
1250
  for (const name of classes)
1150
- (index < last && under ? inside : elsewhere).add(name);
1251
+ (under ? inside : elsewhere).add(name);
1151
1252
  }
1152
- for (const name of elsewhere)
1253
+ for (const name of elsewhere) {
1254
+ own.delete(name);
1153
1255
  inside.delete(name);
1154
- return inside;
1256
+ }
1257
+ return { own, inside };
1258
+ }
1259
+ /**
1260
+ * The classes of a compound asked of the element a rule is for, into `at`, and of the compounds
1261
+ * it puts over that element, into `inside`. What any other part of it names goes `elsewhere`:
1262
+ * a part read here by name is one this knows the reach of, and one added later is not.
1263
+ */
1264
+ function placeClasses(compound, at, inside, elsewhere) {
1265
+ const { classes, is, not, ancestors, parents, type: _t, id: _i, root: _r, pseudo: _p, ...rest } = compound;
1266
+ for (const name of classes)
1267
+ at.add(name);
1268
+ for (const alternatives of is ?? []) {
1269
+ for (const option of alternatives)
1270
+ placeClasses(option, at, inside, elsewhere);
1271
+ }
1272
+ for (const excluded of not ?? [])
1273
+ placeClasses(excluded, at, inside, elsewhere);
1274
+ // What is over the element is over it whichever of these said so, and what is over that is too.
1275
+ for (const above of [...(ancestors ?? []), ...(parents ?? [])]) {
1276
+ placeClasses(above, inside, inside, elsewhere);
1277
+ }
1278
+ // `attributes` among them: a test of the `class` attribute is answered through `classTests`.
1279
+ collectClasses(rest, elsewhere, []);
1155
1280
  }
1156
1281
  /** What a rule declares, which names no element. */
1157
1282
  const DECLARED = new Set(['declarations', 'important', 'tokens']);
1158
- /** Add each class under `value` to `into`, and each test of the `class` attribute to `reads`. */
1159
- function collectClasses(value, into, reads) {
1283
+ /**
1284
+ * Add each class under `value` to `into`, each test of the `class` attribute to `reads`, and the
1285
+ * name of every attribute asked about to `named`.
1286
+ */
1287
+ function collectClasses(value, into, reads, named) {
1160
1288
  if (value === null || typeof value !== 'object')
1161
1289
  return;
1162
- if (Array.isArray(value))
1163
- return value.forEach((inner) => collectClasses(inner, into, reads));
1290
+ if (Array.isArray(value)) {
1291
+ return value.forEach((inner) => collectClasses(inner, into, reads, named));
1292
+ }
1164
1293
  const part = value;
1165
1294
  if (Array.isArray(part.classes))
1166
1295
  for (const name of part.classes)
1167
1296
  into.add(String(name));
1168
1297
  if (Array.isArray(part.attributes)) {
1169
- for (const test of part.attributes)
1298
+ for (const test of part.attributes) {
1299
+ named?.add(test.name);
1170
1300
  if (test.name === 'class')
1171
1301
  reads.push(test);
1302
+ }
1172
1303
  }
1173
1304
  for (const [key, inner] of Object.entries(value)) {
1174
1305
  if (!DECLARED.has(key))
1175
- collectClasses(inner, into, reads);
1306
+ collectClasses(inner, into, reads, named);
1176
1307
  }
1177
1308
  }
1178
1309
  /**
@@ -1215,12 +1346,22 @@ function writtenFor(of, node) {
1215
1346
  }
1216
1347
  return false;
1217
1348
  }
1349
+ /**
1350
+ * Whether a node has its answer for this commit already. Not one marked since it was given the
1351
+ * one cache every box with nothing to style shares, see `unstyled`: another of them resolved in
1352
+ * this commit has set that cache's epoch, and it is not this node's answer.
1353
+ */
1354
+ function resolvedIn(epoch, cache, node) {
1355
+ return (cache !== null && cache.epoch === epoch && (!node.styleDirty || cache.context !== ROOT_CONTEXT));
1356
+ }
1218
1357
  function emptyCacheFor(epoch, generation) {
1219
1358
  return {
1220
1359
  epoch,
1221
1360
  generation,
1222
1361
  context: ROOT_CONTEXT,
1223
1362
  parentContext: ROOT_CONTEXT,
1363
+ scope: SHARED_SCOPE,
1364
+ parentScope: SHARED_SCOPE,
1224
1365
  style: EMPTY,
1225
1366
  inherited: EMPTY,
1226
1367
  tokens: NO_TOKENS,
@@ -1491,10 +1632,21 @@ export class StyleResolver {
1491
1632
  if (!sheet || this.noted.has(sheet))
1492
1633
  return;
1493
1634
  this.noted.add(sheet);
1494
- const { classes, reads } = classReach(sheet);
1635
+ const { classes, reads, named } = classReach(sheet);
1495
1636
  for (const [name, how] of classes)
1496
1637
  this.noteReach(name, how);
1497
1638
  this.classTests.push(...reads);
1639
+ for (const name of named)
1640
+ this.named.add(name);
1641
+ }
1642
+ /** The props a selector of some sheet asks about: an id, and each attribute by its name. */
1643
+ named = new Set(['nativeID']);
1644
+ /**
1645
+ * Whether a prop changing can change what any element matches: only where a selector of a
1646
+ * sheet that was ever loaded names it. A sheet loaded later has every element matched again.
1647
+ */
1648
+ reads(prop) {
1649
+ return this.named.has(prop);
1498
1650
  }
1499
1651
  noteReach(name, how) {
1500
1652
  const had = this.reach.get(name);
@@ -1505,20 +1657,38 @@ export class StyleResolver {
1505
1657
  else
1506
1658
  addSubjects(had ?? this.reach.set(name, noSubjects()).get(name), how);
1507
1659
  }
1660
+ /** Whether a class's reach names any element under the one that has it. */
1661
+ reachesUnder(subjects) {
1662
+ return (subjects.any ||
1663
+ subjects.classes.size > 0 ||
1664
+ subjects.types.size > 0 ||
1665
+ subjects.inside.size > 0);
1666
+ }
1508
1667
  /**
1509
1668
  * What classes coming to an element or going from it can restyle: `true` for it and all
1510
- * under it and after it, as any change to it does; the elements under it to restyle, by what
1511
- * rules are for; or null, where no rule names any of them and nothing is styled again.
1669
+ * under it and after it, as any change to it does; the element itself, where `own`, and the
1670
+ * elements under it to match again, by what rules are for; or null, where no rule names any
1671
+ * of them and nothing is styled again.
1512
1672
  */
1513
1673
  reachOf(classes) {
1514
1674
  let under = null;
1675
+ let own = false;
1515
1676
  for (const name of classes) {
1516
1677
  const how = this.reach.get(name);
1517
1678
  if (how === true || this.classTests.some((test) => answers(test, name)))
1518
1679
  return true;
1519
- if (how)
1680
+ if (!how)
1681
+ continue;
1682
+ own ||= how.own === true;
1683
+ // Made only for a class that reaches under: most are a rule's own, and this is asked for
1684
+ // every element as it is given its classes.
1685
+ if (this.reachesUnder(how))
1520
1686
  addSubjects((under ??= noSubjects()), how);
1521
1687
  }
1688
+ if (!under)
1689
+ return own ? OWN_ALONE : null;
1690
+ if (own)
1691
+ under.own = true;
1522
1692
  return under;
1523
1693
  }
1524
1694
  /** Whether one of the rules `subjects` stands for could be for a node. */
@@ -1579,7 +1749,7 @@ export class StyleResolver {
1579
1749
  const cached = node.styleCache;
1580
1750
  // Within one commit a node is asked for its inherited map once per descendant, so this is the
1581
1751
  // hot path and it must not walk anywhere.
1582
- if (cached !== null && cached.epoch === epoch)
1752
+ if (resolvedIn(epoch, cached, node))
1583
1753
  return cached;
1584
1754
  const parent = node.parent ? this.resolve(node.parent, epoch) : null;
1585
1755
  const parentContext = parent ? parent.context : ROOT_CONTEXT;
@@ -1597,33 +1767,66 @@ export class StyleResolver {
1597
1767
  this.justMatched = null;
1598
1768
  if (kept)
1599
1769
  return kept;
1770
+ return this.styledAgain(node, epoch, parent, found);
1771
+ }
1772
+ /** A node styled now, from the rules found for it or from those it kept: see `matchesStand`. */
1773
+ styledAgain(node, epoch, parent, found) {
1774
+ const cached = node.styleCache;
1775
+ const parentContext = parent ? parent.context : ROOT_CONTEXT;
1600
1776
  const parentInherited = parent ? parent.inherited : EMPTY;
1777
+ const parentScope = parent ? parent.scope : ROOT_SCOPE;
1778
+ const stands = this.matchesStand(cached, node, parentScope);
1779
+ // The scope it keeps, or none: it then has a new one, which its new `context` serves as.
1780
+ const scope = stands ? cached.scope : null;
1601
1781
  // A box the engine made around loose text is in no template, so no rule matches it.
1602
1782
  if (node.anonymous || this.hasNoRules(node)) {
1603
- return this.unstyled(node, epoch, parent, parentContext, parentInherited);
1783
+ return this.unstyled(node, epoch, parent, parentInherited, scope, parentScope);
1604
1784
  }
1605
1785
  styleStats.nodesResolved++;
1606
- const matched = found ?? this.matchedBy(node);
1786
+ // Its own rules are found again for a change of its own, and not for one above it.
1787
+ const rules = stands && !node.ownDirty ? cached.rules : undefined;
1788
+ const matched = found ?? rules ?? this.matchedBy(node);
1607
1789
  const parentTokens = parent ? parent.tokens : this.tokensOnRoot;
1608
1790
  this.heir(node, parent, matched);
1609
1791
  const styled = this.styled(node, matched, parentTokens, parentInherited);
1610
- const cache = this.cached(epoch, parentContext, styled, matched);
1792
+ const cache = this.cached(epoch, parentContext, styled, matched, scope, parentScope);
1611
1793
  node.styleCache = cache;
1612
1794
  node.styleDirty = false;
1795
+ node.ownDirty = false;
1613
1796
  node.hasDirty = false;
1614
1797
  node.stateDirty = false;
1615
1798
  return cache;
1616
1799
  }
1800
+ /**
1801
+ * Whether what an element could match is as it was when it was last styled: nothing that can
1802
+ * change it was marked on the element, its parent's `scope` is the one it was styled under,
1803
+ * and no sheet that could match it has come since. It is being styled again all the same, for
1804
+ * a change of its own that alters nothing under it, or for what it inherits.
1805
+ */
1806
+ matchesStand(cached, node, parentScope) {
1807
+ return (cached !== null &&
1808
+ !node.styleDirty &&
1809
+ !node.hasDirty &&
1810
+ !node.stateDirty &&
1811
+ cached.scope !== SHARED_SCOPE &&
1812
+ parentScope !== SHARED_SCOPE &&
1813
+ cached.parentScope === parentScope &&
1814
+ this.current(cached, node));
1815
+ }
1617
1816
  /**
1618
1817
  * A cache of a node just styled, with a context of its own. It keeps the rules the node
1619
1818
  * matched, for the node to be compared with, once a sheet has a use for them: see `tracksHas`.
1620
1819
  */
1621
- cached(epoch, parentContext, styled, matched) {
1820
+ cached(epoch, parentContext, styled, matched, scope, parentScope) {
1821
+ const context = {};
1622
1822
  const cache = {
1623
1823
  epoch,
1624
1824
  generation: this.generation,
1625
- context: {},
1825
+ context,
1626
1826
  parentContext,
1827
+ scope: scope ?? context,
1828
+ parentScope,
1829
+ rules: matched,
1627
1830
  ...styled,
1628
1831
  };
1629
1832
  if (this.tracksHas)
@@ -1646,6 +1849,7 @@ export class StyleResolver {
1646
1849
  const stands = (node.hasDirty === true || node.stateDirty === true) &&
1647
1850
  cached?.matched !== undefined &&
1648
1851
  !node.styleDirty &&
1852
+ !node.ownDirty &&
1649
1853
  cached.parentContext === parentContext &&
1650
1854
  this.current(cached, node);
1651
1855
  if (!stands)
@@ -1685,6 +1889,7 @@ export class StyleResolver {
1685
1889
  const stands = node.stateDirty === true &&
1686
1890
  cached !== null &&
1687
1891
  !node.styleDirty &&
1892
+ !node.ownDirty &&
1688
1893
  !node.hasDirty &&
1689
1894
  cached.parentContext === parentContext &&
1690
1895
  this.current(cached, node) &&
@@ -1702,6 +1907,14 @@ export class StyleResolver {
1702
1907
  holds(node, parentContext) {
1703
1908
  return this.reusable(node.styleCache, node, parentContext);
1704
1909
  }
1910
+ /**
1911
+ * Whether a node was last styled under a box holding the one cache shared by every box with
1912
+ * nothing to style. Such boxes hand the same `context` down, so the node cannot tell one from
1913
+ * another by it: moved under another box, it has to be told.
1914
+ */
1915
+ styledUnderShared(node) {
1916
+ return node.styleCache !== null && node.styleCache.parentContext === ROOT_CONTEXT;
1917
+ }
1705
1918
  /**
1706
1919
  * Whether a previous result still stands: nothing about the node changed, nothing above it
1707
1920
  * moved, and the conditions have not.
@@ -1709,6 +1922,7 @@ export class StyleResolver {
1709
1922
  reusable(cached, node, parentContext) {
1710
1923
  return (cached !== null &&
1711
1924
  !node.styleDirty &&
1925
+ !node.ownDirty &&
1712
1926
  !node.hasDirty &&
1713
1927
  !node.stateDirty &&
1714
1928
  cached.parentContext === parentContext &&
@@ -1736,35 +1950,43 @@ export class StyleResolver {
1736
1950
  * carry: the shared object holds none, and the surface root answering with it dropped every
1737
1951
  * token the device had seeded.
1738
1952
  */
1739
- unstyled(node, epoch, parent, parentContext, parentInherited) {
1953
+ unstyled(node, epoch, parent, parentInherited, scope, parentScope) {
1740
1954
  const tokens = parent ? parent.tokens : this.tokensOnRoot;
1955
+ const parentContext = parent ? parent.context : ROOT_CONTEXT;
1741
1956
  // What inline style sets is inherited too, with no rule to read it against.
1742
1957
  const inline = node.inlineInherits ? inlineInherited(node.props['style']) : null;
1743
- if (!inline &&
1958
+ const nothing = !inline &&
1744
1959
  parentInherited === EMPTY &&
1745
1960
  parentContext === ROOT_CONTEXT &&
1746
- tokens === NO_TOKENS &&
1747
- !node.styleDirty) {
1748
- if (this.emptyCache.epoch !== epoch || this.emptyCache.generation !== this.generation) {
1749
- this.emptyCache = emptyCacheFor(epoch, this.generation);
1750
- }
1751
- node.styleCache = this.emptyCache;
1752
- return this.emptyCache;
1753
- }
1961
+ tokens === NO_TOKENS;
1962
+ if (nothing && !node.styleDirty && !node.ownDirty)
1963
+ return this.sharedEmpty(node, epoch);
1964
+ const context = {};
1754
1965
  const passthrough = {
1755
1966
  epoch,
1756
1967
  generation: this.generation,
1757
- context: {},
1968
+ context,
1758
1969
  parentContext,
1970
+ scope: scope ?? context,
1971
+ parentScope,
1759
1972
  style: unstyledStyle(parentInherited, inline),
1760
1973
  inherited: inline ? inheritFrom(parentInherited, inline) : parentInherited,
1761
1974
  tokens,
1762
1975
  };
1763
1976
  node.styleCache = passthrough;
1764
1977
  node.styleDirty = false;
1978
+ node.ownDirty = false;
1765
1979
  node.stateDirty = false;
1766
1980
  return passthrough;
1767
1981
  }
1982
+ /** The one cache of every node with nothing to style and nothing to hand down, given to one more. */
1983
+ sharedEmpty(node, epoch) {
1984
+ if (this.emptyCache.epoch !== epoch || this.emptyCache.generation !== this.generation) {
1985
+ this.emptyCache = emptyCacheFor(epoch, this.generation);
1986
+ }
1987
+ node.styleCache = this.emptyCache;
1988
+ return this.emptyCache;
1989
+ }
1768
1990
  /**
1769
1991
  * The rules worth trying against a node, in cascade order.
1770
1992
  *
@@ -2071,6 +2293,17 @@ export class StyleResolver {
2071
2293
  * A declaration with no token and no usable fallback is dropped rather than written as
2072
2294
  * undefined, which is what a browser does with a value it cannot resolve.
2073
2295
  */
2296
+ /**
2297
+ * What declarations that wait for a node's tokens come to on it, where no rule of the node's
2298
+ * carries them: a keyframe's, which is settled against the element that plays it.
2299
+ */
2300
+ settleFor(node, deferred, epoch) {
2301
+ const { tokens } = this.resolve(node, epoch);
2302
+ const above = node.parent ? this.resolve(node.parent, epoch).inherited : EMPTY;
2303
+ const own = {};
2304
+ this.applyDeferred(deferred, own, tokens, above, null);
2305
+ return own;
2306
+ }
2074
2307
  applyDeferred(deferred, own, tokens, parentInherited, important) {
2075
2308
  for (const declaration of settlingOrder(deferred)) {
2076
2309
  if (declaration.inherit) {
@@ -2087,11 +2320,10 @@ export class StyleResolver {
2087
2320
  if (unread && this.onUnreadDisplay && !outranked('display')) {
2088
2321
  this.reportDisplay(declaration, tokens);
2089
2322
  }
2090
- const value = settled ?? declaration.unset;
2323
+ const layers = settled === undefined ? layersOf(declaration, tokens) : undefined;
2091
2324
  for (const prop of declaration.props) {
2092
- if (outranked(prop))
2093
- continue;
2094
- write(own, prop, value, declaration.line !== undefined);
2325
+ if (!outranked(prop))
2326
+ writeSettled(own, prop, declaration, settled, layers);
2095
2327
  }
2096
2328
  }
2097
2329
  }
@@ -2759,6 +2991,41 @@ function tokenForm(token, kind) {
2759
2991
  const value = formOf(token, kind);
2760
2992
  return value === undefined && kind === 'color' && isCurrentColour(token) ? CURRENT_COLOUR : value;
2761
2993
  }
2994
+ /** What a deferred declaration settled to, written to one of its props. */
2995
+ function writeSettled(own, prop, declaration, settled, layers) {
2996
+ const value = settled ?? declaration.unset;
2997
+ if (declaration.layers)
2998
+ writeLayer(own, prop, value, layers);
2999
+ else
3000
+ write(own, prop, value, declaration.line !== undefined);
3001
+ }
3002
+ /**
3003
+ * One prop of a `background: var()`, whose token may hold the images as well as the colour. The
3004
+ * image is written only where the token holds one: a token that is a colour leaves an image
3005
+ * another rule declared as it is. Each prop is ranked in the cascade by itself, so an image a
3006
+ * later rule declares, or an important one, is not written over by the token's.
3007
+ */
3008
+ function writeLayer(own, prop, value, layers) {
3009
+ if (prop !== BACKGROUND_IMAGE)
3010
+ write(own, prop, layers?.color ?? value, false);
3011
+ else if (layers)
3012
+ own[prop] = layers.images;
3013
+ }
3014
+ const BACKGROUND_IMAGE = 'experimental_backgroundImage';
3015
+ /**
3016
+ * The gradients a `background: var()` reads where its token is no colour: see `backgroundLayers`.
3017
+ * Only the shorthand, which the compiler marks: `background-color: var()` is a colour or nothing.
3018
+ * And only where the token is set as text on an element, which is where a library sets one as the
3019
+ * app runs; a stylesheet's is refused where it is written. The token is the one the declaration
3020
+ * settles on, the first that is set.
3021
+ */
3022
+ function layersOf(declaration, tokens) {
3023
+ if (!declaration.layers)
3024
+ return undefined;
3025
+ const names = [declaration.reference, ...(declaration.alternatives ?? [])];
3026
+ const text = firstSet(names, tokens)?.keyword;
3027
+ return text && /gradient\(/i.test(text) ? backgroundLayers(text) : undefined;
3028
+ }
2762
3029
  const BOUND_NUMBERS = new Set([
2763
3030
  'opacity',
2764
3031
  'flex',