@react-x11/components 0.14.1 → 0.16.1

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.
Files changed (96) hide show
  1. package/dist/code-editor/node.d.ts +13 -0
  2. package/dist/code-editor/node.d.ts.map +1 -1
  3. package/dist/code-editor/node.js +23 -5
  4. package/dist/code-editor/node.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +171 -26
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +616 -216
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/color.d.ts +25 -0
  10. package/dist/html/css/color.d.ts.map +1 -1
  11. package/dist/html/css/color.js +307 -19
  12. package/dist/html/css/color.js.map +1 -1
  13. package/dist/html/css/content.d.ts +11 -6
  14. package/dist/html/css/content.d.ts.map +1 -1
  15. package/dist/html/css/content.js +171 -12
  16. package/dist/html/css/content.js.map +1 -1
  17. package/dist/html/css/parse.d.ts.map +1 -1
  18. package/dist/html/css/parse.js +62 -9
  19. package/dist/html/css/parse.js.map +1 -1
  20. package/dist/html/css/shapes.d.ts +18 -0
  21. package/dist/html/css/shapes.d.ts.map +1 -1
  22. package/dist/html/css/shapes.js +21 -1
  23. package/dist/html/css/shapes.js.map +1 -1
  24. package/dist/html/css/style.d.ts +42 -5
  25. package/dist/html/css/style.d.ts.map +1 -1
  26. package/dist/html/css/style.js +74 -12
  27. package/dist/html/css/style.js.map +1 -1
  28. package/dist/html/css/ua.js +27 -8
  29. package/dist/html/css/ua.js.map +1 -1
  30. package/dist/html/css/values.d.ts +5 -0
  31. package/dist/html/css/values.d.ts.map +1 -1
  32. package/dist/html/css/values.js +18 -9
  33. package/dist/html/css/values.js.map +1 -1
  34. package/dist/html/index.d.ts.map +1 -1
  35. package/dist/html/index.js +7 -0
  36. package/dist/html/index.js.map +1 -1
  37. package/dist/html/layout/block.d.ts +29 -9
  38. package/dist/html/layout/block.d.ts.map +1 -1
  39. package/dist/html/layout/block.js +141 -52
  40. package/dist/html/layout/block.js.map +1 -1
  41. package/dist/html/layout/boxes.d.ts +29 -2
  42. package/dist/html/layout/boxes.d.ts.map +1 -1
  43. package/dist/html/layout/boxes.js +172 -23
  44. package/dist/html/layout/boxes.js.map +1 -1
  45. package/dist/html/layout/css-grid.d.ts.map +1 -1
  46. package/dist/html/layout/css-grid.js +11 -9
  47. package/dist/html/layout/css-grid.js.map +1 -1
  48. package/dist/html/layout/flex.d.ts.map +1 -1
  49. package/dist/html/layout/flex.js +1759 -238
  50. package/dist/html/layout/flex.js.map +1 -1
  51. package/dist/html/layout/inline.d.ts +15 -3
  52. package/dist/html/layout/inline.d.ts.map +1 -1
  53. package/dist/html/layout/inline.js +64 -19
  54. package/dist/html/layout/inline.js.map +1 -1
  55. package/dist/html/layout/table.d.ts +27 -1
  56. package/dist/html/layout/table.d.ts.map +1 -1
  57. package/dist/html/layout/table.js +886 -324
  58. package/dist/html/layout/table.js.map +1 -1
  59. package/dist/html/node.d.ts +35 -4
  60. package/dist/html/node.d.ts.map +1 -1
  61. package/dist/html/node.js +122 -26
  62. package/dist/html/node.js.map +1 -1
  63. package/dist/html/paint.d.ts.map +1 -1
  64. package/dist/html/paint.js +558 -178
  65. package/dist/html/paint.js.map +1 -1
  66. package/dist/html/resources.js +14 -1
  67. package/dist/html/resources.js.map +1 -1
  68. package/dist/html/svg.d.ts +23 -7
  69. package/dist/html/svg.d.ts.map +1 -1
  70. package/dist/html/svg.js +264 -59
  71. package/dist/html/svg.js.map +1 -1
  72. package/dist/html/widgets.d.ts.map +1 -1
  73. package/dist/html/widgets.js +57 -4
  74. package/dist/html/widgets.js.map +1 -1
  75. package/package.json +3 -3
  76. package/src/code-editor/node.ts +24 -5
  77. package/src/html/css/cascade.ts +697 -221
  78. package/src/html/css/color.ts +345 -18
  79. package/src/html/css/content.ts +173 -15
  80. package/src/html/css/parse.ts +60 -8
  81. package/src/html/css/shapes.ts +35 -1
  82. package/src/html/css/style.ts +122 -17
  83. package/src/html/css/ua.ts +27 -8
  84. package/src/html/css/values.ts +23 -9
  85. package/src/html/index.ts +7 -0
  86. package/src/html/layout/block.ts +149 -58
  87. package/src/html/layout/boxes.ts +211 -27
  88. package/src/html/layout/css-grid.ts +12 -9
  89. package/src/html/layout/flex.ts +1975 -231
  90. package/src/html/layout/inline.ts +70 -16
  91. package/src/html/layout/table.ts +961 -332
  92. package/src/html/node.ts +155 -27
  93. package/src/html/paint.ts +692 -234
  94. package/src/html/resources.ts +13 -1
  95. package/src/html/svg.ts +299 -56
  96. package/src/html/widgets.ts +61 -4
@@ -951,11 +951,13 @@ const PSEUDO_CLASSES = new Set([
951
951
  /**
952
952
  * Specificity, packed. Counted from the selector text rather than from a
953
953
  * parse: `#a` is an id, `.a`/`[a]`/`:a` is a class, a bare name is a type,
954
- * and a pseudo-*element* (`::before`) counts as a type. Functional
955
- * pseudo-classes are counted as one class each, which is right for
956
- * `:hover`/`:nth-child()` and approximate for `:is()`/`:not()` — whose
957
- * specificity is their argument's. The approximation costs an author who
958
- * writes `:is(#id)` and expects it to beat a class; nothing else.
954
+ * and a pseudo-*element* (`::before`) counts as a type. A pseudo-class
955
+ * that takes selectors counts as Selectors 4 (17) has it: `:is()`,
956
+ * `:not()` and `:has()` as the most specific selector in their list,
957
+ * `:where()` as nothing, and `:nth-child(An+B of S)` as a class and the
958
+ * most specific in S. Counted as a class each, `:where()` was not the
959
+ * zero a library writes it to be: a typography plugin's
960
+ * `.prose :where(p)` beat a page's own `.intro p`.
959
961
  */
960
962
  const LEGACY_PSEUDO_ELEMENTS = /^(?:before|after|first-line|first-letter)$/i;
961
963
 
@@ -966,6 +968,8 @@ export function specificityOf(selector: string): number {
966
968
  let ids = 0;
967
969
  let classes = 0;
968
970
  let types = 0;
971
+ /** What the selectors inside pseudo-classes come to, packed. */
972
+ let inner = 0;
969
973
  let i = 0;
970
974
  const n = selector.length;
971
975
  while (i < n) {
@@ -985,14 +989,29 @@ export function specificityOf(selector: string): number {
985
989
  i = identEnd(selector, i + 2);
986
990
  } else {
987
991
  const name = readIdent(selector, i + 1);
992
+ const lower = name.value.toLowerCase();
993
+ i = name.end;
994
+ let argument: string | null = null;
995
+ if (selector[i] === '(') {
996
+ const end = componentEnd(selector, i);
997
+ argument = selector.slice(i + 1, end - 1);
998
+ i = end;
999
+ }
988
1000
  // CSS 2 spelled the four pseudo-elements it had with one colon
989
1001
  if (LEGACY_PSEUDO_ELEMENTS.test(name.value)) {
990
1002
  types += 1;
1003
+ } else if (argument !== null && SELECTOR_LIST_PSEUDOS.has(lower)) {
1004
+ inner += mostSpecific(argument);
1005
+ } else if (argument !== null && lower === 'where') {
1006
+ // nothing
991
1007
  } else {
992
1008
  classes += 1;
1009
+ if (argument !== null && NTH_OF_PSEUDOS.has(lower)) {
1010
+ const of = /\sof\s/i.exec(argument);
1011
+ if (of)
1012
+ inner += mostSpecific(argument.slice(of.index + of[0].length));
1013
+ }
993
1014
  }
994
- i = name.end;
995
- if (selector[i] === '(') i = componentEnd(selector, i);
996
1015
  }
997
1016
  } else if (c === '*' || c === '|') {
998
1017
  // the universal selector, and a namespace's bar, count nothing
@@ -1004,7 +1023,40 @@ export function specificityOf(selector: string): number {
1004
1023
  i += 1;
1005
1024
  }
1006
1025
  }
1007
- return ids * 1_000_000 + classes * 1_000 + types;
1026
+ return ids * 1_000_000 + classes * 1_000 + types + inner;
1027
+ }
1028
+
1029
+ /** The pseudo-classes whose specificity is their argument's most specific
1030
+ * selector, the old names of `:is()` among them. */
1031
+ const SELECTOR_LIST_PSEUDOS = new Set([
1032
+ 'is',
1033
+ 'not',
1034
+ 'has',
1035
+ 'matches',
1036
+ '-webkit-any',
1037
+ '-moz-any',
1038
+ ]);
1039
+
1040
+ /** The pseudo-classes an `of S` may follow. */
1041
+ const NTH_OF_PSEUDOS = new Set(['nth-child', 'nth-last-child']);
1042
+
1043
+ /** The specificity of the most specific selector in a list, packed. */
1044
+ function mostSpecific(list: string): number {
1045
+ let most = 0;
1046
+ let depth = 0;
1047
+ let start = 0;
1048
+ for (let i = 0; i <= list.length; i += 1) {
1049
+ const c = list[i];
1050
+ if (c === '(' || c === '[') depth += 1;
1051
+ else if (c === ')' || c === ']') depth -= 1;
1052
+ else if (c === '"' || c === "'" || c === '\\') {
1053
+ i = componentEnd(list, i) - 1;
1054
+ } else if ((c === ',' && depth === 0) || i === list.length) {
1055
+ most = Math.max(most, specificityOf(list.slice(start, i)));
1056
+ start = i + 1;
1057
+ }
1058
+ }
1059
+ return most;
1008
1060
  }
1009
1061
 
1010
1062
  // --- the little scanner -----------------------------------------------------
@@ -13,7 +13,7 @@
13
13
  // (`svg.ts`). A `ComputedStyle` per `<path>` would be a few hundred
14
14
  // properties for the dozen a shape has, on a page of hundreds of icons.
15
15
  import type { Element } from 'domhandler';
16
- import { LIGHT_DARK, lightDark } from './color.js';
16
+ import { LIGHT_DARK, SYSTEM_COLOR, lightDark, systemColors } from './color.js';
17
17
  import { inkColor, parseAlpha, parseColor, parseLength } from './values.js';
18
18
  import type { UnitContext } from './values.js';
19
19
 
@@ -25,10 +25,38 @@ export type ShapeStyle = Readonly<Record<string, string>>;
25
25
  /** What the rules give the elements of one drawing, and all of it as a
26
26
  * string: what a drawing's tree was last made with (`SvgDrawing`). */
27
27
  export interface ShapeStyles {
28
+ /** Each element's, where it stands. */
28
29
  readonly of: ReadonlyMap<Element, ShapeStyle>;
30
+ /**
31
+ * What they give the copy a `<use>` draws, by the `<use>`, then by each
32
+ * element of the copy's original; null where they give none. A copy is
33
+ * styled in a tree of its own, which its original is the top of (SVG 2,
34
+ * 5.5.3, as Chrome has it): a rule finds no ancestor of that element's
35
+ * and none of the `<use>`'s, and no sibling of it, and what the top
36
+ * inherits is the `<use>`'s. So `symbol .line` reaches a sprite's line
37
+ * where `.sprite .line` does not, and an element a `<use>` draws may be
38
+ * styled otherwise than where it stands.
39
+ */
40
+ readonly used: ReadonlyMap<Element, ReadonlyMap<Element, ShapeStyle>> | null;
29
41
  readonly key: string;
30
42
  }
31
43
 
44
+ /** The id a `<use>` names the element it draws by: a fragment of its
45
+ * `href`. One in another document is none here, where nothing is fetched
46
+ * for a drawing. */
47
+ export function useHref(el: Element): string | null {
48
+ const href = el.attribs.href ?? el.attribs['xlink:href'] ?? '';
49
+ return href.length > 1 && href.startsWith('#') ? href.slice(1) : null;
50
+ }
51
+
52
+ /** Whether an element is a `<use>`, with a prefix or not. */
53
+ export function isUse(el: Element): boolean {
54
+ const name = el.name;
55
+ if (name === 'use') return true;
56
+ const i = name.indexOf(':');
57
+ return i >= 0 && name.slice(i + 1).toLowerCase() === 'use';
58
+ }
59
+
32
60
  /** Each property read, and whether it is inherited. */
33
61
  const PROPS: Record<string, boolean> = {
34
62
  fill: true,
@@ -143,6 +171,9 @@ const LINE_JOINS: Record<string, string> = {
143
171
  arcs: 'miter',
144
172
  };
145
173
 
174
+ /** The properties of a shape that a colour is written in. */
175
+ const SHAPE_COLORS = new Set(['fill', 'stroke', 'color', 'stop-color']);
176
+
146
177
  /**
147
178
  * One declaration's value as a drawing reads it, or null for a value the
148
179
  * property does not take, which leaves the declaration out of the cascade
@@ -165,6 +196,9 @@ export function shapeValue(
165
196
  if (wide === 'unset' || wide === 'revert' || wide === 'revert-layer') {
166
197
  return PROPS[prop] ? 'inherit' : INITIAL[prop];
167
198
  }
199
+ if (SHAPE_COLORS.has(prop) && SYSTEM_COLOR.test(value)) {
200
+ value = systemColors(value, ctx.units.systemColors);
201
+ }
168
202
  if (LIGHT_DARK.test(value)) {
169
203
  const picked = lightDark(value, ctx.scheme);
170
204
  if (picked === null) return null;
@@ -32,7 +32,6 @@ import {
32
32
  import type { Len, Pct, UnitContext } from './values.js';
33
33
  import { parseUrl, readIdent, startsIdent } from './parse.js';
34
34
  import {
35
- DEFAULT_QUOTES,
36
35
  parseContent,
37
36
  parseCounterList,
38
37
  parseListStyleType,
@@ -42,7 +41,13 @@ import type { ContentItem, CounterChange } from './content.js';
42
41
  import type { CustomProps } from './vars.js';
43
42
  import { parseRotate, parseScale, parseTransform } from './transform.js';
44
43
  import type { TransformFunction } from './transform.js';
45
- import { LIGHT_DARK, lightDark, usedColorScheme } from './color.js';
44
+ import {
45
+ LIGHT_DARK,
46
+ SYSTEM_COLOR,
47
+ lightDark,
48
+ systemColors,
49
+ usedColorScheme,
50
+ } from './color.js';
46
51
  import { svgPaint } from './shapes.js';
47
52
 
48
53
  export type Display =
@@ -457,7 +462,7 @@ export interface ComputedStyle {
457
462
  * `text-align` on a `<table>` to behave. */
458
463
  tableTextAlignSet: boolean;
459
464
  /** The marks `open-quote` and `close-quote` write, pairs outermost first. */
460
- quotes: readonly string[] | 'none';
465
+ quotes: readonly string[] | 'none' | 'auto';
461
466
  /** Custom properties, `--name`, with their `var()`s replaced (`vars.ts`);
462
467
  * null where none is set. Inherited as the same map. */
463
468
  custom: CustomProps | null;
@@ -656,7 +661,8 @@ export interface ComputedStyle {
656
661
  /** Its shadows front to back, the ones that can be seen; null for none. */
657
662
  boxShadow: BoxShadow[] | null;
658
663
  /** Its outline (CSS 2.1 18.4, CSS UI 4): drawn around the border box,
659
- * grown by `outlineOffset`, taking no room; `auto` is drawn solid. */
664
+ * grown by `outlineOffset`, taking no room; `auto` is the palette's
665
+ * focus ring (`settleOutline`), drawn solid. */
660
666
  outlineStyle: BorderStyle | 'auto';
661
667
  outlineWidth: number;
662
668
  outlineColor: string;
@@ -761,16 +767,31 @@ export interface ComputedStyle {
761
767
  | 'space-between'
762
768
  | 'space-around'
763
769
  | 'space-evenly';
764
- alignItems: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
770
+ /** `normal` is `stretch` in a flex box, and is kept apart from it only
771
+ * in a grid, where it does not stretch an item with a natural size or a
772
+ * ratio (`settleAlign`). */
773
+ alignItems:
774
+ 'normal' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
765
775
  alignSelf:
766
- 'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
776
+ | 'auto'
777
+ | 'normal'
778
+ | 'flex-start'
779
+ | 'flex-end'
780
+ | 'center'
781
+ | 'stretch'
782
+ | 'baseline';
783
+ /** `start` and `end` are kept: a flex box that wraps in reverse puts
784
+ * them at the other ends of its lines from `flex-start` and `flex-end`. */
767
785
  alignContent:
768
786
  | 'flex-start'
769
787
  | 'flex-end'
788
+ | 'start'
789
+ | 'end'
770
790
  | 'center'
771
791
  | 'stretch'
772
792
  | 'space-between'
773
- | 'space-around';
793
+ | 'space-around'
794
+ | 'space-evenly';
774
795
  flexGrow: number;
775
796
  flexShrink: number;
776
797
  flexBasis: Len | 'auto' | 'content';
@@ -913,6 +934,51 @@ export interface RootLook {
913
934
  * is in Chrome. Without it, `fontFamily`.
914
935
  */
915
936
  controlFontFamily?: string;
937
+ /**
938
+ * The ring the palette's widgets draw round the one that has the focus —
939
+ * `theme.focusRing`, `focusRingWidth` and `focusRingOffset` — which is
940
+ * what an `outline-style: auto` is, the platform's own (CSS UI 4, 5.3),
941
+ * and what `-webkit-focus-ring-color` names. Without them, core's own
942
+ * defaults.
943
+ */
944
+ focusRing?: string;
945
+ focusRingWidth?: number;
946
+ focusRingOffset?: number;
947
+ }
948
+
949
+ /** Core's focus ring, where a look names none (`DEFAULT_FOCUS_RING`). */
950
+ const FOCUS_RING = { color: '#2980b9', width: 2, offset: 1 };
951
+
952
+ /**
953
+ * An `outline-style: auto` as the platform's focus ring: the palette's
954
+ * width and, where the outline's colour is the text's or the keyword's,
955
+ * its colour, the palette's gap outside `outline-offset` — so a field the
956
+ * document draws is ringed as the window's own fields are. A browser
957
+ * draws `auto` in a style of its own the same way, whatever width it was
958
+ * given. `look` is the device one, as every length here is, and `scale`
959
+ * what makes core's defaults device lengths too.
960
+ */
961
+ export function settleOutline(
962
+ style: ComputedStyle,
963
+ look: RootLook,
964
+ scale: number,
965
+ ): void {
966
+ if (style.outlineStyle !== 'auto') return;
967
+ style.outlineWidth = look.focusRingWidth ?? FOCUS_RING.width * scale;
968
+ style.outlineOffset += look.focusRingOffset ?? FOCUS_RING.offset * scale;
969
+ if (style.outlineColor === 'currentColor') {
970
+ style.outlineColor = look.focusRing ?? FOCUS_RING.color;
971
+ }
972
+ }
973
+
974
+ /** `-webkit-focus-ring-color`, a colour of an outline's. */
975
+ function outlineColorOf(v: string, ctx: UnitContext): string | null {
976
+ const lower = v.trim().toLowerCase();
977
+ if (lower === 'invert') return 'currentColor';
978
+ if (lower === '-webkit-focus-ring-color') {
979
+ return ctx.focusRing ?? FOCUS_RING.color;
980
+ }
981
+ return parseColor(v);
916
982
  }
917
983
 
918
984
  export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
@@ -968,7 +1034,7 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
968
1034
  captionSide: 'top',
969
1035
  emptyCells: 'show',
970
1036
  tableTextAlignSet: false,
971
- quotes: DEFAULT_QUOTES,
1037
+ quotes: 'auto',
972
1038
  custom: null,
973
1039
 
974
1040
  display: 'inline',
@@ -1114,7 +1180,7 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
1114
1180
  flexDirection: 'row',
1115
1181
  flexWrap: 'nowrap',
1116
1182
  justifyContent: 'normal',
1117
- alignItems: 'stretch',
1183
+ alignItems: 'normal',
1118
1184
  alignSelf: 'auto',
1119
1185
  alignContent: 'stretch',
1120
1186
  flexGrow: 0,
@@ -1314,6 +1380,11 @@ export function firstLineParent(
1314
1380
  // --- applying a declaration -------------------------------------------------
1315
1381
 
1316
1382
  /** Longhands that take a colour and nothing else, by property name. */
1383
+ /** The properties a colour is written in, alone or in a shorthand, a
1384
+ * shadow, a gradient or an image's mask. */
1385
+ const TAKES_COLOR =
1386
+ /^(?:color|background|border|outline|box-shadow|text-shadow|text-decoration|text-emphasis|column-rule|fill|stroke|stop-color|flood-color|lighting-color|caret-color|accent-color|scrollbar-color|filter|mask|list-style|-webkit-text-(?:fill|stroke))/;
1387
+
1317
1388
  const COLOR_PROPS: Record<string, keyof ComputedStyle> = {
1318
1389
  color: 'color',
1319
1390
  'background-color': 'backgroundColor',
@@ -1438,6 +1509,11 @@ export function applyDeclaration(
1438
1509
  if (/!\s*important$/i.test(value)) {
1439
1510
  value = value.replace(/!\s*important$/i, '').trim();
1440
1511
  }
1512
+ // a system colour is a `light-dark()` of its own, in a property that
1513
+ // takes a colour: `Menu` is a font in `font`, and `Mark` a family's name
1514
+ if (TAKES_COLOR.test(name) && SYSTEM_COLOR.test(value)) {
1515
+ value = systemColors(value, ctx.systemColors);
1516
+ }
1441
1517
  // the branch of each `light-dark()` this element's scheme picks, before
1442
1518
  // anything reads the value: it stands wherever a colour can, in a
1443
1519
  // shorthand, a shadow, a gradient or a mix, and most often arrives
@@ -2089,7 +2165,7 @@ export function applyDeclaration(
2089
2165
  width = w;
2090
2166
  continue;
2091
2167
  }
2092
- const c = v === 'invert' ? 'currentColor' : parseColor(part);
2168
+ const c = outlineColorOf(part, ctx);
2093
2169
  if (c === null) return;
2094
2170
  color = c;
2095
2171
  }
@@ -2110,8 +2186,7 @@ export function applyDeclaration(
2110
2186
  }
2111
2187
  case 'outline-color': {
2112
2188
  // `invert` is the text's colour where a browser cannot invert
2113
- const v = value.trim().toLowerCase();
2114
- const c = v === 'invert' ? 'currentColor' : parseColor(value);
2189
+ const c = outlineColorOf(value, ctx);
2115
2190
  if (c !== null) style.outlineColor = c;
2116
2191
  return;
2117
2192
  }
@@ -2906,13 +2981,17 @@ export function applyDeclaration(
2906
2981
  return;
2907
2982
  }
2908
2983
  case 'align-items': {
2909
- const v = alignKeyword(value);
2984
+ const v =
2985
+ value.trim().toLowerCase() === 'normal'
2986
+ ? 'normal'
2987
+ : alignKeyword(value);
2910
2988
  if (v) style.alignItems = v as ComputedStyle['alignItems'];
2911
2989
  return;
2912
2990
  }
2913
2991
  case 'align-self': {
2914
- if (value.toLowerCase() === 'auto') {
2915
- style.alignSelf = AUTO;
2992
+ const keyword = value.trim().toLowerCase();
2993
+ if (keyword === 'auto' || keyword === 'normal') {
2994
+ style.alignSelf = keyword;
2916
2995
  return;
2917
2996
  }
2918
2997
  const v = alignKeyword(value);
@@ -2921,11 +3000,15 @@ export function applyDeclaration(
2921
3000
  }
2922
3001
  case 'align-content': {
2923
3002
  // `normal` is `stretch` in a flex and a grid container, the only
2924
- // boxes it moves anything in (CSS Box Alignment 3, 5.1)
3003
+ // boxes it moves anything in (CSS Box Alignment 3, 5.1); `start` and
3004
+ // `end` are the box's own edges, and stay those
3005
+ const plain = overflowAligned(value);
2925
3006
  const v =
2926
3007
  value.trim().toLowerCase() === 'normal'
2927
3008
  ? 'stretch'
2928
- : alignKeyword(value);
3009
+ : plain === 'start' || plain === 'end'
3010
+ ? plain
3011
+ : alignKeyword(value);
2929
3012
  if (v) style.alignContent = v as ComputedStyle['alignContent'];
2930
3013
  return;
2931
3014
  }
@@ -3373,6 +3456,28 @@ export function settleContentVisibility(style: ComputedStyle): void {
3373
3456
  }
3374
3457
  }
3375
3458
 
3459
+ /**
3460
+ * `align-items: normal` and `align-self: normal`, settled once the cascade
3461
+ * knows what the box and its parent are. In a grid `normal` stretches an
3462
+ * item only where it has neither a natural size nor a ratio, and `stretch`
3463
+ * stretches every item (CSS Grid 1, 6.2; CSS Box Alignment 3, 6.1), so the
3464
+ * grid keeps the two apart; everywhere else `normal` is `stretch`, and a
3465
+ * flex box reads nothing new. Unread, `normal` was dropped, and a grid could
3466
+ * not tell an unset `align-items` from `stretch`: an image under an explicit
3467
+ * `stretch` kept the height its ratio gave it.
3468
+ */
3469
+ export function settleAlign(
3470
+ style: ComputedStyle,
3471
+ parent: ComputedStyle | null,
3472
+ ): void {
3473
+ if (style.alignItems === 'normal' && !style.grid) {
3474
+ style.alignItems = 'stretch';
3475
+ }
3476
+ if (style.alignSelf === 'normal' && !parent?.grid) {
3477
+ style.alignSelf = 'stretch';
3478
+ }
3479
+ }
3480
+
3376
3481
  export function settleOverflow(style: ComputedStyle): void {
3377
3482
  const x = style.overflowX;
3378
3483
  const y = style.overflowY;
@@ -211,12 +211,16 @@ col { display: table-column; }
211
211
  /* The form controls are real widgets rather than drawn boxes, so what the
212
212
  UA sheet owes them is a *box* of about the right size in the flow — the
213
213
  widget is painted into it by the component above. 'inline-block' is what
214
- makes a label and its input share a line. */
214
+ makes a label and its input share a line. Chrome's margins and alignment:
215
+ none round a field, a select, a button or a text area, each on its line's
216
+ baseline — a field's text on the text beside it (\`CONTROL_BASELINES\`) —
217
+ and a meter or a progress bar a fifth of an em under it. */
215
218
  input, button, select, textarea, meter, progress {
216
219
  display: inline-block;
217
- vertical-align: middle;
218
- margin: 3px 2px;
220
+ vertical-align: baseline;
221
+ margin: 0;
219
222
  }
223
+ meter, progress { vertical-align: -0.2em; }
220
224
  /* A control's text is a system font's, not its parent's: Chrome gives these
221
225
  four \`font: -webkit-small-control\`, Arial at the default size less 2pt
222
226
  (13.33px) whatever the text around it is set in, and Gecko \`-moz-field\`
@@ -231,8 +235,6 @@ input, button, select, textarea {
231
235
  font-size: ${look.controlFontSize ?? look.fontSize}px;
232
236
  }
233
237
  textarea { font-family: ${mono}; }
234
- /* Chrome's own UA margins for the checkables, near enough: they are the
235
- controls that sit hard against their label text otherwise. */
236
238
  /* HTML's rendering section (15.3.10): a control's text keeps none of the
237
239
  spacing, the line height, the case or the indent of the text around it.
238
240
  A button in a paragraph of 'line-height: 1.5' is its own font's line
@@ -247,8 +249,22 @@ input, select, button, textarea {
247
249
  text-indent: initial;
248
250
  text-shadow: initial;
249
251
  }
250
- input[type=checkbox] { margin: 3px 4px 3px 4px; }
251
- input[type=radio] { margin: 3px 4px 3px 5px; }
252
+ /* Chrome's ring round a control that shows it has the focus, and \`auto\` is
253
+ the platform's own: the palette's ring here (\`settleOutline\`), round
254
+ the element's border box, which the document draws — the field's widget
255
+ draws none, so a page's \`outline: none\`, or a ring of its own, is what
256
+ shows. Only a text field's widget tells the document it has the focus:
257
+ the other controls are core's components, which draw their own ring and
258
+ keep their focus to themselves. */
259
+ input:focus-visible, textarea:focus-visible {
260
+ outline: auto 1px -webkit-focus-ring-color;
261
+ outline-offset: 0;
262
+ }
263
+ /* Chrome's own margins for the checkables and a range, which sit on their
264
+ border box's bottom edge, the margin under the line */
265
+ input[type=checkbox] { margin: 3px 3px 3px 4px; }
266
+ input[type=radio] { margin: 3px 3px 0 5px; }
267
+ input[type=range] { margin: 2px; }
252
268
  input[type=hidden] { display: none; }
253
269
  /* A <button> is drawn rather than mounted: its content is the document's —
254
270
  an icon, a label in spans, a pill of the page's own design, which is what
@@ -269,7 +285,6 @@ button {
269
285
  cursor: pointer;
270
286
  }
271
287
  button[disabled] { color: ${look.mutedColor}; cursor: default; }
272
- textarea { vertical-align: top; }
273
288
  fieldset { margin: 0 2px; padding: 0.35em 0.75em 0.6em; border: 1px solid ${look.borderColor}; }
274
289
  legend { display: block; padding: 0 2px; }
275
290
  label { cursor: pointer; }
@@ -292,6 +307,9 @@ details[open] > summary:first-of-type { list-style-type: disclosure-open; }
292
307
  * and border longhand, radius included — along with the native look it
293
308
  * would lose in a browser. Nothing stays half the palette's and half the
294
309
  * page's.
310
+ *
311
+ * A field's frame is its widget's, but its corners are the element's too:
312
+ * the focus ring the document draws round it is rounded as the frame is.
295
313
  */
296
314
  function chromeText(look: RootLook): string {
297
315
  return `
@@ -300,5 +318,6 @@ button {
300
318
  border: ${look.controlBorder}px solid ${look.borderColor};
301
319
  border-radius: ${look.controlRadius}px;
302
320
  }
321
+ input, textarea { border-radius: ${look.controlRadius}px; }
303
322
  `;
304
323
  }
@@ -127,6 +127,11 @@ export interface UnitContext {
127
127
  rlh?: () => number;
128
128
  /** Every property at its initial value, which `initial` sets one to. */
129
129
  initial?: ComputedStyle;
130
+ /** The palette's focus ring, which `-webkit-focus-ring-color` is. */
131
+ focusRing?: string;
132
+ /** Each system colour as the `light-dark()` it is, the palette's in the
133
+ * palette's scheme (`systemColorTable`); Chrome's where it is missing. */
134
+ systemColors?: Map<string, string>;
130
135
  }
131
136
 
132
137
  /** A CSS number (CSS Syntax 3 4.3.12): a sign, digits with at most one
@@ -348,9 +353,10 @@ export function fourSides<T>(parts: T[]): [T, T, T, T] {
348
353
  * element's own ink, everything else passed through. */
349
354
  export function inkColor(color: string, current: string): string {
350
355
  if (color === 'currentColor') return current;
351
- // a `color-mix()` with `currentColor` in it waits for the colour too;
352
- // every other functional colour was read when it was parsed
353
- if (color.charCodeAt(0) === 99 && color.startsWith('color-mix(')) {
356
+ // a `color-mix()` or a relative colour with `currentColor` in it waits
357
+ // for the colour too; every other functional colour was read when it was
358
+ // parsed, to `#rrggbb` or `rgba()` with no letters in it
359
+ if (color.charCodeAt(color.length - 1) === 41 && HAS_CURRENT.test(color)) {
354
360
  return functionalColor(color.replace(CURRENT, current)) ?? current;
355
361
  }
356
362
  return color;
@@ -358,6 +364,7 @@ export function inkColor(color: string, current: string): string {
358
364
 
359
365
  const CURRENT = /currentcolor/gi;
360
366
  const HAS_CURRENT = /currentcolor/i;
367
+ const RELATIVE_COLOR = /^[a-z]+\(\s*from\s/i;
361
368
 
362
369
  /** Whether a colour paints anything at all. Two string tests rather than a
363
370
  * parse, for the reason at the top of the file. */
@@ -410,12 +417,19 @@ export function parseColor(value: string): string | null {
410
417
  // five or seven, and a typo'd `#ff000` took the application down
411
418
  if (v.startsWith('#')) return HEX_COLOR.test(v) ? v : null;
412
419
  if (/^(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color|color-mix)\(/i.test(v)) {
413
- // a mix with `currentColor` in it is kept to be mixed where it is used
414
- // (`inkColor`), once it is known to be one
415
- if (HAS_CURRENT.test(v) && /^color-mix\(/i.test(v)) {
416
- return functionalColor(v.replace(CURRENT, '#000')) === null
417
- ? null
418
- : `color-mix(${v.slice(v.indexOf('(') + 1)}`;
420
+ // a mix or a relative colour with `currentColor` in it is kept to be
421
+ // worked out where it is used (`inkColor`), once it is known to be one
422
+ if (HAS_CURRENT.test(v)) {
423
+ if (/^color-mix\(/i.test(v)) {
424
+ return functionalColor(v.replace(CURRENT, '#000')) === null
425
+ ? null
426
+ : `color-mix(${v.slice(v.indexOf('(') + 1)}`;
427
+ }
428
+ if (RELATIVE_COLOR.test(v)) {
429
+ return functionalColor(v.replace(CURRENT, '#000')) === null
430
+ ? null
431
+ : `${v.slice(0, v.indexOf('(')).toLowerCase()}${v.slice(v.indexOf('('))}`;
432
+ }
419
433
  }
420
434
  return functionalColor(v);
421
435
  }
package/src/html/index.ts CHANGED
@@ -238,6 +238,13 @@ function deriveLook(
238
238
  // `medium` and the face it reads pages in
239
239
  controlFontSize: Number(theme.fontSize ?? 14),
240
240
  controlFontFamily: String(theme.fontFamily ?? 'sans-serif'),
241
+ ...(typeof theme.focusRing === 'string' && { focusRing: theme.focusRing }),
242
+ ...(typeof theme.focusRingWidth === 'number' && {
243
+ focusRingWidth: theme.focusRingWidth,
244
+ }),
245
+ ...(typeof theme.focusRingOffset === 'number' && {
246
+ focusRingOffset: theme.focusRingOffset,
247
+ }),
241
248
  };
242
249
  }
243
250