@aiquants/virtualscroll 3.8.2 → 3.9.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,75 @@
2
2
 
3
3
  All notable changes to `@aiquants/virtualscroll` are documented here.
4
4
 
5
+ ## 3.9.0 (2026-10-04)
6
+
7
+ ### Added
8
+
9
+ - **自分で動かした位置を同期で知らせる `onScrollAdjust`**: `VirtualScroll` が入力なしにスクロール位置を自分で動かすたびに、
10
+ 間引かず同期で、動かし終えてから呼ぶ。理由 (`cause`) は 4 つ: `"item-resize"` (`updateItemSize` が先頭の可視行より上の
11
+ 行の高さを変え、位置を同じ差だけ動かした。`updateItemSize` から戻る前)、`"reconciliation"` (描画が、描いた行のうち先頭の
12
+ 可視行より上の行に `getItemHeight` の新しい高さを見つけ、その描画の後のマイクロタスクが位置を同じ差だけ動かした)、
13
+ `"drift"` (寸法の変化の後、最後の `scrollToIndex` または `initialScrollAnchor` の保留中の揃えを、確定の後の effect で
14
+ 留め直した)、`"re-issue"` (前の中身の寸法でクランプされた補正を、新しい寸法の確定の後の effect でもう一度発行した)。
15
+ 引数は `{ position, delta, cause }` (`VirtualScrollAdjustment`。論理 px) で、`position` は動かした後の `getScrollPosition()`
16
+ の値、`delta` は適用した変化 (0 にはならない)。中で読む `getScrollPosition()` と `getScrollAnchor()` は動かした後の位置と
17
+ 行の高さを返す。利用者やホストが始めたスクロール (ホイール・ドラッグと慣性・スクロールバー・行のキー移動・`scrollTo` /
18
+ `scrollBy` / `scrollToIndex` / `applyWheel`) と、`getScrollPosition()` を変えない変化では呼ばない。`onScroll` /
19
+ `onRangeChange` は間引いたうえで 1 フレーム遅れるため、項目の同一性で自前の錨を持つホストがそこでだけ錨を記録すると、
20
+ 次の範囲の知らせより前に確定した一覧の変化が、既に動いた位置を巻き戻していた (補正の分ずれる、または隣の項目に着地する)。
21
+ そうしたホストはここでも錨を記録する (README「Position changes the list makes on its own」)。型
22
+ `VirtualScrollAdjustment` / `VirtualScrollAdjustmentCause` をバレルから公開する。`VirtualGrid` は公開しない (位置が 2 軸で、
23
+ 自分で動かすのは縦だけのため)。
24
+ - **スタイルの口の表と空状態のクラス**: README の節を「Custom styling」へ改め、スタイルの口 (描かれる部品と組の
25
+ `aqvs-*` クラス) を表にした。空状態の `.aqvs-no-items-container` (表示域の上端の箱) と `.aqvs-no-items-text`
26
+ (`labels.noItems` の文言。既定の文字色 `#6b7280` は明るい背景でしか 4.5:1 に届かない) を加えた。表のクラスの改名・削除は
27
+ 破壊的変更として扱う。
28
+ - **テスト**: `VirtualScroll.spec.ts` +15 (下記 Fixed の揃える関数の 2 件と守る端の選び方の 3 件、端数のビューポートで
29
+ 揃えた端を越えないことのコンポーネントの 4 件、`onScrollAdjust` の 6 件 — 4 つの理由をそれぞれ実物で起こし 1 回ずつ適用した
30
+ 差で知らせ、中で読むハンドルが動かした後の位置と錨を返すこと、利用者とホストのスクロール (`scrollBy`・`scrollTo`・
31
+ `scrollToIndex`・ホイール・スクロールバーの矢印・行の PageDown・ポインタのドラッグとその慣性・つまみのドラッグ・トラックの
32
+ 押下) では知らせないこと)、`renderedClassContract.spec.tsx` +3 (README の表のクラスがこの契約が部品を引く表とちょうど一致し
33
+ 空状態の 2 つを含むこと、どのクラスも名指す部品 (ARIA のロール・`data-*`・文言で引いた要素) のすべてに付いて描かれること、
34
+ 空の一覧の文言と箱)、`scrollStepRepaint.spec.tsx` +2 (下記 Fixed のつまみ)。
35
+
36
+ ### Fixed
37
+
38
+ - **端に揃えた行の余白を装置の画素への揃えが削っていた**: 行ラッパーの平行移動は最も近い格子点へ揃えていた (`Math.round`。
39
+ ちょうど半分は +∞ 側)。そのためビューポートの高さが端数のとき、最大位置の最後の行や `align: "bottom"` で見せた行が、
40
+ 表示域の終端を最大で半装置画素越えた (比 1 の 705.5px のビューポートで、終端の 8px の余白が 7.5px。ホストの視覚の検査では
41
+ 比 1.5 で 0.3335px が切れた)。揃えは守る端を選ぶ。位置 0 と、最後の `scrollToIndex` の `align: "top"` (既定。offset の
42
+ 有無を問わない) の着地位置では、格子点を正確な値以上に取り (`Math.ceil`)、中身を始端側へ動かさない。最大位置と、最後の
43
+ `align: "bottom"` の着地位置では、正確な値以下に取り (`Math.floor`)、中身を終端側へ動かさない。それ以外 (中央揃え・
44
+ 利用者やホストのスクロール) は従来どおり最も近い格子点。覚えた揃えは、揃えた行をその場に留めるレイアウトシフトの補正と
45
+ ドリフト補正で一緒に動き、ほかのスクロールで忘れる。位置 0 は最大位置に勝ち、どちらも覚えた揃えに勝つ。端から 2^-10 px
46
+ 以内は端とみなす。揃えた端を保つため、`scrollToIndex` は揃えた位置へ厳密に着地し (従来は 0.5px 以下の差ではペインを
47
+ 動かさなかった)、ドリフト補正は 2^-10 px を超えるずれで留め直す (従来は 1px 超)。平行移動は 2D で装置の画素の整数の
48
+ ままなので、`perspective: none` との画素の同一性は変わらない。揃える関数は `snapToDevicePixelGrid(cssPx, ratio, edge)`
49
+ (`edge` は `"none"` / `"start"` / `"end"`) として新しいモジュール `devicePixelGrid.ts` へ移した (モジュールレベル。
50
+ バレル非公開)。
51
+ - **層の中の行も整数の画素に描かれるという説明を正した**: 行ラッパーの docstring と README は「層の中の端数のレイアウト
52
+ 位置は描画が画素へ揃える」としていたが、Chromium は角の丸い枠線・輪・輪郭を層の中の正確な位置で滲ませて描く (比 1.25 で、
53
+ 描画のアンカーから 7814px の行の 1px の枠線が 75% と 50% の被覆で描かれた)。揃えるのは層の平行移動だけで、行が装置の画素
54
+ の整数から始まるのは、その上の行の高さの和が装置 px の整数のときだけ。README に、そのためのホストの契約 (行の高さを、
55
+ L × 比 が整数になる格子 L の倍数にする。比が 4 分の 1 刻みなら L = 4px) を書いた。
56
+ - **スクロールバーのつまみを `top` で動かしていた**: つまみの器を `top` (横のバーは `left`) で動かしていたため、描画の窓を
57
+ 変えないスクロールの 1 段ごとに、文書の根からのレイアウトが 1 回足されていた (Chromium で、描画の窓を変えない 40px の
58
+ 60 段が、`top` で動かすと Layout 60 回・13.9ms、つまみを止めると 30 回・9.1ms)。器は `top: 0; left: 0` に置いたまま、
59
+ 行ラッパーと同じく描くウィンドウの装置の画素の格子へ最も近い点で揃えた `translateY` (横は `translateX`) で動かし、遷移は
60
+ 付けない。描画の窓を変えない 1 段がつまみの器へ書くのは `transform` と `aria-valuenow` だけになった。オーバーレイの props の
61
+ `thumbPosition` / `thumbCenter` は揃える前の厳密な値のまま。ドラッグとホバーの当たり判定は器の `top` を読まない (位置は
62
+ 状態から、当たりは `getBoundingClientRect` から求め、平行移動を含む)。ウィンドウを持たない文書へ描くと、行ラッパーと同じく
63
+ つまみの取り付けで `Error` を投げる (同じ確定で両方が投げると React はまとめた `AggregateError` を投げる)。装置の画素比の
64
+ 監視 (`(resolution: <比>dppx)` のメディアクエリ) はウィンドウごとに 1 つで、行ラッパーとつまみが共有する。
65
+ - **テスト**: `ScrollBar.spec.tsx` (つまみの位置を平行移動から読み、`top` / `left` が 0 であることを確かめる 2 件の改修)、
66
+ `externalScrollBridge.spec.tsx` (`scrollTo` が 1px 未満の差を積み上げないことの対比を整数の位置から始める 1 件の改修。
67
+ `scrollTo` は floor した位置へ厳密に着地するので、端数の位置 0.75 から 1.0 を求めると 1 へ 1 度だけ跳ぶ)、
68
+ `reducedMotion.spec.tsx` (部品をパッケージ自身のクラスで引く箇所に、規則のセレクタが名指すクラスそのものが検査の対象で
69
+ あるという例外の注記を付け、サークルの中かどうかの判定・グリッドのサークルとその器・セルは `data-*` で引く)、E2E
70
+ `scrollbar-handle-speed.spec.ts` (つまみの厳密な位置はデモのつまみのオーバーレイの印から読んで等速の CV を変えずに検査し、
71
+ 描かれたつまみがそれから半装置画素 + レイアウトの単位 1 つ以内であることを全サンプルで確かめる。つまみの `top` が常に 0 に
72
+ なったため、従来の `top` の読み取りでは CV が 0 になり検査が空になるところだった)。
73
+
5
74
  ## 3.8.2 (2026-10-03)
6
75
 
7
76
  ### Fixed
package/README.md CHANGED
@@ -139,29 +139,50 @@ leaves the viewport, or a list that ignores the wheel entirely).
139
139
 
140
140
  The rows move inside one wrapper that is translated with `transform: translateY(…)` on its own compositor
141
141
  layer (`will-change: transform`). That translate is snapped to the device-pixel grid of the window the list
142
- is painted in, `Math.round(offset × devicePixelRatio) / devicePixelRatio`: whole CSS pixels at a ratio of 1,
143
- steps of 0.8 px at 1.25, 2/3 px at 1.5 and 0.5 px at 2. Fractional scroll positions (trackpad and inertia
144
- deltas, centred alignment, fractional row heights or insets) therefore never move the layer by a fraction of
145
- a device pixel. A layer moved by a fraction is resampled as a whole: 2 px borders, outlines, focus rings and
146
- the gaps between them smear across neighbouring pixel rows, and text blurs.
142
+ is painted in: whole CSS pixels at a ratio of 1, steps of 0.8 px at 1.25, 2/3 px at 1.5 and 0.5 px at 2.
143
+ Fractional scroll positions (trackpad and inertia deltas, centred alignment, fractional row heights or
144
+ insets) therefore never move the layer by a fraction of a device pixel. A layer moved by a fraction is
145
+ resampled as a whole: 2 px borders, outlines, focus rings and the gaps between them smear across
146
+ neighbouring pixel rows, and text blurs.
147
147
 
148
+ The snap keeps the edge the list is aligned to, so aligned content never loses part of its gutter to the
149
+ rounding:
150
+
151
+ | Where the list rests | Grid point | Result |
152
+ | --- | --- | --- |
153
+ | At position 0, or where the last `scrollToIndex` with `align: "top"` (the default, with or without `offset`) landed | At or after the exact translate: `Math.ceil(offset × ratio) / ratio` | The content never moves toward the viewport start, so the first row (or the aligned row, less its `offset`) never crosses it |
154
+ | At the maximum position, or where the last `scrollToIndex` with `align: "bottom"` landed | At or before the exact translate: `Math.floor(offset × ratio) / ratio` | The content never moves toward the viewport end, so the last row and the bottom inset (or the aligned row) never cross it |
155
+ | Anywhere else (a centred row, a scroll the user or the host started) | The nearest one: `Math.round(offset × ratio) / ratio` | At most half a device pixel either way |
156
+
157
+ - `scrollToIndex` lands exactly on the aligned position (clamped to the content), and a size change that
158
+ moves a pending alignment by more than 2^-10 px pins it again, so an aligned edge holds through fractional
159
+ row heights. A remembered alignment moves with the layout-shift compensation and the drift
160
+ correction that keep the aligned row in place, and any other scroll forgets it. Position 0 wins over the
161
+ maximum position (a list that does not scroll stays top-aligned), and both win over a remembered
162
+ alignment. A position within 2^-10 px of one of these counts as that position.
163
+ - Only the wrapper translate is snapped. The rows inside it paint at their exact layout positions, where
164
+ rounded borders, rings and outlines are anti-aliased, so a row starts on a whole device pixel only when the
165
+ sum of the row heights above it is a whole number of device pixels. A host that needs crisp row edges keeps
166
+ every row height on a lattice L with L × ratio a whole number: L = 4 px gives whole device pixels at every
167
+ ratio in quarter steps (1, 1.25, 1.5, 1.75, 2, …). Other zoom levels need other lattices (10 px at 1.1);
168
+ at a ratio the host's lattice does not fit, the rows keep their fractional positions.
169
+ - Every position the component reports (`onScroll`, `onRangeChange`, `onScrollAdjust`, `getScrollPosition()`,
170
+ `getScrollAnchor()`) stays exact. The rows are drawn less than one device pixel away from the exact scroll
171
+ position (at most half a device pixel where no edge is kept). `VirtualGrid` rows are drawn by the same
172
+ wrapper, so they are snapped vertically as well.
148
173
  - The ratio comes from the window of the list's own document, and it is read while the wrapper renders.
149
174
  Before the wrapper is attached, the component expects the window of the page that runs React, and the
150
175
  attach confirms it. A list rendered into its own window's document is therefore snapped in its first
151
176
  commit, with no extra render. A list rendered into an iframe or a second window switches to that
152
177
  window's ratio when the wrapper attaches, with one extra synchronous render before the browser paints.
153
178
  The ratio is read again whenever it changes (browser zoom, or moving the window to a screen of another
154
- density), through a `(resolution: <ratio>dppx)` media query.
155
- - Only the wrapper translate is snapped. Row positions inside it keep their exact values (the browser paints
156
- fractional positions inside a layer on whole pixels already), and every position the component reports
157
- (`onScroll`, `onRangeChange`, `getScrollPosition()`, `getScrollAnchor()`) stays exact. The rows are drawn
158
- at most half a device pixel away from the exact scroll position. `VirtualGrid` rows are drawn by the same
159
- wrapper, so they are snapped vertically as well.
179
+ density), through one `(resolution: <ratio>dppx)` media query per window, shared by the wrappers and the
180
+ scrollbar thumbs painted there.
160
181
  - Server-rendered HTML carries the exact offset, since there is no screen on the server. Hydration renders
161
182
  the same exact offset, so it reports no mismatch, and the snapped offset replaces it right after hydration.
162
183
  - Rendering into a document without a window (for example one made with
163
- `document.implementation.createHTMLDocument()`) throws when the wrapper attaches: nothing is painted there,
164
- so there is no ratio to snap to.
184
+ `document.implementation.createHTMLDocument()`) throws when the wrapper or a scrollbar thumb attaches:
185
+ nothing is painted there, so there is no ratio to snap to.
165
186
 
166
187
  ## What a scroll step paints
167
188
 
@@ -191,6 +212,51 @@ Measured in Chromium 148 (1280×800, ratio 1, overscan 15, rows whose text ends
191
212
  outline renders on every side, at ratios 1, 1.25, 1.5, 1.75, 2 and 3, light and dark, and at 8 offsets
192
213
  (0 to 1,572,864.5 px and the end of a 20,000-row list), as a full border pixel, floor(2 × ratio) ring
193
214
  pixels, at most one blended pixel, floor(2 × ratio) gap pixels and floor(2 × ratio) outline pixels.
215
+ - The scrollbar thumb moves the same way. Its wrapper stays at `top: 0` (`left: 0` on a horizontal bar) and is
216
+ moved by a 2D translate (`translateY`, `translateX` on a horizontal bar) snapped to the same device-pixel
217
+ grid, with no transition, so a step writes only that `transform` and the slider's `aria-valuenow`, and the
218
+ thumb adds no layout. A thumb positioned by `top` adds a layout from the document root to every step: in
219
+ Chromium, 60 steps of 40 px that kept the rendering window (200,000 rows of 160 px inside nested flex
220
+ columns beside a 600-row sidebar) ran 60 layouts in 13.9 ms with the thumb moved by `top`, and 30 layouts in
221
+ 9.1 ms with the thumb held still. `thumbPosition` in the `renderThumbOverlay` props stays the exact,
222
+ unsnapped offset.
223
+
224
+ ## Position changes the list makes on its own
225
+
226
+ `onScroll` and `onRangeChange` report every position, throttled (`callbackThrottleMs`) and one animation frame
227
+ late. Four moves are made by the list itself, without a scroll from the user or the host, and `onScrollAdjust`
228
+ reports each of them synchronously and unthrottled once it is complete: `getScrollPosition()` and
229
+ `getScrollAnchor()` read inside the callback already return the moved position and the new row heights.
230
+
231
+ | `cause` | The move | When it runs |
232
+ | --- | --- | --- |
233
+ | `"item-resize"` | `updateItemSize` changed a row above the first visible row, and the position moved by the same delta, so the visible rows stay where they were | Inside `updateItemSize`, before it returns |
234
+ | `"reconciliation"` | A render found a new `getItemHeight` value for a rendered row above the first visible row, and the position moved by the same delta | In the microtask after that render |
235
+ | `"drift"` | After a size change (content, viewport, item count or insets), the pending alignment of the last `scrollToIndex` (or of `initialScrollAnchor`) was pinned again | In an effect after the commit |
236
+ | `"re-issue"` | A compensation that the pane clamped against the previous content size (several compensations in one batch near the end of the list) was issued again once the new size had committed | In an effect after the commit |
237
+
238
+ The argument is `{ position, delta, cause }` (`VirtualScrollAdjustment`): `position` is the **logical**
239
+ position after the move, what `getScrollPosition()` returns, and `delta` is the change the move applied to it
240
+ (never 0). Scrolls the user or the host start (wheel, drags and their inertia, the scrollbar, keyboard row
241
+ navigation, `scrollTo` / `scrollBy` / `scrollToIndex` / `applyWheel`) are not reported here, and neither is a
242
+ change that leaves `getScrollPosition()` where it was (a top-inset change keeps the logical position).
243
+
244
+ A host that keeps its own scroll anchor by item identity, re-finding the first visible item by key after a
245
+ list change (rows inserted or removed above it), records that anchor from `onRangeChange` and after its own
246
+ scrolls. It must also re-record it from `onScrollAdjust`. Otherwise a list change committed before the next
247
+ range report restores a position the list has already moved, and the view shifts by the compensation or
248
+ lands on a neighbouring item.
249
+
250
+ ```tsx
251
+ const recordAnchor = () => {
252
+ const anchor = listRef.current?.getScrollAnchor()
253
+ if (anchor) anchorRef.current = { key: keyOf(anchor.index), offsetPx: anchor.offsetPx }
254
+ }
255
+
256
+ <VirtualScroll ref={listRef} onRangeChange={recordAnchor} onScrollAdjust={recordAnchor} {...props}>
257
+ {renderRow}
258
+ </VirtualScroll>
259
+ ```
194
260
 
195
261
  ## Reduced motion
196
262
 
@@ -718,11 +784,12 @@ exists in the DOM.
718
784
  | `getItemHeight` | `(index: number) => number` | ✅ | Function to get item height |
719
785
  | `viewportSize` | `number` | ❌ | Height of the visible band. **Omit it** — the component then measures its host with a `ResizeObserver`. Pass it only when you know the band by calculation rather than measurement (e.g. a dropdown sized from row count × row height). See [Sizing](#sizing) |
720
786
  | `overscanCount` | `number` | ❌ | Number of items to render outside viewport (default: 15) |
721
- | `className` | `string` | ❌ | CSS class name. Lands on the scroll root (`.aqvs-scroll-pane`) — see [Custom Scrollbar Styling](#custom-scrollbar-styling) |
787
+ | `className` | `string` | ❌ | CSS class name. Lands on the scroll root (`.aqvs-scroll-pane`) — see [Custom styling](#custom-styling) |
722
788
  | `getItemKey` | `(index: number) => React.Key` | ❌ | Stable React key per index (defaults to the index) |
723
789
  | `testId` | `string` | ❌ | Emitted as `data-testid` on the scroll root. DOM hooks should use `data-*`, never class selectors |
724
790
  | `onScroll` | `(position: number, totalHeight: number) => void` | ❌ | Scroll event handler |
725
791
  | `onRangeChange` | `(range: VirtualScrollRange) => void` | ❌ | Range change handler |
792
+ | `onScrollAdjust` | `(adjustment: VirtualScrollAdjustment) => void` | ❌ | Called synchronously, without throttling, each time the list moves its own scroll position (layout-shift compensation, the re-pinning of a pending alignment, the second stage of a clamped compensation). `adjustment` is `{ position, delta, cause }` in **logical** px, and handle reads inside the callback already see the adjusted state. Never called for scrolls the user or the host start. See [Position changes the list makes on its own](#position-changes-the-list-makes-on-its-own) |
726
793
  | `onWheelHorizontal` | `(deltaX: number) => void` | ❌ | Opt-in horizontal delegation. Horizontal-dominant wheel / trackpad gestures (and shift+wheel) are delegated to this handler so the parent can implement horizontal scrolling. ⚠️ **Keyboard deltas arrive here too** when `horizontalKeyInputs` is set. When omitted, horizontal gestures bypass vertical scrolling so parent native scrolling works. See [Horizontal Scrolling](#horizontal-scrolling). |
727
794
  | `onPanHorizontal` | `(deltaX: number) => void` | ❌ | Opt-in delegation of the horizontal component of pointer pans (touch / pen drags) — the pan twin of `onWheelHorizontal` with the same sign convention, per-move increments divided by the press-time x-axis ancestor scale (3.2.0). When set, purely horizontal pans also activate the drag. Same seal family as `onWheelHorizontal` — wrappers sealing the horizontal axis must `Omit` this name too. |
728
795
  | `horizontalKeyInputs` | `readonly "arrow"[]` | ❌ | Keyboard gestures that emit a horizontal delta through `onWheelHorizontal` (default: `[]`, so row-level arrow handling is never stolen). `Shift + ←/→` is never consumed. Fires only when the row itself is the event target. ⚠️ Requires `behaviorOptions.enableKeyboardNavigation` (default `true`) — with it off the rows have no key handler at all and this prop does nothing (the package warns) |
@@ -776,7 +843,7 @@ exists in the DOM.
776
843
  | `scrollBy` | `(delta: number) => number` | Scroll **by a delta** with the pane's own wheel semantics (float accumulation, no anchor). Use this — not `scrollTo` — to bridge continuous input from outside the pane. Returns the applied **logical** position |
777
844
  | `applyWheel` | `(event: WheelEvent) => boolean` | Apply one wheel event with the pane's own rules; returns whether it was consumed. The single entry point for wheel input originating outside the pane — normally reached through `useWheelBridge` |
778
845
  | *(ScrollPaneHandle only)* `scrollTo` | `(pos, dimsOverride?)` | The pane-level jump. `dimsOverride` is an explicit clamp-space for callers that know **fresher-than-committed** dims (the layout-shift compensation passes `committed + delta`). Do not pass it casually — a wrong override clamps against dims that are not on screen |
779
- | `scrollToIndex` | `(index: number, options?: { align?: "top" \| "bottom" \| "center"; offset?: number }) => void` | Scroll to specific item index with optional alignment and offset |
846
+ | `scrollToIndex` | `(index: number, options?: { align?: "top" \| "bottom" \| "center"; offset?: number }) => void` | Scroll to specific item index with optional alignment and offset. Lands exactly on the aligned position (clamped to the content); a `"top"` (default) or `"bottom"` alignment is remembered there, so the device-pixel snap keeps that edge — see [Device-pixel snapping](#device-pixel-snapping) |
780
847
  | `getScrollPosition` | `() => number` | Get current **logical** scroll position (2.0.0; `-1` when the pane is not connected) |
781
848
  | `getContentSize` | `() => number` | Get total content size (insets included). Returns the `-1` sentinel while the pane is unconnected |
782
849
  | `getViewportSize` | `() => number` | Get viewport size. Returns the `-1` sentinel while the pane is unconnected |
@@ -785,7 +852,7 @@ exists in the DOM.
785
852
  | `getScrollAnchor` | `() => { index: number; offsetPx: number } \| null` | Capture the current top visible row anchor for exact restore via `initialScrollAnchor` (`null` when `itemCount` is 0) |
786
853
  | `getFenwickTreeTotalHeight` | `() => number` | Total content height managed by the Fenwick tree (insets excluded) |
787
854
  | `getFenwickSize` | `() => number` | Item count managed by the Fenwick tree |
788
- | `updateItemSize` | `(index: number, size: number) => void` | Manually update one item's size (with layout-shift compensation). `getItemHeight(index)` must return the same value afterwards |
855
+ | `updateItemSize` | `(index: number, size: number) => void` | Manually update one item's size (with layout-shift compensation: a row above the first visible row moves the position by the same delta before this returns, reported through `onScrollAdjust` as `"item-resize"`). `getItemHeight(index)` must return the same value afterwards |
789
856
 
790
857
  **Coordinate system (2.0.0)**: every position the handle and callbacks accept or return is
791
858
  **logical** (content px, insets excluded) — `onScroll` / `onRangeChange` / `initialScroll*` /
@@ -902,10 +969,11 @@ function AdvancedExample() {
902
969
  }
903
970
  ```
904
971
 
905
- ### Custom Scrollbar Styling
972
+ ### Custom styling
906
973
 
907
- `className` lands on the **scroll root** (`.aqvs-scroll-pane`). The bar, its track, thumb, arrow buttons
908
- and the scroll-to-edge pills are all descendants of that element, so one wrapper class reaches every part:
974
+ `className` lands on the **scroll root** (`.aqvs-scroll-pane`). The rows, the bar with its track, thumb and
975
+ arrow buttons, the scroll-to-edge pills and the empty-state message are all descendants of that element, so
976
+ one wrapper class reaches every part:
909
977
 
910
978
  ```tsx
911
979
  <VirtualScroll
@@ -935,6 +1003,9 @@ and the scroll-to-edge pills are all descendants of that element, so one wrapper
935
1003
  .custom-virtual-scroll .aqvs-scrollbar-arrow-button:enabled:hover { background-color: #cfe8f7; }
936
1004
  .custom-virtual-scroll .aqvs-scroll-to-edge-button { background-color: rgba(0, 122, 204, 0.85); }
937
1005
  .custom-virtual-scroll .aqvs-scroll-to-edge-button:hover { background-color: rgba(0, 92, 153, 0.9); }
1006
+
1007
+ /* The empty state (itemCount 0). Restate the colour on a dark background: the default #6b7280 reaches 4.5:1 only on a light one. */
1008
+ .custom-virtual-scroll .aqvs-no-items-text { color: #94a3b8; padding-block: 1rem; }
938
1009
  ```
939
1010
 
940
1011
  The packaged rules live in `@layer components` in **both** artifacts, so an ordinary (unlayered) rule in
@@ -966,11 +1037,28 @@ Three consequences are worth knowing:
966
1037
  users. Forcing `opacity: 1` on the hidden overlay therefore paints a pill that cannot be clicked or focused.
967
1038
  Scope such rules to `[data-visible="true"]` as well.
968
1039
 
969
- The parts you are most likely to target: `.aqvs-scroll-pane` (root), `.aqvs-scroll-pane-content` (row
970
- viewport), `.aqvs-scrollbar` (+ `-vertical` / `-horizontal`), `.aqvs-scrollbar-track`,
971
- `.aqvs-scrollbar-thumb-wrapper`, `.aqvs-scrollbar-thumb` (+ `-vertical` / `-horizontal`),
972
- `.aqvs-scrollbar-arrow-button`, `.aqvs-scroll-to-edge-overlay` / `-button`, `.aqvs-tap-scroll-circle`,
973
- `.aqvs-item-container`.
1040
+ The styling hooks are the classes below. Each one is rendered on the part it names, and renaming or
1041
+ removing one is a breaking change:
1042
+
1043
+ | Class | Part |
1044
+ | --- | --- |
1045
+ | `.aqvs-scroll-pane` | The scroll root, where `className` lands |
1046
+ | `.aqvs-scroll-pane-content` | The row viewport |
1047
+ | `.aqvs-item-container` | Each row's container |
1048
+ | `.aqvs-no-items-container` | The empty state (`itemCount` 0): the box at the top of the viewport |
1049
+ | `.aqvs-no-items-text` | The empty state's message (`labels.noItems`), centred, `color: #6b7280` |
1050
+ | `.aqvs-scrollbar` | The bar (`role="scrollbar"`) |
1051
+ | `.aqvs-scrollbar-vertical` | A vertical bar |
1052
+ | `.aqvs-scrollbar-horizontal` | A horizontal bar |
1053
+ | `.aqvs-scrollbar-track` | The track behind the thumb |
1054
+ | `.aqvs-scrollbar-thumb-wrapper` | The thumb's wrapper (`role="slider"`); the package positions it with an inline translate |
1055
+ | `.aqvs-scrollbar-thumb` | The thumb, with `data-thumb-state` |
1056
+ | `.aqvs-scrollbar-thumb-vertical` | The thumb of a vertical bar |
1057
+ | `.aqvs-scrollbar-thumb-horizontal` | The thumb of a horizontal bar |
1058
+ | `.aqvs-scrollbar-arrow-button` | The arrow buttons |
1059
+ | `.aqvs-scroll-to-edge-overlay` | The scroll-to-edge pills' overlay (`enableScrollToTopBottomButtons`) |
1060
+ | `.aqvs-scroll-to-edge-button` | A scroll-to-edge pill |
1061
+ | `.aqvs-tap-scroll-circle` | The tap scroll circle |
974
1062
 
975
1063
  ### Tap Scroll Circle Configuration
976
1064
 
@@ -296,9 +296,15 @@ export declare const computeTapScrollSpeed: ({ distance, maxDistance, viewportSi
296
296
  */
297
297
  export declare const computeAutoTapScrollMaxSpeedMultiplier: (itemCount?: number) => number;
298
298
  /**
299
- * A custom scrollbar component.
299
+ * A custom scrollbar component. The thumb wrapper stays at the start of the track (`top: 0; left: 0`) and moves along it
300
+ * by a 2D translate snapped to the device-pixel grid of the window that paints it, so a scroll step rewrites only that
301
+ * transform and runs no layout. `thumbPosition` in the overlay props stays the exact, unsnapped offset. Rendering into a
302
+ * document without a window throws when the thumb attaches (there is no device-pixel ratio to snap to).
300
303
  *
301
- * カスタムスクロールバーコンポーネント。
304
+ * カスタムスクロールバーコンポーネント。つまみの器はトラックの始端 (`top: 0; left: 0`) に置いたまま、描くウィンドウの装置の
305
+ * 画素の格子へ揃えた 2D の平行移動でトラックに沿って動くので、スクロールの 1 段が書き換えるのはその transform だけで、
306
+ * レイアウトは走らない。オーバーレイの props の `thumbPosition` は揃える前の厳密なオフセットのまま。ウィンドウを持たない
307
+ * 文書へ描くと、つまみの取り付けで例外を投げる (揃える装置の画素比が無い)。
302
308
  */
303
309
  export declare const ScrollBar: ({ contentSize, viewportSize, scrollPosition, onScroll, enableThumbDrag, enableTrackClick, enableArrowButtons, enableArrowButtonTabStops, horizontal, enableHorizontalTapCircle, stretchMainSize, scrollBarWidth, className, ariaControls, tapScrollCircleOptions, itemCount, renderThumbOverlay, visibleStartIndex, visibleEndIndex, locale, labels, }: ScrollBarProps) => import("react").JSX.Element;
304
310
  export {};
@@ -296,9 +296,15 @@ export declare const computeTapScrollSpeed: ({ distance, maxDistance, viewportSi
296
296
  */
297
297
  export declare const computeAutoTapScrollMaxSpeedMultiplier: (itemCount?: number) => number;
298
298
  /**
299
- * A custom scrollbar component.
299
+ * A custom scrollbar component. The thumb wrapper stays at the start of the track (`top: 0; left: 0`) and moves along it
300
+ * by a 2D translate snapped to the device-pixel grid of the window that paints it, so a scroll step rewrites only that
301
+ * transform and runs no layout. `thumbPosition` in the overlay props stays the exact, unsnapped offset. Rendering into a
302
+ * document without a window throws when the thumb attaches (there is no device-pixel ratio to snap to).
300
303
  *
301
- * カスタムスクロールバーコンポーネント。
304
+ * カスタムスクロールバーコンポーネント。つまみの器はトラックの始端 (`top: 0; left: 0`) に置いたまま、描くウィンドウの装置の
305
+ * 画素の格子へ揃えた 2D の平行移動でトラックに沿って動くので、スクロールの 1 段が書き換えるのはその transform だけで、
306
+ * レイアウトは走らない。オーバーレイの props の `thumbPosition` は揃える前の厳密なオフセットのまま。ウィンドウを持たない
307
+ * 文書へ描くと、つまみの取り付けで例外を投げる (揃える装置の画素比が無い)。
302
308
  */
303
309
  export declare const ScrollBar: ({ contentSize, viewportSize, scrollPosition, onScroll, enableThumbDrag, enableTrackClick, enableArrowButtons, enableArrowButtonTabStops, horizontal, enableHorizontalTapCircle, stretchMainSize, scrollBarWidth, className, ariaControls, tapScrollCircleOptions, itemCount, renderThumbOverlay, visibleStartIndex, visibleEndIndex, locale, labels, }: ScrollBarProps) => import("react").JSX.Element;
304
310
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"ScrollBar.d.ts","sourceRoot":"","sources":["../src/ScrollBar.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ScrollBar.d.ts","sourceRoot":"","sources":["../src/ScrollBar.tsx"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { default as React, ReactNode } from 'react';
2
+ import { DevicePixelSnapEdge } from './devicePixelGrid.cjs';
2
3
  import { VirtualScrollLabelOverrides, VirtualScrollLocale } from './labels.cjs';
3
4
  import { ScrollPaneProps } from './ScrollPane.cjs';
4
5
  import { useFenwickMapTree } from './useFenwickMapTree.cjs';
@@ -21,6 +22,42 @@ export type VirtualScrollRange = {
21
22
  /** Total height of the scroll content / スクロールコンテンツの総高さ */
22
23
  totalHeight: number;
23
24
  };
25
+ /**
26
+ * Why VirtualScroll moved its scroll position on its own (see `VirtualScrollProps["onScrollAdjust"]`).
27
+ *
28
+ * - `"item-resize"`: `updateItemSize` changed the height of a row above the first visible row, and the position moved by
29
+ * the same delta, so the visible rows stay where they were.
30
+ * - `"reconciliation"`: a render found that `getItemHeight` returns a new height for a rendered row above the first
31
+ * visible row; the height reconciliation that follows the render (a microtask) moved the position by the same delta.
32
+ * - `"drift"`: after a size change (content, viewport, item count or insets) the pending alignment of the last
33
+ * `scrollToIndex` (or of `initialScrollAnchor`) was pinned again, in an effect after the commit.
34
+ * - `"re-issue"`: a compensation that the pane clamped against the previous content size was issued again once the new
35
+ * content size had committed, in an effect after the commit.
36
+ *
37
+ * VirtualScroll が自分でスクロール位置を動かした理由 (`VirtualScrollProps["onScrollAdjust"]` を参照)。
38
+ *
39
+ * - `"item-resize"`: `updateItemSize` が先頭の可視行より上の行の高さを変え、位置を同じ差だけ動かした (見えている行はその場に
40
+ * 留まる)。
41
+ * - `"reconciliation"`: 描画が、描いた行のうち先頭の可視行より上の行について `getItemHeight` の新しい高さを見つけ、描画の後の
42
+ * 高さの照合 (マイクロタスク) が位置を同じ差だけ動かした。
43
+ * - `"drift"`: 寸法の変化 (中身・ビューポート・件数・インセット) の後、最後の `scrollToIndex` (または `initialScrollAnchor`)
44
+ * の保留中の揃えを、確定の後の effect で留め直した。
45
+ * - `"re-issue"`: ペインが前の中身の寸法でクランプした補正を、新しい中身の寸法が確定した後の effect でもう一度発行した。
46
+ */
47
+ export type VirtualScrollAdjustmentCause = "item-resize" | "reconciliation" | "drift" | "re-issue";
48
+ /**
49
+ * One position change VirtualScroll made on its own (the argument of `VirtualScrollProps["onScrollAdjust"]`).
50
+ *
51
+ * VirtualScroll が自分で行った 1 回の位置の変化 (`VirtualScrollProps["onScrollAdjust"]` の引数)。
52
+ */
53
+ export type VirtualScrollAdjustment = {
54
+ /** The LOGICAL scroll position after the change — what `getScrollPosition()` returns at that moment / 変化の後の論理スクロール位置 (その時点の `getScrollPosition()` の値) */
55
+ readonly position: number;
56
+ /** The applied change in LOGICAL px: `position` minus the position before the change; never 0 / 適用した変化 (論理 px)。`position` から変化の前の位置を引いた値で、0 にはならない */
57
+ readonly delta: number;
58
+ /** Why the position moved / 位置が動いた理由 */
59
+ readonly cause: VirtualScrollAdjustmentCause;
60
+ };
24
61
  /**
25
62
  * Imperative handle of VirtualScroll. Every position it accepts or returns is in the
26
63
  * LOGICAL coordinate space (content px, insets excluded) — the same space as onScroll /
@@ -94,7 +131,15 @@ export type VirtualScrollHandle = {
94
131
  getContentSize: () => number;
95
132
  /** Viewport size / ビューポートの高さ。未接続時は -1 */
96
133
  getViewportSize: () => number;
97
- /** Scrolls to a specific item index / 指定したアイテムインデックスへスクロール */
134
+ /**
135
+ * Scrolls to a specific item index, landing exactly on the aligned position (clamped to the content). A `"top"`
136
+ * (default) or `"bottom"` alignment is remembered at that position, so the device-pixel snap of the items wrapper
137
+ * keeps the aligned edge there (see the README's device-pixel snapping section).
138
+ *
139
+ * 指定したアイテムインデックスへスクロールする処理。揃えた位置 (中身の範囲へクランプ) へ厳密に着地する。`"top"` (既定) と
140
+ * `"bottom"` の揃えはその位置で覚えるので、そこでは行ラッパーの装置の画素への揃えが揃えた端を守る (README の装置の画素への
141
+ * 揃えの節を参照)。
142
+ */
98
143
  scrollToIndex: (index: number, options?: {
99
144
  align?: "top" | "bottom" | "center";
100
145
  offset?: number;
@@ -123,10 +168,15 @@ export type VirtualScrollHandle = {
123
168
  * `getItemHeight(index)` must return the same `size`; `getItemHeight` is the source of truth,
124
169
  * so if it keeps returning the old value, rows inside the current rendering window (including
125
170
  * overscan) are reverted to the `getItemHeight` value by height reconciliation on the next render.
171
+ * When the item lies above the first visible row, the scroll position moves by the size change
172
+ * before this returns (layout-shift compensation), and `onScrollAdjust` reports it with the cause
173
+ * `"item-resize"`.
126
174
  *
127
175
  * 特定のアイテムのサイズを手動で更新。契約: 呼び出し後は `getItemHeight(index)` も同じ値を
128
176
  * 返すこと。`getItemHeight` が正であるため、旧値を返し続けると描画ウィンドウ (オーバースキャン
129
- * 含む) 内の行は次レンダーの高さ照合で `getItemHeight` の値へ巻き戻る。
177
+ * 含む) 内の行は次レンダーの高さ照合で `getItemHeight` の値へ巻き戻る。アイテムが先頭の可視行より
178
+ * 上にあるときは、戻る前にスクロール位置をサイズの変化だけ動かし (レイアウトシフトの補正)、
179
+ * `onScrollAdjust` が理由 `"item-resize"` で知らせる。
130
180
  */
131
181
  updateItemSize: (index: number, size: number) => void;
132
182
  };
@@ -329,6 +379,33 @@ export type VirtualScrollProps<T> = {
329
379
  testId?: string;
330
380
  onScroll?: (scrollPosition: number, totalHeight: number) => void;
331
381
  onRangeChange?: (range: VirtualScrollRange) => void;
382
+ /**
383
+ * Called synchronously, without throttling, each time VirtualScroll moves its scroll position on its own: a
384
+ * layout-shift compensation (`"item-resize"`, `"reconciliation"`), the re-pinning of a pending alignment
385
+ * (`"drift"`) or the second stage of a clamped compensation (`"re-issue"`) — see `VirtualScrollAdjustmentCause`.
386
+ * It runs after the change is complete, so `getScrollPosition()` and `getScrollAnchor()` read inside it already
387
+ * see the adjusted position and row heights. It never runs for scrolls that the user or the host start (wheel,
388
+ * drag, scrollbar, inertia, keyboard row navigation, `scrollTo` / `scrollBy` / `scrollToIndex` / `applyWheel`),
389
+ * nor for a change that leaves `getScrollPosition()` where it was.
390
+ *
391
+ * `onScroll` and `onRangeChange` report every position, but throttled and one frame later. A host that keeps its
392
+ * own scroll anchor by item identity (re-finding the first visible item by key after a list change) records that
393
+ * anchor from the range report and from its own scrolls; it must also record it from here, or a list change
394
+ * committed before the next range report restores a position VirtualScroll has already moved.
395
+ *
396
+ * VirtualScroll が自分でスクロール位置を動かすたびに、間引かず同期で呼ぶ関数。レイアウトシフトの補正
397
+ * (`"item-resize"`・`"reconciliation"`)、保留中の揃えの留め直し (`"drift"`)、クランプされた補正の二段目
398
+ * (`"re-issue"`) が対象 (`VirtualScrollAdjustmentCause` を参照)。変化を終えてから呼ぶので、中で読む
399
+ * `getScrollPosition()` と `getScrollAnchor()` は動かした後の位置と行の高さを返す。利用者やホストが始めた
400
+ * スクロール (ホイール・ドラッグ・スクロールバー・慣性・行のキーボード移動・`scrollTo` / `scrollBy` /
401
+ * `scrollToIndex` / `applyWheel`) と、`getScrollPosition()` を変えない変化では呼ばない。
402
+ *
403
+ * `onScroll` と `onRangeChange` はどの位置も知らせるが、間引いたうえで 1 フレーム遅れる。項目の同一性で自前の
404
+ * スクロールの錨を持つホスト (一覧の変化の後に先頭の可視項目をキーで探し直す) は、範囲の知らせと自分のスクロールで
405
+ * 錨を記録するが、ここでも記録すること。さもないと、次の範囲の知らせより前に確定した一覧の変化が、VirtualScroll が
406
+ * 既に動かした位置を巻き戻す。
407
+ */
408
+ onScrollAdjust?: (adjustment: VirtualScrollAdjustment) => void;
332
409
  /**
333
410
  * Opt-in `aria-live` region announcing the visible range to assistive technology (default:
334
411
  * none rendered). Virtualization removes off-screen rows from the DOM, so a screen-reader
@@ -534,26 +611,52 @@ export declare const MAX_RENDERED_ITEMS = 2000;
534
611
  */
535
612
  export declare const ANCHOR_REBASE_DISTANCE = 1048576;
536
613
  /**
537
- * Snaps a CSS-px offset to the device-pixel grid of a window: the nearest offset whose device-px value is a
538
- * whole number, `Math.round(cssPx × ratio) / ratio` (an exact half rounds toward +∞, as `Math.round` does). The items-wrapper translate goes through it: a composited layer moved by a fraction of a
539
- * device pixel is resampled as a whole (2-px outlines, gaps and rings smear across neighbouring device rows and
540
- * text blurs), while fractional layout positions inside the layer are painted on whole pixels already.
541
- * Idempotent: a snapped value snaps to itself. A non-finite `cssPx` propagates (`NaN` in, `NaN` out).
542
- * Module-level export (NOT in the package barrel).
614
+ * The viewport edge VirtualScroll aligned a row to, and the pane position at which that alignment holds. Module-level
615
+ * export (NOT in the package barrel), the parameter type of `resolveItemsWrapperSnapEdge`.
616
+ *
617
+ * VirtualScroll が行を揃えた表示域の端と、その揃えが成り立つペイン位置。モジュールレベル export (バレル非公開)。
618
+ * `resolveItemsWrapperSnapEdge` の引数の型。
619
+ */
620
+ export type AlignedEdge = {
621
+ /** `"start"` for a top alignment, `"end"` for a bottom alignment / 上端揃えは `"start"`、下端揃えは `"end"` */
622
+ readonly edge: "start" | "end";
623
+ /** Pane position (PANE coordinates) where the aligned row sits at that edge / 揃えた行がその端にあるペイン位置 (ペイン座標) */
624
+ readonly panePosition: number;
625
+ };
626
+ /**
627
+ * Chooses the edge the items-wrapper translate keeps when it is snapped to the device-pixel grid
628
+ * (`snapToDevicePixelGrid`), so that aligned content never loses part of its edge gutter to the snap:
629
+ *
630
+ * - `"start"` while the pane rests at position 0: the first row and the top inset keep their place.
631
+ * - `"end"` while the pane rests at its maximum position: the last row and the bottom inset keep theirs.
632
+ * - The edge of the remembered alignment while the pane is at the position where it holds: a row revealed by
633
+ * `scrollToIndex` with `align: "top"` (or the default) or `align: "bottom"`, kept through layout-shift compensation and
634
+ * drift correction.
635
+ * - `"none"` (nearest) everywhere else.
636
+ *
637
+ * Position 0 wins over the maximum position (a list that does not scroll stays top-aligned), and both win over a
638
+ * remembered alignment, since a clamp means that alignment was not reached. A position within `EDGE_POSITION_TOLERANCE`
639
+ * of one of these counts as that position. Module-level export (NOT in the package barrel).
640
+ *
641
+ * 行ラッパーの平行移動を装置の画素の格子へ揃えるとき (`snapToDevicePixelGrid`) に守る端を選ぶ処理。揃えた中身の端の余白を
642
+ * 丸めが削らないようにする。
643
+ *
644
+ * - ペインが位置 0 に止まっている間は `"start"`。最初の行と上のインセットがその場に留まる。
645
+ * - ペインが最大位置に止まっている間は `"end"`。最後の行と下のインセットがその場に留まる。
646
+ * - 覚えた揃えが成り立つ位置にペインがある間は、その揃えの端。`scrollToIndex` が `align: "top"` (既定を含む) か
647
+ * `align: "bottom"` で見せた行で、レイアウトシフトの補正とドリフト補正を通して保つ。
648
+ * - それ以外は `"none"` (最も近い格子点)。
543
649
  *
544
- * CSS px のオフセットをウィンドウの装置の画素の格子へ揃える処理。装置 px で整数になる最も近いオフセット
545
- * `Math.round(cssPx × ratio) / ratio` (ちょうど半分は `Math.round` どおり +∞ 側)。
546
- * 行ラッパーの平行移動はここを通す — 合成層を装置の画素の端数だけ動かすとブラウザは層全体を再標本化し
547
- * (2px の輪郭・隙間・輪が隣の装置の行へ滲み、文字もぼける)、層の中の端数のレイアウト位置は描画が既に画素へ
548
- * 揃えるため。冪等 (揃えた値はそのまま)。有限でない `cssPx` はそのまま伝わる (`NaN` は `NaN`)。
549
- * モジュールレベル export (バレル非公開)。
650
+ * 位置 0 は最大位置に勝ち (スクロールしない一覧は上端揃えのまま)、どちらも覚えた揃えに勝つ (クランプは揃えが届かなかった
651
+ * ことを意味する)。これらの位置から `EDGE_POSITION_TOLERANCE` 以内の位置はその位置とみなす。モジュールレベル export
652
+ * (バレル非公開)。
550
653
  *
551
- * @param cssPx - Offset in CSS px; negative and fractional values included / CSS px のオフセット (負・小数を含む)
552
- * @param ratio - Device px per CSS px of the window that paints the offset (its `devicePixelRatio`); a finite number > 0 / オフセットを描くウィンドウの CSS px あたりの装置 px (そのウィンドウの `devicePixelRatio`。0 より大きい有限数)
553
- * @returns The snapped offset in CSS px / 揃えたオフセット (CSS px)
554
- * @throws {RangeError} When `ratio` is not a finite number greater than 0 / `ratio` が 0 より大きい有限数でないとき
654
+ * @param panePosition - The pane position the wrapper is rendered at (PANE coordinates) / ラッパーを描くペイン位置 (ペイン座標)
655
+ * @param maxPanePosition - The pane's maximum position: content plus insets minus the viewport / ペインの最大位置 (中身とインセットの和からビューポートを引いた値)
656
+ * @param alignedEdge - The remembered alignment, or `null` / 覚えた揃え (無ければ `null`)
657
+ * @returns The edge the snapped translate keeps / 揃えた平行移動が守る端
555
658
  */
556
- export declare const snapToDevicePixelGrid: (cssPx: number, ratio: number) => number;
659
+ export declare const resolveItemsWrapperSnapEdge: (panePosition: number, maxPanePosition: number, alignedEdge: AlignedEdge | null) => DevicePixelSnapEdge;
557
660
  /**
558
661
  * Retrieves a high-resolution timestamp when available.
559
662
  *