@react-x11/components 0.11.0 → 0.13.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 (126) hide show
  1. package/dist/flow/node.d.ts +14 -6
  2. package/dist/flow/node.d.ts.map +1 -1
  3. package/dist/flow/node.js +82 -64
  4. package/dist/flow/node.js.map +1 -1
  5. package/dist/html/controls.d.ts +38 -10
  6. package/dist/html/controls.d.ts.map +1 -1
  7. package/dist/html/controls.js +43 -58
  8. package/dist/html/controls.js.map +1 -1
  9. package/dist/html/css/cascade.d.ts +134 -20
  10. package/dist/html/css/cascade.d.ts.map +1 -1
  11. package/dist/html/css/cascade.js +538 -128
  12. package/dist/html/css/cascade.js.map +1 -1
  13. package/dist/html/css/color.d.ts +19 -0
  14. package/dist/html/css/color.d.ts.map +1 -1
  15. package/dist/html/css/color.js +86 -0
  16. package/dist/html/css/color.js.map +1 -1
  17. package/dist/html/css/parse.d.ts +40 -2
  18. package/dist/html/css/parse.d.ts.map +1 -1
  19. package/dist/html/css/parse.js +391 -13
  20. package/dist/html/css/parse.js.map +1 -1
  21. package/dist/html/css/style.d.ts +52 -1
  22. package/dist/html/css/style.d.ts.map +1 -1
  23. package/dist/html/css/style.js +328 -11
  24. package/dist/html/css/style.js.map +1 -1
  25. package/dist/html/css/ua.d.ts +11 -1
  26. package/dist/html/css/ua.d.ts.map +1 -1
  27. package/dist/html/css/ua.js +73 -7
  28. package/dist/html/css/ua.js.map +1 -1
  29. package/dist/html/css/values.d.ts +10 -1
  30. package/dist/html/css/values.d.ts.map +1 -1
  31. package/dist/html/css/values.js +27 -2
  32. package/dist/html/css/values.js.map +1 -1
  33. package/dist/html/css/vars.d.ts +5 -0
  34. package/dist/html/css/vars.d.ts.map +1 -1
  35. package/dist/html/css/vars.js +2 -2
  36. package/dist/html/css/vars.js.map +1 -1
  37. package/dist/html/dom.d.ts.map +1 -1
  38. package/dist/html/dom.js +38 -3
  39. package/dist/html/dom.js.map +1 -1
  40. package/dist/html/fonts.d.ts +25 -1
  41. package/dist/html/fonts.d.ts.map +1 -1
  42. package/dist/html/fonts.js +99 -6
  43. package/dist/html/fonts.js.map +1 -1
  44. package/dist/html/form.d.ts +200 -0
  45. package/dist/html/form.d.ts.map +1 -0
  46. package/dist/html/form.js +801 -0
  47. package/dist/html/form.js.map +1 -0
  48. package/dist/html/index.d.ts +12 -0
  49. package/dist/html/index.d.ts.map +1 -1
  50. package/dist/html/index.js +30 -264
  51. package/dist/html/index.js.map +1 -1
  52. package/dist/html/layout/block.d.ts +11 -2
  53. package/dist/html/layout/block.d.ts.map +1 -1
  54. package/dist/html/layout/block.js +186 -19
  55. package/dist/html/layout/block.js.map +1 -1
  56. package/dist/html/layout/boxes.d.ts +65 -7
  57. package/dist/html/layout/boxes.d.ts.map +1 -1
  58. package/dist/html/layout/boxes.js +87 -22
  59. package/dist/html/layout/boxes.js.map +1 -1
  60. package/dist/html/layout/cache.d.ts +2 -2
  61. package/dist/html/layout/cache.d.ts.map +1 -1
  62. package/dist/html/layout/cache.js +8 -3
  63. package/dist/html/layout/cache.js.map +1 -1
  64. package/dist/html/layout/flex.js +4 -1
  65. package/dist/html/layout/flex.js.map +1 -1
  66. package/dist/html/layout/floats.d.ts +4 -0
  67. package/dist/html/layout/floats.d.ts.map +1 -1
  68. package/dist/html/layout/floats.js +26 -0
  69. package/dist/html/layout/floats.js.map +1 -1
  70. package/dist/html/layout/inline.d.ts +26 -2
  71. package/dist/html/layout/inline.d.ts.map +1 -1
  72. package/dist/html/layout/inline.js +1232 -115
  73. package/dist/html/layout/inline.js.map +1 -1
  74. package/dist/html/node.d.ts +120 -15
  75. package/dist/html/node.d.ts.map +1 -1
  76. package/dist/html/node.js +993 -174
  77. package/dist/html/node.js.map +1 -1
  78. package/dist/html/paint.d.ts +75 -2
  79. package/dist/html/paint.d.ts.map +1 -1
  80. package/dist/html/paint.js +836 -200
  81. package/dist/html/paint.js.map +1 -1
  82. package/dist/html/surfaces.d.ts +7 -2
  83. package/dist/html/surfaces.d.ts.map +1 -1
  84. package/dist/html/surfaces.js +16 -0
  85. package/dist/html/surfaces.js.map +1 -1
  86. package/dist/html/widgets.d.ts +29 -0
  87. package/dist/html/widgets.d.ts.map +1 -0
  88. package/dist/html/widgets.js +620 -0
  89. package/dist/html/widgets.js.map +1 -0
  90. package/dist/index.d.ts +1 -1
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js.map +1 -1
  93. package/dist/internal/window.d.ts +22 -0
  94. package/dist/internal/window.d.ts.map +1 -1
  95. package/dist/internal/window.js +35 -8
  96. package/dist/internal/window.js.map +1 -1
  97. package/dist/richtext/node.d.ts +7 -0
  98. package/dist/richtext/node.d.ts.map +1 -1
  99. package/dist/richtext/node.js.map +1 -1
  100. package/package.json +6 -4
  101. package/src/flow/node.ts +80 -64
  102. package/src/html/controls.ts +79 -58
  103. package/src/html/css/cascade.ts +610 -146
  104. package/src/html/css/color.ts +87 -0
  105. package/src/html/css/parse.ts +431 -13
  106. package/src/html/css/style.ts +367 -11
  107. package/src/html/css/ua.ts +76 -8
  108. package/src/html/css/values.ts +31 -3
  109. package/src/html/css/vars.ts +2 -2
  110. package/src/html/dom.ts +41 -3
  111. package/src/html/fonts.ts +116 -6
  112. package/src/html/form.ts +962 -0
  113. package/src/html/index.ts +49 -300
  114. package/src/html/layout/block.ts +231 -16
  115. package/src/html/layout/boxes.ts +133 -32
  116. package/src/html/layout/cache.ts +8 -3
  117. package/src/html/layout/flex.ts +4 -1
  118. package/src/html/layout/floats.ts +24 -0
  119. package/src/html/layout/inline.ts +1445 -108
  120. package/src/html/node.ts +1010 -166
  121. package/src/html/paint.ts +980 -270
  122. package/src/html/surfaces.ts +21 -1
  123. package/src/html/widgets.ts +735 -0
  124. package/src/index.ts +1 -0
  125. package/src/internal/window.ts +36 -8
  126. package/src/richtext/node.ts +7 -0
@@ -40,6 +40,7 @@ import {
40
40
  } from './content.js';
41
41
  import type { ContentItem, CounterChange } from './content.js';
42
42
  import type { CustomProps } from './vars.js';
43
+ import { LIGHT_DARK, lightDark, usedColorScheme } from './color.js';
43
44
 
44
45
  export type Display =
45
46
  | 'none'
@@ -202,6 +203,14 @@ export type ContentSize =
202
203
  export interface ComputedStyle {
203
204
  // --- inherited ------------------------------------------------------------
204
205
  color: string;
206
+ /**
207
+ * The colour scheme the element is drawn in (CSS Color Adjust 1, 2.2):
208
+ * its `color-scheme` resolved against the scheme the palette is, which
209
+ * stands for the reader's preference, as it does for `@media
210
+ * (prefers-color-scheme)`. What a `light-dark()` picks by. `normal` is
211
+ * the palette's own, since the palette is this renderer's default look.
212
+ */
213
+ colorScheme: 'light' | 'dark';
205
214
  fontFamily: string;
206
215
  fontSize: number;
207
216
  fontWeight: number;
@@ -320,6 +329,11 @@ export interface ComputedStyle {
320
329
  float: 'none' | 'left' | 'right';
321
330
  clear: 'none' | 'left' | 'right' | 'both';
322
331
  boxSizing: 'content-box' | 'border-box';
332
+ /** `box-decoration-break` (CSS Fragmentation 3, 5.4): how an inline box
333
+ * that goes on to another line places its background images and
334
+ * gradients — `slice`, from its fragments laid end to end as one box,
335
+ * or `clone`, from each fragment's own. */
336
+ boxDecorationBreak: 'slice' | 'clone';
323
337
  /** Whether a block is a line-clamp container, and how many lines of its
324
338
  * formatting context it shows (CSS Overflow 4, 5.3.1): its `max-lines`,
325
339
  * or Infinity for `line-clamp: auto`, which shows as many as its height
@@ -505,6 +519,10 @@ export interface ComputedStyle {
505
519
  * replaced whole when any of them changes, and `NO_BORDER_IMAGE` where
506
520
  * nothing sets them. */
507
521
  borderImage: BorderImage;
522
+ /** `mask-image` and the longhands that place its layers (CSS Masking 1,
523
+ * 7): one object, replaced whole when any of them changes, and
524
+ * `NO_MASK` where nothing sets them. */
525
+ mask: MaskLayers;
508
526
  /** `-webkit-text-fill-color`: what the glyphs are filled with where it
509
527
  * is not the text's `color` — Tailwind's `text-transparent` over a
510
528
  * `bg-clip-text` gradient; null for `color`, as `currentColor` is. */
@@ -608,6 +626,7 @@ export interface ComputedStyle {
608
626
  * same list. */
609
627
  export const INHERITED = [
610
628
  'color',
629
+ 'colorScheme',
611
630
  'textFillColor',
612
631
  'fontFamily',
613
632
  'fontSize',
@@ -681,6 +700,20 @@ export interface RootLook {
681
700
  controlPadY: number;
682
701
  controlBorder: number;
683
702
  controlRadius: number;
703
+ /**
704
+ * The size the palette's widgets set their text at, `theme.fontSize` — and
705
+ * so the size of a control's text, whatever the text around it is: a
706
+ * browser's controls take a system font rather than their parent's, and
707
+ * this is the system's. Without it, `fontSize`.
708
+ */
709
+ controlFontSize?: number;
710
+ /**
711
+ * The family they set it in, `theme.fontFamily`, for the same reason: a
712
+ * browser's system font is its own, Arial in Chrome whatever the page is
713
+ * set in. A `<textarea>` is `monoFamily`, a browser's `monospace`, as it
714
+ * is in Chrome. Without it, `fontFamily`.
715
+ */
716
+ controlFontFamily?: string;
684
717
  }
685
718
 
686
719
  export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
@@ -689,6 +722,7 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
689
722
  // a copy of the literal, not the literal: see `copyStyle`
690
723
  return copyStyle({
691
724
  color: look.color,
725
+ colorScheme: look.colorScheme,
692
726
  fontFamily: look.fontFamily,
693
727
  fontSize: look.fontSize,
694
728
  fontWeight: 400,
@@ -747,6 +781,7 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
747
781
  float: 'none',
748
782
  clear: 'none',
749
783
  boxSizing: 'content-box',
784
+ boxDecorationBreak: 'slice',
750
785
  lineClamp: null,
751
786
  maxLines: null,
752
787
  clampContinue: 'auto',
@@ -846,6 +881,7 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
846
881
  backgroundClips: null,
847
882
  backgroundOrigins: null,
848
883
  borderImage: NO_BORDER_IMAGE,
884
+ mask: NO_MASK,
849
885
  textFillColor: null,
850
886
 
851
887
  textDecorationLine: 'none',
@@ -941,6 +977,7 @@ export function inherit(
941
977
  // quarters of the cost of every element's style, a store through a
942
978
  // computed name costing thirty times one through a written one
943
979
  out.color = parent.color;
980
+ out.colorScheme = parent.colorScheme;
944
981
  out.textFillColor = parent.textFillColor;
945
982
  out.fontFamily = parent.fontFamily;
946
983
  out.fontSize = parent.fontSize;
@@ -1162,6 +1199,15 @@ export function applyDeclaration(
1162
1199
  if (/!\s*important$/i.test(value)) {
1163
1200
  value = value.replace(/!\s*important$/i, '').trim();
1164
1201
  }
1202
+ // the branch of each `light-dark()` this element's scheme picks, before
1203
+ // anything reads the value: it stands wherever a colour can, in a
1204
+ // shorthand, a shadow, a gradient or a mix, and most often arrives
1205
+ // through a `var()` from a theme on `:root`
1206
+ if (LIGHT_DARK.test(value)) {
1207
+ const picked = lightDark(value, style.colorScheme);
1208
+ if (picked === null) return;
1209
+ value = picked;
1210
+ }
1165
1211
 
1166
1212
  const color = COLOR_PROPS[name];
1167
1213
  if (color) {
@@ -1178,6 +1224,12 @@ export function applyDeclaration(
1178
1224
  }
1179
1225
 
1180
1226
  switch (name) {
1227
+ case 'color-scheme': {
1228
+ const scheme = usedColorScheme(value, ctx.initial?.colorScheme);
1229
+ if (scheme) style.colorScheme = scheme;
1230
+ return;
1231
+ }
1232
+
1181
1233
  // --- box ----------------------------------------------------------------
1182
1234
  case 'display': {
1183
1235
  const v = value.toLowerCase();
@@ -1366,6 +1418,12 @@ export function applyDeclaration(
1366
1418
  if (v === 'border-box' || v === 'content-box') style.boxSizing = v;
1367
1419
  return;
1368
1420
  }
1421
+ case 'box-decoration-break':
1422
+ case '-webkit-box-decoration-break': {
1423
+ const v = value.trim().toLowerCase();
1424
+ if (v === 'slice' || v === 'clone') style.boxDecorationBreak = v;
1425
+ return;
1426
+ }
1369
1427
  case 'aspect-ratio': {
1370
1428
  const parsed = parseAspectRatio(value);
1371
1429
  if (parsed !== undefined) style.aspectRatio = parsed;
@@ -1906,6 +1964,67 @@ export function applyDeclaration(
1906
1964
  return;
1907
1965
  }
1908
1966
 
1967
+ // --- masking --------------------------------------------------------------
1968
+ // Each `-webkit-` name is the same property's (Compatibility, 5): every
1969
+ // engine takes both, and a page writes both — Wikipedia's icons do.
1970
+ case 'mask':
1971
+ case '-webkit-mask': {
1972
+ const mask = readMask(value, ctx);
1973
+ if (mask) style.mask = mask;
1974
+ return;
1975
+ }
1976
+ case 'mask-image':
1977
+ case '-webkit-mask-image': {
1978
+ // `none` a layer as much as an image is; a bad url drops it all
1979
+ const images: BackgroundImage[] = [];
1980
+ for (const part of splitCommas(value)) {
1981
+ const image = maskImageOf(part, ctx);
1982
+ if (image === undefined) return;
1983
+ images.push(image);
1984
+ }
1985
+ if (images.length) style.mask = { ...style.mask, images };
1986
+ return;
1987
+ }
1988
+ case 'mask-repeat':
1989
+ case '-webkit-mask-repeat': {
1990
+ const repeats = layerValues(value, (part) => {
1991
+ const words = splitValue(part.toLowerCase());
1992
+ return words.length <= 2 ? readRepeat(words) : null;
1993
+ });
1994
+ if (repeats) style.mask = { ...style.mask, repeats };
1995
+ return;
1996
+ }
1997
+ case 'mask-size':
1998
+ case '-webkit-mask-size': {
1999
+ const sizes = layerValues(value, (part) =>
2000
+ backgroundSizeOf(splitValue(part), ctx),
2001
+ );
2002
+ if (sizes) style.mask = { ...style.mask, sizes };
2003
+ return;
2004
+ }
2005
+ case 'mask-position':
2006
+ case '-webkit-mask-position': {
2007
+ const positions = layerValues(value, (part) =>
2008
+ positionPair(splitValue(part), ctx),
2009
+ );
2010
+ if (positions) style.mask = { ...style.mask, positions };
2011
+ return;
2012
+ }
2013
+ case 'mask-origin':
2014
+ case '-webkit-mask-origin': {
2015
+ const origins = layerValues(value, (part) => maskBox(part));
2016
+ if (origins) style.mask = { ...style.mask, origins };
2017
+ return;
2018
+ }
2019
+ case 'mask-clip':
2020
+ case '-webkit-mask-clip': {
2021
+ const clips = layerValues(value, (part) =>
2022
+ part.toLowerCase() === 'no-clip' ? 'border-box' : maskBox(part),
2023
+ );
2024
+ if (clips) style.mask = { ...style.mask, clips };
2025
+ return;
2026
+ }
2027
+
1909
2028
  // --- generated content --------------------------------------------------
1910
2029
  case 'content': {
1911
2030
  const parsed = parseContent(value);
@@ -1962,7 +2081,11 @@ export function applyDeclaration(
1962
2081
  // handed a face at a size of millions shapes and caches glyphs that
1963
2082
  // size, and a nest of `larger`s gets there on its own
1964
2083
  const most = MAX_FONT_SIZE * ctx.scale;
1965
- const kw = keywordFontSize(value, parent.fontSize, ctx.rem);
2084
+ const kw = keywordFontSize(
2085
+ value,
2086
+ parent.fontSize,
2087
+ ctx.initial?.fontSize ?? ctx.rem,
2088
+ );
1966
2089
  if (kw !== null) {
1967
2090
  style.fontSize = Math.min(kw, most);
1968
2091
  return;
@@ -2013,7 +2136,13 @@ export function applyDeclaration(
2013
2136
  style.lineHeight = len;
2014
2137
  style.lineHeightIsLength = true;
2015
2138
  } else if (len && typeof len === 'object') {
2016
- const px = resolve(len, style.fontSize);
2139
+ // a percentage of the element's own font size, the one it is
2140
+ // computed with (CSS 2.1 10.8.1), which `em` already is: not the
2141
+ // size in `style` as this declaration comes up, which a `font`
2142
+ // earlier in the same rule has just set to its own — `p { font:
2143
+ // 12px …; line-height: 100% }` under a `.lead { font-size: 16px }`
2144
+ // set sixteen-pixel text on twelve-pixel lines
2145
+ const px = resolve(len, ctx.em);
2017
2146
  if (px < 0) return;
2018
2147
  style.lineHeight = px;
2019
2148
  style.lineHeightIsLength = true;
@@ -2462,7 +2591,12 @@ export function applyDeclaration(
2462
2591
  return;
2463
2592
  }
2464
2593
  case 'align-content': {
2465
- const v = alignKeyword(value);
2594
+ // `normal` is `stretch` in a flex and a grid container, the only
2595
+ // boxes it moves anything in (CSS Box Alignment 3, 5.1)
2596
+ const v =
2597
+ value.trim().toLowerCase() === 'normal'
2598
+ ? 'stretch'
2599
+ : alignKeyword(value);
2466
2600
  if (v) style.alignContent = v as ComputedStyle['alignContent'];
2467
2601
  return;
2468
2602
  }
@@ -2697,6 +2831,30 @@ export function applyDeclaration(
2697
2831
  }
2698
2832
  return;
2699
2833
  }
2834
+ case 'place-content': {
2835
+ // `<'align-content'> <'justify-content'>?` (CSS Box Alignment 3,
2836
+ // 5.3), either half one keyword or two — `first baseline`, `safe
2837
+ // center` — and the second the first again where it is left out,
2838
+ // but for a baseline, which is no `justify-content`: `start`
2839
+ const words = value.trim().toLowerCase().split(/\s+/);
2840
+ const span = (at: number) =>
2841
+ /^(?:safe|unsafe|first|last)$/.test(words[at] ?? '') ? 2 : 1;
2842
+ const cut = span(0);
2843
+ const align = words.slice(0, cut).join(' ');
2844
+ const rest = words.slice(cut);
2845
+ if (rest.length && rest.length !== span(cut)) return;
2846
+ const justify = rest.length
2847
+ ? rest.join(' ')
2848
+ : /baseline$/.test(align)
2849
+ ? 'start'
2850
+ : align;
2851
+ // a shorthand is one declaration: either half unread drops both
2852
+ if (align !== 'normal' && !alignKeyword(align)) return;
2853
+ if (!justifyKeyword(justify)) return;
2854
+ applyDeclaration(style, parent, 'align-content', align, ctx);
2855
+ applyDeclaration(style, parent, 'justify-content', justify, ctx);
2856
+ return;
2857
+ }
2700
2858
  case 'place-items':
2701
2859
  case 'place-self': {
2702
2860
  const parts = splitValue(value);
@@ -3144,6 +3302,157 @@ export const NO_BORDER_IMAGE: BorderImage = {
3144
3302
  repeat: ['stretch', 'stretch'],
3145
3303
  };
3146
3304
 
3305
+ /**
3306
+ * An element's mask layers (CSS Masking 1, 7), top first, a list a
3307
+ * property as a background's are: each layer takes the images' place in
3308
+ * the others, and they repeat where they are fewer. A layer's image is a
3309
+ * url, a gradient, or null for `none` — which is the one layer of an
3310
+ * element that is not masked.
3311
+ */
3312
+ export interface MaskLayers {
3313
+ images: readonly BackgroundImage[];
3314
+ repeats: readonly BackgroundRepeat[];
3315
+ sizes: readonly ComputedStyle['backgroundSize'][];
3316
+ positions: readonly (readonly [Len, Len])[];
3317
+ /** The box a layer is placed in, and the one it is painted in: the
3318
+ * border box unless a layer says (7.5, 7.6). */
3319
+ origins: readonly VisualBox[];
3320
+ clips: readonly VisualBox[];
3321
+ }
3322
+
3323
+ /** Whether a style masks its element: a layer of its mask has an image. */
3324
+ export function masked(style: ComputedStyle): boolean {
3325
+ const mask = style.mask;
3326
+ if (mask === NO_MASK) return false;
3327
+ for (const image of mask.images) if (image !== null) return true;
3328
+ return false;
3329
+ }
3330
+
3331
+ /**
3332
+ * One layer's `mask-image`: a url, a gradient — or none, for an image
3333
+ * function this does not draw, or a `url(#id)`, which names an SVG
3334
+ * `<mask>` element in the document rather than an image to fetch (7.2):
3335
+ * drawn with no mask rather than as the transparent layer a mask that
3336
+ * cannot be had is, since this draws no `<mask>`. Undefined where it is no
3337
+ * image at all.
3338
+ */
3339
+ function maskImageOf(
3340
+ text: string,
3341
+ ctx: UnitContext,
3342
+ ): BackgroundImage | undefined {
3343
+ const image = backgroundImageOf(text, ctx);
3344
+ return typeof image === 'string' && image[0] === '#' ? null : image;
3345
+ }
3346
+
3347
+ /** A `<geometry-box>` as an HTML element has it (CSS Masking 1, 7.5): the
3348
+ * SVG boxes are its content box and its border box. */
3349
+ function maskBox(text: string): VisualBox | null {
3350
+ const v = text.trim().toLowerCase();
3351
+ if (isVisualBox(v)) return v;
3352
+ if (v === 'fill-box') return 'content-box';
3353
+ if (v === 'stroke-box' || v === 'view-box') return 'border-box';
3354
+ return null;
3355
+ }
3356
+
3357
+ /**
3358
+ * The `mask` shorthand (CSS Masking 1, 7.10): a layer a comma group, each
3359
+ * an image, a position with a size after a `/`, a repeat, one box for the
3360
+ * origin and the clip or two, `no-clip`, a compositing operator and a
3361
+ * mode, each at most once, in any order. The operator and the mode are
3362
+ * read and not honoured: layers are added, and an image masks by its
3363
+ * alpha, which is what `match-source` makes of one. Null where a layer is
3364
+ * not one, which drops the declaration.
3365
+ */
3366
+ function readMask(value: string, ctx: UnitContext): MaskLayers | null {
3367
+ const out = {
3368
+ images: [] as BackgroundImage[],
3369
+ repeats: [] as BackgroundRepeat[],
3370
+ sizes: [] as ComputedStyle['backgroundSize'][],
3371
+ positions: [] as [Len, Len][],
3372
+ origins: [] as VisualBox[],
3373
+ clips: [] as VisualBox[],
3374
+ };
3375
+ for (const text of splitCommas(value)) {
3376
+ const parts = splitValue(text).flatMap(splitSlash);
3377
+ let image: BackgroundImage = null;
3378
+ let repeat = NO_MASK.repeats[0];
3379
+ let size: ComputedStyle['backgroundSize'] = 'auto';
3380
+ let position: [Len, Len] = [0, 0];
3381
+ const boxes: VisualBox[] = [];
3382
+ let seen = 0;
3383
+ const once = (bit: number): boolean => {
3384
+ if (seen & bit) return false;
3385
+ seen |= bit;
3386
+ return true;
3387
+ };
3388
+ for (let i = 0; i < parts.length;) {
3389
+ const part = parts[i];
3390
+ const v = part.toLowerCase();
3391
+ if (v === 'none' || v.startsWith('url(') || IMAGE_FUNCTION.test(v)) {
3392
+ if (!once(1)) return null;
3393
+ const read = maskImageOf(part, ctx);
3394
+ if (read === undefined) return null;
3395
+ image = read;
3396
+ i += 1;
3397
+ } else if (v === 'repeat-x' || v === 'repeat-y' || REPEATS.has(v)) {
3398
+ if (!once(2)) return null;
3399
+ const pair = REPEATS.has(v) && REPEATS.has(parts[i + 1]?.toLowerCase());
3400
+ const read = readRepeat(pair ? [v, parts[i + 1].toLowerCase()] : [v]);
3401
+ if (!read) return null;
3402
+ repeat = read;
3403
+ i += pair ? 2 : 1;
3404
+ } else if (maskBox(v) || v === 'no-clip') {
3405
+ if (boxes.length === 2 || (v === 'no-clip' && !once(4))) return null;
3406
+ boxes.push(maskBox(v) ?? 'border-box');
3407
+ i += 1;
3408
+ } else if (/^(?:add|subtract|intersect|exclude)$/.test(v)) {
3409
+ if (!once(8)) return null;
3410
+ i += 1;
3411
+ } else if (/^(?:alpha|luminance|match-source)$/.test(v)) {
3412
+ if (!once(16)) return null;
3413
+ i += 1;
3414
+ } else if (isPositionPart(v, ctx)) {
3415
+ if (!once(32)) return null;
3416
+ let end = i;
3417
+ while (
3418
+ end < parts.length &&
3419
+ isPositionPart(parts[end].toLowerCase(), ctx)
3420
+ ) {
3421
+ end += 1;
3422
+ }
3423
+ const read = positionPair(parts.slice(i, end), ctx);
3424
+ if (!read) return null;
3425
+ position = read;
3426
+ i = end;
3427
+ if (parts[i] !== '/') continue;
3428
+ const first = parts[i + 1]?.toLowerCase();
3429
+ if (first === 'cover' || first === 'contain') {
3430
+ size = first;
3431
+ i += 2;
3432
+ } else if (first !== undefined && isSizePart(first, ctx)) {
3433
+ const two =
3434
+ i + 2 < parts.length && isSizePart(parts[i + 2].toLowerCase(), ctx);
3435
+ size =
3436
+ backgroundSizeOf(parts.slice(i + 1, i + (two ? 3 : 2)), ctx) ??
3437
+ 'auto';
3438
+ i += two ? 3 : 2;
3439
+ } else {
3440
+ return null;
3441
+ }
3442
+ } else {
3443
+ return null;
3444
+ }
3445
+ }
3446
+ out.images.push(image);
3447
+ out.repeats.push(repeat);
3448
+ out.sizes.push(size);
3449
+ out.positions.push(position);
3450
+ out.origins.push(boxes[0] ?? 'border-box');
3451
+ out.clips.push(boxes[1] ?? boxes[0] ?? 'border-box');
3452
+ }
3453
+ return out.images.length ? out : null;
3454
+ }
3455
+
3147
3456
  /** One to four values, one a side as `margin` takes them; null where one
3148
3457
  * is not a value. */
3149
3458
  function sidesOf<T>(
@@ -3338,6 +3647,15 @@ function repeatPair(x: RepeatMode, y: RepeatMode): BackgroundRepeat {
3338
3647
 
3339
3648
  const REPEAT = repeatPair('repeat', 'repeat');
3340
3649
 
3650
+ export const NO_MASK: MaskLayers = {
3651
+ images: [null],
3652
+ repeats: [REPEAT],
3653
+ sizes: ['auto'],
3654
+ positions: [[0, 0]],
3655
+ origins: ['border-box'],
3656
+ clips: ['border-box'],
3657
+ };
3658
+
3341
3659
  function applyBorderShorthand(
3342
3660
  style: ComputedStyle,
3343
3661
  name: string,
@@ -4007,8 +4325,11 @@ function applyFontShorthand(
4007
4325
  // dropped whole, as CSS drops any value it cannot read — a line height
4008
4326
  // below nought included: `font: 4em/-2em serif` set the text at 4em.
4009
4327
  const size =
4010
- keywordFontSize(sizeText ?? '', parent.fontSize, ctx.rem) ??
4011
- parseLength(sizeText ?? '', { ...ctx, em: parent.fontSize });
4328
+ keywordFontSize(
4329
+ sizeText ?? '',
4330
+ parent.fontSize,
4331
+ ctx.initial?.fontSize ?? ctx.rem,
4332
+ ) ?? parseLength(sizeText ?? '', { ...ctx, em: parent.fontSize });
4012
4333
  if (!family || size === null || size === AUTO) return;
4013
4334
  if (lineText && negativeLength(lineText, ctx)) return;
4014
4335
  // What the shorthand does not name goes back to its initial value rather
@@ -4026,12 +4347,10 @@ function applyFontShorthand(
4026
4347
  style.lineHeight = 'normal';
4027
4348
  style.lineHeightIsLength = false;
4028
4349
  applyDeclaration(style, parent, 'font-size', sizeText, ctx);
4029
- if (lineText) {
4030
- applyDeclaration(style, parent, 'line-height', lineText, {
4031
- ...ctx,
4032
- em: style.fontSize,
4033
- });
4034
- }
4350
+ // against the element's own size, which `ctx.em` is once the cascade has
4351
+ // settled it: a later rule's `font-size` outranks this one's, and
4352
+ // `font: 12px/150%` under a `font-size: 16px` is on 24px lines
4353
+ if (lineText) applyDeclaration(style, parent, 'line-height', lineText, ctx);
4035
4354
  applyDeclaration(style, parent, 'font-family', family, ctx);
4036
4355
  }
4037
4356
 
@@ -4751,6 +5070,7 @@ function camel(name: string): string {
4751
5070
 
4752
5071
  const INHERITED_NAMES = new Set<string>([
4753
5072
  'color',
5073
+ 'color-scheme',
4754
5074
  '-webkit-text-fill-color',
4755
5075
  'font',
4756
5076
  'font-family',
@@ -4801,6 +5121,11 @@ export function initialOne(
4801
5121
  initial: ComputedStyle,
4802
5122
  name: string,
4803
5123
  ): void {
5124
+ const list = MASK_LISTS[name];
5125
+ if (list) {
5126
+ style.mask = { ...style.mask, [list]: initial.mask[list] };
5127
+ return;
5128
+ }
4804
5129
  const keys = INHERIT_TARGETS[name];
4805
5130
  if (!keys) return;
4806
5131
  for (const key of keys) {
@@ -4813,6 +5138,11 @@ function inheritOne(
4813
5138
  parent: ComputedStyle,
4814
5139
  name: string,
4815
5140
  ): void {
5141
+ const list = MASK_LISTS[name];
5142
+ if (list) {
5143
+ style.mask = { ...style.mask, [list]: parent.mask[list] };
5144
+ return;
5145
+ }
4816
5146
  const keys = INHERIT_TARGETS[name];
4817
5147
  if (!keys) return;
4818
5148
  // A border colour left to `currentColor` inherits as the keyword and
@@ -4828,8 +5158,30 @@ const sides = (
4828
5158
  make: (side: (typeof SIDES)[number]) => keyof ComputedStyle,
4829
5159
  ): (keyof ComputedStyle)[] => SIDES.map(make);
4830
5160
 
5161
+ /**
5162
+ * The list of the mask's layers each longhand is, which `initial`,
5163
+ * `inherit` and a `var()` that fails reset alone: one field holds them all,
5164
+ * and reset whole, a `mask-size: calc(var(--x) - 4px)` put back the
5165
+ * `mask-repeat: no-repeat` before it, and Wikipedia's chevron repeated.
5166
+ */
5167
+ const MASK_LISTS: Record<string, keyof MaskLayers> = {
5168
+ 'mask-image': 'images',
5169
+ '-webkit-mask-image': 'images',
5170
+ 'mask-repeat': 'repeats',
5171
+ '-webkit-mask-repeat': 'repeats',
5172
+ 'mask-size': 'sizes',
5173
+ '-webkit-mask-size': 'sizes',
5174
+ 'mask-position': 'positions',
5175
+ '-webkit-mask-position': 'positions',
5176
+ 'mask-origin': 'origins',
5177
+ '-webkit-mask-origin': 'origins',
5178
+ 'mask-clip': 'clips',
5179
+ '-webkit-mask-clip': 'clips',
5180
+ };
5181
+
4831
5182
  const INHERIT_TARGETS: Record<string, readonly (keyof ComputedStyle)[]> = {
4832
5183
  color: ['color'],
5184
+ 'color-scheme': ['colorScheme'],
4833
5185
  '-webkit-text-fill-color': ['textFillColor'],
4834
5186
  'background-clip': [
4835
5187
  'backgroundClipText',
@@ -4843,6 +5195,8 @@ const INHERIT_TARGETS: Record<string, readonly (keyof ComputedStyle)[]> = {
4843
5195
  ],
4844
5196
  'background-origin': ['backgroundOrigin', 'backgroundOrigins'],
4845
5197
  'border-image': ['borderImage'],
5198
+ mask: ['mask'],
5199
+ '-webkit-mask': ['mask'],
4846
5200
  'border-image-source': ['borderImage'],
4847
5201
  'border-image-slice': ['borderImage'],
4848
5202
  'border-image-width': ['borderImage'],
@@ -4923,6 +5277,8 @@ const INHERIT_TARGETS: Record<string, readonly (keyof ComputedStyle)[]> = {
4923
5277
  'min-height': ['minHeight', 'minHeightKeyword'],
4924
5278
  'max-height': ['maxHeight', 'maxHeightKeyword'],
4925
5279
  'box-sizing': ['boxSizing'],
5280
+ 'box-decoration-break': ['boxDecorationBreak'],
5281
+ '-webkit-box-decoration-break': ['boxDecorationBreak'],
4926
5282
  margin: sides((s) => `margin${s}`),
4927
5283
  'margin-top': ['marginTop'],
4928
5284
  'margin-right': ['marginRight'],
@@ -9,9 +9,20 @@
9
9
  // author stylesheet still overrides all of it — that is what makes it a UA
10
10
  // sheet and not a skin.
11
11
  import { parseStylesheet } from './parse.js';
12
- import type { Stylesheet } from './parse.js';
12
+ import type { Declaration, Stylesheet } from './parse.js';
13
13
  import type { RootLook } from './style.js';
14
14
 
15
+ /**
16
+ * The declaration blocks that draw a control as the palette's widget rather
17
+ * than as the web's UA sheet has it. The cascade drops them from an element
18
+ * whose page styles its background or border (`Cascade`'s
19
+ * `dropPaletteChrome`), as Blink drops a control's native appearance
20
+ * (`LayoutTheme::IsControlStyled`), since a page that styles a control
21
+ * builds on the UA values a browser gives it. Keyed by the array, which is
22
+ * what a cascade candidate carries.
23
+ */
24
+ export const PALETTE_CHROME = new WeakSet<Declaration[]>();
25
+
15
26
  /** Cache key → parsed sheet. The text depends only on the look, so two
16
27
  * documents in one themed window parse this once between them. */
17
28
  const CACHE = new Map<string, Stylesheet>();
@@ -30,6 +41,8 @@ export function lookKey(look: RootLook): string {
30
41
  look.controlPadY,
31
42
  look.controlBorder,
32
43
  look.controlRadius,
44
+ look.controlFontSize,
45
+ look.controlFontFamily,
33
46
  ].join('|');
34
47
  }
35
48
 
@@ -38,6 +51,13 @@ export function uaStylesheet(look: RootLook): Stylesheet {
38
51
  const hit = CACHE.get(key);
39
52
  if (hit) return hit;
40
53
  const sheet = parseStylesheet(uaText(look), -1_000_000);
54
+ // after the rest, so its values win over theirs while it applies
55
+ const last = sheet.rules[sheet.rules.length - 1];
56
+ const chrome = parseStylesheet(chromeText(look), last.order + 1);
57
+ for (const rule of chrome.rules) {
58
+ PALETTE_CHROME.add(rule.declarations);
59
+ sheet.rules.push(rule);
60
+ }
41
61
  if (CACHE.size > 8) CACHE.clear();
42
62
  CACHE.set(key, sheet);
43
63
  return sheet;
@@ -181,26 +201,55 @@ col { display: table-column; }
181
201
  makes a label and its input share a line. */
182
202
  input, button, select, textarea, meter, progress {
183
203
  display: inline-block;
184
- font-family: ${look.fontFamily};
185
- font-size: 1em;
186
204
  vertical-align: middle;
187
205
  margin: 3px 2px;
188
206
  }
207
+ /* A control's text is a system font's, not its parent's: Chrome gives these
208
+ four \`font: -webkit-small-control\`, Arial at the default size less 2pt
209
+ (13.33px) whatever the text around it is set in, and Gecko \`-moz-field\`
210
+ the same. The system here is the palette, and its family and size are the
211
+ ones core's widgets are set in — so a document's form is its window's,
212
+ and the box a control is measured into is the size the widget mounted in
213
+ it draws. A <textarea> is then \`monospace\` in Chrome, the sheet's code
214
+ face here; a <meter> and a <progress> keep their parent's font, as they
215
+ do in Chrome. */
216
+ input, button, select, textarea {
217
+ font-family: ${look.controlFontFamily ?? look.fontFamily};
218
+ font-size: ${look.controlFontSize ?? look.fontSize}px;
219
+ }
220
+ textarea { font-family: ${mono}; }
189
221
  /* Chrome's own UA margins for the checkables, near enough: they are the
190
222
  controls that sit hard against their label text otherwise. */
223
+ /* HTML's rendering section (15.3.10): a control's text keeps none of the
224
+ spacing, the line height, the case or the indent of the text around it.
225
+ A button in a paragraph of 'line-height: 1.5' is its own font's line
226
+ tall, as it is in a browser. */
227
+ input, button, textarea {
228
+ letter-spacing: initial;
229
+ word-spacing: initial;
230
+ line-height: initial;
231
+ }
232
+ input, select, button, textarea {
233
+ text-transform: initial;
234
+ text-indent: initial;
235
+ text-shadow: initial;
236
+ }
191
237
  input[type=checkbox] { margin: 3px 4px 3px 4px; }
192
238
  input[type=radio] { margin: 3px 4px 3px 5px; }
193
239
  input[type=hidden] { display: none; }
194
240
  /* A <button> is drawn rather than mounted: its content is the document's —
195
241
  an icon, a label in spans, a pill of the page's own design, which is what
196
242
  most buttons on the web are — and the page restyles it as it restyles
197
- anything. What the sheet gives one the page left alone is the palette's
198
- control, the chrome a mounted widget around it has. */
243
+ anything. The edges here are Chrome's, which a page that styles its
244
+ buttons builds on: Codex gives Wikipedia's search button its side padding
245
+ and a 32px min-height, and leaves the 1px above and below the label, and
246
+ the border box the minimum holds, to the browser. One the page left alone
247
+ is the palette's control instead, below the rest of this sheet. */
199
248
  button {
200
249
  vertical-align: baseline;
201
- padding: ${look.controlPadY}px 0.75em;
202
- border: ${look.controlBorder}px solid ${look.borderColor};
203
- border-radius: ${look.controlRadius}px;
250
+ box-sizing: border-box;
251
+ padding: 1px 6px;
252
+ border: 2px outset ${look.borderColor};
204
253
  background-color: ${look.surface};
205
254
  color: ${look.color};
206
255
  text-align: center;
@@ -221,3 +270,22 @@ details[open] > summary:first-of-type { list-style-type: disclosure-open; }
221
270
  [hidden] { display: none; }
222
271
  `;
223
272
  }
273
+
274
+ /**
275
+ * The palette's control, for a `<button>` whose page left its background and
276
+ * border alone: the chrome a mounted widget has, so a form in a document is
277
+ * the size of the window's own. The rules land in `PALETTE_CHROME`, and a
278
+ * page that styles either loses all of them — Blink counts every background
279
+ * and border longhand, radius included — along with the native look it
280
+ * would lose in a browser. Nothing stays half the palette's and half the
281
+ * page's.
282
+ */
283
+ function chromeText(look: RootLook): string {
284
+ return `
285
+ button {
286
+ padding: ${look.controlPadY}px 0.75em;
287
+ border: ${look.controlBorder}px solid ${look.borderColor};
288
+ border-radius: ${look.controlRadius}px;
289
+ }
290
+ `;
291
+ }