@aiquants/virtualscroll 3.1.1 → 3.4.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.
@@ -338,6 +338,20 @@ export type VirtualScrollProps<T> = {
338
338
  * ここへ流れる。名前は 2.x で公開済みのため据え置くが、意味は「横軸が N px 動いた。横軸はあなたの所有物である」。
339
339
  */
340
340
  onWheelHorizontal?: (deltaX: number) => void
341
+ /**
342
+ * Delegates the horizontal component of a pointer pan (touch / pen drag) upstream — the pan
343
+ * twin of `onWheelHorizontal`, same sign convention (positive = scroll right), per-move
344
+ * increments divided by the press-time x-axis ancestor scale (v3.2.0 VirtualGrid seam).
345
+ * ❗ Belongs to the SAME horizontal-seam family as `onWheelHorizontal` and is deliberately a
346
+ * top-level prop: wrappers that seal the horizontal axis with `Omit<>` (e.g.
347
+ * `@aiquants/directory-tree`) must add this name to their seal list on the 3.2.0 bump
348
+ * (the pass-through review their seal guard mandates).
349
+ * ポインタパン (タッチ / ペン) の横成分を上流へ委譲する — `onWheelHorizontal` のパン対
350
+ * (符号同一・per-move 増分・押下時横 scale 除算、v3.2.0 VirtualGrid シーム)。横シーム
351
+ * ファミリの一員として意図的にトップレベル: `Omit<>` で横軸を封じるラッパーは 3.2.0
352
+ * 更新時に本名を封印リストへ追加すること (封印ガードが義務化する pass-through review)。
353
+ */
354
+ onPanHorizontal?: (deltaX: number) => void
341
355
  /**
342
356
  * Keyboard gestures that emit a horizontal scroll delta through `onWheelHorizontal` (default: none).
343
357
  * `onWheelHorizontal` へ横スクロール量を流すキーボード操作の種別 (既定: 無効)。
@@ -453,20 +467,28 @@ const toPanePositionWithInset = (logical: number, top: number) => (logical <= 0
453
467
 
454
468
  /**
455
469
  * Maximum run length of consecutive zero-height rows scanned linearly before attempting an
456
- * O(log n) jump to the next non-zero row via the Fenwick tree.
470
+ * O(log n) jump to the next non-zero row via the Fenwick tree. Module-level export (NOT in the
471
+ * package barrel) so VirtualGrid's column axis shares the SAME constant instead of duplicating
472
+ * the value (the §5 no-copy rule — same treatment as ANCHOR_REBASE_DISTANCE).
457
473
  *
458
- * 高さ 0 行の連続をこの件数まで線形走査し、超えたら Fenwick 木で次の非 0 行へ O(log n) ジャンプする閾値。
474
+ * 高さ 0 行の連続をこの件数まで線形走査し、超えたら Fenwick 木で次の非 0 行へ O(log n) ジャンプ
475
+ * する閾値。モジュールレベル export (バレル非公開) — VirtualGrid 列軸が値の複製でなく同一定数を
476
+ * 共有するため (§5 複製禁止規範 — ANCHOR_REBASE_DISTANCE と同じ扱い)。
459
477
  */
460
- const ZERO_HEIGHT_RUN_LIMIT = 1000
478
+ export const ZERO_HEIGHT_RUN_LIMIT = 1000
461
479
 
462
480
  /**
463
481
  * Safety cap for the number of item nodes materialized in a single render pass.
464
482
  * Guards against pathological rendering ranges (e.g. a huge contiguous run of zero-height rows).
483
+ * Module-level export (NOT in the package barrel) — VirtualGrid's rendered-row estimator caps
484
+ * its materialized-row walk with the SAME constant (zero-height rows DO materialize as DOM rows).
465
485
  *
466
486
  * 1 回のレンダーで具現化するアイテムノード数の安全上限。
467
- * 病的な描画範囲 (巨大な高さ 0 行の連続など) からの防御。
487
+ * 病的な描画範囲 (巨大な高さ 0 行の連続など) からの防御。モジュールレベル export (バレル非公開) —
488
+ * VirtualGrid の描画行見積りが具現化行ウォークの上限として同一定数を共有する
489
+ * (高さ 0 行も DOM 行として具現化されるため)。
468
490
  */
469
- const MAX_RENDERED_ITEMS = 2000
491
+ export const MAX_RENDERED_ITEMS = 2000
470
492
 
471
493
  /**
472
494
  * Distance (px) between the rendering window start and the current render anchor beyond which
@@ -477,7 +499,7 @@ const MAX_RENDERED_ITEMS = 2000
477
499
  * 行 top とラッパー translateY をブラウザのレイアウト座標上限 (LayoutUnit 約 2^25 px) と
478
500
  * compositor の f32 精度内に収めるための量子化距離。
479
501
  */
480
- const ANCHOR_REBASE_DISTANCE = 1_048_576 // 2^20 px
502
+ export const ANCHOR_REBASE_DISTANCE = 1_048_576 // 2^20 px — VirtualGrid の横アンカーが共有 import する (パッケージバレルへは非公開)
481
503
 
482
504
  /**
483
505
  * Pixels emitted per horizontal arrow key press. Matches the ~40px browsers scroll for an arrow key.
@@ -996,6 +1018,7 @@ const VirtualScrollInner = <T,>(
996
1018
  scrollBarOptions,
997
1019
  behaviorOptions,
998
1020
  onWheelHorizontal,
1021
+ onPanHorizontal,
999
1022
  horizontalKeyInputs,
1000
1023
  horizontalKeyStep = DEFAULT_HORIZONTAL_KEY_STEP,
1001
1024
  contentProps,
@@ -2559,6 +2582,7 @@ const VirtualScrollInner = <T,>(
2559
2582
  renderThumbOverlay={renderThumbOverlay}
2560
2583
  wheelSpeedMultiplier={wheelSpeedMultiplier}
2561
2584
  onWheelHorizontal={onWheelHorizontal}
2585
+ onPanHorizontal={onPanHorizontal}
2562
2586
  contentInsets={resolvedInsets}
2563
2587
  contentProps={contentProps}
2564
2588
  visibleStartIndex={visibleStartIndex}
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export { useHeightCache } from "./useHeightCache.ts"
15
15
  export { useLruCache } from "./useLruCache.ts"
16
16
  export { useWheelBridge, type WheelBridgeOptions, type WheelBridgeTarget } from "./useWheelBridge.ts"
17
17
  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"
18
19
  export {
19
20
  VirtualScroll,
20
21
  type VirtualScrollBehaviorOptions,
@@ -348,3 +348,123 @@
348
348
  clip-path: inset(50%);
349
349
  white-space: nowrap;
350
350
  }
351
+
352
+ /* ============================================================
353
+ * VirtualGrid (v3.2.0) — 汎用 2D 仮想化。横軸は残差 transform
354
+ * (--aqvs-grid-hx-residual — 書き手は VirtualGrid の root 直書きのみ。
355
+ * 消費者は .aqvs-grid-row の translateX、凍結モード (v3.3.0) では
356
+ * .aqvs-grid-row-scroll-inner の calc(残差 − W_F))。
357
+ * grid-template-rows の minmax(0, 1fr) は必須 — 素の 1fr は暗黙
358
+ * min-content 下限で縮小追随が壊れる (既知クラスの再発防止)。
359
+ * ============================================================ */
360
+ .aqvs-grid {
361
+ position: relative;
362
+ display: grid;
363
+ grid-template-rows: minmax(0, 1fr) auto;
364
+ width: 100%;
365
+ height: 100%;
366
+ overflow: hidden;
367
+ }
368
+
369
+ /* 凍結行帯 (行凍結設計 §4-2): R > 0 のときだけルートへ行を 1 本足す (auto = H_F)。
370
+ * R = 0 はクラスも帯要素も出力されず従来テンプレートと恒等 (F = 0 恒等契約の行版)。 */
371
+ .aqvs-grid-has-frozen-rows {
372
+ grid-template-rows: auto minmax(0, 1fr) auto;
373
+ }
374
+
375
+ .aqvs-grid-frozen-rows {
376
+ position: relative;
377
+ min-width: 0;
378
+ overflow: hidden;
379
+ }
380
+
381
+ /* 帯行の中身 (.aqvs-grid-row — height 100%) は帯行枠へそのまま追随する (VirtualScroll の
382
+ * item-container 内と同じ前提が成立するため追加宣言は不要)。 */
383
+ .aqvs-grid-frozen-band-row {
384
+ position: absolute;
385
+ left: 0;
386
+ /* right はインライン (= scrollBarWidth — 縦バーコーナーの予約。ペインのクリップ箱と一致させ、
387
+ * 帯行のセル / 消費側背景がバー上へはみ出さない)。overflow はその実クリップ器。 */
388
+ overflow: hidden;
389
+ }
390
+
391
+ .aqvs-grid-main {
392
+ position: relative;
393
+ min-width: 0;
394
+ min-height: 0;
395
+ }
396
+
397
+ /* 行ラッパー — セルの containing block、かつ横残差の消費者。
398
+ * 変数は残差のみ (生 hx は配らない — 深部の f32/LayoutUnit 破綻の根治)。
399
+ * ❗ 消費者はグリッド所有のこのクラスに限る: contentProps className 経由でペインの
400
+ * コンテンツ要素へ載せる方式は、ScrollPane が contentProps を構造クラスより後に
401
+ * 展開するため `aqvs-scroll-pane-content` (overflow/position/flex) を破壊する。
402
+ * クリップはペインのコンテンツ要素 (overflow: hidden) が従来どおり担う。
403
+ * 入れ子の素の VirtualScroll / 内側の VirtualGrid には波及しない (このクラスは
404
+ * VirtualGrid の renderRow だけが出力し、変数はそれぞれの root でスコープされる)。 */
405
+ .aqvs-grid-row {
406
+ position: relative;
407
+ width: 100%;
408
+ height: 100%;
409
+ overflow: visible;
410
+ transform: translateX(var(--aqvs-grid-hx-residual, 0px));
411
+ }
412
+
413
+ /* セル — left は「列絶対座標 − 列アンカー」(常に有界)。 */
414
+ .aqvs-grid-cell {
415
+ position: absolute;
416
+ top: 0;
417
+ height: 100%;
418
+ overflow: hidden;
419
+ }
420
+
421
+ /* 凍結モード (v3.3.0 — 設計 §8): 行は静的化する (残差はスクロール帯 inner が消費)。
422
+ * 複合セレクタ (0,2,0) は意図的 — .aqvs-grid-row (0,1,0) の translateX をソース順序に
423
+ * 依存せず常に上書きする (ルール並べ替えで沈黙退行しない特異度設計)。 */
424
+ .aqvs-grid-row.aqvs-grid-row-frozen-host {
425
+ transform: none;
426
+ }
427
+
428
+ /* スクロール帯の静的クリップ (§8-b/c): 左端 = W_F。transform を持つ要素は自分のクリップ箱ごと
429
+ * 動くため、クリップ (この要素) と残差 translate (子の inner) は必ず分離する。 */
430
+ .aqvs-grid-row-scroll {
431
+ position: absolute;
432
+ top: 0;
433
+ height: 100%;
434
+ left: var(--aqvs-grid-frozen-width, 0px);
435
+ right: 0;
436
+ overflow: hidden;
437
+ }
438
+
439
+ /* スクロール帯の座標ホルダー — 残差 − W_F の原点シフト項 (§8-b)。クリップ原点が W_F にあるため
440
+ * セル最終位置は (列絶対 − colAnchor) + (colAnchor − hx − W_F) + W_F = 列絶対 − hx で不変。 */
441
+ .aqvs-grid-row-scroll-inner {
442
+ position: absolute;
443
+ top: 0;
444
+ height: 100%;
445
+ left: 0;
446
+ width: 100%;
447
+ transform: translateX(calc(var(--aqvs-grid-hx-residual, 0px) - var(--aqvs-grid-frozen-width, 0px)));
448
+ }
449
+
450
+ /* 横バー帯。 */
451
+ .aqvs-grid-hbar-strip {
452
+ position: relative;
453
+ min-width: 0;
454
+ /* 凍結列帯の整列 (実証是正): トラック原点をスクロール帯開始 (W_F) へ。F = 0 は変数不在で
455
+ * 0px — DOM / 描画とも従来恒等 (インライン属性を足さないため §8 の恒等契約を保つ)。 */
456
+ padding-left: var(--aqvs-grid-frozen-width, 0px);
457
+ }
458
+
459
+ /* 読み上げ用リージョン (視覚非表示 — sr-only 相当)。 */
460
+ .aqvs-grid-live-region {
461
+ position: absolute;
462
+ width: 1px;
463
+ height: 1px;
464
+ margin: -1px;
465
+ padding: 0;
466
+ overflow: hidden;
467
+ clip: rect(0 0 0 0);
468
+ white-space: nowrap;
469
+ border: 0;
470
+ }
@@ -160,9 +160,9 @@ export class FenwickMapTree {
160
160
  * @description Fenwick Tree の初期化。
161
161
  * @param {number} size - The total number of items.
162
162
  * @param {number | ((index: number) => number)} valueOrFn - The value for all elements, or a function to generate values.
163
- * @param {{ sampleRange?: { from: number; to: number }, materialize?: boolean }} [options] - Optional settings for initialization.
163
+ * @param {{ sampleRange?: { from: number; to: number }, materialize?: boolean, baseValue?: number }} [options] - Optional settings for initialization (`baseValue` skips sampling — see reset). / 初期化オプション (`baseValue` はサンプリング省略 — reset 参照)。
164
164
  */
165
- constructor(size: number, valueOrFn: number | ((index: number) => number), options?: { sampleRange?: { from: number; to: number }; materialize?: boolean }) {
165
+ constructor(size: number, valueOrFn: number | ((index: number) => number), options?: { sampleRange?: { from: number; to: number }; materialize?: boolean; baseValue?: number }) {
166
166
  this.reset(size, valueOrFn, options)
167
167
  }
168
168
 
@@ -179,9 +179,9 @@ export class FenwickMapTree {
179
179
  * throw しつつ木を半構築で残さず直前の状態を無傷に保つ方針。
180
180
  * @param {number} size - The total number of items.
181
181
  * @param {number | ((index: number) => number)} valueOrFn - The value for all elements, or a function to generate values.
182
- * @param {{ sampleRange?: { from: number; to: number }, materialize?: boolean }} [options] - Optional settings for initialization.
182
+ * @param {{ sampleRange?: { from: number; to: number }, materialize?: boolean, baseValue?: number }} [options] - Optional settings for initialization. `baseValue` (finite, >= 0 — RangeError otherwise) skips all sampling and fixes the uniform estimate for unmaterialized items (v3.2.0, the VirtualGrid defaultColWidth path). / 初期化オプション。`baseValue` (有限かつ 0 以上 — 違反は RangeError) は全サンプリングを省略し未具現化アイテムの見積りを固定する (v3.2.0、VirtualGrid defaultColWidth 経路)。
183
183
  */
184
- reset(size: number, valueOrFn: number | ((index: number) => number), options?: { sampleRange?: { from: number; to: number }; materialize?: boolean }) {
184
+ reset(size: number, valueOrFn: number | ((index: number) => number), options?: { sampleRange?: { from: number; to: number }; materialize?: boolean; baseValue?: number }) {
185
185
  // size を正規化する: 非有限 (NaN/Infinity)・非正は 0、小数は切り捨て。
186
186
  // NaN size は `this.size > 0` 系の判定をすり抜けて total を NaN 汚染するため入口で遮断する。
187
187
  const safeSize = Number.isFinite(size) && size > 0 ? Math.trunc(size) : 0
@@ -201,7 +201,27 @@ export class FenwickMapTree {
201
201
  // (半構築状態を残さない)。検証済みの値だけを後段のコミットフェーズで木へ流し込む。
202
202
  let baseValue = 0
203
203
  let seed: { from: number; values: number[] } | undefined
204
- if (safeSize > 0) {
204
+ // 明示 baseValue (v3.2.0): サンプリングを丸ごと省略し、未具現化アイテムの見積りを
205
+ // 呼び出し側が確定させる (VirtualGrid の defaultColWidth 経路)。非有限・負は fail-fast
206
+ // (黙って 0 やサンプリングへ読み替えない)
207
+ const explicitBase = options?.baseValue
208
+ if (explicitBase !== undefined && !(Number.isFinite(explicitBase) && explicitBase >= 0)) {
209
+ throw new RangeError(`[FenwickMapTree] options.baseValue must be a finite number >= 0, received ${explicitBase}.`)
210
+ }
211
+ if (safeSize > 0 && explicitBase !== undefined) {
212
+ baseValue = explicitBase
213
+ // 具現化要求は従来どおり初期可視ウィンドウのみ対象 (正確性の維持)
214
+ if (options?.materialize) {
215
+ const range = options?.sampleRange ?? { from: 0, to: Math.min(SAMPLE_COUNT - 1, safeSize - 1) }
216
+ let from = Math.max(0, Math.trunc(range.from))
217
+ let to = Math.min(Math.trunc(range.to), safeSize - 1)
218
+ if (from > to) {
219
+ from = 0
220
+ to = Math.min(SAMPLE_COUNT - 1, safeSize - 1)
221
+ }
222
+ seed = { from, values: FenwickMapTree._collectValidatedValues(valueOrFn, from, to) }
223
+ }
224
+ } else if (safeSize > 0) {
205
225
  // 初期可視ウィンドウ (= 具現化対象) の範囲を決定する。
206
226
  // baseValue の推定範囲とは分離する: 具現化は利用者が最初に見る領域だけ正確化すればよいが、
207
227
  // baseValue は未具現化の全アイテムの総高さ推定に効くため、リスト全体を代表する値が必要。
@@ -1358,12 +1378,12 @@ const sampleRangeChanged = (a?: { sampleRange?: { from: number; to: number } },
1358
1378
  * @param {number} size - The total number of items.
1359
1379
  * @param {number | ((index: number) => number)} valueOrFn - The value for all elements, or a function to generate values.
1360
1380
  * @param {number | ((index: number) => number)} valueOrFn - 全要素の均一な値、または値を生成する関数。不要なツリーの再作成を防ぐため、この関数は `useCallback` でメモ化すること。
1361
- * @param {{ sampleRange?: { from: number; to: number }, resetOnValueFnChange?: boolean, debug?: boolean }} [options] - Optional settings.
1362
- * @param {{ sampleRange?: { from: number; to: number } }} [options] - 初期化時のオプション設定。不要なツリーの再作成を防ぐため、このオブジェクトは `useMemo` でメモ化すること。
1381
+ * @param {{ sampleRange?: { from: number; to: number }, resetOnValueFnChange?: boolean, debug?: boolean, baseValue?: number }} [options] - Optional settings (`baseValue` skips sampling — see FenwickMapTree.reset). / オプション (`baseValue` はサンプリング省略 — FenwickMapTree.reset 参照)。
1382
+ * @param {{ sampleRange?: { from: number; to: number }, resetOnValueFnChange?: boolean, debug?: boolean, baseValue?: number }} [options] - 初期化時のオプション設定 (`baseValue` はサンプリング省略 — FenwickMapTree.reset 参照)。不要なツリーの再作成を防ぐため、このオブジェクトは `useMemo` でメモ化すること。
1363
1383
  * @returns {FenwickMapTree} The FenwickMapTree instance.
1364
1384
  * @returns {FenwickMapTree} FenwickMapTree インスタンス。
1365
1385
  */
1366
- export const useFenwickMapTree = (size: number, valueOrFn: number | ((index: number) => number), options?: { sampleRange?: { from: number; to: number }; debug?: boolean; resetOnValueFnChange?: boolean }): FenwickMapTree => {
1386
+ export const useFenwickMapTree = (size: number, valueOrFn: number | ((index: number) => number), options?: { sampleRange?: { from: number; to: number }; debug?: boolean; resetOnValueFnChange?: boolean; baseValue?: number }): FenwickMapTree => {
1367
1387
  // size を正規化する: 非有限 (NaN は Math.max(0, NaN) を素通りする)・非正は 0、小数は切り捨て。
1368
1388
  // reset / changeSize へ渡る前にここで遮断し、木の内部を NaN 汚染させない (reset と同一規則)。
1369
1389
  const validSize = Number.isFinite(size) && size > 0 ? Math.trunc(size) : 0
@@ -1380,10 +1400,12 @@ export const useFenwickMapTree = (size: number, valueOrFn: number | ((index: num
1380
1400
  // Detect changes
1381
1401
  const sizeChanged = prev.size !== validSize
1382
1402
  const valueOrFnChanged = prev.valueOrFn !== valueOrFn
1383
- // options は参照比較ではなく sampleRange の値で比較する。
1384
- // 非メモ化のインライン options でも sampleRange が同一なら reset を起こさない
1385
- // (計測済み delta を毎レンダーで破棄しないため)。
1386
- const optionsChanged = sampleRangeChanged(prev.options, options)
1403
+ // options は参照比較ではなく sampleRange / baseValue の値で比較する。
1404
+ // 非メモ化のインライン options でも値が同一なら reset を起こさない
1405
+ // (計測済み delta を毎レンダーで破棄しないため)。baseValue の値変化は未具現化アイテムの
1406
+ // 見積り総和そのものを変えるため reset ( base での再構築) を要する — 黙って旧 base の
1407
+ // まま握り潰すと総和・バー写像・深部着地が stale 化する。
1408
+ const optionsChanged = sampleRangeChanged(prev.options, options) || prev.options?.baseValue !== options?.baseValue
1387
1409
 
1388
1410
  // 旧 size が 0 の木は baseValue が未確立 (0) のまま。増分伸長 (changeSize) では
1389
1411
  // total = 0 * N = 0 で固着するため、0 -> N の伸長は resetOnValueFnChange の設定に