@aiquants/virtualscroll 3.7.1 → 3.8.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.
@@ -127,13 +127,49 @@ export type VirtualScrollHandle = {
127
127
  updateItemSize: (index: number, size: number) => void
128
128
  }
129
129
 
130
+ /**
131
+ * Scrollbar options of `VirtualScroll` (forwarded to its ScrollPane and bar) and of `VirtualGrid`
132
+ * (see `VirtualGridProps["scrollBarOptions"]` for which members reach which of the grid's two bars).
133
+ *
134
+ * `VirtualScroll` のスクロールバー設定 (ScrollPane とそのバーへ転送) であり、`VirtualGrid` の
135
+ * スクロールバー設定でもある (グリッドの 2 本のバーのどちらへ届くかは `VirtualGridProps["scrollBarOptions"]` 参照)。
136
+ */
130
137
  export type VirtualScrollScrollBarOptions = {
138
+ /** Scrollbar thickness in px (default 12). / スクロールバーの太さ (px、既定 12)。 */
131
139
  width?: number
140
+ /** Whether dragging the thumb scrolls (default `true`). / つまみのドラッグ操作を許可するかどうか (既定 `true`)。 */
132
141
  enableThumbDrag?: boolean
142
+ /** Whether pressing the track scrolls there (default `true`). / トラックの押下によるスクロールを許可するかどうか (既定 `true`)。 */
133
143
  enableTrackClick?: boolean
144
+ /** Whether the arrow buttons scroll (default `true`). / 矢印ボタンによるスクロールを許可するかどうか (既定 `true`)。 */
134
145
  enableArrowButtons?: boolean
146
+ /**
147
+ * Whether the scrollbar's arrow buttons are Tab stops (default `true`). Set it to `false` when
148
+ * the host already provides keyboard scrolling (roving row focus with Arrow / Page / Home / End):
149
+ * the arrows then only add two redundant Tab stops per bar — native scrollbars are never Tab
150
+ * stops either. `false` changes `tabIndex` alone (`-1`): the arrows stay pointer-operable and
151
+ * named, and Enter / Space still scroll when one is focused from script.
152
+ * The default stays `true` because the rows are not Tab stops, so without a host keyboard model
153
+ * the arrows are the only scrolling control a keyboard user can reach with Tab. Full contract:
154
+ * `ScrollBarProps["enableArrowButtonTabStops"]`.
155
+ * スクロールバーの矢印ボタンを Tab の止まり先にするかどうか (既定 `true`)。ホストがキーボード
156
+ * スクロールを既に提供する場合 (行のロービングフォーカスと矢印 / Page / Home / End) に `false` —
157
+ * 矢印はバー 1 本あたり冗長な Tab の止まり先を 2 つ足すだけになるため (ネイティブのスクロールバーも
158
+ * Tab の止まり先にならない)。`false` が変えるのは `tabIndex` (`-1`) だけで、ポインタ操作・
159
+ * アクセシブルネームと、スクリプトからフォーカスした矢印の Enter / Space は維持。既定が `true`
160
+ * なのは、行が Tab の止まり先ではないため、ホストのキーボードモデルが無ければ矢印がキーボード
161
+ * 利用者の Tab で届く唯一のスクロール操作部品だから。契約の全文は
162
+ * `ScrollBarProps["enableArrowButtonTabStops"]`。
163
+ */
164
+ enableArrowButtonTabStops?: boolean
165
+ /**
166
+ * Whether to show the auto-hiding Top / Bottom pills (default `false`; texts from `labels.scrollToTop` / `labels.scrollToBottom`).
167
+ * 自動で隠れる Top / Bottom ピルを表示するかどうか (既定 `false`。文言は `labels.scrollToTop` / `labels.scrollToBottom`)。
168
+ */
135
169
  enableScrollToTopBottomButtons?: boolean
170
+ /** Renderer for UI anchored near the thumb. / つまみ付近に重ねる UI のレンダラー。 */
136
171
  renderThumbOverlay?: ScrollPaneProps["renderThumbOverlay"]
172
+ /** Tap-scroll circle options (VirtualGrid: its single two-axis circle). / タップスクロールサークルの設定 (VirtualGrid では唯一の 2 軸サークル)。 */
137
173
  tapScrollCircleOptions?: ScrollPaneProps["tapScrollCircleOptions"]
138
174
  }
139
175
 
@@ -488,11 +524,11 @@ const toPanePositionWithInset = (logical: number, top: number) => (logical <= 0
488
524
  * Maximum run length of consecutive zero-height rows scanned linearly before attempting an
489
525
  * O(log n) jump to the next non-zero row via the Fenwick tree. Module-level export (NOT in the
490
526
  * package barrel) so VirtualGrid's column axis shares the SAME constant instead of duplicating
491
- * the value (the §5 no-copy rule — same treatment as ANCHOR_REBASE_DISTANCE).
527
+ * the value (a shared constant is never copied — same treatment as ANCHOR_REBASE_DISTANCE).
492
528
  *
493
529
  * 高さ 0 行の連続をこの件数まで線形走査し、超えたら Fenwick 木で次の非 0 行へ O(log n) ジャンプ
494
530
  * する閾値。モジュールレベル export (バレル非公開) — VirtualGrid 列軸が値の複製でなく同一定数を
495
- * 共有するため (§5 複製禁止規範 — ANCHOR_REBASE_DISTANCE と同じ扱い)。
531
+ * 共有するため (共有定数は複製しない — ANCHOR_REBASE_DISTANCE と同じ扱い)。
496
532
  */
497
533
  export const ZERO_HEIGHT_RUN_LIMIT = 1000
498
534
 
@@ -520,6 +556,51 @@ export const MAX_RENDERED_ITEMS = 2000
520
556
  */
521
557
  export const ANCHOR_REBASE_DISTANCE = 1_048_576 // 2^20 px — VirtualGrid の横アンカーが共有 import する (パッケージバレルへは非公開)
522
558
 
559
+ /**
560
+ * Snaps a CSS-px offset to the device-pixel grid of a window: the nearest offset whose device-px value is a
561
+ * 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
562
+ * device pixel is resampled as a whole (2-px outlines, gaps and rings smear across neighbouring device rows and
563
+ * text blurs), while fractional layout positions inside the layer are painted on whole pixels already.
564
+ * Idempotent: a snapped value snaps to itself. A non-finite `cssPx` propagates (`NaN` in, `NaN` out).
565
+ * Module-level export (NOT in the package barrel).
566
+ *
567
+ * CSS px のオフセットをウィンドウの装置の画素の格子へ揃える処理。装置 px で整数になる最も近いオフセット
568
+ * `Math.round(cssPx × ratio) / ratio` (ちょうど半分は `Math.round` どおり +∞ 側)。
569
+ * 行ラッパーの平行移動はここを通す — 合成層を装置の画素の端数だけ動かすとブラウザは層全体を再標本化し
570
+ * (2px の輪郭・隙間・輪が隣の装置の行へ滲み、文字もぼける)、層の中の端数のレイアウト位置は描画が既に画素へ
571
+ * 揃えるため。冪等 (揃えた値はそのまま)。有限でない `cssPx` はそのまま伝わる (`NaN` は `NaN`)。
572
+ * モジュールレベル export (バレル非公開)。
573
+ *
574
+ * @param cssPx - Offset in CSS px; negative and fractional values included / CSS px のオフセット (負・小数を含む)
575
+ * @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 より大きい有限数)
576
+ * @returns The snapped offset in CSS px / 揃えたオフセット (CSS px)
577
+ * @throws {RangeError} When `ratio` is not a finite number greater than 0 / `ratio` が 0 より大きい有限数でないとき
578
+ */
579
+ export const snapToDevicePixelGrid = (cssPx: number, ratio: number): number => {
580
+ if (!(Number.isFinite(ratio) && ratio > 0)) {
581
+ throw new RangeError(`[VirtualScroll] devicePixelRatio must be a finite number > 0, received ${ratio}`)
582
+ }
583
+ return Math.round(cssPx * ratio) / ratio
584
+ }
585
+
586
+ /**
587
+ * Returns the window of the document the element belongs to — the window whose screen paints it.
588
+ *
589
+ * 要素が属する文書のウィンドウ (要素を描く画面を持つウィンドウ) を返す処理。大域の `window` は使わない
590
+ * (別ウィンドウ・iframe へ描いた一覧の装置の画素比は、そのウィンドウのもの)。
591
+ *
592
+ * @param element - Element of the list / 一覧の要素
593
+ * @returns The window / ウィンドウ
594
+ * @throws {Error} When the element's document has no window (a document outside any browsing context, which paints nothing) / 要素の文書がウィンドウを持たないとき (閲覧の文脈の外の文書で、何も描かれない)
595
+ */
596
+ const windowOf = (element: Element): Window => {
597
+ const view = element.ownerDocument.defaultView
598
+ if (view === null) {
599
+ throw new Error("[VirtualScroll] the items wrapper belongs to a document without a window, so it has no device-pixel ratio")
600
+ }
601
+ return view
602
+ }
603
+
523
604
  /**
524
605
  * Pixels emitted per horizontal arrow key press. Matches the ~40px browsers scroll for an arrow key.
525
606
  *
@@ -1047,7 +1128,7 @@ const VirtualScrollInner = <T,>(
1047
1128
  }: VirtualScrollProps<T>,
1048
1129
  ref: React.Ref<VirtualScrollHandle>,
1049
1130
  ) => {
1050
- const { width: scrollBarWidth, enableThumbDrag, enableTrackClick, enableArrowButtons, enableScrollToTopBottomButtons, renderThumbOverlay, tapScrollCircleOptions } = scrollBarOptions ?? {}
1131
+ const { width: scrollBarWidth, enableThumbDrag, enableTrackClick, enableArrowButtons, enableArrowButtonTabStops, enableScrollToTopBottomButtons, renderThumbOverlay, tapScrollCircleOptions } = scrollBarOptions ?? {}
1051
1132
  const resolvedLabels = useMemo(() => resolveVirtualScrollLabels(locale, labels), [locale, labels])
1052
1133
 
1053
1134
  const { enablePointerDrag, pointerDragInputs, enableKeyboardNavigation = true, enableEscapeRowReturn = false, wheelSpeedMultiplier, inertiaOptions, overscrollBehavior, clipItemHeight = false, resetOnGetItemHeightChange = false } = behaviorOptions ?? {}
@@ -2426,6 +2507,78 @@ const VirtualScrollInner = <T,>(
2426
2507
  visibleStartIndex,
2427
2508
  ])
2428
2509
 
2510
+ // 行ラッパーを描くウィンドウの装置の画素比。ラッパーの最初の取り付けまで (サーバー描画と初回描画) は null。
2511
+ // 大域の devicePixelRatio と同名にしない — 宣言を消したとき参照が黙って大域のウィンドウの比へ化けるため
2512
+ const [wrapperDevicePixelRatio, setWrapperDevicePixelRatio] = useState<number | null>(null)
2513
+
2514
+ /**
2515
+ * Ref callback of the items wrapper. Commits the device-pixel ratio of the window the wrapper is painted in
2516
+ * (`windowOf`, never the global `window`), and commits it again whenever that ratio changes (browser zoom,
2517
+ * or the window moving to a screen of another density) through a `(resolution: <ratio>dppx)` media-query
2518
+ * watch that is re-armed at each new ratio. The first commit runs in React's commit phase, so a client mount
2519
+ * paints the snapped translate in its first frame. A window without media queries (jsdom) cannot change its
2520
+ * ratio: the ratio read at attach applies and no watch is armed. The callback identity is stable, so React
2521
+ * calls it once per attach. React 19 calls the returned cleanup on detach; it calls the ref with `null`
2522
+ * only when the attach threw (no cleanup exists), and that call does nothing.
2523
+ *
2524
+ * 行ラッパーの ref コールバック。ラッパーを描くウィンドウ (`windowOf`。大域の `window` ではない) の装置の
2525
+ * 画素比を状態へ確定し、比が変わるたび (ブラウザの拡大縮小・密度の違う画面へのウィンドウの移動) に
2526
+ * `(resolution: <比>dppx)` のメディアクエリの監視で確定し直す (監視は新しい比で張り直す)。最初の確定は
2527
+ * React の確定の段で走るので、クライアントでのマウントは最初のフレームから揃えた平行移動で描く。
2528
+ * メディアクエリを持たないウィンドウ (jsdom) は比が変わり得ないので、取り付け時に読んだ比を使い監視は
2529
+ * 張らない。コールバックの同一性は安定しているので、React は取り付け 1 回につき 1 回だけ呼ぶ。取り外し時に
2530
+ * React 19 は返した後始末を呼び、ref を `null` で呼ぶのは取り付けが投げて後始末が無いときだけ (何もしない)。
2531
+ *
2532
+ * @param wrapper - The attached items wrapper, or `null` on the detach that follows an attach that threw / 取り付けた行ラッパー (投げた取り付けの後の取り外しでは `null`)
2533
+ * @returns Cleanup that removes the resolution watch, or `undefined` for `null` / 解像度の監視を外す後始末 (`null` には `undefined`)
2534
+ * @throws {Error} When the wrapper's document has no window (see `windowOf`) / ラッパーの文書がウィンドウを持たないとき (`windowOf` を参照)
2535
+ */
2536
+ const observeItemsWrapperDevicePixelRatio = useCallback((wrapper: HTMLDivElement | null): (() => void) | undefined => {
2537
+ // 後始末を返した ref を React 19 は null で呼ばない。null が届くのは取り付けが投げた後だけで、外す監視も無い
2538
+ if (wrapper === null) {
2539
+ return undefined
2540
+ }
2541
+ const view = windowOf(wrapper)
2542
+ let resolutionQuery: MediaQueryList | undefined
2543
+ /**
2544
+ * Commits the window's current device-pixel ratio and re-arms the resolution watch at that ratio.
2545
+ *
2546
+ * ウィンドウの現在の装置の画素比を確定し、その比で解像度の監視を張り直す処理。
2547
+ */
2548
+ const syncDevicePixelRatio = (): void => {
2549
+ resolutionQuery?.removeEventListener("change", syncDevicePixelRatio)
2550
+ const ratio = view.devicePixelRatio
2551
+ setWrapperDevicePixelRatio(ratio)
2552
+ // ❗ `(resolution: Ndppx)` が知らせるのは比 N との一致・不一致の切り替わりだけ。N 以外の比どうしの変化
2553
+ // (もう一度の拡大縮小・さらに別の画面への移動) を捉えるには、変化のたびに新しい比で張り直すしかない。
2554
+ // matchMedia を持たないウィンドウ (jsdom) は比が変わり得ないので監視だけを省く (読んだ比は使う)
2555
+ resolutionQuery = typeof view.matchMedia === "function" ? view.matchMedia(`(resolution: ${ratio}dppx)`) : undefined
2556
+ resolutionQuery?.addEventListener("change", syncDevicePixelRatio)
2557
+ }
2558
+ syncDevicePixelRatio()
2559
+ /**
2560
+ * Removes the resolution watch; React calls it when the wrapper detaches.
2561
+ *
2562
+ * 解像度の監視を外す後始末 (ラッパーの取り外し時に React が呼ぶ)。
2563
+ */
2564
+ const stopObservingDevicePixelRatio = (): void => {
2565
+ resolutionQuery?.removeEventListener("change", syncDevicePixelRatio)
2566
+ }
2567
+ return stopObservingDevicePixelRatio
2568
+ }, [])
2569
+
2570
+ /**
2571
+ * Renders the items wrapper for the pane's current scroll position (ScrollPane's `children` render prop).
2572
+ * The wrapper carries the scroll offset, the top inset and the render anchor as one translate, snapped to
2573
+ * the device-pixel grid once the wrapper is attached (`snapToDevicePixelGrid`); rows keep their exact tops.
2574
+ *
2575
+ * ペインの現在のスクロール位置に対する行ラッパーを描く処理 (ScrollPane の `children` の描画関数)。スクロール位置・
2576
+ * 上のインセット・描画アンカーを 1 つの平行移動としてラッパーが運び、ラッパーの取り付け後はそれを装置の画素の
2577
+ * 格子へ揃える (`snapToDevicePixelGrid`)。行の上端は厳密値のまま。
2578
+ *
2579
+ * @param currentScrollPosition - The pane's scroll position in PANE coordinates / ペイン座標のスクロール位置
2580
+ * @returns The items wrapper, or the empty state when there are no items / 行ラッパー (項目が無ければ空状態)
2581
+ */
2429
2582
  const renderVisibleItems = useCallback(
2430
2583
  (currentScrollPosition: number) => {
2431
2584
  const shouldUseThrottledPosition = (callbackThrottleMs ?? 0) > 0
@@ -2458,6 +2611,12 @@ const VirtualScrollInner = <T,>(
2458
2611
  // 毎フレームのリフローを避けコンポジタのみで完結させる。
2459
2612
  // アンカーは visibleItems と同じ memo の返り値を使い、行 top との整合を保証する。
2460
2613
  const containerTop = resolvedInsets.top + renderAnchor - rawEffectiveScrollPosition
2614
+ // ❗ ラッパーは合成層 (willChange: transform) なので、平行移動が装置の画素の端数を持つとブラウザは層ごと
2615
+ // 再標本化する (実測: -2440.5px で 2px の輪郭・2px の隙間・2px の輪が 3+1+3 行に滲み、下辺の隙間が消える)。
2616
+ // 揃えるのは層の平行移動だけでよい — 層の中の行の端数の位置は描画が画素へ揃える。比が決まるのは
2617
+ // ラッパーの取り付け時なので、それまで (サーバー描画と初回描画) は描く画面が無く厳密値のまま書く。
2618
+ // クライアントのマウントは取り付けの確定が paint 前に再描画するので、端数のまま描かれることはない
2619
+ const wrapperTranslateY = wrapperDevicePixelRatio === null ? containerTop : snapToDevicePixelGrid(containerTop, wrapperDevicePixelRatio)
2461
2620
 
2462
2621
  const bottomInset =
2463
2622
  resolvedInsets.bottom > 0 ? (
@@ -2474,19 +2633,21 @@ const VirtualScrollInner = <T,>(
2474
2633
 
2475
2634
  Logger.debug("[VirtualScroll] Rendering items", () => ({
2476
2635
  containerTop,
2636
+ wrapperDevicePixelRatio,
2637
+ wrapperTranslateY,
2477
2638
  logicalScrollPosition,
2478
2639
  resolvedInsets,
2479
2640
  effectiveScrollPosition,
2480
2641
  }))
2481
2642
 
2482
2643
  return (
2483
- <div className="aqvs-items-wrapper" style={{ top: 0, transform: `translateY(${containerTop}px)`, willChange: "transform" }}>
2644
+ <div ref={observeItemsWrapperDevicePixelRatio} className="aqvs-items-wrapper" style={{ top: 0, transform: `translateY(${wrapperTranslateY}px)`, willChange: "transform" }}>
2484
2645
  {visibleItems}
2485
2646
  {bottomInset}
2486
2647
  </div>
2487
2648
  )
2488
2649
  },
2489
- [callbackThrottleMs, itemCount, fenwickTree, logicalScrollPosition, renderAnchor, renderingEndIndex, renderingStartIndex, resolvedInsets, scrollPosition, viewportSize, visibleItems],
2650
+ [callbackThrottleMs, itemCount, fenwickTree, logicalScrollPosition, observeItemsWrapperDevicePixelRatio, renderAnchor, renderingEndIndex, renderingStartIndex, resolvedInsets, scrollPosition, viewportSize, visibleItems, wrapperDevicePixelRatio],
2490
2651
  )
2491
2652
 
2492
2653
  const currentRange = useMemo<VirtualScrollRange>(
@@ -2602,6 +2763,7 @@ const VirtualScrollInner = <T,>(
2602
2763
  enableThumbDrag={enableThumbDrag}
2603
2764
  enableTrackClick={enableTrackClick}
2604
2765
  enableArrowButtons={enableArrowButtons}
2766
+ enableArrowButtonTabStops={enableArrowButtonTabStops}
2605
2767
  enablePointerDrag={enablePointerDrag}
2606
2768
  pointerDragInputs={pointerDragInputs}
2607
2769
  renderThumbOverlay={renderThumbOverlay}
@@ -60,7 +60,7 @@ export type TapScrollVelocityInput = {
60
60
  * so never assert bit-equality (`toBe` / `Object.is`) of a pure-axis component against
61
61
  * `computeTapScrollSpeed` outside the committed exact fixtures. The rejected order
62
62
  * `s * (o / r)` would be bit-exact on pure axes (`o / r = ±1` exactly); the pin below
63
- * trades that for fixture-order independence (plan §4.2 T1 / §4.3).
63
+ * trades that for fixture-order independence (see the evaluation-order pin below).
64
64
  * - D2 (radial law under isotropy): when `s_x = s_y = s`, `|v| = s(r)` at every angle.
65
65
  * - D3 (separability): `v_a = s_a(r) * u_a(theta)` for a direction-only unit field `u`.
66
66
  * - D4 (isotropic direction fidelity — a CHOSEN axiom, not derived): when `s_x = s_y`,
@@ -69,8 +69,8 @@ export type TapScrollVelocityInput = {
69
69
  * D1-D3 alone do NOT force this law: they admit the whole family
70
70
  * `u = (cos phi(theta), sin phi(theta))` for any continuous strictly increasing angle remap
71
71
  * `phi` fixing the four cardinal angles (e.g. `phi(theta) = theta - epsilon * sin(4 theta)`).
72
- * D4 pins `phi = id`. That family is the registered invariant-preserving tuning point for
73
- * near-axis leakage (plan §4.4); v3.6.0 ships `phi = id`.
72
+ * D4 pins `phi = id`. That family is the invariant-preserving tuning point for near-axis
73
+ * leakage; the package ships `phi = id`.
74
74
  *
75
75
  * Evaluation-order pin: each component is computed literally as `s_a(r) * o_a / r`
76
76
  * (left-to-right, i.e. `(s * o) / r`). The two orders differ by 1 ulp for SOME inputs —
@@ -482,7 +482,7 @@
482
482
  transform: translateX(calc(var(--aqvs-grid-hx-residual, 0px) - var(--aqvs-grid-frozen-width, 0px)));
483
483
  }
484
484
 
485
- /* 末尾列クリップ (3.5.0 — ADR-19-3): left の max() は先頭優先の CSS エンコード。
485
+ /* 末尾列クリップ: left の max() は先頭優先の CSS エンコード。
486
486
  * ❗ この max() 形は trailingVisibleSize の calc() 版そのもの (クリップ左端 = extent − W_T_vis
487
487
  * が恒等) — CSS は JS ヘルパーを読めないための登記済み例外 (単一情報源レジスタのエントリ 1)。 */
488
488
  .aqvs-grid-row-trailing {
@@ -16,7 +16,7 @@ import { minmax } from "./utils.ts"
16
16
 
17
17
  /**
18
18
  * Default corner-relative offset (px) for BOTH grid tap-circle axes — the reachability arm (R)
19
- * of the three-arm placement law (plan §5). Derivation: full speed needs
19
+ * of the three-arm placement law. Derivation: full speed needs
20
20
  * `maxVisualDistance = 240 px` of advancing pull toward the far screen edge, which is a hard
21
21
  * stop on touch (`touch-action: none`). With `off = −200` the disc center sits
22
22
  * `sbw − off − size/2 = 12 + 200 − 20 = 192 px` inboard of each far root edge, giving a
@@ -26,11 +26,11 @@ import { minmax } from "./utils.ts"
26
26
  * nav-zone envelope ≥ ~2/3. Deliberately a LITERAL: it must NOT track a consumer-supplied
27
27
  * `maxVisualDistance` (coupling two knobs would make one silently move the other), and
28
28
  * shrinking `maxVisualDistance` instead is forbidden (it changes the speed law's `maxDistance`
29
- * input and breaks the T1 identity anchor).
29
+ * input, so a pure-axis drag would no longer reproduce the bar's own speed law).
30
30
  * グリッドタップサークル両軸のコーナー相対既定オフセット (px) — 3 アーム配置則の到達性アーム
31
31
  * (R)。導出: 全速には前進方向へ 240 px の引きが要り、タッチではスクリーン端がハードストップ。
32
32
  * −200 で中心は各遠端から 192 px 内側 = 最悪ケース引き分率 0.80 (ナビ帯込み 0.66)。意図的な
33
- * リテラル — 消費側 `maxVisualDistance` へ連動させない (ノブ連動の登記済み禁止)。
33
+ * リテラル — 消費側 `maxVisualDistance` へ連動させない (2 つのノブを連動させると一方が他方を黙って動かす)。
34
34
  */
35
35
  export const GRID_TAP_CIRCLE_DEFAULT_OFFSET = -200
36
36
 
@@ -51,27 +51,27 @@ export type UseGridTapScrollParams = {
51
51
  enabled: boolean
52
52
  /** Shared pull range (= `max(maxVisualDistance, 1)`). / 共有引き範囲 (= `max(maxVisualDistance, 1)`)。 */
53
53
  maxDistance: number
54
- /** Horizontal speed parameters (§4.1 Pₓ). / 横軸速度パラメータ (§4.1 Pₓ)。 */
54
+ /** Horizontal speed parameters (Pₓ, built from the scroll-band width and the column count). / 横軸速度パラメータ (Pₓ。スクロール帯幅と列数から構成)。 */
55
55
  xSpeedParams: TapScrollAxisSpeedParams
56
- /** Vertical speed parameters (§4.1 P_y). / 縦軸速度パラメータ (§4.1 P_y)。 */
56
+ /** Vertical speed parameters (P_y, built from the embedded pane height and the scroll-row count). / 縦軸速度パラメータ (P_y。埋め込みペイン高とスクロール行数から構成)。 */
57
57
  ySpeedParams: TapScrollAxisSpeedParams
58
58
  /** The grid's x apply seam (absolute position in, clamped applied position out). / グリッドの x 適用シーム (絶対位置 → クランプ後位置)。 */
59
59
  applyHxRef: { readonly current: (next: number) => number }
60
60
  /** Synchronous-fresh hx read. / hx の同期・最新読み。 */
61
61
  getHx: () => number
62
- /** The ONE extracted x clamp law (`getMaxHx` SSOT — §3.5-7). / 唯一抽出の x クランプ則 (`getMaxHx` SSOT — §3.5-7)。 */
62
+ /** The ONE extracted x clamp law (`getMaxHx` SSOT — never re-derived from the grid's tree). / 唯一抽出の x クランプ則 (`getMaxHx` SSOT — グリッドの木から再導出しない)。 */
63
63
  getMaxHx: () => number
64
- /** y apply seam: wraps `scrollBy(delta)` AND syncs `vyRef.current = applied` before returning (§3.5-8 — NOT a bare handle passthrough). / y 適用シーム: `scrollBy(delta)` をラップし `vyRef.current = applied` を同期してから返す (§3.5-8 — 素のハンドル素通しではない)。 */
64
+ /** y apply seam: wraps `scrollBy(delta)` AND syncs `vyRef.current = applied` before returning (NOT a bare handle passthrough — without the sync, x-frame notifications during a diagonal drag carry a stale y). / y 適用シーム: `scrollBy(delta)` をラップし `vyRef.current = applied` を同期してから返す (素のハンドル素通しではない — 同期が無いと対角ドラッグ中の x フレーム通知が stale な y を運ぶ)。 */
65
65
  applyVy: (delta: number) => number
66
66
  /** Synchronous-fresh vy read — MUST read the same authority `applyVy` resolves against (the embedded pane's position), never a notification mirror with a second writer (v3.6.1 — a lagging mirror base overstates `actualDelta` and drives the residual negative = yo-yo). / vy の同期・最新読み — `applyVy` が解決するのと同一権威 (埋め込みペイン位置) を読むこと。第 2 の書き手を持つ通知鏡像は禁止 (v3.6.1 — 遅延鏡像基準は `actualDelta` を過大化し残差を負へ落とすヨーヨー)。 */
67
67
  getVy: () => number
68
68
  /** y extent read through the embedded handle's freshness channel (never re-derived). / 埋め込みハンドルの鮮度チャネル経由の y 延長 (再導出禁止)。 */
69
69
  getMaxVy: () => number
70
- /** Pending column anchor — cleared ONLY on frames whose APPLIED hx delta ≠ 0 (§3.5-5). / 保留列アンカー — 適用済み hx デルタ ≠ 0 のフレームのみ解除 (§3.5-5)。 */
70
+ /** Pending column anchor — cleared ONLY on frames whose APPLIED hx delta ≠ 0 (a request the clamp swallows keeps it). / 保留列アンカー — 適用済み hx デルタ ≠ 0 のフレームのみ解除 (クランプに吸収された要求では残す)。 */
71
71
  pendingColAnchorRef: { current: unknown }
72
- /** x extent-growth freshness key (columnWindow / total width — §3.5-3). / x 延長成長の鮮度キー (columnWindow / 総幅 — §3.5-3)。 */
72
+ /** x extent-growth freshness key (columnWindow / total width — a change re-arms a loop parked at the edge). / x 延長成長の鮮度キー (columnWindow / 総幅 — 変化で端に止まったループを再武装)。 */
73
73
  xExtentFreshness: unknown
74
- /** y extent-growth freshness key (the vertical range state — §3.5-3). / y 延長成長の鮮度キー (縦レンジ state — §3.5-3)。 */
74
+ /** y extent-growth freshness key (the vertical range state — a change re-arms a loop parked at the edge). / y 延長成長の鮮度キー (縦レンジ state — 変化で端に止まったループを再武装)。 */
75
75
  yExtentFreshness: unknown
76
76
  }
77
77
 
@@ -89,7 +89,7 @@ export type UseGridTapScrollResult = {
89
89
  * Owns the unified tap circle's drag state and the two-axis rAF integration loop.
90
90
  * 統合タップサークルのドラッグ状態と 2 軸 rAF 積分ループを所有するフック。
91
91
  *
92
- * Per-frame semantics (plan §8.3 — implemented verbatim):
92
+ * Per-frame semantics:
93
93
  *
94
94
  * ```text
95
95
  * dt = min(max((t − last)/1000, 0), TAP_SCROLL_MAX_FRAME_DELTA_SECONDS); skip if dt ≤ 0
@@ -186,7 +186,7 @@ export const useGridTapScroll = (params: UseGridTapScrollParams): UseGridTapScro
186
186
  */
187
187
  const step = useCallback(
188
188
  (timestamp: number) => {
189
- // この rAF ループ意味論は ScrollBar.tsx の tap ループ (stepAutoScroll) と双子 — 片方を直したらもう片方も直すこと (ADR-27)
189
+ // この rAF ループ意味論は ScrollBar.tsx の tap ループ (stepAutoScroll) と双子 — 片方を直したらもう片方も直すこと
190
190
  const current = paramsRef.current
191
191
  const state = tapDragStateRef.current
192
192
  if (!state.active || state.direction === 0) {