@aiquants/virtualscroll 3.4.0 → 3.6.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.
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Pure two-axis velocity decomposition for the unified grid tap circle (v3.6.0).
3
+ * 統合 2 軸タップサークル (v3.6.0) 用の純関数 2 軸速度分解モジュール。
4
+ */
5
+
6
+ import { computeTapScrollSpeed } from "./ScrollBar.tsx"
7
+
8
+ /**
9
+ * Per-axis speed parameters for `computeTapScrollVelocity` — the exact tuple one bar resolves
10
+ * today. Deliberately an explicit structural type (NOT `Omit<TapScrollSpeedInput, …>`):
11
+ * api-extractor cannot roll up `Omit<>` on the public surface (house rule).
12
+ * `computeTapScrollVelocity` の軸別速度パラメータ — 今日 1 本のバーが解決する組そのもの。
13
+ * 意図的な明示構造型 (`Omit<>` 非使用 — api-extractor がロールアップできないための既定方針)。
14
+ */
15
+ export type TapScrollAxisSpeedParams = {
16
+ /** Viewport length on this axis (px). / この軸のビューポート長 (px)。 */
17
+ viewportSize: number
18
+ /** Minimum speed multiplier (viewport-relative). / 最小速度倍率 (ビューポート比)。 */
19
+ minSpeedMultiplier: number
20
+ /** Maximum speed multiplier (viewport-relative). / 最大速度倍率 (ビューポート比)。 */
21
+ maxSpeedMultiplier: number
22
+ /** Whether `maxSpeedMultiplier` was explicitly supplied (controls the 1,200 px/s auto floor). / `maxSpeedMultiplier` が明示指定か (1,200 px/s 自動床の制御)。 */
23
+ hasCustomMaxSpeedMultiplier: boolean
24
+ /** Optional speed-curve shape (same contract as `TapScrollSpeedInput.curve`). / 任意の速度曲線形状 (`TapScrollSpeedInput.curve` と同契約)。 */
25
+ curve?: { exponentialSteepness: number; exponentialScale?: number; easedOffset?: number }
26
+ }
27
+
28
+ /**
29
+ * Input for `computeTapScrollVelocity`.
30
+ * `computeTapScrollVelocity` の入力。
31
+ */
32
+ export type TapScrollVelocityInput = {
33
+ /** Signed px from the circle center (state canonical field). / サークル中心からの符号付き px (state 正準フィールド)。 */
34
+ offsetX: number
35
+ /** Signed px from the circle center (state canonical field). / サークル中心からの符号付き px (state 正準フィールド)。 */
36
+ offsetY: number
37
+ /** Shared pull range (= `max(maxVisualDistance, 1)`). / 共有引き範囲 (= `max(maxVisualDistance, 1)`)。 */
38
+ maxDistance: number
39
+ /** Horizontal-axis speed parameters. / 横軸の速度パラメータ。 */
40
+ x: TapScrollAxisSpeedParams
41
+ /** Vertical-axis speed parameters. / 縦軸の速度パラメータ。 */
42
+ y: TapScrollAxisSpeedParams
43
+ }
44
+
45
+ /**
46
+ * Decomposes a 2-D tap-circle pull into per-axis scroll velocities (px/s, signed):
47
+ *
48
+ * ```text
49
+ * r = 0 => v = (0, 0)
50
+ * r > 0 => v = ( s_x(r) * ox / r , s_y(r) * oy / r )
51
+ * ```
52
+ *
53
+ * where `r = ||(ox, oy)||_2` and `s_a` is the UNCHANGED `computeTapScrollSpeed` under axis `a`'s
54
+ * parameters (`distance: r`, shared `maxDistance`).
55
+ *
56
+ * Derivation summary (the axioms this law is the unique solution of):
57
+ *
58
+ * - D1 (legacy reproduction): pure-axis inputs reproduce each 1-D bar law to within 1 ulp
59
+ * under the pinned order — deviations hit a config-dependent ~9% of probed pure-axis points,
60
+ * so never assert bit-equality (`toBe` / `Object.is`) of a pure-axis component against
61
+ * `computeTapScrollSpeed` outside the committed exact fixtures. The rejected order
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).
64
+ * - D2 (radial law under isotropy): when `s_x = s_y = s`, `|v| = s(r)` at every angle.
65
+ * - D3 (separability): `v_a = s_a(r) * u_a(theta)` for a direction-only unit field `u`.
66
+ * - D4 (isotropic direction fidelity — a CHOSEN axiom, not derived): when `s_x = s_y`,
67
+ * `v` is parallel to the offset with a positive scalar (rotational equivariance).
68
+ *
69
+ * D1-D3 alone do NOT force this law: they admit the whole family
70
+ * `u = (cos phi(theta), sin phi(theta))` for any continuous strictly increasing angle remap
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`.
74
+ *
75
+ * Evaluation-order pin: each component is computed literally as `s_a(r) * o_a / r`
76
+ * (left-to-right, i.e. `(s * o) / r`). The two orders differ by 1 ulp for SOME inputs —
77
+ * verified witness: `40 * (7 / 25) = 11.200000000000001` while `(40 * 7) / 25 = 11.2` exactly.
78
+ * The committed spec fixtures happen to agree under BOTH orders (`200 * (30 / 50) === 120`
79
+ * etc. — the products land inside half-ulp and round back); the pin exists so the fixtures'
80
+ * `toBe` exactness never depends on that luck of the operand set. Do NOT "test" the old,
81
+ * numerically false claim that `200 * (30 / 50)` misrounds — it does not; deleting this pin
82
+ * because that claim fails would discard a sound pin.
83
+ *
84
+ * Totality: non-finite `ox` / `oy` are sanitized to 0 and `r = 0` returns `(0, 0)` (the origin
85
+ * has no continuous extension — the limit depends on the approach angle). Components are always
86
+ * finite: `s_a` is the existing sole NaN barrier and `|o_a| / r <= 1`.
87
+ *
88
+ * External reproduction of what a bar/grid actually resolves (same pattern as the
89
+ * `computeTapScrollSpeed` docstring):
90
+ *
91
+ * ```ts
92
+ * const velocity = computeTapScrollVelocity({
93
+ * offsetX,
94
+ * offsetY,
95
+ * maxDistance: options.maxVisualDistance ?? TAP_SCROLL_SPEED_DEFAULTS.maxDistance,
96
+ * x: {
97
+ * viewportSize: bandWidth,
98
+ * minSpeedMultiplier: options.minSpeedMultiplier ?? TAP_SCROLL_SPEED_DEFAULTS.minSpeedMultiplier,
99
+ * maxSpeedMultiplier: options.maxSpeedMultiplier ?? computeAutoTapScrollMaxSpeedMultiplier(colCount),
100
+ * hasCustomMaxSpeedMultiplier: options.maxSpeedMultiplier !== undefined,
101
+ * curve: options.maxSpeedCurve,
102
+ * },
103
+ * y: { …same with the pane height and rowCount… },
104
+ * })
105
+ * ```
106
+ *
107
+ * 2 次元のタップサークル引きを軸別スクロール速度 (px/s、符号付き) へ分解する処理。方向余弦
108
+ * 分解 `v = (s_x(r)·ox/r, s_y(r)·oy/r)` — `s_a` は無改造の `computeTapScrollSpeed`。D4
109
+ * (等方時の方向忠実) は導出ではなく選択公理で、許容リマップ族 `u = (cos φ(θ), sin φ(θ))` を
110
+ * `φ = id` に固定する。評価順序は `s_a(r) * o_a / r` (左から右) にピン — 1 ulp 証人は
111
+ * `40*(7/25) ≠ (40*7)/25`。D1 の純軸再現はピン順序下で ≤ 1 ulp (構成依存の約 9% の点で乖離 —
112
+ * ビット恒等は棄却順序 `s·(o/r)` の側で、ピンはそれをフィクスチャ順序非依存と引き換えた)。
113
+ * 非有限オフセットは 0 へ消毒し、`r = 0` は `(0, 0)`。
114
+ *
115
+ * @param input Offsets, shared pull range, and per-axis speed parameters. / オフセット・共有引き範囲・軸別速度パラメータ。
116
+ * @returns Signed per-axis velocities in px/s (always finite). / 軸別の符号付き速度 (px/s、常に有限)。
117
+ */
118
+ export const computeTapScrollVelocity = ({ offsetX, offsetY, maxDistance, x, y }: TapScrollVelocityInput): { x: number; y: number } => {
119
+ // 非有限オフセットの消毒 (finiteOr 規律): 素通しすると r と両成分が NaN 化し、
120
+ // スクロール位置の NaN 汚染 (文書化済みの回復不能故障) へ直結する
121
+ const ox = Number.isFinite(offsetX) ? offsetX : 0
122
+ const oy = Number.isFinite(offsetY) ? offsetY : 0
123
+ const r = Math.hypot(ox, oy)
124
+ // 原点は連続拡張が存在しない (極限が接近角に依存) ため全域関数規約で (0, 0) を返す。
125
+ // ドライバのゲートが積分時 r >= 6 を保証するので、この分岐は純関数側の防御でもある
126
+ if (r === 0) {
127
+ return { x: 0, y: 0 }
128
+ }
129
+ const speedX = computeTapScrollSpeed({
130
+ distance: r,
131
+ maxDistance,
132
+ viewportSize: x.viewportSize,
133
+ minSpeedMultiplier: x.minSpeedMultiplier,
134
+ maxSpeedMultiplier: x.maxSpeedMultiplier,
135
+ hasCustomMaxSpeedMultiplier: x.hasCustomMaxSpeedMultiplier,
136
+ curve: x.curve,
137
+ })
138
+ const speedY = computeTapScrollSpeed({
139
+ distance: r,
140
+ maxDistance,
141
+ viewportSize: y.viewportSize,
142
+ minSpeedMultiplier: y.minSpeedMultiplier,
143
+ maxSpeedMultiplier: y.maxSpeedMultiplier,
144
+ hasCustomMaxSpeedMultiplier: y.hasCustomMaxSpeedMultiplier,
145
+ curve: y.curve,
146
+ })
147
+ // 評価順序ピン (§4.3): s_a(r) * o_a / r — 左から右。括弧で順序を固定し、
148
+ // 「同値だから」と (s·(o/r)) へ並べ替える 1 ulp ドリフトを封じる
149
+ return { x: (speedX * ox) / r, y: (speedY * oy) / r }
150
+ }
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * 可変なアイテム高さに対応したReact用の高性能仮想スクロールコンポーネント。
6
6
  */
7
7
 
8
+ export { computeTapScrollVelocity, type TapScrollAxisSpeedParams, type TapScrollVelocityInput } from "./computeTapScrollVelocity.ts"
8
9
  export { createResidualQuantizer, type ResidualQuantizer, type ResidualQuantizerOptions } from "./residualQuantizer.ts"
9
10
  export { computeAutoTapScrollMaxSpeedMultiplier, computeTapScrollSpeed, ScrollBar, type ScrollBarProps, type ScrollBarTapCircleOptions, type ScrollBarThumbOverlayRenderProps, TAP_SCROLL_SPEED_DEFAULTS, type TapScrollSpeedInput } from "./ScrollBar.tsx"
10
11
  export { ScrollPane, type ScrollPaneContentInsets, type ScrollPaneHandle, type ScrollPaneInertiaOptions, type ScrollPaneProps } from "./ScrollPane.tsx"
@@ -15,7 +16,20 @@ export { useHeightCache } from "./useHeightCache.ts"
15
16
  export { useLruCache } from "./useLruCache.ts"
16
17
  export { useWheelBridge, type WheelBridgeOptions, type WheelBridgeTarget } from "./useWheelBridge.ts"
17
18
  export { minmax } from "./utils.ts"
18
- export { MAX_FROZEN_LEADING_COLS, MAX_FROZEN_LEADING_ROWS, MAX_RENDERED_CELLS, MAX_TRACK_SIZE, VirtualGrid, type VirtualGridBehaviorOptions, type VirtualGridHandle, type VirtualGridLiveRegionOptions, type VirtualGridProps, type VirtualGridRange } from "./VirtualGrid.tsx"
19
+ export {
20
+ MAX_FROZEN_LEADING_COLS,
21
+ MAX_FROZEN_LEADING_ROWS,
22
+ MAX_FROZEN_TRAILING_COLS,
23
+ MAX_FROZEN_TRAILING_ROWS,
24
+ MAX_RENDERED_CELLS,
25
+ MAX_TRACK_SIZE,
26
+ VirtualGrid,
27
+ type VirtualGridBehaviorOptions,
28
+ type VirtualGridHandle,
29
+ type VirtualGridLiveRegionOptions,
30
+ type VirtualGridProps,
31
+ type VirtualGridRange,
32
+ } from "./VirtualGrid.tsx"
19
33
  export {
20
34
  VirtualScroll,
21
35
  type VirtualScrollBehaviorOptions,
@@ -372,6 +372,16 @@
372
372
  grid-template-rows: auto minmax(0, 1fr) auto;
373
373
  }
374
374
 
375
+ /* 末尾行帯 (3.5.0): T > 0 のときだけ最終 hbar 行の直前に auto 行を足す。テンプレート合成は
376
+ * 4 通り — 基本 / 先頭のみ (既存) / 末尾のみ / 両方。行高そのものはインライン H_T_vis
377
+ * (高さの CSS 変数は存在しない — 帯寸の変数は幅 2 つのみ。残差 var --aqvs-grid-hx-residual は別系)。 */
378
+ .aqvs-grid-has-trailing-rows {
379
+ grid-template-rows: minmax(0, 1fr) auto auto;
380
+ }
381
+ .aqvs-grid-has-frozen-rows.aqvs-grid-has-trailing-rows {
382
+ grid-template-rows: auto minmax(0, 1fr) auto auto;
383
+ }
384
+
375
385
  .aqvs-grid-frozen-rows {
376
386
  position: relative;
377
387
  min-width: 0;
@@ -388,6 +398,29 @@
388
398
  overflow: hidden;
389
399
  }
390
400
 
401
+ /* 末尾行帯のクリップ (テンプレート行 = H_T_vis インライン) と bottom 定着 inner (木高 H_T
402
+ * インライン)。inner の bottom: 0 が「帯上端 = extent − H_T_vis」を全構成で成立させ、
403
+ * 重複縮退では帯の末尾行が可視に残る (フロー配置帯の誤登録クラスの構造的封鎖)。 */
404
+ .aqvs-grid-trailing-rows {
405
+ position: relative;
406
+ min-width: 0;
407
+ overflow: hidden;
408
+ }
409
+ .aqvs-grid-trailing-rows-inner {
410
+ position: absolute;
411
+ bottom: 0;
412
+ left: 0;
413
+ right: 0;
414
+ }
415
+
416
+ /* 帯行の実クリップ器 (先頭帯行 .aqvs-grid-frozen-band-row の転置 — right はインライン
417
+ * scrollBarWidth のコーナー予約)。 */
418
+ .aqvs-grid-trailing-band-row {
419
+ position: absolute;
420
+ left: 0;
421
+ overflow: hidden;
422
+ }
423
+
391
424
  .aqvs-grid-main {
392
425
  position: relative;
393
426
  min-width: 0;
@@ -432,7 +465,9 @@
432
465
  top: 0;
433
466
  height: 100%;
434
467
  left: var(--aqvs-grid-frozen-width, 0px);
435
- right: 0;
468
+ /* 末尾凍結 (3.5.0): 右インセット = W_T。変数不在 (T = 0) は 0px フォールバックで
469
+ * 計算結果・出力ともバイト恒等 (T = 0 恒等契約)。 */
470
+ right: var(--aqvs-grid-trailing-width, 0px);
436
471
  overflow: hidden;
437
472
  }
438
473
 
@@ -447,6 +482,29 @@
447
482
  transform: translateX(calc(var(--aqvs-grid-hx-residual, 0px) - var(--aqvs-grid-frozen-width, 0px)));
448
483
  }
449
484
 
485
+ /* 末尾列クリップ (3.5.0 — ADR-19-3): left の max() は先頭優先の CSS エンコード。
486
+ * ❗ この max() 形は trailingVisibleSize の calc() 版そのもの (クリップ左端 = extent − W_T_vis
487
+ * が恒等) — CSS は JS ヘルパーを読めないための登記済み例外 (単一情報源レジスタのエントリ 1)。 */
488
+ .aqvs-grid-row-trailing {
489
+ position: absolute;
490
+ top: 0;
491
+ height: 100%;
492
+ left: max(var(--aqvs-grid-frozen-width, 0px), calc(100% - var(--aqvs-grid-trailing-width, 0px)));
493
+ right: 0;
494
+ overflow: hidden;
495
+ }
496
+
497
+ /* 右アンカーの座標ホルダー — 末尾セルは帯ローカル left で置かれ、inner の右定着が
498
+ * 「末尾が 2 番目に潰れる」を幾何で成立させる (クリップ / transform 分離の恒久契約に従い
499
+ * transform は持たない)。 */
500
+ .aqvs-grid-row-trailing-inner {
501
+ position: absolute;
502
+ top: 0;
503
+ height: 100%;
504
+ right: 0;
505
+ width: var(--aqvs-grid-trailing-width, 0px);
506
+ }
507
+
450
508
  /* 横バー帯。 */
451
509
  .aqvs-grid-hbar-strip {
452
510
  position: relative;
@@ -468,3 +526,34 @@
468
526
  white-space: nowrap;
469
527
  border: 0;
470
528
  }
529
+
530
+ /* 統合 2 軸タップサークル (v3.6.0)。
531
+ * 純配置 — 意図的に transition を持たない: フェードする opacity は子 (サークルルート) に
532
+ * 住む。バー配置の「効果における双子」(DOM 形状の双子ではない)。 */
533
+ .aqvs-grid-tap-circle-wrapper {
534
+ position: absolute;
535
+ z-index: 50; /* バーと同層 (.aqvs-scrollbar) — DOM 後勝ちで最前 */
536
+ pointer-events: auto;
537
+ }
538
+
539
+ /* ❗ このファイル内で .aqvs-tap-scroll-circle より「後」に置くこと: 両規則が同一要素へ
540
+ * 同特異度で transition longhand を設定し、後の規則が丸ごと勝つ — 出荷済み
541
+ * .aqvs-scrollbar-tap-circle-wrapper が今日それを行っているのと同一機構。バーサークルの
542
+ * 実効 transition (opacity のみ 150ms) をバイト再現し、transform 平滑化は意図的に無し
543
+ * (出荷挙動と一致)。順序はテストでピンする (プラン §10.2)。 */
544
+ .aqvs-grid-tap-circle {
545
+ transition-property: opacity;
546
+ transition-duration: 150ms;
547
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
548
+ }
549
+
550
+ /* パッケージ全域・単一所有者 — 本 CSS 初の forced-colors 規則: グラデーション視覚は
551
+ * forced-colors で平坦化され、輪郭なしではサークルが不可視になる (描画モード修正 —
552
+ * 単体サークルにも輪郭が付くのは登記済みの挙動非変更デルタ)。 */
553
+ @media (forced-colors: active) {
554
+ .aqvs-tap-scroll-circle {
555
+ outline: 1px solid CanvasText;
556
+ outline-offset: -1px;
557
+ border-radius: 50%;
558
+ }
559
+ }