@ng-native/fabric 0.1.2 → 0.2.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 CHANGED
@@ -49,7 +49,7 @@ const platform = nativePlatform(); // 'ios' | 'android'
49
49
  [what CSS reaches a device](https://ng-native.com/packages/fabric/supported-css) and
50
50
  [animation](https://ng-native.com/packages/fabric/animation)
51
51
  - [Root README](https://github.com/ng-native/ng-native/blob/main/README.md) and
52
- [ARCHITECTURE.md](https://github.com/ng-native/ng-native/blob/main/ARCHITECTURE.md)
52
+ [ARCHITECTURE.md](https://github.com/ng-native/ng-native/blob/main/docs/ARCHITECTURE.md)
53
53
 
54
54
  ## License
55
55
 
package/dist/css.d.ts CHANGED
@@ -125,15 +125,24 @@ export interface TokenValue {
125
125
  readonly transform?: readonly unknown[];
126
126
  /** One font variant, as the one-entry list `fontVariant` takes: a numeric variant's slot. */
127
127
  readonly fontVariant?: readonly string[];
128
- /** Bare colour channels, `13, 110, 253`, for an `rgba(var(--x), <alpha>)` to finish. */
128
+ /** Bare colour channels as `rgb()` reads them, 0 to 255: `13, 110, 253` or `100% 0% 0%`. */
129
129
  readonly channels?: readonly number[];
130
+ /** The same as `hsl()` reads them: degrees, then saturation and lightness as fractions. */
131
+ readonly hslChannels?: readonly number[];
132
+ /** A transition's curve, as the cubic-bezier control points a timing keyword stands for. */
133
+ readonly easing?: readonly number[];
134
+ /** A whole animation, as the spec the engine plays: `--animate-spin`. */
135
+ readonly animation?: Readonly<Record<string, unknown>>;
130
136
  /**
131
- * Defined as `rgba(var(--channels), <alpha>)`: a colour made of a channels token and an alpha
132
- * written or tokened. Settled on the node that defines it, as an `hsl` one is.
137
+ * Defined as a colour made of other tokens: `rgba(var(--channels), <alpha>)`, or a `color-mix()`
138
+ * of one. Settled on the node that defines it, as an `hsl` one is.
133
139
  */
134
- readonly deferredColour?: Extract<ColourExpression, {
135
- channels: unknown;
136
- }>;
140
+ readonly deferredColour?: ColourExpression;
141
+ /**
142
+ * Defined as arithmetic of other tokens, `calc(var(--spacing) * 4)`, read as a length and as a
143
+ * number. Settled on the node that defines it, as an `hsl` one is.
144
+ */
145
+ readonly deferredCalc?: readonly CalcMarker[];
137
146
  /**
138
147
  * Defined as another token, `var(--name)`. Resolved on the node that defines it, against the
139
148
  * tokens in scope there, and passed down resolved - which is what a browser does.
@@ -159,7 +168,7 @@ export type HslChannel = number | {
159
168
  readonly fallback?: number;
160
169
  };
161
170
  /** The forms a token can be read in: one per kind of value a use site can need. */
162
- export type TokenKind = 'length' | 'color' | 'keyword' | 'number' | 'weight' | 'family' | 'lineHeight' | 'shadow' | 'filter' | 'transform' | 'fontVariant' | 'channels';
171
+ export type TokenKind = 'length' | 'color' | 'keyword' | 'number' | 'weight' | 'family' | 'lineHeight' | 'shadow' | 'filter' | 'transform' | 'fontVariant' | 'channels' | 'hslChannels' | 'easing' | 'time' | 'animation';
163
172
  export interface DeferredDeclaration {
164
173
  /**
165
174
  * The React Native style properties this lands in. Usually one, but a four-sided shorthand is
@@ -175,6 +184,11 @@ export interface DeferredDeclaration {
175
184
  */
176
185
  readonly alternatives?: readonly string[];
177
186
  readonly fallback?: unknown;
187
+ /**
188
+ * A fallback made of other tokens, `var(--missing, calc(var(--gap) * 2))`, worked out from the
189
+ * tokens in scope where it is used, as a browser substitutes it there.
190
+ */
191
+ readonly fallbackToken?: TokenValue;
178
192
  /**
179
193
  * What to write when the reference resolves, whatever it resolves to: `flex: var(--grow)` sets a
180
194
  * shrink of 1 and a basis of 0%, but only when there is a grow for them to go with.
@@ -194,6 +208,8 @@ export interface DeferredDeclaration {
194
208
  readonly reference: string;
195
209
  readonly fallback?: number;
196
210
  };
211
+ /** For a colour built from `channels`, read through `hsl()` rather than `rgb()`. */
212
+ readonly space?: ChannelSpace;
197
213
  /**
198
214
  * A gradient whose stops are custom properties, filled in once a node's tokens are known. What
199
215
  * a utility framework's `from-*` and `to-*` classes compile to, since the class that paints the
@@ -247,6 +263,7 @@ export type ColourExpression = {
247
263
  readonly channels: {
248
264
  readonly reference: string;
249
265
  readonly fallback?: readonly number[];
266
+ readonly space?: ChannelSpace;
250
267
  };
251
268
  readonly alpha?: number | {
252
269
  readonly reference: string;
@@ -314,12 +331,18 @@ export interface StyleSheet {
314
331
  * has to re-resolve a whole child list when it changes. It only does that where a sheet asks.
315
332
  */
316
333
  readonly structural?: true;
334
+ /**
335
+ * The `@font-face` rules the sheet declares, each `source` the bundler's `require` of the file.
336
+ * The engine does not read them: `loadFonts()` in `@ng-native/expo/fonts` registers them with
337
+ * the platform before mount.
338
+ */
339
+ readonly fonts?: readonly {
340
+ readonly family: string;
341
+ readonly source: unknown;
342
+ readonly weight?: number;
343
+ readonly style?: string;
344
+ }[];
317
345
  }
318
- /**
319
- * What one node resolved to. `style` is what it renders with; `inherited` is what its children
320
- * start from, which is the parent's own `inherited` plus whatever inheritable values this node
321
- * set. Keeping the two apart is what lets resolution go downwards.
322
- */
323
346
  export interface StyleCache {
324
347
  epoch: number;
325
348
  /**
@@ -560,7 +583,26 @@ export declare class StyleResolver {
560
583
  * survives and a later sheet wins equal weight.
561
584
  */
562
585
  private merge;
563
- /** The declarations that apply to a node. The list is sorted, so later simply wins. */
586
+ /**
587
+ * A node's style from the rules it matched, shared with every other node that matched the same
588
+ * rules under the same inherited values and tokens: a list's rows, and what is inside each.
589
+ *
590
+ * Everything past matching depends on those alone, and it is most of the work where a sheet
591
+ * leaves values to the device: settling each `var()`, mixing colours, filling a shadow's slots.
592
+ * A node that sets custom properties of its own is worked out alone. What is shared is never
593
+ * written to after, so sharing it is safe; each node still has a cache and a context of its own.
594
+ */
595
+ private styled;
596
+ private readonly shared;
597
+ private sharedGeneration;
598
+ private readonly ruleIds;
599
+ private nextRuleId;
600
+ private ruleId;
601
+ /** A node's style from the rules it matched, worked out. */
602
+ private style;
603
+ /** The rules that apply to a node, weakest first. */
604
+ private matched;
605
+ /** The declarations of the rules a node matched. The list is sorted, so later simply wins. */
564
606
  private cascade;
565
607
  /**
566
608
  * Whether a rule's media query holds, memoised per rule.
@@ -601,6 +643,20 @@ export declare class StyleResolver {
601
643
  /** A length relative to the viewport or to the font size in scope. */
602
644
  private computed;
603
645
  }
646
+ /** `hsl()` reads a channels token as hue, saturation and lightness; `rgb()` as sRGB. */
647
+ type ChannelSpace = 'rgb' | 'hsl';
648
+ /**
649
+ * Arithmetic with tokens in it, as the compiler leaves it: numbers, token references and
650
+ * `[op, a, b]`, in points, degrees or plain numbers as the slot it fills counts.
651
+ */
652
+ type CalcExpression = number | {
653
+ readonly reference: string;
654
+ readonly fallback?: CalcExpression;
655
+ } | readonly ['+' | '-' | '*' | '/' | 'max' | 'min', CalcExpression, CalcExpression];
656
+ interface CalcMarker {
657
+ readonly expression: CalcExpression;
658
+ readonly kind: 'length' | 'angle' | 'time' | 'number';
659
+ }
604
660
  /**
605
661
  * Where a component's compiled sheet lives: a static on the component class, written by the
606
662
  * Metro transformer at build time.
package/dist/css.js CHANGED
@@ -594,25 +594,15 @@ export class StyleResolver {
594
594
  return this.unstyled(node, epoch, parent, parentContext, parentInherited);
595
595
  }
596
596
  styleStats.nodesResolved++;
597
- const result = this.cascade(node, this.rulesFor(node));
598
- // Tokens are in scope for this node's own declarations as well as its descendants', so they
599
- // are merged before any `var()` here is resolved.
597
+ const matched = this.matched(node, this.rulesFor(node));
600
598
  const parentTokens = parent ? parent.tokens : this.tokensOnRoot;
601
- const tokens = tokensInScope(parentTokens, result.tokens, node.customProperties);
602
- const own = result.declarations;
603
- if (result.deferred) {
604
- this.applyDeferred(result.deferred, own, tokens, parentInherited, result.important);
605
- }
606
- if (own['borderStyle'] === 'none')
607
- drawNoBorder(own);
599
+ const styled = this.styled(node, matched, parentTokens, parentInherited);
608
600
  const cache = {
609
601
  epoch,
610
602
  generation: this.generation,
611
603
  context: {},
612
604
  parentContext,
613
- style: { ...parentInherited, ...own },
614
- inherited: inheritFrom(parentInherited, own),
615
- tokens,
605
+ ...styled,
616
606
  };
617
607
  node.styleCache = cache;
618
608
  node.styleDirty = false;
@@ -713,16 +703,81 @@ export class StyleResolver {
713
703
  add(sheet, COMPONENT_SPECIFICITY_BUMP);
714
704
  return entries.sort((a, b) => a.weight - b.weight);
715
705
  }
716
- /** The declarations that apply to a node. The list is sorted, so later simply wins. */
717
- cascade(node, entries) {
706
+ /**
707
+ * A node's style from the rules it matched, shared with every other node that matched the same
708
+ * rules under the same inherited values and tokens: a list's rows, and what is inside each.
709
+ *
710
+ * Everything past matching depends on those alone, and it is most of the work where a sheet
711
+ * leaves values to the device: settling each `var()`, mixing colours, filling a shadow's slots.
712
+ * A node that sets custom properties of its own is worked out alone. What is shared is never
713
+ * written to after, so sharing it is safe; each node still has a cache and a context of its own.
714
+ */
715
+ styled(node, matched, parentTokens, parentInherited) {
716
+ if (node.customProperties)
717
+ return this.style(node, matched, parentTokens, parentInherited);
718
+ if (this.sharedGeneration !== this.generation) {
719
+ this.shared.clear();
720
+ this.sharedGeneration = this.generation;
721
+ }
722
+ const key = matched.map((rule) => this.ruleId(rule)).join(',');
723
+ let byTokens = this.shared.get(key);
724
+ if (!byTokens)
725
+ this.shared.set(key, (byTokens = new WeakMap()));
726
+ let byInherited = byTokens.get(parentTokens);
727
+ if (!byInherited)
728
+ byTokens.set(parentTokens, (byInherited = new WeakMap()));
729
+ let styled = byInherited.get(parentInherited);
730
+ if (!styled) {
731
+ styled = this.style(node, matched, parentTokens, parentInherited);
732
+ byInherited.set(parentInherited, styled);
733
+ }
734
+ return styled;
735
+ }
736
+ shared = new Map();
737
+ sharedGeneration = 0;
738
+ ruleIds = new WeakMap();
739
+ nextRuleId = 0;
740
+ ruleId(rule) {
741
+ let id = this.ruleIds.get(rule);
742
+ if (id === undefined)
743
+ this.ruleIds.set(rule, (id = this.nextRuleId++));
744
+ return id;
745
+ }
746
+ /** A node's style from the rules it matched, worked out. */
747
+ style(node, matched, parentTokens, parentInherited) {
748
+ const result = this.cascade(matched);
749
+ // Tokens are in scope for this node's own declarations as well as its descendants', so they
750
+ // are merged before any `var()` here is resolved.
751
+ const tokens = tokensInScope(parentTokens, result.tokens, node.customProperties);
752
+ const own = result.declarations;
753
+ if (result.deferred) {
754
+ this.applyDeferred(result.deferred, own, tokens, parentInherited, result.important);
755
+ }
756
+ if (own['borderStyle'] === 'none')
757
+ drawNoBorder(own);
758
+ return {
759
+ style: { ...parentInherited, ...own },
760
+ inherited: inheritFrom(parentInherited, own),
761
+ tokens,
762
+ };
763
+ }
764
+ /** The rules that apply to a node, weakest first. */
765
+ matched(node, entries) {
766
+ const out = [];
767
+ for (const { rule, sheet } of entries) {
768
+ if (this.conditionHolds(rule) && matches(node, rule, sheet))
769
+ out.push(rule);
770
+ }
771
+ return out;
772
+ }
773
+ /** The declarations of the rules a node matched. The list is sorted, so later simply wins. */
774
+ cascade(matched) {
718
775
  const normal = {};
719
776
  const important = {};
720
777
  let hasImportant = false;
721
778
  let tokens = null;
722
779
  let deferred = null;
723
- for (const { rule, sheet } of entries) {
724
- if (!this.conditionHolds(rule) || !matches(node, rule, sheet))
725
- continue;
780
+ for (const rule of matched) {
726
781
  Object.assign(normal, rule.declarations);
727
782
  if (rule.important) {
728
783
  Object.assign(important, rule.important);
@@ -841,7 +896,9 @@ export class StyleResolver {
841
896
  /** A reference with nothing defined for it and nothing to fall back to. */
842
897
  reportUndefined(declaration, tokens) {
843
898
  const name = declaration.reference;
844
- if (name === undefined || declaration.fallback !== undefined || name in tokens)
899
+ if (name === undefined || name in tokens)
900
+ return;
901
+ if (declaration.fallback !== undefined || declaration.fallbackToken)
845
902
  return;
846
903
  if (declaration.alternatives?.some((alternative) => alternative in tokens))
847
904
  return;
@@ -917,7 +974,10 @@ export class StyleResolver {
917
974
  const base = declaration.props.includes('fontSize')
918
975
  ? parentInherited['fontSize']
919
976
  : (own['fontSize'] ?? parentInherited['fontSize']);
920
- return factor * (typeof base === 'number' ? base : DEFAULT_FONT_SIZE) + offset;
977
+ // To a thousandth of a point: a factor rounded at build time, times a font size, is otherwise
978
+ // 19.99998 where the web draws 20.
979
+ const points = factor * (typeof base === 'number' ? base : DEFAULT_FONT_SIZE) + offset;
980
+ return Math.round(points * 1000) / 1000;
921
981
  }
922
982
  }
923
983
  /**
@@ -957,25 +1017,103 @@ function tokensInScope(parentTokens, ruleTokens, custom) {
957
1017
  }
958
1018
  function resolveAliases(own, merged) {
959
1019
  const names = Object.keys(own);
960
- for (const name of names) {
961
- if (own[name].alias)
962
- settle(merged, name, followAlias(name, merged, new Set()));
963
- }
964
- for (const name of names) {
965
- const { hsl, deferredColour } = own[name];
966
- if (hsl)
967
- settle(merged, name, colourToken(resolveHsl(hsl, merged)));
968
- if (deferredColour)
969
- settle(merged, name, colourToken(channelsColour(deferredColour, merged)));
970
- }
971
- // An alias to one of those copied it unsettled above: follow it again, now it is a colour.
972
- for (const name of names) {
973
- const target = own[name].alias;
974
- if (!target || !(own[target]?.hsl || own[target]?.deferredColour))
1020
+ const aliases = names.filter((name) => own[name].alias);
1021
+ followAliases(aliases, merged);
1022
+ // Left to work out: each token made of others and each alias that came to one, by a link or a
1023
+ // fallback, `var(--missing, calc(var(--gap) * 2))`, which is worked out here as well.
1024
+ const pending = new Set(names.filter((name) => isDerived(merged[name])));
1025
+ if (pending.size)
1026
+ settleDerived(own, merged, pending);
1027
+ return merged;
1028
+ }
1029
+ /** What a token not yet worked out reads as while another is: set, with no form of any kind. */
1030
+ const PENDING = Object.freeze({});
1031
+ /**
1032
+ * The tokens left to work out, each settled once none it reads is still to be, whatever order they
1033
+ * are defined in. One that reads another still to be waits, rather than taking a fallback: a
1034
+ * fallback is for a token that is unset or invalid. When every one left waits for another, those
1035
+ * in a cycle, counting only the references read, are invalid, and the rest go on without them,
1036
+ * taking their fallbacks, as CSS has it.
1037
+ */
1038
+ function settleDerived(own, merged, pending) {
1039
+ const waitingOn = new Map();
1040
+ let read = new Set();
1041
+ const view = new Proxy(merged, {
1042
+ get: (tokens, name) => {
1043
+ if (!pending.has(name))
1044
+ return tokens[name];
1045
+ read.add(name);
1046
+ return PENDING;
1047
+ },
1048
+ });
1049
+ while (pending.size) {
1050
+ let settled = false;
1051
+ for (const name of pending) {
1052
+ read = new Set();
1053
+ const token = own[name];
1054
+ const value = token.alias ? substitutedIn(token, view) : derived(token, view);
1055
+ if (read.size) {
1056
+ waitingOn.set(name, read);
1057
+ continue;
1058
+ }
1059
+ pending.delete(name);
1060
+ settle(merged, name, value);
1061
+ settled = true;
1062
+ }
1063
+ if (settled)
975
1064
  continue;
976
- settle(merged, name, followAlias(name, { ...merged, [name]: own[name] }, new Set()));
1065
+ for (const name of inCycles(pending, waitingOn)) {
1066
+ pending.delete(name);
1067
+ delete merged[name];
1068
+ }
977
1069
  }
978
- return merged;
1070
+ }
1071
+ /** An alias's target, or the first of its fallbacks that is set, with one made of others worked out. */
1072
+ function substitutedIn(token, tokens) {
1073
+ let link = token;
1074
+ while (link?.alias) {
1075
+ const target = tokens[link.alias];
1076
+ if (target !== undefined)
1077
+ return target;
1078
+ link = link.fallback;
1079
+ }
1080
+ return link && isDerived(link) ? derived(link, tokens) : link;
1081
+ }
1082
+ /** The names among `names` that reach themselves by what each waits on. */
1083
+ function inCycles(names, waitingOn) {
1084
+ const reaches = (from, to, seen) => {
1085
+ for (const next of waitingOn.get(from) ?? []) {
1086
+ if (next === to)
1087
+ return true;
1088
+ if (names.has(next) && !seen.has(next) && seen.add(next) && reaches(next, to, seen)) {
1089
+ return true;
1090
+ }
1091
+ }
1092
+ return false;
1093
+ };
1094
+ return [...names].filter((name) => reaches(name, name, new Set()));
1095
+ }
1096
+ /** Whether a token is made of others, and so worked out where it is defined. */
1097
+ function isDerived(token) {
1098
+ return Boolean(token?.hsl || token?.deferredColour || token?.deferredCalc);
1099
+ }
1100
+ /** A token made of others, worked out from the tokens in scope. */
1101
+ function derived(token, tokens) {
1102
+ if (token.hsl)
1103
+ return colourToken(resolveHsl(token.hsl, tokens));
1104
+ if (token.deferredColour)
1105
+ return colourToken(resolveColour(token.deferredColour, tokens));
1106
+ return calcToken(token.deferredCalc, tokens);
1107
+ }
1108
+ /**
1109
+ * Each alias settled to what it names. `follow` writes back every one that resolves; one in a
1110
+ * cycle is removed only once all are followed, as removed any sooner, the next one round the
1111
+ * cycle would find it unset and take its own fallback.
1112
+ */
1113
+ function followAliases(aliases, merged) {
1114
+ const cyclic = aliases.filter((name) => typeof follow(name, merged, []) === 'number');
1115
+ for (const name of cyclic)
1116
+ delete merged[name];
979
1117
  }
980
1118
  /** A token set to what it resolved to, or removed when it resolved to nothing, as CSS unsets it. */
981
1119
  function settle(merged, name, value) {
@@ -985,9 +1123,23 @@ function settle(merged, name, value) {
985
1123
  delete merged[name];
986
1124
  }
987
1125
  const colourToken = (color) => color === undefined ? undefined : { color };
1126
+ /** A `calc()` token's forms, from the tokens in scope: undefined when it makes neither. */
1127
+ function calcToken(markers, tokens) {
1128
+ let token;
1129
+ for (const marker of markers) {
1130
+ const value = resolveCalc(marker, tokens);
1131
+ if (value === undefined)
1132
+ continue;
1133
+ if (marker.kind === 'length')
1134
+ token = { ...token, length: value };
1135
+ else if (typeof value === 'number')
1136
+ token = { ...token, number: value };
1137
+ }
1138
+ return token;
1139
+ }
988
1140
  /** An `hsl()` token's channels, read from the tokens in scope, as the colour they make. */
989
1141
  function resolveHsl(hsl, tokens) {
990
- const h = hslChannel(hsl.h, tokens);
1142
+ const h = hslChannel(hsl.h, tokens, true);
991
1143
  const s = hslChannel(hsl.s, tokens);
992
1144
  const l = hslChannel(hsl.l, tokens);
993
1145
  const alpha = hsl.alpha === undefined ? 1 : hslChannel(hsl.alpha, tokens);
@@ -996,10 +1148,22 @@ function resolveHsl(hsl, tokens) {
996
1148
  }
997
1149
  return hslToRgb(h, s, l, alpha);
998
1150
  }
999
- function hslChannel(channel, tokens) {
1000
- return typeof channel === 'number'
1001
- ? channel
1002
- : (tokens[channel.reference]?.number ?? channel.fallback);
1151
+ /**
1152
+ * A channel's number, or undefined when its token is the wrong kind for the slot: a hue is a
1153
+ * number or an angle, read in degrees, as `0.5turn` is 180 and not 0.5, and never a percentage; a
1154
+ * saturation, a lightness or an alpha is never an angle. The fallback is only for a token that is
1155
+ * not set, as a set one is substituted, whatever it holds.
1156
+ */
1157
+ function hslChannel(channel, tokens, hue = false) {
1158
+ if (typeof channel === 'number')
1159
+ return channel;
1160
+ const token = tokens[channel.reference];
1161
+ if (!token)
1162
+ return channel.fallback;
1163
+ if (!hue)
1164
+ return token.angle === undefined ? token.number : undefined;
1165
+ const percentage = typeof token.length === 'string' && token.length.endsWith('%');
1166
+ return percentage ? undefined : (token.angle ?? token.number);
1003
1167
  }
1004
1168
  /**
1005
1169
  * `hsl(h, s, l)` converted to sRGB, printed the same way `fromChannels` prints an rgba() built
@@ -1008,7 +1172,11 @@ function hslChannel(channel, tokens) {
1008
1172
  * `h` is degrees; `s` and `l` are fractions, which is what a percentage token already reads as
1009
1173
  * (see `formOf`), so nothing here rescales them.
1010
1174
  */
1011
- function hslToRgb(h, s, l, alpha) {
1175
+ function hslToRgb(h, saturation, l, opacity) {
1176
+ // Out of range as CSS Color 4 takes it: a saturation below 0 is 0, an alpha is clamped, and
1177
+ // anything else is converted as it is, each channel it makes then clamped into sRGB.
1178
+ const s = Math.max(0, saturation);
1179
+ const alpha = Math.min(1, Math.max(0, opacity));
1012
1180
  const hue = ((h % 360) + 360) % 360;
1013
1181
  const chroma = (1 - Math.abs(2 * l - 1)) * s;
1014
1182
  const x = chroma * (1 - Math.abs(((hue / 60) % 2) - 1));
@@ -1024,21 +1192,41 @@ function hslToRgb(h, s, l, alpha) {
1024
1192
  : hue < 300
1025
1193
  ? [x, 0, chroma]
1026
1194
  : [chroma, 0, x];
1027
- const r = Math.round((r1 + m) * 255);
1028
- const g = Math.round((g1 + m) * 255);
1029
- const b = Math.round((b1 + m) * 255);
1195
+ const [r, g, b] = [r1, g1, b1].map((c) => Math.round(Math.min(1, Math.max(0, c + m)) * 255));
1030
1196
  return alpha >= 1
1031
1197
  ? `rgb(${r}, ${g}, ${b})`
1032
1198
  : `rgba(${r}, ${g}, ${b}, ${Math.round(alpha * 1000) / 1000})`;
1033
1199
  }
1034
- function followAlias(name, tokens, seen) {
1200
+ /**
1201
+ * A token with its `var()` substituted, through as many aliases and fallbacks as it takes.
1202
+ *
1203
+ * `path` is the aliases being followed on the way here. A number is a cycle, and the index in
1204
+ * `path` where it starts: every property from there on is in it and invalid at computed-value
1205
+ * time, so none of them takes its fallback, while one before it names an invalid property and
1206
+ * does. Anything else is final wherever the chain started, so it is written back to `tokens`, and
1207
+ * no alias is followed twice.
1208
+ */
1209
+ function follow(name, tokens, path) {
1035
1210
  const value = tokens[name];
1036
1211
  if (!value?.alias)
1037
1212
  return value;
1038
- if (seen.has(name))
1039
- return undefined;
1040
- seen.add(name);
1041
- return followAlias(value.alias, tokens, seen) ?? value.fallback;
1213
+ const start = path.indexOf(name);
1214
+ if (start >= 0)
1215
+ return start;
1216
+ const depth = path.push(name) - 1;
1217
+ const result = substituted(value, tokens, path, depth);
1218
+ path.pop();
1219
+ if (typeof result !== 'number')
1220
+ settle(tokens, name, result);
1221
+ return result;
1222
+ }
1223
+ /** An alias's target, or its fallback, which may be another `var()`, when the target is unset. */
1224
+ function substituted(value, tokens, path, depth) {
1225
+ const target = follow(value.alias, tokens, path);
1226
+ if (typeof target === 'object' || (typeof target === 'number' && target <= depth))
1227
+ return target;
1228
+ const { fallback } = value;
1229
+ return fallback?.alias ? substituted(fallback, tokens, path, depth) : fallback;
1042
1230
  }
1043
1231
  /** What a `var()` resolves to: a gradient's stops, or one value with its arithmetic applied. */
1044
1232
  function referenced(declaration, tokens) {
@@ -1055,18 +1243,31 @@ function referenced(declaration, tokens) {
1055
1243
  break;
1056
1244
  value = formOf(tokens[alternative], declaration.kind);
1057
1245
  }
1058
- value ??= declaration.fallback;
1059
- const base = declaration.kind === 'channels' ? fromChannels(value, declaration.alpha, tokens) : value;
1246
+ value ??= fallbackOf(declaration, tokens);
1247
+ const base = CHANNEL_KINDS.has(declaration.kind)
1248
+ ? fromChannels(value, declaration.alpha, tokens, declaration.space)
1249
+ : value;
1060
1250
  return declaration.adjust ? adjusted(base, declaration.adjust) : base;
1061
1251
  }
1062
- /** `13, 110, 253` as the colour it is, at the alpha the token beside it says, if one does. */
1063
- function fromChannels(value, alpha, tokens) {
1252
+ /** The fallback written, or one made of other tokens, worked out from the tokens where it is used. */
1253
+ function fallbackOf(declaration, tokens) {
1254
+ const token = declaration.fallbackToken;
1255
+ return declaration.fallback ?? (token && formOf(derived(token, tokens), declaration.kind));
1256
+ }
1257
+ const CHANNEL_KINDS = new Set(['channels', 'hslChannels']);
1258
+ /**
1259
+ * `13, 110, 253` as the colour it is, at the alpha the token beside it says, if one does: the
1260
+ * channels form for `rgb()`, the hslChannels form for `hsl()`.
1261
+ */
1262
+ function fromChannels(value, alpha, tokens, space) {
1064
1263
  if (!Array.isArray(value) || value.length !== 3)
1065
1264
  return undefined;
1066
1265
  // An alpha naming a token nothing set, with no fallback, is no colour: CSS drops it.
1067
1266
  const opacity = alpha ? (tokens[alpha.reference]?.number ?? alpha.fallback) : 1;
1068
1267
  if (opacity === undefined)
1069
1268
  return undefined;
1269
+ if (space === 'hsl')
1270
+ return hslToRgb(value[0], value[1], value[2], opacity);
1070
1271
  const [r, g, b] = value;
1071
1272
  return opacity >= 1
1072
1273
  ? `rgb(${r}, ${g}, ${b})`
@@ -1223,7 +1424,8 @@ function calculated(expression, kind, tokens) {
1223
1424
  return expression;
1224
1425
  if (!Array.isArray(expression)) {
1225
1426
  const leaf = expression;
1226
- return tokenNumber(tokens[leaf.reference], kind) ?? leaf.fallback;
1427
+ const own = tokenNumber(tokens[leaf.reference], kind);
1428
+ return (own ?? (leaf.fallback === undefined ? undefined : calculated(leaf.fallback, kind, tokens)));
1227
1429
  }
1228
1430
  const [op, a, b] = expression;
1229
1431
  const left = calculated(a, kind, tokens);
@@ -1236,6 +1438,10 @@ function calculated(expression, kind, tokens) {
1236
1438
  return left - right;
1237
1439
  if (op === '*')
1238
1440
  return left * right;
1441
+ if (op === 'max')
1442
+ return Math.max(left, right);
1443
+ if (op === 'min')
1444
+ return Math.min(left, right);
1239
1445
  return left / right;
1240
1446
  }
1241
1447
  /** Where each kind of slot reads a token from, before its bare number. */
@@ -1328,17 +1534,34 @@ function settledMarker(value, tokens) {
1328
1534
  const sum = value.__calc;
1329
1535
  if (sum)
1330
1536
  return resolveCalc(sum, tokens);
1537
+ const shadow = value.__dropShadow;
1538
+ if (shadow)
1539
+ return dropShadowOf(tokens[shadow.reference]?.shadow);
1331
1540
  return NOT_A_MARKER;
1332
1541
  }
1542
+ /**
1543
+ * A shadow token as the drop shadow a filter draws: `drop-shadow(var(--drop-shadow-xs))`. One
1544
+ * shadow, never inset and with no spread, which is all a drop shadow can be; nothing otherwise.
1545
+ */
1546
+ function dropShadowOf(shadow) {
1547
+ if (!Array.isArray(shadow) || shadow.length !== 1)
1548
+ return undefined;
1549
+ const [{ offsetX, offsetY, blurRadius, spreadDistance, color, inset }] = shadow;
1550
+ if (spreadDistance !== 0 || inset || typeof color !== 'string')
1551
+ return undefined;
1552
+ return { offsetX, offsetY, standardDeviation: blurRadius, color };
1553
+ }
1333
1554
  /** What a colour that cannot be settled makes of the whole value it is in. */
1334
1555
  const UNSETTLED = Symbol('unsettled');
1335
1556
  /** `rgba(var(--channels), <alpha>)`: the token's channels, at the alpha written or tokened. */
1336
1557
  function channelsColour(expression, tokens) {
1337
- const value = formOf(tokens[expression.channels.reference], 'channels') ?? expression.channels.fallback;
1558
+ const { space } = expression.channels;
1559
+ const kind = space === 'hsl' ? 'hslChannels' : 'channels';
1560
+ const value = formOf(tokens[expression.channels.reference], kind) ?? expression.channels.fallback;
1338
1561
  const { alpha } = expression;
1339
1562
  if (typeof alpha !== 'number')
1340
- return fromChannels(value, alpha, tokens);
1341
- const opaque = fromChannels(value, undefined, tokens);
1563
+ return fromChannels(value, alpha, tokens, space);
1564
+ const opaque = fromChannels(value, undefined, tokens, space);
1342
1565
  return opaque === undefined ? undefined : faded(opaque, alpha);
1343
1566
  }
1344
1567
  /** A token's colour, or the first of its alternatives that is defined, or its fallback. */
@@ -1364,11 +1587,20 @@ function colourMarker(value) {
1364
1587
  function adjusted(value, adjust) {
1365
1588
  if (adjust.alpha !== undefined)
1366
1589
  return faded(value, adjust.alpha);
1590
+ const em = value?.__defer;
1591
+ if (em && adjust.floor === undefined)
1592
+ return adjustedEm(em, adjust);
1367
1593
  if (typeof value !== 'number')
1368
1594
  return value;
1369
1595
  const scaled = value * (adjust.scale ?? 1) + (adjust.offset ?? 0);
1370
1596
  return adjust.floor === undefined ? scaled : Math.max(scaled, adjust.floor);
1371
1597
  }
1598
+ /** A length in em, worked out against the font size later, with the arithmetic moved inside it. */
1599
+ function adjustedEm(em, adjust) {
1600
+ const scale = adjust.scale ?? 1;
1601
+ const offset = (em.offset ?? 0) * scale + (adjust.offset ?? 0);
1602
+ return { __defer: { ...em, factor: em.factor * scale, ...(offset ? { offset } : {}) } };
1603
+ }
1372
1604
  /** `rgb(r, g, b)` and `rgba(r, g, b, a)`, which is every colour the compiler emits. */
1373
1605
  const RGB = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/;
1374
1606
  /**