@react-x11/components 0.14.0 → 0.16.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.
Files changed (97) 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 +209 -23
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +873 -175
  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 +62 -0
  21. package/dist/html/css/shapes.d.ts.map +1 -0
  22. package/dist/html/css/shapes.js +214 -0
  23. package/dist/html/css/shapes.js.map +1 -0
  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 +75 -27
  27. package/dist/html/css/style.js.map +1 -1
  28. package/dist/html/css/ua.js +15 -0
  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 +38 -7
  38. package/dist/html/layout/block.d.ts.map +1 -1
  39. package/dist/html/layout/block.js +160 -50
  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 +159 -20
  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 +69 -19
  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 +1648 -225
  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 +23 -8
  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 +907 -323
  58. package/dist/html/layout/table.js.map +1 -1
  59. package/dist/html/node.d.ts +36 -4
  60. package/dist/html/node.d.ts.map +1 -1
  61. package/dist/html/node.js +174 -37
  62. package/dist/html/node.js.map +1 -1
  63. package/dist/html/paint.d.ts +4 -1
  64. package/dist/html/paint.d.ts.map +1 -1
  65. package/dist/html/paint.js +769 -204
  66. package/dist/html/paint.js.map +1 -1
  67. package/dist/html/resources.js +14 -1
  68. package/dist/html/resources.js.map +1 -1
  69. package/dist/html/svg.d.ts +50 -17
  70. package/dist/html/svg.d.ts.map +1 -1
  71. package/dist/html/svg.js +501 -83
  72. package/dist/html/svg.js.map +1 -1
  73. package/dist/html/widgets.d.ts.map +1 -1
  74. package/dist/html/widgets.js +57 -4
  75. package/dist/html/widgets.js.map +1 -1
  76. package/package.json +3 -3
  77. package/src/code-editor/node.ts +24 -5
  78. package/src/html/css/cascade.ts +987 -176
  79. package/src/html/css/color.ts +345 -18
  80. package/src/html/css/content.ts +173 -15
  81. package/src/html/css/parse.ts +60 -8
  82. package/src/html/css/shapes.ts +273 -0
  83. package/src/html/css/style.ts +123 -30
  84. package/src/html/css/ua.ts +15 -0
  85. package/src/html/css/values.ts +23 -9
  86. package/src/html/index.ts +7 -0
  87. package/src/html/layout/block.ts +169 -56
  88. package/src/html/layout/boxes.ts +198 -24
  89. package/src/html/layout/css-grid.ts +89 -22
  90. package/src/html/layout/flex.ts +1863 -219
  91. package/src/html/layout/inline.ts +27 -7
  92. package/src/html/layout/table.ts +989 -333
  93. package/src/html/node.ts +204 -37
  94. package/src/html/paint.ts +999 -252
  95. package/src/html/resources.ts +13 -1
  96. package/src/html/svg.ts +600 -78
  97. 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 -----------------------------------------------------
@@ -0,0 +1,273 @@
1
+ // What a style sheet says of the shapes in a drawing.
2
+ //
3
+ // An inline `<svg>` is a replaced box: the box builder stops at it, and no
4
+ // computed style is made for what is inside. `fill`, `stroke` and their kin
5
+ // are properties all the same (SVG 2, 6.2 and 13.2), which a rule of the
6
+ // document's sets on a `<path>` as it would on a paragraph — a logo's
7
+ // `.dark .logo path { fill: #fff }` — and a drawing exported with a style
8
+ // sheet of its own, `.st0 { fill: #fff }` on `<path class="st0">`, has all
9
+ // its colours in one. So the cascade is run for them narrowly
10
+ // (`Cascade.shapeStyles`): over the rules that declare one of the
11
+ // properties here, and down to values `SvgView` reads from a `style`
12
+ // attribute, which is where the drawing's copy of an element carries them
13
+ // (`svg.ts`). A `ComputedStyle` per `<path>` would be a few hundred
14
+ // properties for the dozen a shape has, on a page of hundreds of icons.
15
+ import type { Element } from 'domhandler';
16
+ import { LIGHT_DARK, SYSTEM_COLOR, lightDark, systemColors } from './color.js';
17
+ import { inkColor, parseAlpha, parseColor, parseLength } from './values.js';
18
+ import type { UnitContext } from './values.js';
19
+
20
+ /** What the rules give one element of a drawing: each property they set,
21
+ * as `SvgView` reads it — `inherit` for one they put back to what the
22
+ * element inherits, over its own attribute. */
23
+ export type ShapeStyle = Readonly<Record<string, string>>;
24
+
25
+ /** What the rules give the elements of one drawing, and all of it as a
26
+ * string: what a drawing's tree was last made with (`SvgDrawing`). */
27
+ export interface ShapeStyles {
28
+ /** Each element's, where it stands. */
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;
41
+ readonly key: string;
42
+ }
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
+
60
+ /** Each property read, and whether it is inherited. */
61
+ const PROPS: Record<string, boolean> = {
62
+ fill: true,
63
+ stroke: true,
64
+ 'stroke-width': true,
65
+ 'stroke-linecap': true,
66
+ 'stroke-linejoin': true,
67
+ 'stroke-miterlimit': true,
68
+ 'fill-rule': true,
69
+ 'fill-opacity': true,
70
+ 'stroke-opacity': true,
71
+ color: true,
72
+ visibility: true,
73
+ opacity: false,
74
+ display: false,
75
+ 'stop-color': false,
76
+ 'stop-opacity': false,
77
+ };
78
+
79
+ /** Whether a rule with this declaration is one to keep for drawings. */
80
+ export function isShapeProp(prop: string): boolean {
81
+ return PROPS[prop] !== undefined;
82
+ }
83
+
84
+ /** The properties the drawing's box has from its computed style already:
85
+ * its paint, which `paintSvg` hands the drawing, and what makes it a box,
86
+ * shown and see-through as one. */
87
+ export const ROOT_BOX_PROPS: ReadonlySet<string> = new Set([
88
+ 'fill',
89
+ 'stroke',
90
+ 'color',
91
+ 'display',
92
+ 'visibility',
93
+ 'opacity',
94
+ ]);
95
+
96
+ /** Each at its initial value (SVG 2, 13; CSS Color 4; CSS Display 3). */
97
+ const INITIAL: Record<string, string> = {
98
+ fill: '#000000',
99
+ stroke: 'none',
100
+ 'stroke-width': '1',
101
+ 'stroke-linecap': 'butt',
102
+ 'stroke-linejoin': 'miter',
103
+ 'stroke-miterlimit': '4',
104
+ 'fill-rule': 'nonzero',
105
+ 'fill-opacity': '1',
106
+ 'stroke-opacity': '1',
107
+ visibility: 'visible',
108
+ opacity: '1',
109
+ display: 'inline',
110
+ 'stop-color': '#000000',
111
+ 'stop-opacity': '1',
112
+ };
113
+
114
+ /** The elements a rule's subject can be, where it names one by its type,
115
+ * for the rule to reach into a drawing: the ones `SvgView` draws, or
116
+ * reads a paint from. `a` is both languages'. */
117
+ export const SHAPE_TAGS: ReadonlySet<string> = new Set([
118
+ 'svg',
119
+ 'g',
120
+ 'a',
121
+ 'path',
122
+ 'rect',
123
+ 'circle',
124
+ 'ellipse',
125
+ 'line',
126
+ 'polyline',
127
+ 'polygon',
128
+ 'text',
129
+ 'tspan',
130
+ 'use',
131
+ 'symbol',
132
+ 'switch',
133
+ 'image',
134
+ 'stop',
135
+ ]);
136
+
137
+ /** What a shape's values are read against: the drawing's own `color` and
138
+ * colour scheme, and its font size for a length in `em`. */
139
+ export interface ShapeContext {
140
+ color: string;
141
+ scheme: 'light' | 'dark';
142
+ units: UnitContext;
143
+ }
144
+
145
+ /**
146
+ * A `fill` or a `stroke` (SVG 2, 13.2): `none`, a colour, or a `url()`
147
+ * naming a gradient in the drawing, with what follows it, its fallback,
148
+ * left off. Null for anything else, `context-fill` and `context-stroke`
149
+ * among it, which only a marker or a `<use>` has a context for.
150
+ */
151
+ export function svgPaint(value: string): string | null {
152
+ const v = value.trim();
153
+ if (v.toLowerCase() === 'none') return 'none';
154
+ const url = /^url\(\s*(['"]?)(#[^'")\s]+)\1\s*\)/i.exec(v);
155
+ if (url) return `url(${url[2]})`;
156
+ return parseColor(v);
157
+ }
158
+
159
+ const NUMBER = /^[+-]?(?:\d*\.\d+|\d+)(?:e[+-]?\d+)?$/i;
160
+
161
+ /** A word of a `display` that makes a box (CSS Display 3, 2). */
162
+ const DISPLAY =
163
+ /^(?:block|inline|run-in|flow|flow-root|table|flex|grid|ruby|list-item|contents|inline-[a-z]+|table-[a-z-]+|ruby-[a-z-]+|-webkit-[a-z-]+)$/;
164
+
165
+ const LINE_JOINS: Record<string, string> = {
166
+ miter: 'miter',
167
+ round: 'round',
168
+ bevel: 'bevel',
169
+ // the two a canvas has no join for end in a miter, clipped or not
170
+ 'miter-clip': 'miter',
171
+ arcs: 'miter',
172
+ };
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
+
177
+ /**
178
+ * One declaration's value as a drawing reads it, or null for a value the
179
+ * property does not take, which leaves the declaration out of the cascade
180
+ * as any invalid one is left. `inherit` for a CSS-wide keyword that puts an
181
+ * inherited property back to what the element inherits.
182
+ */
183
+ export function shapeValue(
184
+ prop: string,
185
+ raw: string,
186
+ ctx: ShapeContext,
187
+ ): string | null {
188
+ let value = raw.trim();
189
+ const wide = value.toLowerCase();
190
+ if (wide === 'inherit') return PROPS[prop] ? 'inherit' : null;
191
+ if (wide === 'initial') {
192
+ return prop === 'color'
193
+ ? (ctx.units.initial?.color ?? '#000000')
194
+ : INITIAL[prop];
195
+ }
196
+ if (wide === 'unset' || wide === 'revert' || wide === 'revert-layer') {
197
+ return PROPS[prop] ? 'inherit' : INITIAL[prop];
198
+ }
199
+ if (SHAPE_COLORS.has(prop) && SYSTEM_COLOR.test(value)) {
200
+ value = systemColors(value, ctx.units.systemColors);
201
+ }
202
+ if (LIGHT_DARK.test(value)) {
203
+ const picked = lightDark(value, ctx.scheme);
204
+ if (picked === null) return null;
205
+ value = picked;
206
+ }
207
+ switch (prop) {
208
+ case 'fill':
209
+ case 'stroke': {
210
+ const paint = svgPaint(value);
211
+ // `currentColor` is the shape's own `color`, which the drawing knows
212
+ return paint === null || paint === 'currentColor'
213
+ ? paint
214
+ : inkColor(paint, ctx.color);
215
+ }
216
+ case 'color':
217
+ case 'stop-color': {
218
+ const color = parseColor(value);
219
+ return color === null ? null : inkColor(color, ctx.color);
220
+ }
221
+ case 'stroke-width': {
222
+ // a number is user units, as a length in pixels is; a percentage is
223
+ // of the viewport's diagonal, which only the drawing has
224
+ const px = NUMBER.test(value)
225
+ ? Number(value)
226
+ : lengthIn(value, ctx.units);
227
+ return px !== null && px >= 0 ? String(px) : null;
228
+ }
229
+ case 'stroke-miterlimit': {
230
+ const n = NUMBER.test(value) ? Number(value) : NaN;
231
+ return n >= 1 ? String(n) : null;
232
+ }
233
+ case 'fill-opacity':
234
+ case 'stroke-opacity':
235
+ case 'stop-opacity':
236
+ case 'opacity': {
237
+ const a = parseAlpha(value);
238
+ return a === null ? null : String(a);
239
+ }
240
+ case 'stroke-linecap': {
241
+ const v = value.toLowerCase();
242
+ return v === 'butt' || v === 'round' || v === 'square' ? v : null;
243
+ }
244
+ case 'stroke-linejoin':
245
+ return LINE_JOINS[value.toLowerCase()] ?? null;
246
+ case 'fill-rule': {
247
+ const v = value.toLowerCase();
248
+ return v === 'nonzero' || v === 'evenodd' ? v : null;
249
+ }
250
+ case 'visibility': {
251
+ const v = value.toLowerCase();
252
+ if (v === 'visible') return v;
253
+ return v === 'hidden' || v === 'collapse' ? 'hidden' : null;
254
+ }
255
+ case 'display': {
256
+ // `none`, or some box: which one is nothing to a shape
257
+ const v = value.toLowerCase();
258
+ if (v === 'none') return v;
259
+ return v.split(/\s+/).every((word) => DISPLAY.test(word))
260
+ ? 'inline'
261
+ : null;
262
+ }
263
+ default:
264
+ return null;
265
+ }
266
+ }
267
+
268
+ /** A length in user units, which are CSS pixels; null for none, and for a
269
+ * percentage. */
270
+ function lengthIn(value: string, units: UnitContext): number | null {
271
+ const length = parseLength(value, units);
272
+ return typeof length === 'number' ? length / units.scale : null;
273
+ }
@@ -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,14 @@ 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';
51
+ import { svgPaint } from './shapes.js';
46
52
 
47
53
  export type Display =
48
54
  | 'none'
@@ -456,7 +462,7 @@ export interface ComputedStyle {
456
462
  * `text-align` on a `<table>` to behave. */
457
463
  tableTextAlignSet: boolean;
458
464
  /** The marks `open-quote` and `close-quote` write, pairs outermost first. */
459
- quotes: readonly string[] | 'none';
465
+ quotes: readonly string[] | 'none' | 'auto';
460
466
  /** Custom properties, `--name`, with their `var()`s replaced (`vars.ts`);
461
467
  * null where none is set. Inherited as the same map. */
462
468
  custom: CustomProps | null;
@@ -655,7 +661,8 @@ export interface ComputedStyle {
655
661
  /** Its shadows front to back, the ones that can be seen; null for none. */
656
662
  boxShadow: BoxShadow[] | null;
657
663
  /** Its outline (CSS 2.1 18.4, CSS UI 4): drawn around the border box,
658
- * 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. */
659
666
  outlineStyle: BorderStyle | 'auto';
660
667
  outlineWidth: number;
661
668
  outlineColor: string;
@@ -760,16 +767,31 @@ export interface ComputedStyle {
760
767
  | 'space-between'
761
768
  | 'space-around'
762
769
  | 'space-evenly';
763
- 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';
764
775
  alignSelf:
765
- '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`. */
766
785
  alignContent:
767
786
  | 'flex-start'
768
787
  | 'flex-end'
788
+ | 'start'
789
+ | 'end'
769
790
  | 'center'
770
791
  | 'stretch'
771
792
  | 'space-between'
772
- | 'space-around';
793
+ | 'space-around'
794
+ | 'space-evenly';
773
795
  flexGrow: number;
774
796
  flexShrink: number;
775
797
  flexBasis: Len | 'auto' | 'content';
@@ -912,6 +934,51 @@ export interface RootLook {
912
934
  * is in Chrome. Without it, `fontFamily`.
913
935
  */
914
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);
915
982
  }
916
983
 
917
984
  export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
@@ -967,7 +1034,7 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
967
1034
  captionSide: 'top',
968
1035
  emptyCells: 'show',
969
1036
  tableTextAlignSet: false,
970
- quotes: DEFAULT_QUOTES,
1037
+ quotes: 'auto',
971
1038
  custom: null,
972
1039
 
973
1040
  display: 'inline',
@@ -1113,7 +1180,7 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
1113
1180
  flexDirection: 'row',
1114
1181
  flexWrap: 'nowrap',
1115
1182
  justifyContent: 'normal',
1116
- alignItems: 'stretch',
1183
+ alignItems: 'normal',
1117
1184
  alignSelf: 'auto',
1118
1185
  alignContent: 'stretch',
1119
1186
  flexGrow: 0,
@@ -1313,6 +1380,11 @@ export function firstLineParent(
1313
1380
  // --- applying a declaration -------------------------------------------------
1314
1381
 
1315
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
+
1316
1388
  const COLOR_PROPS: Record<string, keyof ComputedStyle> = {
1317
1389
  color: 'color',
1318
1390
  'background-color': 'backgroundColor',
@@ -1437,6 +1509,11 @@ export function applyDeclaration(
1437
1509
  if (/!\s*important$/i.test(value)) {
1438
1510
  value = value.replace(/!\s*important$/i, '').trim();
1439
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
+ }
1440
1517
  // the branch of each `light-dark()` this element's scheme picks, before
1441
1518
  // anything reads the value: it stands wherever a colour can, in a
1442
1519
  // shorthand, a shadow, a gradient or a mix, and most often arrives
@@ -2088,7 +2165,7 @@ export function applyDeclaration(
2088
2165
  width = w;
2089
2166
  continue;
2090
2167
  }
2091
- const c = v === 'invert' ? 'currentColor' : parseColor(part);
2168
+ const c = outlineColorOf(part, ctx);
2092
2169
  if (c === null) return;
2093
2170
  color = c;
2094
2171
  }
@@ -2109,8 +2186,7 @@ export function applyDeclaration(
2109
2186
  }
2110
2187
  case 'outline-color': {
2111
2188
  // `invert` is the text's colour where a browser cannot invert
2112
- const v = value.trim().toLowerCase();
2113
- const c = v === 'invert' ? 'currentColor' : parseColor(value);
2189
+ const c = outlineColorOf(value, ctx);
2114
2190
  if (c !== null) style.outlineColor = c;
2115
2191
  return;
2116
2192
  }
@@ -2905,13 +2981,17 @@ export function applyDeclaration(
2905
2981
  return;
2906
2982
  }
2907
2983
  case 'align-items': {
2908
- const v = alignKeyword(value);
2984
+ const v =
2985
+ value.trim().toLowerCase() === 'normal'
2986
+ ? 'normal'
2987
+ : alignKeyword(value);
2909
2988
  if (v) style.alignItems = v as ComputedStyle['alignItems'];
2910
2989
  return;
2911
2990
  }
2912
2991
  case 'align-self': {
2913
- if (value.toLowerCase() === 'auto') {
2914
- style.alignSelf = AUTO;
2992
+ const keyword = value.trim().toLowerCase();
2993
+ if (keyword === 'auto' || keyword === 'normal') {
2994
+ style.alignSelf = keyword;
2915
2995
  return;
2916
2996
  }
2917
2997
  const v = alignKeyword(value);
@@ -2920,11 +3000,15 @@ export function applyDeclaration(
2920
3000
  }
2921
3001
  case 'align-content': {
2922
3002
  // `normal` is `stretch` in a flex and a grid container, the only
2923
- // 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);
2924
3006
  const v =
2925
3007
  value.trim().toLowerCase() === 'normal'
2926
3008
  ? 'stretch'
2927
- : alignKeyword(value);
3009
+ : plain === 'start' || plain === 'end'
3010
+ ? plain
3011
+ : alignKeyword(value);
2928
3012
  if (v) style.alignContent = v as ComputedStyle['alignContent'];
2929
3013
  return;
2930
3014
  }
@@ -3372,6 +3456,28 @@ export function settleContentVisibility(style: ComputedStyle): void {
3372
3456
  }
3373
3457
  }
3374
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
+
3375
3481
  export function settleOverflow(style: ComputedStyle): void {
3376
3482
  const x = style.overflowX;
3377
3483
  const y = style.overflowY;
@@ -5154,19 +5260,6 @@ const POINTER_EVENTS = new Set([
5154
5260
  'stroke',
5155
5261
  'all',
5156
5262
  ]);
5157
- /**
5158
- * A `fill` or a `stroke` (SVG 2, 13.2): `none`, a colour, or a `url()`
5159
- * naming a gradient in the drawing, with what follows it, its fallback,
5160
- * left off. Null for anything else, `context-fill` and `context-stroke`
5161
- * among it, which only a marker or a `<use>` has a context for.
5162
- */
5163
- function svgPaint(value: string): string | null {
5164
- const v = value.trim();
5165
- if (v.toLowerCase() === 'none') return 'none';
5166
- const url = /^url\(\s*(['"]?)(#[^'")\s]+)\1\s*\)/i.exec(v);
5167
- if (url) return `url(${url[2]})`;
5168
- return parseColor(v);
5169
- }
5170
5263
 
5171
5264
  const OBJECT_FITS = new Set(['fill', 'contain', 'cover', 'none', 'scale-down']);
5172
5265
 
@@ -247,6 +247,17 @@ input, select, button, textarea {
247
247
  text-indent: initial;
248
248
  text-shadow: initial;
249
249
  }
250
+ /* Chrome's ring round a control that shows it has the focus, and \`auto\` is
251
+ the platform's own: the palette's ring here (\`settleOutline\`), round
252
+ the element's border box, which the document draws — the field's widget
253
+ draws none, so a page's \`outline: none\`, or a ring of its own, is what
254
+ shows. Only a text field's widget tells the document it has the focus:
255
+ the other controls are core's components, which draw their own ring and
256
+ keep their focus to themselves. */
257
+ input:focus-visible, textarea:focus-visible {
258
+ outline: auto 1px -webkit-focus-ring-color;
259
+ outline-offset: 0;
260
+ }
250
261
  input[type=checkbox] { margin: 3px 4px 3px 4px; }
251
262
  input[type=radio] { margin: 3px 4px 3px 5px; }
252
263
  input[type=hidden] { display: none; }
@@ -292,6 +303,9 @@ details[open] > summary:first-of-type { list-style-type: disclosure-open; }
292
303
  * and border longhand, radius included — along with the native look it
293
304
  * would lose in a browser. Nothing stays half the palette's and half the
294
305
  * page's.
306
+ *
307
+ * A field's frame is its widget's, but its corners are the element's too:
308
+ * the focus ring the document draws round it is rounded as the frame is.
295
309
  */
296
310
  function chromeText(look: RootLook): string {
297
311
  return `
@@ -300,5 +314,6 @@ button {
300
314
  border: ${look.controlBorder}px solid ${look.borderColor};
301
315
  border-radius: ${look.controlRadius}px;
302
316
  }
317
+ input, textarea { border-radius: ${look.controlRadius}px; }
303
318
  `;
304
319
  }