@ng-native/fabric 0.6.0 → 0.8.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.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A `background` a token holds as text, where it is no single colour: the gradients a library
3
+ * works out as the app runs and sets as a custom property, a colour slider's track or a colour
4
+ * area's two fades over a colour. A stylesheet's gradient is compiled when the app is built; one
5
+ * set on an element is read here, into what a view's `experimental_backgroundImage` takes.
6
+ *
7
+ * ponytail: `linear-gradient()` with a side or an angle and colour stops at percentages, which
8
+ * is what such a library writes. A corner, a length or two positions on a stop, `radial-` and
9
+ * `repeating-` are not read, and the whole background is then nothing, as an invalid one is.
10
+ * Native has no conic gradient at all.
11
+ */
12
+ export interface BackgroundLayers {
13
+ readonly images: readonly object[];
14
+ /** The colour under the images: the last layer, where it is one. */
15
+ readonly color?: string;
16
+ }
17
+ /** The layers a background's text holds, or nothing where any of it is not read. */
18
+ export declare function backgroundLayers(text: string): BackgroundLayers | undefined;
@@ -0,0 +1,70 @@
1
+ import { parseColor } from "./transition.js";
2
+ const SIDES = {
3
+ 'to top': 0,
4
+ 'to right': 90,
5
+ 'to bottom': 180,
6
+ 'to left': 270,
7
+ };
8
+ /** Split at the commas that are not inside brackets. */
9
+ function split(text) {
10
+ const parts = [];
11
+ let depth = 0;
12
+ let from = 0;
13
+ for (let at = 0; at < text.length; at++) {
14
+ const char = text[at];
15
+ if (char === '(')
16
+ depth++;
17
+ else if (char === ')')
18
+ depth--;
19
+ else if (char === ',' && depth === 0) {
20
+ parts.push(text.slice(from, at).trim());
21
+ from = at + 1;
22
+ }
23
+ }
24
+ parts.push(text.slice(from).trim());
25
+ return parts;
26
+ }
27
+ const rgba = (text) => {
28
+ const parsed = parseColor(text);
29
+ return parsed ? `rgba(${parsed[0]}, ${parsed[1]}, ${parsed[2]}, ${parsed[3]})` : undefined;
30
+ };
31
+ /** A direction in degrees, or nothing for a first argument that is a colour stop. */
32
+ function direction(first) {
33
+ const side = SIDES[first.toLowerCase()];
34
+ if (side !== undefined)
35
+ return side;
36
+ const angle = /^(-?[\d.]+)deg$/i.exec(first);
37
+ return angle ? Number(angle[1]) : undefined;
38
+ }
39
+ /** `rgba(…) 40%` as a stop, or nothing for one this does not read. */
40
+ function stop(text) {
41
+ const placed = /^(.*\S)\s+(-?[\d.]+%)$/.exec(text);
42
+ const color = rgba(placed ? placed[1] : text);
43
+ return color === undefined ? undefined : { color, position: placed ? placed[2] : null };
44
+ }
45
+ function linear(layer) {
46
+ const inside = /^linear-gradient\((.*)\)$/is.exec(layer)?.[1];
47
+ if (inside === undefined)
48
+ return undefined;
49
+ const parts = split(inside);
50
+ const angle = direction(parts[0]);
51
+ const stops = (angle === undefined ? parts : parts.slice(1)).map(stop);
52
+ if (stops.length < 2 || stops.some((each) => each === undefined))
53
+ return undefined;
54
+ return {
55
+ type: 'linear-gradient',
56
+ direction: { type: 'angle', value: angle ?? 180 },
57
+ colorStops: stops,
58
+ };
59
+ }
60
+ /** The layers a background's text holds, or nothing where any of it is not read. */
61
+ export function backgroundLayers(text) {
62
+ const layers = split(text.trim());
63
+ const last = layers.at(-1);
64
+ const color = parseColor(last) ? last : undefined;
65
+ const images = (color === undefined ? layers : layers.slice(0, -1)).map(linear);
66
+ if (!images.length || images.some((each) => each === undefined))
67
+ return undefined;
68
+ return { images: images, color };
69
+ }
70
+ //# sourceMappingURL=background-layers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"background-layers.js","sourceRoot":"","sources":["../src/background-layers.ts"],"names":[],"mappings":"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","sourcesContent":["import { parseColor } from './transition.ts';\n\n/**\n * A `background` a token holds as text, where it is no single colour: the gradients a library\n * works out as the app runs and sets as a custom property, a colour slider's track or a colour\n * area's two fades over a colour. A stylesheet's gradient is compiled when the app is built; one\n * set on an element is read here, into what a view's `experimental_backgroundImage` takes.\n *\n * ponytail: `linear-gradient()` with a side or an angle and colour stops at percentages, which\n * is what such a library writes. A corner, a length or two positions on a stop, `radial-` and\n * `repeating-` are not read, and the whole background is then nothing, as an invalid one is.\n * Native has no conic gradient at all.\n */\nexport interface BackgroundLayers {\n readonly images: readonly object[];\n /** The colour under the images: the last layer, where it is one. */\n readonly color?: string;\n}\n\nconst SIDES: Readonly<Record<string, number>> = {\n 'to top': 0,\n 'to right': 90,\n 'to bottom': 180,\n 'to left': 270,\n};\n\n/** Split at the commas that are not inside brackets. */\nfunction split(text: string): string[] {\n const parts: string[] = [];\n let depth = 0;\n let from = 0;\n for (let at = 0; at < text.length; at++) {\n const char = text[at];\n if (char === '(') depth++;\n else if (char === ')') depth--;\n else if (char === ',' && depth === 0) {\n parts.push(text.slice(from, at).trim());\n from = at + 1;\n }\n }\n parts.push(text.slice(from).trim());\n return parts;\n}\n\nconst rgba = (text: string): string | undefined => {\n const parsed = parseColor(text);\n return parsed ? `rgba(${parsed[0]}, ${parsed[1]}, ${parsed[2]}, ${parsed[3]})` : undefined;\n};\n\n/** A direction in degrees, or nothing for a first argument that is a colour stop. */\nfunction direction(first: string): number | undefined {\n const side = SIDES[first.toLowerCase()];\n if (side !== undefined) return side;\n const angle = /^(-?[\\d.]+)deg$/i.exec(first);\n return angle ? Number(angle[1]) : undefined;\n}\n\n/** `rgba(…) 40%` as a stop, or nothing for one this does not read. */\nfunction stop(text: string): { color: string; position: string | null } | undefined {\n const placed = /^(.*\\S)\\s+(-?[\\d.]+%)$/.exec(text);\n const color = rgba(placed ? placed[1]! : text);\n return color === undefined ? undefined : { color, position: placed ? placed[2]! : null };\n}\n\nfunction linear(layer: string): object | undefined {\n const inside = /^linear-gradient\\((.*)\\)$/is.exec(layer)?.[1];\n if (inside === undefined) return undefined;\n const parts = split(inside);\n const angle = direction(parts[0]!);\n const stops = (angle === undefined ? parts : parts.slice(1)).map(stop);\n if (stops.length < 2 || stops.some((each) => each === undefined)) return undefined;\n return {\n type: 'linear-gradient',\n direction: { type: 'angle', value: angle ?? 180 },\n colorStops: stops,\n };\n}\n\n/** The layers a background's text holds, or nothing where any of it is not read. */\nexport function backgroundLayers(text: string): BackgroundLayers | undefined {\n const layers = split(text.trim());\n const last = layers.at(-1)!;\n const color = parseColor(last) ? last : undefined;\n const images = (color === undefined ? layers : layers.slice(0, -1)).map(linear);\n if (!images.length || images.some((each) => each === undefined)) return undefined;\n return { images: images as object[], color };\n}\n"]}
package/dist/css.d.ts CHANGED
@@ -222,6 +222,11 @@ export interface DeferredDeclaration {
222
222
  * unambiguous given a single-valued token, so `padding: var(--gap)` writes all four.
223
223
  */
224
224
  readonly props: readonly string[];
225
+ /**
226
+ * `background: var()`: the token may hold gradients over a colour, so `props` names the image
227
+ * beside the colour and each is written from its own part of the token. See `layersOf`.
228
+ */
229
+ readonly layers?: true;
225
230
  /**
226
231
  * `inherit`, written for these props: each is the parent's value for it, or nothing where the
227
232
  * parent has none.
@@ -299,6 +304,11 @@ export interface DeferredDeclaration {
299
304
  readonly unit: 'em' | 'vw' | 'vh' | 'vmin' | 'vmax';
300
305
  readonly factor: number;
301
306
  readonly offset?: number;
307
+ /**
308
+ * A unitless line-height: the node renders `factor` em, and what it hands down is the number,
309
+ * which each text under it multiplies its own font size by. See `LineHeightMultiple`.
310
+ */
311
+ readonly multiple?: true;
302
312
  };
303
313
  /**
304
314
  * A structured value - a transform list, a shadow, a filter - with lengths like `compute`'s
@@ -693,6 +703,10 @@ interface IndexedEntry {
693
703
  export interface RuleIndex {
694
704
  readonly buckets: Map<string, IndexedEntry[]>;
695
705
  readonly universal: IndexedEntry[];
706
+ /** Whether any rule is filed under an attribute, which a node's props are then looked up for. */
707
+ attributes?: boolean;
708
+ /** Whether any is filed under the class of an element its own is inside. */
709
+ under?: boolean;
696
710
  }
697
711
  /**
698
712
  * File rules by key selector, each at `from` and on: its place among every rule there is, which
@@ -843,6 +857,13 @@ export declare class StyleResolver {
843
857
  private readonly noted;
844
858
  /** Read the classes a sheet names, once, as soon as any node is given it. */
845
859
  noteSheet(sheet: StyleSheet | null | undefined): void;
860
+ /** The props a selector of some sheet asks about: an id, and each attribute by its name. */
861
+ private readonly named;
862
+ /**
863
+ * Whether a prop changing can change what any element matches: only where a selector of a
864
+ * sheet that was ever loaded names it. A sheet loaded later has every element matched again.
865
+ */
866
+ reads(prop: string): boolean;
846
867
  private noteReach;
847
868
  /**
848
869
  * What classes coming to an element or going from it can restyle: `true` for it and all
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. */
@@ -229,22 +230,30 @@ function matchesAttribute(node, test) {
229
230
  return false;
230
231
  if (test.operator === undefined)
231
232
  return true;
232
- const value = attributeText(actual, test);
233
- switch (test.operator) {
233
+ // `^=`, `$=`, `*=` and `~=` with an empty value match nothing, as Selectors 4 says.
234
+ if (test.value === '' && !EMPTY_MATCHES.has(test.operator))
235
+ return false;
236
+ return matchesValue(attributeText(actual, test), test.operator, test.value);
237
+ }
238
+ /** The operators an empty value can still match by: `=` and `|=`. */
239
+ const EMPTY_MATCHES = new Set(['equal', 'dash-match']);
240
+ /** Whether an attribute's value, as `attributeText` gives it, passes an operator's test. */
241
+ function matchesValue(value, operator, expected) {
242
+ switch (operator) {
234
243
  case 'equal':
235
- return value === test.value;
244
+ return value === expected;
236
245
  case 'prefix':
237
- return value.startsWith(test.value);
246
+ return value.startsWith(expected);
238
247
  case 'suffix':
239
- return value.endsWith(test.value);
248
+ return value.endsWith(expected);
240
249
  case 'substring':
241
- return value.includes(test.value);
250
+ return value.includes(expected);
242
251
  case 'includes':
243
252
  // `~=` matches one whole word of a space-separated list.
244
- return value.split(/\s+/).includes(test.value);
253
+ return value.split(/\s+/).includes(expected);
245
254
  case 'dash-match':
246
255
  // `|=` matches the value itself or the value followed by a hyphen, as `lang` uses.
247
- return value === test.value || value.startsWith(`${test.value}-`);
256
+ return value === expected || value.startsWith(`${expected}-`);
248
257
  }
249
258
  }
250
259
  /*
@@ -905,7 +914,34 @@ export function ruleKeys(rule) {
905
914
  const held = parentClass(rule, key);
906
915
  if (held !== undefined)
907
916
  return [`in:${held}`];
908
- return alternativeKeys(key) ?? EVERY_NODE;
917
+ return alternativeKeys(key) ?? attributeKey(key) ?? ancestorKeys(rule, key) ?? EVERY_NODE;
918
+ }
919
+ /**
920
+ * The buckets of a rule for anything inside an element with a class, `.item:focus *`: under the
921
+ * class, or each class the element may have instead. Offered to what has such an element
922
+ * somewhere over it, where the universal bucket has every element walk to the root to find none.
923
+ */
924
+ function ancestorKeys(rule, key) {
925
+ const joined = rule.combinators[rule.combinators.length - 1];
926
+ const above = key.ancestors?.[0] ??
927
+ (joined === 'descendant' ? rule.compounds[rule.compounds.length - 2] : undefined);
928
+ if (!above)
929
+ return undefined;
930
+ if (above.classes.length)
931
+ return [`under:${above.classes[0]}`];
932
+ const any = above.is?.[0];
933
+ if (!any?.length || any.some((one) => !one.classes.length))
934
+ return undefined;
935
+ return any.map((one) => `under:${one.classes[0]}`);
936
+ }
937
+ /**
938
+ * The bucket of a rule that names its element by an attribute and nothing else, `[ngpButton]`:
939
+ * a prop the element has to have, which is how a headless library names every element it
940
+ * styles. Not `class`, which is no prop of the element's to find the bucket by.
941
+ */
942
+ function attributeKey(key) {
943
+ const named = key.attributes?.find((test) => test.name !== 'class');
944
+ return named ? [`attr:${named.name}`] : undefined;
909
945
  }
910
946
  /** A class the node's parent has to have for the rule to match, where its selector says one. */
911
947
  function parentClass(rule, key) {
@@ -958,6 +994,10 @@ export function indexRules(entries, into = { buckets: new Map(), universal: [] }
958
994
  into.universal.push(indexed);
959
995
  continue;
960
996
  }
997
+ if (key.startsWith('attr:'))
998
+ into.attributes = true;
999
+ else if (key.startsWith('under:'))
1000
+ into.under = true;
961
1001
  const bucket = into.buckets.get(key);
962
1002
  if (bucket)
963
1003
  bucket.push(indexed);
@@ -990,6 +1030,27 @@ function reach(node, index, found) {
990
1030
  if (around)
991
1031
  for (const name of around)
992
1032
  take(`in:${name}`);
1033
+ if (index.attributes)
1034
+ reachByAttribute(node, take);
1035
+ if (index.under)
1036
+ reachUnder(node, take);
1037
+ }
1038
+ /** The buckets of the classes of every element over a node. */
1039
+ function reachUnder(node, take) {
1040
+ for (let above = node.parent; above; above = above.parent) {
1041
+ if (above.classes)
1042
+ for (const name of above.classes)
1043
+ take(`under:${name}`);
1044
+ }
1045
+ }
1046
+ /** The buckets of the attributes a node has: a prop of its own that is set. */
1047
+ function reachByAttribute(node, take) {
1048
+ for (const name in node.props) {
1049
+ const value = node.props[name];
1050
+ // As `matchesAttribute` has it: a prop that is false is an attribute that is not there.
1051
+ if (value !== undefined && value !== null && value !== false)
1052
+ take(`attr:${name}`);
1053
+ }
993
1054
  }
994
1055
  /**
995
1056
  * The rules worth trying against a node, in cascade order: `order` says which of two comes
@@ -1082,9 +1143,10 @@ function classReach(sheet) {
1082
1143
  return found;
1083
1144
  const classes = new Map();
1084
1145
  const reads = [];
1146
+ const named = new Set();
1085
1147
  for (const rule of sheet.rules) {
1086
1148
  const every = new Set();
1087
- collectClasses(rule, every, reads);
1149
+ collectClasses(rule, every, reads, named);
1088
1150
  const inside = classesInside(rule);
1089
1151
  for (const name of every) {
1090
1152
  if (!inside.has(name))
@@ -1093,7 +1155,7 @@ function classReach(sheet) {
1093
1155
  noteInside(classes, name, rule);
1094
1156
  }
1095
1157
  }
1096
- reaches.set(sheet, (found = { classes, reads }));
1158
+ reaches.set(sheet, (found = { classes, reads, named }));
1097
1159
  return found;
1098
1160
  }
1099
1161
  /** Note a rule as one that names a class as what its element is inside. */
@@ -1155,24 +1217,30 @@ function classesInside(rule) {
1155
1217
  }
1156
1218
  /** What a rule declares, which names no element. */
1157
1219
  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) {
1220
+ /**
1221
+ * Add each class under `value` to `into`, each test of the `class` attribute to `reads`, and the
1222
+ * name of every attribute asked about to `named`.
1223
+ */
1224
+ function collectClasses(value, into, reads, named) {
1160
1225
  if (value === null || typeof value !== 'object')
1161
1226
  return;
1162
- if (Array.isArray(value))
1163
- return value.forEach((inner) => collectClasses(inner, into, reads));
1227
+ if (Array.isArray(value)) {
1228
+ return value.forEach((inner) => collectClasses(inner, into, reads, named));
1229
+ }
1164
1230
  const part = value;
1165
1231
  if (Array.isArray(part.classes))
1166
1232
  for (const name of part.classes)
1167
1233
  into.add(String(name));
1168
1234
  if (Array.isArray(part.attributes)) {
1169
- for (const test of part.attributes)
1235
+ for (const test of part.attributes) {
1236
+ named?.add(test.name);
1170
1237
  if (test.name === 'class')
1171
1238
  reads.push(test);
1239
+ }
1172
1240
  }
1173
1241
  for (const [key, inner] of Object.entries(value)) {
1174
1242
  if (!DECLARED.has(key))
1175
- collectClasses(inner, into, reads);
1243
+ collectClasses(inner, into, reads, named);
1176
1244
  }
1177
1245
  }
1178
1246
  /**
@@ -1226,6 +1294,40 @@ function emptyCacheFor(epoch, generation) {
1226
1294
  tokens: NO_TOKENS,
1227
1295
  };
1228
1296
  }
1297
+ function isMultiple(value) {
1298
+ return typeof value === 'object' && value !== null && 'multiple' in value;
1299
+ }
1300
+ /** A line-height as points, for a node of font size `size`: what an em of it comes to. */
1301
+ function lineHeightPoints(line, size) {
1302
+ const points = line.multiple * (typeof size === 'number' ? size : DEFAULT_FONT_SIZE);
1303
+ return Math.round(points * 1000) / 1000;
1304
+ }
1305
+ /** A node's style with a unitless line-height it set or inherited worked out against its font size. */
1306
+ function settleLineHeight(style) {
1307
+ const line = style['lineHeight'];
1308
+ if (isMultiple(line))
1309
+ style['lineHeight'] = lineHeightPoints(line, style['fontSize']);
1310
+ }
1311
+ /** The styles of nodes with no rules, by the map they inherit, for one that hands down a multiple. */
1312
+ const unstyledStyles = new WeakMap();
1313
+ /**
1314
+ * The style of a node no rule matches: what it inherits, with a unitless line-height worked out
1315
+ * against its font size, which an inline style can set. The same object where there is none.
1316
+ */
1317
+ function unstyledStyle(inherited, inline) {
1318
+ const line = inherited['lineHeight'];
1319
+ if (!isMultiple(line))
1320
+ return inherited;
1321
+ const size = inline?.['fontSize'];
1322
+ if (typeof size === 'number')
1323
+ return { ...inherited, lineHeight: lineHeightPoints(line, size) };
1324
+ let style = unstyledStyles.get(inherited);
1325
+ if (!style) {
1326
+ style = { ...inherited, lineHeight: lineHeightPoints(line, inherited['fontSize']) };
1327
+ unstyledStyles.set(inherited, style);
1328
+ }
1329
+ return style;
1330
+ }
1229
1331
  /**
1230
1332
  * What this node's children inherit: the parent's map, overlaid with any inheritable value the
1231
1333
  * node set itself. Copied on first write only, so a node that sets none hands the same object on
@@ -1457,10 +1559,21 @@ export class StyleResolver {
1457
1559
  if (!sheet || this.noted.has(sheet))
1458
1560
  return;
1459
1561
  this.noted.add(sheet);
1460
- const { classes, reads } = classReach(sheet);
1562
+ const { classes, reads, named } = classReach(sheet);
1461
1563
  for (const [name, how] of classes)
1462
1564
  this.noteReach(name, how);
1463
1565
  this.classTests.push(...reads);
1566
+ for (const name of named)
1567
+ this.named.add(name);
1568
+ }
1569
+ /** The props a selector of some sheet asks about: an id, and each attribute by its name. */
1570
+ named = new Set(['nativeID']);
1571
+ /**
1572
+ * Whether a prop changing can change what any element matches: only where a selector of a
1573
+ * sheet that was ever loaded names it. A sheet loaded later has every element matched again.
1574
+ */
1575
+ reads(prop) {
1576
+ return this.named.has(prop);
1464
1577
  }
1465
1578
  noteReach(name, how) {
1466
1579
  const had = this.reach.get(name);
@@ -1722,7 +1835,7 @@ export class StyleResolver {
1722
1835
  generation: this.generation,
1723
1836
  context: {},
1724
1837
  parentContext,
1725
- style: parentInherited,
1838
+ style: unstyledStyle(parentInherited, inline),
1726
1839
  inherited: inline ? inheritFrom(parentInherited, inline) : parentInherited,
1727
1840
  tokens,
1728
1841
  };
@@ -1892,6 +2005,7 @@ export class StyleResolver {
1892
2005
  if (own['pointerEvents'] === 'inherit')
1893
2006
  delete own['pointerEvents'];
1894
2007
  const style = { ...parentInherited, ...own };
2008
+ settleLineHeight(style);
1895
2009
  const inherited = decorate(style, inheritFrom(parentInherited, own), own);
1896
2010
  const rtl = (style['direction'] ?? this.conditions.direction) === 'rtl';
1897
2011
  const important = importantNames(result, () => TWIN[rtl ? 'rtl' : 'ltr']);
@@ -2052,11 +2166,10 @@ export class StyleResolver {
2052
2166
  if (unread && this.onUnreadDisplay && !outranked('display')) {
2053
2167
  this.reportDisplay(declaration, tokens);
2054
2168
  }
2055
- const value = settled ?? declaration.unset;
2169
+ const layers = settled === undefined ? layersOf(declaration, tokens) : undefined;
2056
2170
  for (const prop of declaration.props) {
2057
- if (outranked(prop))
2058
- continue;
2059
- write(own, prop, value, declaration.line !== undefined);
2171
+ if (!outranked(prop))
2172
+ writeSettled(own, prop, declaration, settled, layers);
2060
2173
  }
2061
2174
  }
2062
2175
  }
@@ -2233,9 +2346,11 @@ export class StyleResolver {
2233
2346
  }
2234
2347
  /** A length relative to the viewport or to the font size in scope. */
2235
2348
  computed(declaration, own, parentInherited) {
2236
- const { unit, factor, offset = 0 } = declaration.compute;
2349
+ const { unit, factor, offset = 0, multiple } = declaration.compute;
2237
2350
  if (unit !== 'em')
2238
2351
  return this.viewportLength(unit, factor) + offset;
2352
+ if (multiple && !offset)
2353
+ return { multiple: factor };
2239
2354
  // The font size in scope. On `font-size` itself that is the inherited size rather than the
2240
2355
  // one being computed, which is what makes a nested `1.5em` compound as on the web.
2241
2356
  const base = declaration.props.includes('fontSize')
@@ -2722,6 +2837,41 @@ function tokenForm(token, kind) {
2722
2837
  const value = formOf(token, kind);
2723
2838
  return value === undefined && kind === 'color' && isCurrentColour(token) ? CURRENT_COLOUR : value;
2724
2839
  }
2840
+ /** What a deferred declaration settled to, written to one of its props. */
2841
+ function writeSettled(own, prop, declaration, settled, layers) {
2842
+ const value = settled ?? declaration.unset;
2843
+ if (declaration.layers)
2844
+ writeLayer(own, prop, value, layers);
2845
+ else
2846
+ write(own, prop, value, declaration.line !== undefined);
2847
+ }
2848
+ /**
2849
+ * One prop of a `background: var()`, whose token may hold the images as well as the colour. The
2850
+ * image is written only where the token holds one: a token that is a colour leaves an image
2851
+ * another rule declared as it is. Each prop is ranked in the cascade by itself, so an image a
2852
+ * later rule declares, or an important one, is not written over by the token's.
2853
+ */
2854
+ function writeLayer(own, prop, value, layers) {
2855
+ if (prop !== BACKGROUND_IMAGE)
2856
+ write(own, prop, layers?.color ?? value, false);
2857
+ else if (layers)
2858
+ own[prop] = layers.images;
2859
+ }
2860
+ const BACKGROUND_IMAGE = 'experimental_backgroundImage';
2861
+ /**
2862
+ * The gradients a `background: var()` reads where its token is no colour: see `backgroundLayers`.
2863
+ * Only the shorthand, which the compiler marks: `background-color: var()` is a colour or nothing.
2864
+ * And only where the token is set as text on an element, which is where a library sets one as the
2865
+ * app runs; a stylesheet's is refused where it is written. The token is the one the declaration
2866
+ * settles on, the first that is set.
2867
+ */
2868
+ function layersOf(declaration, tokens) {
2869
+ if (!declaration.layers)
2870
+ return undefined;
2871
+ const names = [declaration.reference, ...(declaration.alternatives ?? [])];
2872
+ const text = firstSet(names, tokens)?.keyword;
2873
+ return text && /gradient\(/i.test(text) ? backgroundLayers(text) : undefined;
2874
+ }
2725
2875
  const BOUND_NUMBERS = new Set([
2726
2876
  'opacity',
2727
2877
  'flex',