@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
@@ -1,4 +1,5 @@
1
1
  import * as ntk from 'react-x11/ntk';
2
+ import { closingParen, topLevelComma } from './vars.js';
2
3
 
3
4
  // CSS Color 4's functional colours: read here, and written back in the one
4
5
  // form every drawing context reads — `#rrggbb`, or `rgba(r, g, b, a)` with
@@ -753,3 +754,89 @@ const REC2020_TO_XYZ: readonly Triple[] = [
753
754
  [26158966 / 99577255, 472592308 / 697040785, 8267143 / 139408157],
754
755
  [0, 19567812 / 697040785, 295819943 / 278816314],
755
756
  ];
757
+
758
+ // --- colour schemes ---------------------------------------------------------
759
+
760
+ /** Whether a value has a `light-dark()` in it. */
761
+ export const LIGHT_DARK = /(?:^|[^\w-])light-dark\(/i;
762
+ const LIGHT_DARK_AT = /(?:^|[^\w-])(light-dark\()/gi;
763
+
764
+ /**
765
+ * A value with each `light-dark()` in it replaced by the branch `scheme`
766
+ * picks (CSS Color 5, 7): the first where the element's used colour
767
+ * scheme is light, the second where it is dark. Replaced in the text, as a
768
+ * `var()` is, so a branch can be any colour — or another `light-dark()` —
769
+ * in any value a colour stands in. Null where one is not two arguments,
770
+ * which makes the declaration invalid.
771
+ */
772
+ export function lightDark(
773
+ value: string,
774
+ scheme: 'light' | 'dark',
775
+ ): string | null {
776
+ let out = value;
777
+ // a branch can hold another, which the next round replaces
778
+ for (let round = 0; round < 32; round += 1) {
779
+ LIGHT_DARK_AT.lastIndex = 0;
780
+ const m = LIGHT_DARK_AT.exec(out);
781
+ if (!m) return out;
782
+ const open = m.index + m[0].length;
783
+ const start = open - m[1].length;
784
+ const close = closingParen(out, open);
785
+ if (close < 0) return null;
786
+ const inner = out.slice(open, close);
787
+ const comma = topLevelComma(inner);
788
+ if (comma < 0) return null;
789
+ const light = inner.slice(0, comma).trim();
790
+ const dark = inner.slice(comma + 1).trim();
791
+ if (!light || !dark || topLevelComma(dark) >= 0) return null;
792
+ out =
793
+ out.slice(0, start) +
794
+ (scheme === 'dark' ? dark : light) +
795
+ out.slice(close + 1);
796
+ }
797
+ return null;
798
+ }
799
+
800
+ /**
801
+ * The scheme an element with this `color-scheme` is drawn in (CSS Color
802
+ * Adjust 1, 2.2), given the one `preferred` — the palette's, which stands
803
+ * for the reader's preference: that one where the element supports it,
804
+ * else the first it names that is one, else the one `normal` gives, which
805
+ * is the palette's own too. Null for a value that is not a `color-scheme`.
806
+ */
807
+ export function usedColorScheme(
808
+ value: string,
809
+ preferred: 'light' | 'dark' = 'light',
810
+ ): 'light' | 'dark' | null {
811
+ const words = value.trim().toLowerCase().split(/\s+/);
812
+ if (words.length === 1 && words[0] === 'normal') return preferred;
813
+ // `[ light | dark | <custom-ident> ]+ && only?`: `only` once, first or
814
+ // last, and never on its own
815
+ const only = words.indexOf('only');
816
+ if (only >= 0) {
817
+ if (only !== 0 && only !== words.length - 1) return null;
818
+ words.splice(only, 1);
819
+ if (!words.length) return null;
820
+ }
821
+ let first: 'light' | 'dark' | null = null;
822
+ for (const word of words) {
823
+ if (!SCHEME_IDENT.test(word) || NOT_SCHEMES.has(word)) return null;
824
+ if (word === preferred) return preferred;
825
+ if (!first && (word === 'light' || word === 'dark')) first = word;
826
+ }
827
+ return first ?? preferred;
828
+ }
829
+
830
+ const SCHEME_IDENT = /^-?(?:[a-z_]|--)[\w-]*$/;
831
+ /** What a scheme's name cannot be: the keywords the grammar has, and the
832
+ * CSS-wide ones, which reach here only in a list. */
833
+ const NOT_SCHEMES = new Set([
834
+ 'normal',
835
+ 'only',
836
+ 'inherit',
837
+ 'initial',
838
+ 'unset',
839
+ 'revert',
840
+ 'revert-layer',
841
+ 'default',
842
+ ]);
@@ -60,6 +60,14 @@ export interface Stylesheet {
60
60
  * keeps these so a resize can tell "the layout changed" from "the
61
61
  * *cascade* changed", and restyle only when it crossed one. */
62
62
  breakpoints: number[];
63
+ /** Whether a `@media` rule in this sheet tests the viewport's height,
64
+ * which makes a document restyle when the height moves, as a `vh`
65
+ * does. */
66
+ readsHeight?: boolean;
67
+ /** Whether one tests its aspect ratio, which a width that crosses no
68
+ * breakpoint can change: the document restyles as one with a `vw`
69
+ * does. */
70
+ readsWidth?: boolean;
63
71
  /** The `@font-face` rules, in order (see `fonts.ts`). */
64
72
  fontFaces: FontFaceRule[];
65
73
  /** `@counter-style` rules, in order: a name and its descriptors, which
@@ -94,12 +102,26 @@ export interface FontFaceSource {
94
102
  format: string | null;
95
103
  }
96
104
 
97
- /** The tests this evaluates live: a width, a colour scheme, or both.
105
+ /** The tests this evaluates live: a width, a height, a colour scheme.
98
106
  * Anything else — `orientation`, `print`, `prefers-reduced-motion` — is
99
107
  * decided once, at parse time, by `staticPass`. */
100
108
  export interface MediaCondition {
101
109
  min?: number;
102
110
  max?: number;
111
+ /** The viewport's height, as `min`/`max` its width: `min-height`,
112
+ * `max-height` and a range on `height` (Media Queries 4, 4.2). */
113
+ minHeight?: number;
114
+ maxHeight?: number;
115
+ /** The display's resolution in dots per CSS pixel, its scale: `min-`
116
+ * and `max-resolution`, and WebKit's `-webkit-min-device-pixel-ratio`,
117
+ * which a design writes to swap in its high-DPI images. */
118
+ minResolution?: number;
119
+ maxResolution?: number;
120
+ /** The viewport's width over its height: `aspect-ratio` and its `min-`
121
+ * and `max-`, the device's the viewport's too, and `orientation`, which
122
+ * is landscape where the width is the greater. */
123
+ minAspect?: number;
124
+ maxAspect?: number;
103
125
  /** `prefers-color-scheme`, answered from the palette in force. */
104
126
  scheme?: 'light' | 'dark';
105
127
  /** Set when the query could not be evaluated as a width or scheme test:
@@ -229,6 +251,13 @@ export function parseStylesheet(
229
251
  for (const c of conditions) {
230
252
  if (c.min !== undefined) breakpoints.add(c.min);
231
253
  if (c.max !== undefined) breakpoints.add(c.max + MAX_EDGE);
254
+ if (c.minHeight !== undefined || c.maxHeight !== undefined) {
255
+ sheet.readsHeight = true;
256
+ }
257
+ if (c.minAspect !== undefined || c.maxAspect !== undefined) {
258
+ sheet.readsHeight = true;
259
+ sheet.readsWidth = true;
260
+ }
232
261
  }
233
262
  // A nested `@media` intersects with the one above it; pushing a
234
263
  // level rather than merging keeps "all of these blocks hold" exact
@@ -242,8 +271,11 @@ export function parseStylesheet(
242
271
  } else if (name === 'supports' && at.block !== null) {
243
272
  // Everything in a `@supports` block is markup this renderer either
244
273
  // understands or ignores per-declaration, so entering it is closer
245
- // to right than skipping it.
246
- walk(at.block, media, layer, path);
274
+ // to right than skipping it — but for a condition it can answer
275
+ // false (`supportsCondition`)
276
+ if (supportsCondition(at.prelude) !== false) {
277
+ walk(at.block, media, layer, path);
278
+ }
247
279
  } else if (name === 'layer') {
248
280
  // `@layer a, b;` names layers, and so fixes their order, and
249
281
  // `@layer a { … }` puts rules in one; one with no name is a layer
@@ -332,6 +364,13 @@ export function parseStylesheet(
332
364
  for (const c of conditions) {
333
365
  if (c.min !== undefined) breakpoints.add(c.min);
334
366
  if (c.max !== undefined) breakpoints.add(c.max + MAX_EDGE);
367
+ if (c.minHeight !== undefined || c.maxHeight !== undefined) {
368
+ sheet.readsHeight = true;
369
+ }
370
+ if (c.minAspect !== undefined || c.maxAspect !== undefined) {
371
+ sheet.readsHeight = true;
372
+ sheet.readsWidth = true;
373
+ }
335
374
  }
336
375
  styleRule(
337
376
  selectors,
@@ -342,7 +381,9 @@ export function parseStylesheet(
342
381
  depth + 1,
343
382
  );
344
383
  } else if (name === 'supports') {
345
- styleRule(selectors, item.block, media, layer, path, depth + 1);
384
+ if (supportsCondition(item.prelude) !== false) {
385
+ styleRule(selectors, item.block, media, layer, path, depth + 1);
386
+ }
346
387
  } else if (name === 'layer' && LAYER_NAME.test(item.prelude.trim())) {
347
388
  const [ranks, full] = enter(item.prelude.trim(), layer, path);
348
389
  styleRule(selectors, item.block, media, ranks, full, depth + 1);
@@ -1350,17 +1391,36 @@ function readAtRule(text: string, at: number): AtRule {
1350
1391
  * divides a device width into (`640.5` at 2x) fall on the right side. */
1351
1392
  const MAX_EDGE = 1 / 64;
1352
1393
 
1394
+ /**
1395
+ * The media features that are the viewport's width and its height here.
1396
+ * `device-width` and `device-height` are the Web-exposed screen area's
1397
+ * (Media Queries 4, appendix A), which a user agent may answer with the
1398
+ * viewport's (CSSOM View 2.3), and a document drawn into an element has no
1399
+ * screen of its own to answer with. Deprecated, and still what a page from
1400
+ * before `width` asks with: DuckDuckGo Lite's phone sheet is
1401
+ * `(max-device-width: 700px)`, and a query that went unread held, so a
1402
+ * desktop window got the phone's 12px dropdowns.
1403
+ */
1404
+ const VIEWPORT_SIDES = {
1405
+ width: new Set(['width', 'device-width']),
1406
+ height: new Set(['height', 'device-height']),
1407
+ };
1408
+
1353
1409
  /**
1354
1410
  * A width range in Media Queries 4's syntax — `(width >= 48rem)`, `(60rem >
1355
1411
  * width)`, `(40rem <= width < 60rem)` — as the widths it holds between, or
1356
1412
  * null for a term that is not one. A strict bound is a sixty-fourth of a
1357
1413
  * pixel inside the value, where a viewport's width never lands.
1358
1414
  */
1359
- function widthRange(term: string): { min?: number; max?: number } | null {
1415
+ function widthRange(
1416
+ term: string,
1417
+ feature: 'width' | 'height' = 'width',
1418
+ ): { min?: number; max?: number } | null {
1360
1419
  const inner = /^\(\s*(.*?)\s*\)$/.exec(term)?.[1];
1361
1420
  if (!inner || !/[<>=]/.test(inner)) return null;
1362
1421
  const parts = inner.split(/\s*(<=|>=|<|>|=)\s*/);
1363
- const isWidth = (part: string) => part.toLowerCase() === 'width';
1422
+ const isWidth = (part: string) =>
1423
+ VIEWPORT_SIDES[feature].has(part.toLowerCase());
1364
1424
  const out: { min?: number; max?: number } = {};
1365
1425
  // `width OP value`, with the operator read from the width's side
1366
1426
  const bound = (op: string, value: string): boolean => {
@@ -1755,8 +1815,15 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
1755
1815
  const condition: MediaCondition = {};
1756
1816
  let pass = true;
1757
1817
  let sawWidth = false;
1818
+ let sawHeight = false;
1819
+ let sawResolution = false;
1820
+ let sawAspect = false;
1758
1821
  const negated = /^\s*not\b/i.test(group);
1759
- for (const part of group.split(/\s+and\s+/i)) {
1822
+ // `and` needs white space after it, where a `(` would make it a
1823
+ // function, and none after the `)` it follows: DuckDuckGo writes
1824
+ // `(max-device-width: 701px)and (orientation: landscape)`, which read
1825
+ // as one term that was no feature, and held
1826
+ for (const part of group.split(/(?:\s+|(?<=\)))and\s+/i)) {
1760
1827
  const term = part
1761
1828
  .trim()
1762
1829
  .replace(/^not\s+/i, '')
@@ -1775,17 +1842,61 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
1775
1842
  sawWidth = true;
1776
1843
  continue;
1777
1844
  }
1778
- const feature = /^\(\s*([a-z-]+)\s*:\s*([^)]+?)\s*\)$/i.exec(term);
1845
+ const heights = widthRange(term, 'height');
1846
+ if (heights) {
1847
+ if (heights.min !== undefined) {
1848
+ condition.minHeight = Math.max(condition.minHeight ?? 0, heights.min);
1849
+ }
1850
+ if (heights.max !== undefined) {
1851
+ condition.maxHeight = Math.min(
1852
+ condition.maxHeight ?? Infinity,
1853
+ heights.max,
1854
+ );
1855
+ }
1856
+ sawHeight = true;
1857
+ continue;
1858
+ }
1859
+ const feature = mediaFeature(term);
1779
1860
  if (feature) {
1780
1861
  const key = feature[1].toLowerCase();
1781
1862
  const len = parseLength(feature[2], ZERO_UNITS);
1782
1863
  const px = typeof len === 'number' ? len : null;
1783
- if (key === 'min-width' && px !== null) {
1864
+ if (
1865
+ (key === 'min-width' || key === 'min-device-width') &&
1866
+ px !== null
1867
+ ) {
1784
1868
  condition.min = Math.max(condition.min ?? 0, px);
1785
1869
  sawWidth = true;
1786
- } else if (key === 'max-width' && px !== null) {
1870
+ } else if (
1871
+ (key === 'max-width' || key === 'max-device-width') &&
1872
+ px !== null
1873
+ ) {
1787
1874
  condition.max = Math.min(condition.max ?? Infinity, px);
1788
1875
  sawWidth = true;
1876
+ } else if (
1877
+ px === null &&
1878
+ /^(?:min-|max-)?(?:device-)?(?:width|height)$/.test(key)
1879
+ ) {
1880
+ // a size that is no length is a query that does not parse, which
1881
+ // Media Queries 4 (3.2) makes `not all`: `(min-width:0\0)`, the
1882
+ // hack that kept a block for Internet Explorer 9 and 10, held
1883
+ // here as a query that asked nothing, and its rules applied
1884
+ pass = false;
1885
+ } else if (
1886
+ (key === 'min-height' || key === 'min-device-height') &&
1887
+ px !== null
1888
+ ) {
1889
+ // the viewport's height, answered live as its width is: a
1890
+ // design that sets its heading's size by the window's height
1891
+ // took the tallest step's at every height
1892
+ condition.minHeight = Math.max(condition.minHeight ?? 0, px);
1893
+ sawHeight = true;
1894
+ } else if (
1895
+ (key === 'max-height' || key === 'max-device-height') &&
1896
+ px !== null
1897
+ ) {
1898
+ condition.maxHeight = Math.min(condition.maxHeight ?? Infinity, px);
1899
+ sawHeight = true;
1789
1900
  } else if (key === 'prefers-color-scheme') {
1790
1901
  // Answered live, from the palette in force: a document dropped
1791
1902
  // into a dark application takes its dark branch, and follows the
@@ -1804,7 +1915,73 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
1804
1915
  // renderer needs.
1805
1916
  pass = false;
1806
1917
  } else if (key === 'orientation') {
1807
- pass = feature[2].trim().toLowerCase() === 'landscape';
1918
+ // the viewport's, as its aspect ratio: portrait where its height
1919
+ // is at least its width (Media Queries 4, 4.5)
1920
+ const orientation = feature[2].trim().toLowerCase();
1921
+ if (orientation === 'portrait') {
1922
+ condition.maxAspect = Math.min(condition.maxAspect ?? Infinity, 1);
1923
+ sawAspect = true;
1924
+ } else if (orientation === 'landscape') {
1925
+ condition.minAspect = Math.max(
1926
+ condition.minAspect ?? 0,
1927
+ 1 + ASPECT_EDGE,
1928
+ );
1929
+ sawAspect = true;
1930
+ } else pass = false;
1931
+ } else if (ASPECT_FEATURES.test(key)) {
1932
+ const ratio = ratioOf(feature[2]);
1933
+ if (ratio === null) pass = false;
1934
+ else {
1935
+ if (!key.startsWith('max-')) {
1936
+ condition.minAspect = Math.max(condition.minAspect ?? 0, ratio);
1937
+ }
1938
+ if (!key.startsWith('min-')) {
1939
+ condition.maxAspect = Math.min(
1940
+ condition.maxAspect ?? Infinity,
1941
+ ratio,
1942
+ );
1943
+ }
1944
+ sawAspect = true;
1945
+ }
1946
+ } else if (RESOLUTION_FEATURES.has(key)) {
1947
+ const dppx = resolutionOf(feature[2], key.startsWith('-webkit-'));
1948
+ if (dppx === null) pass = false;
1949
+ else {
1950
+ if (!key.includes('max-')) {
1951
+ condition.minResolution = Math.max(
1952
+ condition.minResolution ?? 0,
1953
+ dppx,
1954
+ );
1955
+ }
1956
+ if (!key.includes('min-')) {
1957
+ condition.maxResolution = Math.min(
1958
+ condition.maxResolution ?? Infinity,
1959
+ dppx,
1960
+ );
1961
+ }
1962
+ sawResolution = true;
1963
+ }
1964
+ } else if (desktopFeature(key, feature[2]) !== true) {
1965
+ // any other feature as a desktop screen with a mouse answers
1966
+ // it, and one nothing knows is false (Media Queries 4, 3.2):
1967
+ // Firefox's and Opera's resolution features, which a design lists
1968
+ // beside WebKit's, held here, and a page at one dot to the pixel
1969
+ // took the rules it keeps for two
1970
+ pass = false;
1971
+ }
1972
+ continue;
1973
+ }
1974
+ const flag = /^\(\s*(-?[a-z][a-z0-9-]*)\s*\)$/i.exec(term);
1975
+ if (flag) {
1976
+ // a feature in the boolean context, `(hover)`: true where its value
1977
+ // is anything but zero or none
1978
+ const key = flag[1].toLowerCase();
1979
+ if (
1980
+ !BOOLEAN_TRUE.has(key) &&
1981
+ !RESOLUTION_FEATURES.has(key) &&
1982
+ desktopFeature(key, null) !== true
1983
+ ) {
1984
+ pass = false;
1808
1985
  }
1809
1986
  continue;
1810
1987
  }
@@ -1820,11 +1997,23 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
1820
1997
  // `not` over a scheme is the other scheme. `not` over a width range
1821
1998
  // is not expressible as one range; the honest reduction is to decide
1822
1999
  // it statically rather than invert it wrongly.
1823
- if (!sawWidth && pass && condition.scheme) {
2000
+ if (
2001
+ !sawWidth &&
2002
+ !sawHeight &&
2003
+ !sawResolution &&
2004
+ !sawAspect &&
2005
+ pass &&
2006
+ condition.scheme
2007
+ ) {
1824
2008
  out.push({ scheme: condition.scheme === 'dark' ? 'light' : 'dark' });
1825
2009
  continue;
1826
2010
  }
1827
- out.push({ staticPass: !sawWidth && pass ? false : !pass });
2011
+ out.push({
2012
+ staticPass:
2013
+ !sawWidth && !sawHeight && !sawResolution && !sawAspect && pass
2014
+ ? false
2015
+ : !pass,
2016
+ });
1828
2017
  continue;
1829
2018
  }
1830
2019
  if (!pass) {
@@ -1833,6 +2022,9 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
1833
2022
  }
1834
2023
  if (
1835
2024
  !sawWidth &&
2025
+ !sawHeight &&
2026
+ !sawResolution &&
2027
+ !sawAspect &&
1836
2028
  condition.scheme === undefined &&
1837
2029
  condition.min === undefined &&
1838
2030
  condition.max === undefined
@@ -1845,6 +2037,221 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
1845
2037
  return out.length ? out : [{ staticPass: true }];
1846
2038
  }
1847
2039
 
2040
+ /**
2041
+ * The properties whose support a `@supports` condition is answered for:
2042
+ * the mask's, which pages test before they draw an icon as a masked colour
2043
+ * and keep a background image under `not` for an engine without them.
2044
+ * Drawn here, and the fallback drawn too, an icon was its image in black
2045
+ * under the mask, over the colour it was meant to be.
2046
+ */
2047
+ const ANSWERED = new Set(
2048
+ [
2049
+ 'mask',
2050
+ 'mask-image',
2051
+ 'mask-repeat',
2052
+ 'mask-position',
2053
+ 'mask-size',
2054
+ 'mask-origin',
2055
+ 'mask-clip',
2056
+ ].flatMap((name) => [name, `-webkit-${name}`]),
2057
+ );
2058
+
2059
+ /**
2060
+ * Whether a `@supports` condition holds (CSS Conditional 3, 6): true or
2061
+ * false where it turns on declarations of the properties this answers for
2062
+ * (`ANSWERED`), combined by `not`, `and` and `or`; null where it turns on
2063
+ * anything else, or is none, and the block is entered as every one was.
2064
+ * Answering more is not safe without a list of what this draws: Tailwind 4
2065
+ * keeps its variables' starting values under a test for engines without
2066
+ * `@property`, which this is, and false there would drop every shadow and
2067
+ * gradient it writes.
2068
+ */
2069
+ export function supportsCondition(prelude: string): boolean | null {
2070
+ const text = prelude.trim();
2071
+ if (/^not\b/i.test(text)) {
2072
+ const inner = supportsCondition(text.slice(3));
2073
+ return inner === null ? null : !inner;
2074
+ }
2075
+ // the terms at the top level, and the one operator between them
2076
+ const terms: string[] = [];
2077
+ let op = '';
2078
+ let depth = 0;
2079
+ let start = 0;
2080
+ for (let i = 0; i < text.length; i += 1) {
2081
+ const c = text[i];
2082
+ if (c === '(') depth += 1;
2083
+ else if (c === ')') depth -= 1;
2084
+ else if (depth === 0 && /\s/.test(c)) {
2085
+ const word = /^\s+(and|or)\s+/i.exec(text.slice(i));
2086
+ if (!word) continue;
2087
+ const next = word[1].toLowerCase();
2088
+ // `and` and `or` mixed without parentheses is no condition
2089
+ if (op && op !== next) return null;
2090
+ op = next;
2091
+ terms.push(text.slice(start, i));
2092
+ i += word[0].length - 1;
2093
+ start = i + 1;
2094
+ }
2095
+ if (depth < 0) return null;
2096
+ }
2097
+ if (terms.length) {
2098
+ terms.push(text.slice(start));
2099
+ const values = terms.map(supportsCondition);
2100
+ if (op === 'and') {
2101
+ if (values.includes(false)) return false;
2102
+ return values.includes(null) ? null : true;
2103
+ }
2104
+ if (values.includes(true)) return true;
2105
+ return values.includes(null) ? null : false;
2106
+ }
2107
+ // one term: a condition in parentheses, or a declaration in them
2108
+ if (!(text.startsWith('(') && text.endsWith(')'))) return null;
2109
+ const inner = text.slice(1, -1).trim();
2110
+ if (inner.startsWith('(') || /^not\b/i.test(inner)) {
2111
+ return supportsCondition(inner);
2112
+ }
2113
+ const declaration = /^([a-z-]+)\s*:\s*(\S[\s\S]*)$/i.exec(inner);
2114
+ if (!declaration || !ANSWERED.has(declaration[1].toLowerCase())) return null;
2115
+ return true;
2116
+ }
2117
+
2118
+ /**
2119
+ * A `(name: value)` media feature: the whole term, its name, and its value
2120
+ * read to the parenthesis that closes the feature, or null where the term is
2121
+ * no such thing. A value may hold parentheses of its own: MediaWiki's
2122
+ * breakpoints are `(max-width: calc(640px - 1px))`, and a value read to the
2123
+ * first `)` found no feature there — the term was passed over as though it
2124
+ * said nothing, and every narrow-screen rule held at every width.
2125
+ */
2126
+ /** `aspect-ratio` and the device's, bare or `min-`/`max-`. */
2127
+ const ASPECT_FEATURES = /^(?:min-|max-)?(?:device-)?aspect-ratio$/;
2128
+
2129
+ /** How far past square a landscape viewport is: any width over the height. */
2130
+ const ASPECT_EDGE = 1e-9;
2131
+
2132
+ /** A ratio, `16/9` or `16 / 9` or `1.5`, as a number; null for anything
2133
+ * else, or a ratio of nothing. */
2134
+ function ratioOf(value: string): number | null {
2135
+ const m = /^\s*(\d*\.?\d+)\s*(?:\/\s*(\d*\.?\d+)\s*)?$/.exec(value);
2136
+ if (!m) return null;
2137
+ const ratio = Number(m[1]) / (m[2] === undefined ? 1 : Number(m[2]));
2138
+ return Number.isFinite(ratio) && ratio > 0 ? ratio : null;
2139
+ }
2140
+
2141
+ /** The resolution features, the standard's and WebKit's, which Chrome
2142
+ * answers; `min--moz-device-pixel-ratio` and `-o-min-device-pixel-ratio`
2143
+ * are no feature of its, and none of this. */
2144
+ const RESOLUTION_FEATURES = new Set([
2145
+ 'resolution',
2146
+ 'min-resolution',
2147
+ 'max-resolution',
2148
+ '-webkit-device-pixel-ratio',
2149
+ '-webkit-min-device-pixel-ratio',
2150
+ '-webkit-max-device-pixel-ratio',
2151
+ ]);
2152
+
2153
+ /** A resolution in dots per CSS pixel: `2dppx`, `2x`, `192dpi`, `75.6dpcm`,
2154
+ * or WebKit's device pixel ratio, a number. Null for anything else. */
2155
+ function resolutionOf(value: string, ratio: boolean): number | null {
2156
+ const m = /^\s*(\d*\.?\d+(?:e[+-]?\d+)?)\s*(dppx|x|dpi|dpcm)?\s*$/i.exec(
2157
+ value,
2158
+ );
2159
+ if (!m) return null;
2160
+ const n = Number(m[1]);
2161
+ const unit = (m[2] ?? '').toLowerCase();
2162
+ if (ratio) return unit ? null : n;
2163
+ if (unit === 'dppx' || unit === 'x') return n;
2164
+ if (unit === 'dpi') return n / 96;
2165
+ if (unit === 'dpcm') return (n * 2.54) / 96;
2166
+ return null;
2167
+ }
2168
+
2169
+ /** The features true in the boolean context whatever the viewport: a
2170
+ * size, a scheme, an orientation, there is always one. */
2171
+ const BOOLEAN_TRUE = new Set([
2172
+ 'width',
2173
+ 'height',
2174
+ 'device-width',
2175
+ 'device-height',
2176
+ 'aspect-ratio',
2177
+ 'orientation',
2178
+ 'prefers-color-scheme',
2179
+ ]);
2180
+
2181
+ /**
2182
+ * A media feature as a desktop screen driven by a mouse answers it — the
2183
+ * device `<Html>` draws on — for the features that do not depend on the
2184
+ * viewport: true, false, or null for a feature nothing knows, which is
2185
+ * false (Media Queries 4, 3.2). `value` is null in the boolean context.
2186
+ * The colour is eight bits a component on no palette and no grid; scripts
2187
+ * never run here, so `scripting` is `none`.
2188
+ */
2189
+ function desktopFeature(key: string, value: string | null): boolean | null {
2190
+ const v = value?.trim().toLowerCase() ?? null;
2191
+ const numeric = (feature: string, has: number): boolean | null => {
2192
+ const bare = key.replace(/^(min|max)-/, '');
2193
+ if (bare !== feature) return null;
2194
+ if (v === null) return key === feature ? has !== 0 : null;
2195
+ const n = Number(v);
2196
+ if (!Number.isFinite(n)) return false;
2197
+ if (key.startsWith('min-')) return has >= n;
2198
+ if (key.startsWith('max-')) return has <= n;
2199
+ return has === n;
2200
+ };
2201
+ const keyword = (answers: Record<string, boolean>, bool: boolean) =>
2202
+ v === null ? bool : (answers[v] ?? false);
2203
+ switch (key) {
2204
+ case 'hover':
2205
+ case 'any-hover':
2206
+ return keyword({ hover: true, none: false }, true);
2207
+ case 'pointer':
2208
+ case 'any-pointer':
2209
+ return keyword({ fine: true, coarse: false, none: false }, true);
2210
+ case 'update':
2211
+ return keyword({ fast: true, slow: false, none: false }, true);
2212
+ case 'overflow-block':
2213
+ case 'overflow-inline':
2214
+ return keyword({ scroll: true }, true);
2215
+ case 'scan':
2216
+ return false;
2217
+ case 'color-gamut':
2218
+ return keyword({ srgb: true }, true);
2219
+ case 'dynamic-range':
2220
+ case 'video-dynamic-range':
2221
+ return keyword({ standard: true }, true);
2222
+ case 'prefers-contrast':
2223
+ case 'prefers-reduced-transparency':
2224
+ case 'prefers-reduced-data':
2225
+ return keyword({ 'no-preference': true }, false);
2226
+ case 'forced-colors':
2227
+ return keyword({ none: true }, false);
2228
+ case 'scripting':
2229
+ return keyword({ none: true }, false);
2230
+ case 'display-mode':
2231
+ return keyword({ browser: true }, true);
2232
+ case '-webkit-transform-3d':
2233
+ return v === null || v === '1';
2234
+ }
2235
+ return (
2236
+ numeric('color', 8) ??
2237
+ numeric('monochrome', 0) ??
2238
+ numeric('color-index', 0) ??
2239
+ numeric('grid', 0)
2240
+ );
2241
+ }
2242
+
2243
+ function mediaFeature(term: string): [string, string, string] | null {
2244
+ const m = /^\(\s*([a-z-]+)\s*:([\s\S]*)\)$/i.exec(term);
2245
+ if (!m) return null;
2246
+ let depth = 0;
2247
+ for (const c of m[2]) {
2248
+ if (c === '(') depth += 1;
2249
+ else if (c === ')' && --depth < 0) return null;
2250
+ }
2251
+ const value = m[2].trim();
2252
+ return depth === 0 && value ? [term, m[1], value] : null;
2253
+ }
2254
+
1848
2255
  /** A `@media` width is compared with the viewport in CSS pixels — the
1849
2256
  * cascade divides the device width by the scale before asking — so the
1850
2257
  * thresholds parse at scale 1 whatever panel the document lands on. */
@@ -1856,6 +2263,11 @@ export function mediaMatches(
1856
2263
  media: MediaCondition[][] | null,
1857
2264
  width: number,
1858
2265
  scheme: 'light' | 'dark' = 'light',
2266
+ /** The viewport's height in CSS pixels; a test of it holds at no height
2267
+ * where none is given. */
2268
+ height = NaN,
2269
+ /** The display's scale, device pixels to the CSS pixel. */
2270
+ resolution = 1,
1859
2271
  ): boolean {
1860
2272
  if (!media) return true;
1861
2273
  for (const block of media) {
@@ -1868,6 +2280,12 @@ export function mediaMatches(
1868
2280
  if (
1869
2281
  (c.min === undefined || width >= c.min) &&
1870
2282
  (c.max === undefined || width <= c.max) &&
2283
+ (c.minHeight === undefined || height >= c.minHeight) &&
2284
+ (c.maxHeight === undefined || height <= c.maxHeight) &&
2285
+ (c.minResolution === undefined || resolution >= c.minResolution) &&
2286
+ (c.minAspect === undefined || width >= c.minAspect * height) &&
2287
+ (c.maxAspect === undefined || width <= c.maxAspect * height) &&
2288
+ (c.maxResolution === undefined || resolution <= c.maxResolution) &&
1871
2289
  (c.scheme === undefined || c.scheme === scheme)
1872
2290
  ) {
1873
2291
  any = true;