@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
@@ -38,6 +38,7 @@ import {
38
38
  BOX_RAISES,
39
39
  Box,
40
40
  CLAMPED,
41
+ CLEARED_FROM,
41
42
  CUT_BLOCKS,
42
43
  GRID_TRACKS,
43
44
  FIRST_LINE,
@@ -60,6 +61,7 @@ import type {
60
61
  import { FloatContext } from './floats.js';
61
62
  import {
62
63
  faceExtentOf,
64
+ joinsBefore,
63
65
  layoutInline,
64
66
  lineHeightMultiplier,
65
67
  spaceBreaksOnly,
@@ -71,7 +73,15 @@ import type { TextRun } from '../../richtext/index.js';
71
73
  import { layoutFlex } from './flex.js';
72
74
  import { finishCaptions, layoutTable } from './table.js';
73
75
  import { collapseEdges } from './collapse.js';
74
- import { computePaintBounds, hoistNegative, stackLayers } from '../paint.js';
76
+ import {
77
+ clipsFor,
78
+ computePaintBounds,
79
+ FIXED_BOXES,
80
+ fixedToViewport,
81
+ hoistNegative,
82
+ OUT_OF_FLOW_REACH,
83
+ stackLayers,
84
+ } from '../paint.js';
75
85
 
76
86
  export interface LayoutContext {
77
87
  fonts: FontsLike | null;
@@ -238,19 +248,35 @@ export function layoutDocument(
238
248
  // against where it *was* — which is the whole of what makes it relative —
239
249
  // and the ink bounds are computed after that, so culling sees where boxes
240
250
  // ended up rather than where they were laid out.
241
- if (tree.relative) applyRelativeOffsets(root);
251
+ if (tree.relative) applyRelativeOffsets(root, viewportWidth, viewportHeight);
242
252
  const reach = computePaintBounds(root, tree.movedInline);
243
253
  if (tree.negative) hoistNegative(root);
244
254
  stackLayers(root);
245
255
 
246
256
  // The document is as tall as what overflows the root, not the root: an
247
257
  // `html, body { height: 100% }` a window tall holds a message longer than
248
- // the window, and the element sizes to all of it.
258
+ // the window, and the element sizes to all of it. Not what a box that
259
+ // clips its overflow holds: that is its own to scroll, and the root's
260
+ // scrollable overflow takes in a positioned box only where no box on the
261
+ // way to it clips it (CSS Overflow 3, 2.2): an absolute box 900px down
262
+ // an `overflow: hidden` card made a page a browser shows 400px tall run
263
+ // on to 980, blank
249
264
  let bottom = Math.max(root.height, reach);
265
+ let fixed: Box[] | null = null;
250
266
  for (const { box } of ctx.positioned) {
251
- if (box.style.position !== 'fixed')
252
- bottom = Math.max(bottom, box.y + box.height);
267
+ if (box.style.position === 'fixed') {
268
+ if (fixedToViewport(box)) (fixed ??= []).push(box);
269
+ continue;
270
+ }
271
+ if (clipsFor(box, root).length) continue;
272
+ // and what it holds, where it does not clip it: a page set in an
273
+ // absolute wrapper 497px tall runs on below it, and the document with
274
+ // it — design 094 was a window tall and no taller, its text past the
275
+ // end of the scroll
276
+ bottom = Math.max(bottom, OUT_OF_FLOW_REACH.get(box) ?? box.y + box.height);
253
277
  }
278
+ if (fixed) FIXED_BOXES.set(tree, fixed);
279
+ else FIXED_BOXES.delete(tree);
254
280
  return {
255
281
  width: viewportWidth,
256
282
  height: bottom,
@@ -768,8 +794,12 @@ function layoutChildren(
768
794
  // this box's first formatted line is its first child's in flow (CSS 2.1
769
795
  // 5.12.1), which its `::first-line` is handed to
770
796
  let firstLine = ctx.firstLine ? firstLineOf(box) : null;
771
- // and the room a list item's marker takes on it (`MARKER_ROOM`)
772
- let marked = MARKER_ROOM.get(box) ?? null;
797
+ // and the room a list item's marker takes on it (`MARKER_ROOM`): an
798
+ // inside marker's is handed down to the line it is on, and an outside
799
+ // one's is room above the baseline of the first child that has a line,
800
+ // which goes lower where the marker reaches higher (`MARKER_ASCENT`)
801
+ let pushed = MARKER_ASCENT.get(box) ?? null;
802
+ let marked = pushed === null ? (MARKER_ROOM.get(box) ?? null) : null;
773
803
  const clamp = ctx.clamp ?? null;
774
804
 
775
805
  for (const child of box.children) {
@@ -863,6 +893,7 @@ function layoutChildren(
863
893
  }
864
894
 
865
895
  const counted = clamp?.fit?.lines ?? 0;
896
+ const floatMark = floats.count;
866
897
  if (
867
898
  !floats.isEmpty &&
868
899
  (child.kind === 'replaced' || establishesBFC(child))
@@ -871,6 +902,17 @@ function layoutChildren(
871
902
  } else {
872
903
  layoutBlockLevel(child, ctx, floats, contentLeft, childY, contentWidth);
873
904
  }
905
+ if (pushed !== null) {
906
+ const line = firstLineIn(child);
907
+ if (line) {
908
+ const push = pushed - (line.y + line.baseline - child.y);
909
+ if (push > 0) {
910
+ translate(child, 0, push);
911
+ floats.moveSince(floatMark, push);
912
+ }
913
+ pushed = null;
914
+ }
915
+ }
874
916
  // a block with none of the flow's lines in it — an image, a box of a
875
917
  // formatting context of its own — puts the clamp point of an `auto`
876
918
  // clamp before it where it ends below the room
@@ -886,6 +928,8 @@ function layoutChildren(
886
928
  }
887
929
  first = false;
888
930
  const moved = childY !== at;
931
+ if (moved) CLEARED_FROM.set(child, at);
932
+ else CLEARED_FROM.delete(child);
889
933
  if (!moved && collapsesThrough(child)) {
890
934
  // nothing in it parts its margins: they and the ones either side of
891
935
  // it are one (CSS 2.1 8.3.1), still hanging for what comes next —
@@ -1030,11 +1074,20 @@ function layoutBesideFloats(
1030
1074
  band = over;
1031
1075
  continue;
1032
1076
  }
1077
+ // Where no float narrows the room on either side, the box is where
1078
+ // it would be were there none, and one too wide for its containing
1079
+ // block overflows it here: below the floats it would be the same box
1080
+ // in the same room. Blink tests a fit only against a side a float is
1081
+ // on (`can_expand_outside_opportunity`, BlockLayoutAlgorithm::
1082
+ // LayoutNewFormattingContext). Design 209's 247px heading, which
1083
+ // clips its overflow, in a 240px column beside a float that ends
1084
+ // left of the column, went under the float, 555px down.
1085
+ if (lo === -Infinity && hi === Infinity) fits = true;
1033
1086
  // What is too wide overflows at the end of the line, as far as a
1034
1087
  // negative margin there takes it, but no further into a float or
1035
1088
  // past the room; at the start it would run into the floats, and
1036
1089
  // waits below them (the suite's floats-wrap-bfc-with-margin tests)
1037
- if (inRoom) {
1090
+ else if (inRoom) {
1038
1091
  fits = box.marginLeft + box.width + box.marginRight <= room + 0.5;
1039
1092
  } else if (rtl) {
1040
1093
  const limit = Math.max(contentLeft + Math.min(0, box.marginLeft), lo);
@@ -1744,6 +1797,8 @@ function layoutBox(
1744
1797
  const room = markerRoom(box, box.marker, ctx.fonts);
1745
1798
  if (room) MARKER_ROOM.set(box, room);
1746
1799
  else MARKER_ROOM.delete(box);
1800
+ if (box.style.listStylePosition === 'inside') MARKER_ASCENT.delete(box);
1801
+ else MARKER_ASCENT.set(box, markerAscent(box, box.marker, ctx.fonts));
1747
1802
  }
1748
1803
  const flow = layoutChildren(box, ctx, floats, box.contentY, contentWidth);
1749
1804
  // A list item with a marker and no line holds one, the marker's, a line
@@ -1830,6 +1885,28 @@ function markerRoom(
1830
1885
  : null;
1831
1886
  }
1832
1887
 
1888
+ /**
1889
+ * How far an outside marker reaches above the baseline it sits on. The
1890
+ * marker is a line of its own face, the item's or a `::marker` rule's, so a
1891
+ * bullet or a number reaches its face's ascent on that line, and an image,
1892
+ * set on the line bottom on the baseline, the higher of that and its own
1893
+ * height. Where the item's first line is in a block inside it — a link set
1894
+ * `display: block`, a paragraph — and the block's first baseline is nearer
1895
+ * its top than that, the block goes lower by the difference, and its line
1896
+ * stays the height it was: CSS 2.1 (12.5.1) leaves this to the user agent,
1897
+ * and Blink aligns the marker's baseline with the block's and pushes the
1898
+ * block down (`UnpositionedListMarker::AddToBox`). Its first line grew
1899
+ * instead, and design 196's links, under 18px bullets, were 21px tall where
1900
+ * Chrome has them 14px, 7px lower. On the item's own line the two come to
1901
+ * the same: the line grows by what the push would have moved it.
1902
+ */
1903
+ const MARKER_ASCENT = new WeakMap<Box, number>();
1904
+
1905
+ function markerAscent(box: Box, marker: Marker, fonts: FontsLike): number {
1906
+ const ascent = strutOf(fonts, marker.style ?? box.style).ascent;
1907
+ return marker.image ? Math.max(marker.image.height, ascent) : ascent;
1908
+ }
1909
+
1833
1910
  /** The first line box anywhere under a box, in layout order. */
1834
1911
  function firstLineIn(box: Box): LineBox | null {
1835
1912
  if (box.lines?.length) return box.lines[0];
@@ -2827,10 +2904,19 @@ function wordBound(box: Box, fonts: FontsLike, measured: boolean): number {
2827
2904
  let last: object | null = null;
2828
2905
  for (const line of box.lines) {
2829
2906
  if (unbroken) widest = Math.max(widest, line.width);
2907
+ // a word across inline boxes' edges is in two fragments of text, and
2908
+ // as wide as its parts in both and the edges between: bound by the
2909
+ // widest word of each, a float in a narrow room took a letter of a
2910
+ // word of letters in bordered spans for its least width, and was cut
2911
+ // to the room (`joinsBefore`)
2912
+ let chain = 0;
2830
2913
  for (const text of line.texts) {
2831
- if (text.layout === last) continue;
2914
+ const joins = joinsBefore(text);
2915
+ if (joins === undefined && text.layout === last) continue;
2832
2916
  last = text.layout;
2833
- widest = Math.max(widest, widestWord(fonts, text.layout, measured));
2917
+ const own = widestWord(fonts, text.layout, measured);
2918
+ chain = joins === undefined ? own : chain + joins + own;
2919
+ widest = Math.max(widest, chain);
2834
2920
  }
2835
2921
  for (const placed of line.atomics) {
2836
2922
  const b = placed.box;
@@ -4103,9 +4189,15 @@ export function establishesBFC(box: Box): boolean {
4103
4189
 
4104
4190
  /** What a box's `position: relative` offset moves it by, applied after
4105
4191
  * layout so it does not affect anything else's position — which is the
4106
- * whole of what makes it *relative* — and its translation with it. */
4107
- export function applyRelativeOffsets(box: Box): void {
4108
- for (const child of box.children) applyRelativeOffsets(child);
4192
+ * whole of what makes it *relative* — and its translation with it, and
4193
+ * where `position: sticky` puts one at rest (`stickyOffset`). */
4194
+ export function applyRelativeOffsets(
4195
+ box: Box,
4196
+ viewportWidth: number,
4197
+ viewportHeight: number,
4198
+ ): void {
4199
+ for (const child of box.children)
4200
+ applyRelativeOffsets(child, viewportWidth, viewportHeight);
4109
4201
  const style = box.style;
4110
4202
  if (transformed(style) && box.kind !== 'inline') {
4111
4203
  // a transform moves the box it is on, and an inline box is none
@@ -4116,7 +4208,10 @@ export function applyRelativeOffsets(box: Box): void {
4116
4208
  }
4117
4209
  }
4118
4210
  if (style.position !== 'relative' && style.position !== 'sticky') return;
4119
- const [dx, dy] = relativeOffset(box);
4211
+ const [dx, dy] =
4212
+ style.position === 'sticky'
4213
+ ? stickyOffset(box, viewportWidth, viewportHeight)
4214
+ : relativeOffset(box);
4120
4215
  if (!(dx || dy)) return;
4121
4216
  translate(box, dx, dy);
4122
4217
  if (box.kind !== 'inline') {
@@ -4137,10 +4232,10 @@ export function transformed(style: ComputedStyle): boolean {
4137
4232
 
4138
4233
  /** How far `translate` and a `transform` move a box: a percentage is of
4139
4234
  * its own border box (CSS Transforms 1, 7). */
4140
- function translationOf(box: Box): [number, number] {
4235
+ function translationOf(box: Box, style = box.style): [number, number] {
4141
4236
  let dx = 0;
4142
4237
  let dy = 0;
4143
- for (const moved of [box.style.translate, box.style.transformTranslate]) {
4238
+ for (const moved of [style.translate, style.transformTranslate]) {
4144
4239
  if (!moved) continue;
4145
4240
  dx += resolve(moved[0], box.width, 0);
4146
4241
  dy += resolve(moved[1], box.height, 0);
@@ -4148,6 +4243,21 @@ function translationOf(box: Box): [number, number] {
4148
4243
  return [dx, dy];
4149
4244
  }
4150
4245
 
4246
+ /**
4247
+ * Move a laid-out box, and everything in it, by how far its translation
4248
+ * changed when its style went from `was` to the one it has — what
4249
+ * `applyRelativeOffsets` moves it by now, less what it moved it by then.
4250
+ * For a style changed in place, where nothing else moves (a `transform` is
4251
+ * what makes that true). Not for an inline box, whose text is what moves.
4252
+ */
4253
+ export function retranslate(box: Box, was: ComputedStyle): void {
4254
+ const [x0, y0] = translationOf(box, was);
4255
+ const [x1, y1] = translationOf(box);
4256
+ if (x1 === x0 && y1 === y0) return;
4257
+ translate(box, x1 - x0, y1 - y0);
4258
+ movedOffLine(box);
4259
+ }
4260
+
4151
4261
  /** How far `position: relative` moves a box. */
4152
4262
  function relativeOffset(box: Box): [number, number] {
4153
4263
  const style = box.style;
@@ -4166,6 +4276,111 @@ function relativeOffset(box: Box): [number, number] {
4166
4276
  return [dx, dy];
4167
4277
  }
4168
4278
 
4279
+ /**
4280
+ * Where `position: sticky` puts a box at rest (CSS Positioned Layout 3,
4281
+ * 3.4): moved only as far as keeps its border box inside the sticky view
4282
+ * rectangle — its nearest scroll container's scrollport, scrolled to its
4283
+ * start, less its insets — and no further than keeps its margin box in its
4284
+ * containing block, `top` winning over `bottom` and the start side over the
4285
+ * end. Nothing here scrolls a box the document holds, so for one of those
4286
+ * this is where the box stays. The viewport does scroll, and the document
4287
+ * with it, and a sticky box does not follow: against the viewport only its
4288
+ * `top` and its start side are kept, which at rest put a box as a browser
4289
+ * does, while a `bottom: 0` would pin a footer to the middle of the page
4290
+ * once it was scrolled. Taken as `relative`, a box stood its `top` below
4291
+ * where a browser starts it: Wikipedia's contents, 24px low.
4292
+ */
4293
+ function stickyOffset(
4294
+ box: Box,
4295
+ viewportWidth: number,
4296
+ viewportHeight: number,
4297
+ ): [number, number] {
4298
+ const style = box.style;
4299
+ // the scroll container a sticky box keeps to: the nearest box that
4300
+ // scrolls, or the viewport, whose are the root's and the body's overflow
4301
+ let port = box.parent;
4302
+ while (port && !scrolls(port.style)) port = port.parent;
4303
+ const tag = port?.el?.name;
4304
+ const inside = !!port && tag !== 'html' && tag !== 'body';
4305
+ let cb = box.parent;
4306
+ while (cb && cb.kind === 'inline') cb = cb.parent;
4307
+ if (!cb) return [0, 0];
4308
+ // the scrollport, at its start: a scroll container's padding box, or
4309
+ // the viewport, which the document's top is at
4310
+ const px = port && inside ? port.x + port.borderLeft : 0;
4311
+ const py = port && inside ? port.y + port.borderTop : 0;
4312
+ const pw =
4313
+ port && inside
4314
+ ? port.width - port.borderLeft - port.borderRight
4315
+ : viewportWidth;
4316
+ const ph =
4317
+ port && inside
4318
+ ? port.height - port.borderTop - port.borderBottom
4319
+ : viewportHeight;
4320
+ const dx = stuck(
4321
+ box.x,
4322
+ box.width,
4323
+ box.marginLeft,
4324
+ box.marginRight,
4325
+ cb.contentX,
4326
+ cb.contentWidth,
4327
+ px,
4328
+ pw,
4329
+ resolveOrNull(style.left, pw),
4330
+ inside ? resolveOrNull(style.right, pw) : null,
4331
+ // `left` wins where both hold, or `right` in a right-to-left block
4332
+ cb.style.direction !== 'rtl',
4333
+ );
4334
+ const dy = stuck(
4335
+ box.y,
4336
+ box.height,
4337
+ box.marginTop,
4338
+ box.marginBottom,
4339
+ cb.contentY,
4340
+ cb.contentHeight,
4341
+ py,
4342
+ ph,
4343
+ resolveOrNull(style.top, ph),
4344
+ inside ? resolveOrNull(style.bottom, ph) : null,
4345
+ true,
4346
+ );
4347
+ return [dx, dy];
4348
+ }
4349
+
4350
+ /**
4351
+ * How far a sticky box moves along one axis: toward the end until its
4352
+ * start edge is `start` inside the scrollport's, or toward the start until
4353
+ * its end edge is `end` inside the scrollport's, each no further than its
4354
+ * margin box stays in its containing block — and where both would move
4355
+ * it, `startWins` says which does.
4356
+ */
4357
+ function stuck(
4358
+ at: number,
4359
+ size: number,
4360
+ marginStart: number,
4361
+ marginEnd: number,
4362
+ cbAt: number,
4363
+ cbSize: number,
4364
+ portAt: number,
4365
+ portSize: number,
4366
+ start: number | null,
4367
+ end: number | null,
4368
+ startWins: boolean,
4369
+ ): number {
4370
+ let byStart = 0;
4371
+ if (start !== null && Number.isFinite(start) && at < portAt + start) {
4372
+ const room = cbAt + cbSize - (at + size + marginEnd);
4373
+ byStart = Math.max(0, Math.min(portAt + start - at, room));
4374
+ }
4375
+ let byEnd = 0;
4376
+ if (end !== null && Number.isFinite(end)) {
4377
+ const over = at + size - (portAt + portSize - end);
4378
+ const room = at - marginStart - cbAt;
4379
+ if (over > 0) byEnd = -Math.max(0, Math.min(over, room));
4380
+ }
4381
+ return startWins ? byStart || byEnd : byEnd || byStart;
4382
+ }
4383
+
4169
4384
  /**
4170
4385
  * Move an inline box's text where `position: relative` puts the box. Its
4171
4386
  * text is on its block's lines, laid out apart from the text around it