@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.
- package/CHANGELOG.md +86 -0
- package/README.md +34 -4
- package/dist/ScrollBar.d.cts +24 -1
- package/dist/ScrollBar.d.ts +24 -1
- package/dist/ScrollBar.d.ts.map +1 -1
- package/dist/ScrollPane.d.ts.map +1 -1
- package/dist/TapScrollCircle.d.cts +12 -5
- package/dist/TapScrollCircle.d.ts +12 -5
- package/dist/TapScrollCircle.d.ts.map +1 -1
- package/dist/VirtualGrid.d.cts +105 -3
- package/dist/VirtualGrid.d.ts +105 -3
- package/dist/VirtualGrid.d.ts.map +1 -1
- package/dist/computeTapScrollVelocity.d.cts +120 -0
- package/dist/computeTapScrollVelocity.d.ts +121 -0
- package/dist/computeTapScrollVelocity.d.ts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2428 -2152
- package/dist/styles/virtualscroll.css +1 -1
- package/dist/styles/virtualscroll.standalone.css +1 -1
- package/dist/useGridTapScroll.d.cts +83 -0
- package/dist/useGridTapScroll.d.ts +84 -0
- package/dist/useGridTapScroll.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/ScrollBar.tsx +16 -4
- package/src/ScrollPane.tsx +1 -0
- package/src/TapScrollCircle.tsx +42 -10
- package/src/VirtualGrid.tsx +559 -76
- package/src/computeTapScrollVelocity.ts +150 -0
- package/src/index.ts +15 -1
- package/src/styles/virtualscroll.css +90 -1
- package/src/useGridTapScroll.ts +370 -0
package/src/VirtualGrid.tsx
CHANGED
|
@@ -7,21 +7,29 @@
|
|
|
7
7
|
* row-axis scale profile (<= 2^53 - 1). Grid semantics (selection / editing / clipboard)
|
|
8
8
|
* are deliberately NOT owned here — consumers attach them via getCellProps / getRowProps /
|
|
9
9
|
* contentProps. Design: docs/plans/2026.09.01 virtualgrid-horizontal-axis-design-plan (v6).
|
|
10
|
+
* Since v3.6.0 the grid owns ONE unified two-axis tap circle (axis="xy", corner-anchored via the
|
|
11
|
+
* three-arm placement law) driven by useGridTapScroll; both per-bar circles are suppressed
|
|
12
|
+
* grid-side (docs/plans/2026.09.16 unified-tap-circle plan).
|
|
10
13
|
*
|
|
11
14
|
* VirtualGrid — 実証済みの VirtualScroll 行軸に、対称の横列エンジン (同一の兆スケール機構:
|
|
12
15
|
* 疎 Fenwick 幅木・共有 computeRenderingRanges (BigInt huge 分岐込み)・ANCHOR_REBASE_DISTANCE
|
|
13
16
|
* の量子化アンカー再基準化・総サイズ px を DOM に着地させない合成スクロール) を合成した
|
|
14
17
|
* 汎用 2D 仮想化プリミティブ。列数は行軸のスケールプロファイル (≤ 2^53 − 1) を完全継承する。
|
|
15
18
|
* グリッド操作の意味論 (選択 / 編集 / クリップボード) は意図的に非所有 — 消費側が
|
|
16
|
-
* getCellProps / getRowProps / contentProps で装着する。
|
|
19
|
+
* getCellProps / getRowProps / contentProps で装着する。v3.6.0 からグリッドは統合 2 軸
|
|
20
|
+
* タップサークル (axis="xy"・3 アーム則のコーナーアンカー・useGridTapScroll 駆動) を 1 つ
|
|
21
|
+
* 所有し、両バーのサークルはグリッド側で抑止する。
|
|
17
22
|
*/
|
|
18
23
|
|
|
19
24
|
import { type CSSProperties, forwardRef, type HTMLAttributes, type ReactNode, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react"
|
|
20
25
|
import { twMerge } from "tailwind-merge"
|
|
26
|
+
import type { TapScrollAxisSpeedParams } from "./computeTapScrollVelocity.ts"
|
|
21
27
|
import { Logger } from "./logger.ts"
|
|
22
|
-
import { ScrollBar } from "./ScrollBar.tsx"
|
|
28
|
+
import { resolveTapScrollCircleOptions, ScrollBar, type ScrollBarTapCircleOptions } from "./ScrollBar.tsx"
|
|
23
29
|
import type { ScrollPaneProps } from "./ScrollPane.tsx"
|
|
30
|
+
import { TapScrollCircle } from "./TapScrollCircle.tsx"
|
|
24
31
|
import { useFenwickMapTree } from "./useFenwickMapTree.ts"
|
|
32
|
+
import { GRID_TAP_CIRCLE_DEFAULT_OFFSET, useGridTapScroll } from "./useGridTapScroll.ts"
|
|
25
33
|
import { minmax } from "./utils.ts"
|
|
26
34
|
import { ANCHOR_REBASE_DISTANCE, computeRenderingRanges, MAX_RENDERED_ITEMS, VirtualScroll, type VirtualScrollBehaviorOptions, type VirtualScrollHandle, type VirtualScrollRange, type VirtualScrollScrollBarOptions, ZERO_HEIGHT_RUN_LIMIT } from "./VirtualScroll.tsx"
|
|
27
35
|
|
|
@@ -89,6 +97,36 @@ export const MAX_FROZEN_LEADING_COLS = 128
|
|
|
89
97
|
*/
|
|
90
98
|
export const MAX_FROZEN_LEADING_ROWS = 128
|
|
91
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Hard cap for `frozenTrailingCols` (v3.5.0 trailing-freeze plan §3). The trailing band is NOT
|
|
102
|
+
* windowed — every trailing column materializes in every rendered row and is EXEMPT from the
|
|
103
|
+
* MAX_RENDERED_CELLS truncation (the scroll-band budget is reduced by the trailing count
|
|
104
|
+
* instead) — so the cap is what bounds the band: worst case 128 x MAX_RENDERED_ITEMS rows, and
|
|
105
|
+
* W_T <= 128 x MAX_TRACK_SIZE = 2^25 (the measured layout wall; practical W_T <= viewport <<
|
|
106
|
+
* 2^24). Violations fail fast with a RangeError.
|
|
107
|
+
*
|
|
108
|
+
* `frozenTrailingCols` の上限 (3.5.0 末尾凍結プラン §3)。末尾帯は窓化されず全描画行で具現化し、
|
|
109
|
+
* かつ MAX_RENDERED_CELLS の打切り対象外 (代わりにスクロール帯予算を末尾数ぶん控除) のため、
|
|
110
|
+
* 帯を有界にするのはこの上限そのもの: 最悪 128 × MAX_RENDERED_ITEMS 行、W_T ≤ 128 ×
|
|
111
|
+
* MAX_TRACK_SIZE = 2^25 (実測壁ちょうど — 実運用は W_T ≤ viewport ≪ 2^24)。違反は fail-fast の
|
|
112
|
+
* RangeError。
|
|
113
|
+
*/
|
|
114
|
+
export const MAX_FROZEN_TRAILING_COLS = 128
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Hard cap for `frozenTrailingRows` (v3.5.0 trailing-freeze plan §3). The trailing-row band is
|
|
118
|
+
* grid-owned and OUT of the embedded VirtualScroll (the SECOND out-of-pane band, below the
|
|
119
|
+
* pane): every band row materializes every rendered column and is exempt from
|
|
120
|
+
* MAX_RENDERED_CELLS (the budget denominator counts band rows instead), so the cap bounds the
|
|
121
|
+
* band itself. Violations fail fast with a RangeError.
|
|
122
|
+
*
|
|
123
|
+
* `frozenTrailingRows` の上限 (3.5.0 末尾凍結プラン §3)。末尾行帯はグリッド所有で埋め込み
|
|
124
|
+
* VirtualScroll の外 (ペインの**下**の第 2 帯外バンド)。帯行は描画列を全て具現化し
|
|
125
|
+
* MAX_RENDERED_CELLS の打切り対象外 (予算分母に帯行数を算入) のため、帯を有界にするのは
|
|
126
|
+
* この上限そのもの。違反は fail-fast の RangeError。
|
|
127
|
+
*/
|
|
128
|
+
export const MAX_FROZEN_TRAILING_ROWS = 128
|
|
129
|
+
|
|
92
130
|
/**
|
|
93
131
|
* Maximum number of CONSECUTIVE NON-CONVERGENT self-heal flushes. A stable accessor converges in
|
|
94
132
|
* ONE flush (tree == accessor afterwards), so the next collect pass is clean and RESETS this
|
|
@@ -303,6 +341,40 @@ export type VirtualGridProps<T> = {
|
|
|
303
341
|
* 通知しゼロ描画)。
|
|
304
342
|
*/
|
|
305
343
|
frozenLeadingRows?: number
|
|
344
|
+
/**
|
|
345
|
+
* Number of trailing columns frozen at the right edge (v3.5.0 trailing-freeze plan §3;
|
|
346
|
+
* default 0). Integer in [0, MAX_FROZEN_TRAILING_COLS] — RangeError otherwise (fail-fast).
|
|
347
|
+
* Dynamic clamp (documented contract): min(T, colCount − effectiveFrozenCols) — the LEADING
|
|
348
|
+
* band wins the count space. Trailing cells render in a right-anchored clip outside the
|
|
349
|
+
* anchor machinery (band-local lefts); `--aqvs-grid-trailing-width` is written iff T > 0.
|
|
350
|
+
* `scrollToCell` / `initialScrollAnchor` targeting a trailing column are horizontal no-ops
|
|
351
|
+
* (always visible). T = 0 is structurally identical to the pre-trailing DOM.
|
|
352
|
+
* 右端に凍結する末尾列数 (3.5.0 末尾凍結プラン §3。既定 0)。[0, MAX_FROZEN_TRAILING_COLS]
|
|
353
|
+
* の整数 — 違反は RangeError (fail-fast)。動的クランプ (文書化契約): min(T, colCount −
|
|
354
|
+
* effectiveFrozenCols) — カウント空間は**先頭が勝つ**。末尾セルはアンカー機構の外の
|
|
355
|
+
* 右アンカークリップに帯ローカル left で描画し、`--aqvs-grid-trailing-width` は T > 0 の
|
|
356
|
+
* ときのみ書かれる。`scrollToCell` / `initialScrollAnchor` の末尾列狙いは横 no-op
|
|
357
|
+
* (常時可視)。T = 0 は DOM 構造まで従来と恒等。
|
|
358
|
+
*/
|
|
359
|
+
frozenTrailingCols?: number
|
|
360
|
+
/**
|
|
361
|
+
* Number of trailing rows pinned at the bottom (v3.5.0 trailing-freeze plan §3; default 0).
|
|
362
|
+
* Integer in [0, MAX_FROZEN_TRAILING_ROWS] — RangeError otherwise (fail-fast). Dynamic
|
|
363
|
+
* clamp (documented contract): min(T, rowCount − effectiveFrozenRows) — leading wins. The
|
|
364
|
+
* band is grid-owned BELOW the pane; the embedded scroll rows shrink at the TAIL only
|
|
365
|
+
* (itemCount = rowCount − R − T, getItem = i + R unchanged), so a runtime T change does NOT
|
|
366
|
+
* remount the embedded VirtualScroll (ADR-23). `scrollToCell` / `initialScrollAnchor`
|
|
367
|
+
* targeting a trailing row are vertical no-ops (always visible). T = 0 is structurally
|
|
368
|
+
* identical to the pre-trailing DOM.
|
|
369
|
+
* 下端に凍結する末尾行数 (3.5.0 末尾凍結プラン §3。既定 0)。[0, MAX_FROZEN_TRAILING_ROWS]
|
|
370
|
+
* の整数 — 違反は RangeError (fail-fast)。動的クランプ (文書化契約): min(T, rowCount −
|
|
371
|
+
* effectiveFrozenRows) — 先頭が勝つ。帯はペインの**下**のグリッド所有バンドで、埋め込み
|
|
372
|
+
* スクロール行は**末尾**だけが伸縮する (itemCount = rowCount − R − T、getItem = i + R
|
|
373
|
+
* 不変) — T の実行時変更は埋め込み VirtualScroll を再マウントしない (ADR-23)。
|
|
374
|
+
* `scrollToCell` / `initialScrollAnchor` の末尾行狙いは縦 no-op (常時可視)。T = 0 は
|
|
375
|
+
* DOM 構造まで従来と恒等。
|
|
376
|
+
*/
|
|
377
|
+
frozenTrailingRows?: number
|
|
306
378
|
/** Row-axis overscan (default 3 — a declared, intentional deviation from the bare VirtualScroll default 15; see the bounding contract). / 行軸 overscan (既定 3 — 素の既定 15 からの宣言済み意図的乖離)。 */
|
|
307
379
|
overscanRows?: number
|
|
308
380
|
/** Column-axis overscan (default 3). / 列軸 overscan (既定 3)。 */
|
|
@@ -338,7 +410,24 @@ export type VirtualGridHandle = {
|
|
|
338
410
|
scrollTo(pos: { x?: number; y?: number }): { x: number; y: number }
|
|
339
411
|
/** Relative float-accumulating path (wheel semantics; returns the applied clamped position — drag loops consume it synchronously). / 相対 float 累積経路 (ホイール同義。クランプ後位置を同期 return — ドラッグループが消費)。 */
|
|
340
412
|
scrollBy(delta: { x?: number; y?: number }): { x: number; y: number }
|
|
341
|
-
/**
|
|
413
|
+
/**
|
|
414
|
+
* Cell-targeted jump. alignY uses the row vocabulary (top/bottom/center), alignX its
|
|
415
|
+
* horizontal twin (start/end/center). No "nearest". Targeting a LEADING frozen or TRAILING
|
|
416
|
+
* frozen column is a horizontal no-op that leaves any previously armed column anchor in
|
|
417
|
+
* place (the visual status quo is preserved; manual horizontal input clears anchors as
|
|
418
|
+
* usual); targeting a leading/trailing frozen row is a vertical no-op. Band membership is
|
|
419
|
+
* judged at CALL time — a T change AFTER arming follows the ADR-23 clamp semantics: a
|
|
420
|
+
* pending row anchor whose target row lands inside the trailing band after a T increase
|
|
421
|
+
* clamps to the last scroll row. Overlap registration (ADR-19-1): when the leading +
|
|
422
|
+
* trailing band extents exceed the viewport, occluded trailing cells cannot be scrolled
|
|
423
|
+
* into view — the no-op is exact there too.
|
|
424
|
+
* セル狙いジャンプ。"nearest" は無し。先頭 / 末尾凍結列狙いの横成分は no-op で、既存の
|
|
425
|
+
* 列アンカーはそのまま残る (視覚的現状維持 — 手動横入力が従来どおり解除する)。先頭 /
|
|
426
|
+
* 末尾凍結行狙いの縦成分も no-op。帯所属の判定は**呼出し時** — 張った後の T 変更は
|
|
427
|
+
* ADR-23 のクランプ意味論 (T 増加で末尾帯入りする行を狙った保留行アンカーは最終
|
|
428
|
+
* スクロール行へクランプ) に従う。重複登記 (ADR-19-1): 先頭 + 末尾の帯寸がビューポートを
|
|
429
|
+
* 超えるとき、隠れた末尾セルはどのスクロール位置でも可視化できない — no-op はそこでも正確。
|
|
430
|
+
*/
|
|
342
431
|
scrollToCell(row: number, col: number, options?: { alignY?: "top" | "bottom" | "center"; alignX?: "start" | "end" | "center"; offsetX?: number; offsetY?: number }): void
|
|
343
432
|
/** Synchronous-fresh position read (internal refs — safe mid-event; {-1,-1} pre-attach). / 同期・最新の位置読み (内部 ref — イベント中も安全。未接続 {-1,-1})。 */
|
|
344
433
|
getScrollPosition(): { x: number; y: number }
|
|
@@ -347,7 +436,7 @@ export type VirtualGridHandle = {
|
|
|
347
436
|
getViewportSize(): { width: number; height: number }
|
|
348
437
|
getContentSize(): { width: number; height: number }
|
|
349
438
|
getRange(): VirtualGridRange | null
|
|
350
|
-
/** Pair-call seam: getRowHeight stays the truth;
|
|
439
|
+
/** Pair-call seam: getRowHeight stays the truth. 3-way split — leading band rows bump the frozen-row epoch, trailing band rows bump the trailing-row epoch (accessor re-read; the bands hold no tree), scroll rows delegate to the row updateItemSize at −R. / 対呼び出しシーム (getRowHeight が正)。3 分岐 — 先頭帯行は凍結行 epoch、末尾帯行は末尾行 epoch の繰上げ (アクセサ再読 — 帯に木は無い)、スクロール行は −R で行 updateItemSize へ委譲。 */
|
|
351
440
|
updateRowSize(row: number, px: number): void
|
|
352
441
|
/**
|
|
353
442
|
* Column twin: validates (MAX_TRACK_SIZE fail-fast), updates the width tree, and bumps the
|
|
@@ -360,8 +449,25 @@ export type VirtualGridHandle = {
|
|
|
360
449
|
* バッチングで 1 コミットに合流 (設計の rAF 合流意図のコミット粒度での実現)。
|
|
361
450
|
*/
|
|
362
451
|
updateColSize(col: number, px: number): void
|
|
363
|
-
/**
|
|
364
|
-
|
|
452
|
+
/**
|
|
453
|
+
* Effective frozen-band sizes on both axes and both ends (v3.5.0 — a non-breaking field
|
|
454
|
+
* extension of the {cols, width} → {cols, width, rows, height} precedent). cols/rows/
|
|
455
|
+
* trailingCols/trailingRows = dynamic post-clamp counts; width/height/trailingWidth/
|
|
456
|
+
* trailingHeight = TREE px (the trailing sizes are tree SUFFIX sums); trailingVisibleWidth/
|
|
457
|
+
* trailingVisibleHeight = viewport-clipped `min(tree, max(0, viewport − leading))` — THE
|
|
458
|
+
* single source for W_T_vis / H_T_vis that every consumer judgment/write path cites (never
|
|
459
|
+
* re-derive the formula). Pre-attach the grid viewport reads {0, 0}, so the vis fields are
|
|
460
|
+
* 0 (an initial measured state, not a sentinel) while the tree fields stay tree px.
|
|
461
|
+
* 実効凍結帯寸法 — 両軸・両端 (v3.5.0。{cols, width} → {cols, width, rows, height} の
|
|
462
|
+
* 前例に続く非破壊のフィールド追加)。cols / rows / trailingCols / trailingRows は動的
|
|
463
|
+
* クランプ後の実効カウント、width / height / trailingWidth / trailingHeight は**木** px
|
|
464
|
+
* (末尾寸は木の接尾辞和)、trailingVisibleWidth / trailingVisibleHeight はビューポート
|
|
465
|
+
* クリップ `min(木, max(0, viewport − 先頭))` — W_T_vis / H_T_vis の**単一情報源**で、
|
|
466
|
+
* 消費側の判定 / 書込み経路は全てこのフィールドを名指しで読む (式の再導出禁止)。
|
|
467
|
+
* プレアタッチはビューポート {0, 0} のため vis フィールドは 0 (番兵ではなく初期実測状態)、
|
|
468
|
+
* 木フィールドは常に木 px。
|
|
469
|
+
*/
|
|
470
|
+
getFrozenSize(): { cols: number; width: number; rows: number; height: number; trailingCols: number; trailingRows: number; trailingWidth: number; trailingHeight: number; trailingVisibleWidth: number; trailingVisibleHeight: number }
|
|
365
471
|
/** 2-axis wheel bridge entry: vertical → embedded VirtualScroll, horizontal → hx. Returns whether the event was consumed (WheelBridgeTarget contract). / 2 軸ホイールブリッジ口。消費有無を返す (ブリッジ契約)。 */
|
|
366
472
|
applyWheel(event: WheelEvent): boolean
|
|
367
473
|
/** Focuses the row wrapper at `row` (scroll rows via the −R-shifted row-side focusItemAtIndex — enableKeyboardNavigation applies; frozen band rows are focused directly and need no option). / 行ラッパーへフォーカス (スクロール行は −R シフトの行側 focusItemAtIndex — enableKeyboardNavigation が前提。凍結帯行は直接フォーカスでオプション不要)。 */
|
|
@@ -525,6 +631,58 @@ const collectFrozenColumns = (frozenCols: number, getColWidth: (col: number) =>
|
|
|
525
631
|
return { columns, widthUpdates, width: runningLeft }
|
|
526
632
|
}
|
|
527
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Enumerates the trailing frozen band (v3.5.0 trailing-freeze plan §6.1-5 — the suffix twin of
|
|
636
|
+
* collectFrozenColumns): columns [colCount − trailingCols, colCount) with BAND-LOCAL lefts (a
|
|
637
|
+
* running prefix that starts at 0 — the right-anchored inner owns the placement, so trailing
|
|
638
|
+
* cells never take the anchor subtraction; W_T is bounded by 128 x MAX_TRACK_SIZE = 2^25),
|
|
639
|
+
* zero-width columns skipped (no DOM — the shared hidden-track discipline), and width
|
|
640
|
+
* self-heal deltas collected under the same tree-is-position / accessor-is-width split as
|
|
641
|
+
* collectVisibleColumns. Returns width = W_T, the TREE suffix width of the band.
|
|
642
|
+
*
|
|
643
|
+
* 末尾凍結帯の列挙 (3.5.0 末尾凍結プラン §6.1-5 — collectFrozenColumns の接尾辞双子)。列
|
|
644
|
+
* [colCount − trailingCols, colCount) を**帯ローカル** left (0 始まりの running 接頭辞 —
|
|
645
|
+
* 配置は右アンカー inner が担い、末尾セルはアンカー減算を決して取らない。W_T は 128 × 2^18 =
|
|
646
|
+
* 2^25 で有界) で並べ、幅 0 列は DOM 非生成 (非表示トラックの共有規律)、幅 self-heal 差分は
|
|
647
|
+
* collectVisibleColumns と同じ「位置は木・幅はアクセサ」分担で収集する。返す width = W_T
|
|
648
|
+
* (帯の**木**接尾辞幅)。
|
|
649
|
+
*/
|
|
650
|
+
const collectTrailingColumns = (colCount: number, trailingCols: number, getColWidth: (col: number) => number, colTree: ReturnType<typeof useFenwickMapTree>): { columns: PlacedColumn[]; widthUpdates: Array<{ index: number; value: number }>; width: number } => {
|
|
651
|
+
const columns: PlacedColumn[] = []
|
|
652
|
+
const widthUpdates: Array<{ index: number; value: number }> = []
|
|
653
|
+
let runningLeft = 0
|
|
654
|
+
// T ≤ MAX_FROZEN_TRAILING_COLS のため素朴な O(T) 走査で十分 (幅 0 ジャンプ機構は不要)
|
|
655
|
+
for (let col = colCount - trailingCols; col < colCount; col += 1) {
|
|
656
|
+
const width = getColWidth(col)
|
|
657
|
+
const cached = colTree.get(col)
|
|
658
|
+
if (width !== cached) {
|
|
659
|
+
widthUpdates.push({ index: col, value: width })
|
|
660
|
+
}
|
|
661
|
+
if (width > 0) {
|
|
662
|
+
columns.push({ col, left: runningLeft, width })
|
|
663
|
+
}
|
|
664
|
+
runningLeft += cached
|
|
665
|
+
}
|
|
666
|
+
return { columns, widthUpdates, width: runningLeft }
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* THE single JS source for W_T_vis / H_T_vis (v3.5.0 trailing-freeze plan §2.1-4): clips a
|
|
671
|
+
* trailing band's TREE extent to what the viewport can show after the leading band took its
|
|
672
|
+
* share — `min(trailing, max(0, viewport − leading))`. Consumers are exactly three: the
|
|
673
|
+
* trailing-rows band template height, the hbar paddingRight, and the getFrozenSize
|
|
674
|
+
* trailingVisibleWidth/Height export. Never re-derive the formula elsewhere (the CSS max()
|
|
675
|
+
* left edge of `.aqvs-grid-row-trailing` is the registered calc() equivalent — the ONLY
|
|
676
|
+
* exception, annotated in the stylesheet).
|
|
677
|
+
*
|
|
678
|
+
* W_T_vis / H_T_vis の**単一 JS 源** (3.5.0 末尾凍結プラン §2.1-4)。末尾帯の**木**寸を
|
|
679
|
+
* 「先頭帯が取った残り」へクリップする `min(末尾, max(0, viewport − 先頭))`。消費先は
|
|
680
|
+
* 末尾行帯テンプレート高 / hbar paddingRight / getFrozenSize の vis export の 3 点ちょうど —
|
|
681
|
+
* 式を他所へ複製しない (`.aqvs-grid-row-trailing` の CSS max() 左端だけが登記済みの calc()
|
|
682
|
+
* 等価形で、スタイルシート側にインライン登記済み)。
|
|
683
|
+
*/
|
|
684
|
+
const trailingVisibleSize = (trailingSize: number, frozenSize: number, viewportSize: number): number => Math.min(trailingSize, Math.max(0, viewportSize - frozenSize))
|
|
685
|
+
|
|
528
686
|
/** Internal committed column-window state (anchor rides with the window — the M10 coherence unit). / コミット済み列窓状態 (アンカーは窓と一体 — M10 整合の単位)。 */
|
|
529
687
|
type ColumnWindowState = {
|
|
530
688
|
renderingColStart: number
|
|
@@ -575,6 +733,8 @@ const VirtualGridInner = <T,>(
|
|
|
575
733
|
onRowFocus,
|
|
576
734
|
frozenLeadingCols = 0,
|
|
577
735
|
frozenLeadingRows = 0,
|
|
736
|
+
frozenTrailingCols = 0,
|
|
737
|
+
frozenTrailingRows = 0,
|
|
578
738
|
horizontalKeyInputs,
|
|
579
739
|
horizontalKeyStep,
|
|
580
740
|
onScroll,
|
|
@@ -650,17 +810,34 @@ const VirtualGridInner = <T,>(
|
|
|
650
810
|
/** Fresh W_F for drag loops / clamps (mirrored from the frozen memo each render). / ドラッグループ / クランプ用の鮮度 W_F (凍結 memo から毎レンダー写像)。 */
|
|
651
811
|
const frozenWidthRef = useRef(0)
|
|
652
812
|
|
|
813
|
+
// ---- 末尾凍結列帯 (3.5.0 プラン §6.1-3) — 先頭側と同文の fail-fast。動的クランプは
|
|
814
|
+
// min(T, colCount − 先頭実効) で**先頭がカウント空間で勝つ** (ADR-19-1 の文書化契約) ----
|
|
815
|
+
if (!(Number.isInteger(frozenTrailingCols) && frozenTrailingCols >= 0 && frozenTrailingCols <= MAX_FROZEN_TRAILING_COLS)) {
|
|
816
|
+
throw new RangeError(`[VirtualGrid] frozenTrailingCols must be an integer in [0, ${MAX_FROZEN_TRAILING_COLS}], received ${frozenTrailingCols}.`)
|
|
817
|
+
}
|
|
818
|
+
const effectiveTrailingCols = Math.min(frozenTrailingCols, colCount - effectiveFrozenCols)
|
|
819
|
+
/** Fresh W_T for drag loops / clamps (mirrored from the trailing memo each render). / ドラッグループ / クランプ用の鮮度 W_T (末尾 memo から毎レンダー写像)。 */
|
|
820
|
+
const trailingWidthRef = useRef(0)
|
|
821
|
+
|
|
653
822
|
// ---- 凍結行帯 (行凍結設計 §4-1) — 列と同文の fail-fast + 動的クランプ ----
|
|
654
823
|
if (!(Number.isInteger(frozenLeadingRows) && frozenLeadingRows >= 0 && frozenLeadingRows <= MAX_FROZEN_LEADING_ROWS)) {
|
|
655
824
|
throw new RangeError(`[VirtualGrid] frozenLeadingRows must be an integer in [0, ${MAX_FROZEN_LEADING_ROWS}], received ${frozenLeadingRows}.`)
|
|
656
825
|
}
|
|
657
826
|
const effectiveFrozenRows = Math.min(frozenLeadingRows, rowCount)
|
|
658
|
-
|
|
659
|
-
|
|
827
|
+
|
|
828
|
+
// ---- 末尾凍結行帯 (3.5.0 プラン §6.1-3) — 列側と同文の fail-fast + 先頭勝ちクランプ ----
|
|
829
|
+
if (!(Number.isInteger(frozenTrailingRows) && frozenTrailingRows >= 0 && frozenTrailingRows <= MAX_FROZEN_TRAILING_ROWS)) {
|
|
830
|
+
throw new RangeError(`[VirtualGrid] frozenTrailingRows must be an integer in [0, ${MAX_FROZEN_TRAILING_ROWS}], received ${frozenTrailingRows}.`)
|
|
831
|
+
}
|
|
832
|
+
const effectiveTrailingRows = Math.min(frozenTrailingRows, rowCount - effectiveFrozenRows)
|
|
833
|
+
/** Scroll-row count in the SHIFTED space (design §2; the tail loses T — the embedded VirtualScroll's itemCount). / シフト空間のスクロール行数 (末尾は −T — 埋め込み VirtualScroll の itemCount)。 */
|
|
834
|
+
const scrollRowCount = Math.max(0, rowCount - effectiveFrozenRows - effectiveTrailingRows)
|
|
660
835
|
/** Degenerate-band flag ref (H_F >= measured viewport — §3.3 の縮退帯と同じ正準空窓へ). / 縮退帯フラグ ref (H_F ≥ 実測ビューポート — §3.3 縮退帯と同じ正準空窓)。 */
|
|
661
836
|
const degenerateBandRef = useRef(false)
|
|
662
837
|
/** Fresh H_F for handle reads (mirrored from the band memo each render). / ハンドル読み用の鮮度 H_F (帯 memo から毎レンダー写像)。 */
|
|
663
838
|
const frozenHeightRef = useRef(0)
|
|
839
|
+
/** Fresh H_T for handle reads (mirrored from the trailing band memo each render). / ハンドル読み用の鮮度 H_T (末尾帯 memo から毎レンダー写像)。 */
|
|
840
|
+
const trailingHeightRef = useRef(0)
|
|
664
841
|
|
|
665
842
|
// ---- 凍結行帯の列挙 (行凍結設計 §4-2) — グリッド所有の帯外バンド。行高の真実は
|
|
666
843
|
// アクセサ直読 (列帯と違い第 2 の格納庫 = 木 が無いためヒール機構は不要 — 更新経路は
|
|
@@ -683,6 +860,28 @@ const VirtualGridInner = <T,>(
|
|
|
683
860
|
return { frozenRows: rows, frozenHeight: running }
|
|
684
861
|
}, [effectiveFrozenRows, frozenRowEpoch, validatedGetRowHeight])
|
|
685
862
|
frozenHeightRef.current = frozenHeight
|
|
863
|
+
|
|
864
|
+
// ---- 末尾行帯の列挙 (3.5.0 プラン §6.1-11) — 先頭行帯と同形の帯外バンド。木もヒール機構も
|
|
865
|
+
// 持たない (アクセサが唯一の真実 — 先頭行帯の宣言済み非対称と同文)。top は帯ローカル
|
|
866
|
+
// 接頭辞和 (inner の bottom 定着が右アンカーの行版を担う)。高さ 0 行は DOM 非生成 ----
|
|
867
|
+
const [trailingRowEpoch, setTrailingRowEpoch] = useState(0)
|
|
868
|
+
const { trailingRows, trailingHeight } = useMemo(() => {
|
|
869
|
+
void trailingRowEpoch
|
|
870
|
+
if (effectiveTrailingRows === 0) {
|
|
871
|
+
return { trailingRows: [] as Array<{ row: number; top: number; height: number }>, trailingHeight: 0 }
|
|
872
|
+
}
|
|
873
|
+
const rows: Array<{ row: number; top: number; height: number }> = []
|
|
874
|
+
let running = 0
|
|
875
|
+
for (let row = rowCount - effectiveTrailingRows; row < rowCount; row += 1) {
|
|
876
|
+
const height = validatedGetRowHeight(row)
|
|
877
|
+
if (height > 0) {
|
|
878
|
+
rows.push({ row, top: running, height })
|
|
879
|
+
}
|
|
880
|
+
running += height
|
|
881
|
+
}
|
|
882
|
+
return { trailingRows: rows, trailingHeight: running }
|
|
883
|
+
}, [effectiveTrailingRows, rowCount, trailingRowEpoch, validatedGetRowHeight])
|
|
884
|
+
trailingHeightRef.current = trailingHeight
|
|
686
885
|
/** Shifted row-height accessor for the embedded VirtualScroll (design §2). / 埋め込み VirtualScroll 用のシフト済み行高アクセサ (設計 §2)。 */
|
|
687
886
|
const scrollGetRowHeight = useCallback((index: number) => validatedGetRowHeight(index + effectiveFrozenRows), [validatedGetRowHeight, effectiveFrozenRows])
|
|
688
887
|
// 行フォーカス通知はフル行空間へ +R 翻訳する (シフト漏れの是正 — 素通しは R > 0 でフル行 r の
|
|
@@ -696,6 +895,8 @@ const VirtualGridInner = <T,>(
|
|
|
696
895
|
const shiftedRowFocus = useMemo(() => (hasRowFocus ? (index: number) => onRowFocusRef.current?.(index + effectiveFrozenRows) : undefined), [effectiveFrozenRows, hasRowFocus])
|
|
697
896
|
/** Reactive key for H_F consumers (memo edge — the ref alone would not invalidate). / H_F 消費 memo の反応辺キー (ref 直読は失効しない)。 */
|
|
698
897
|
const frozenHeightEpochKey = frozenHeight
|
|
898
|
+
/** Reactive key for H_T consumers (the trailing twin). / H_T 消費 memo の反応辺キー (末尾双子)。 */
|
|
899
|
+
const trailingHeightEpochKey = trailingHeight
|
|
699
900
|
|
|
700
901
|
// ---- R の実行時変更 (R1 是正): シフト空間の再ペアリングは埋め込み木にとって「中間削除」で、
|
|
701
902
|
// 木の changeSize (末尾意味論) では歪む (実測 +228px の総高汚染)。key 再マウントで木を
|
|
@@ -756,8 +957,8 @@ const VirtualGridInner = <T,>(
|
|
|
756
957
|
const viewport = explicitViewport ?? measuredViewport
|
|
757
958
|
const viewportRef = useRef(viewport)
|
|
758
959
|
viewportRef.current = viewport
|
|
759
|
-
/** Degenerate band: H_F fills the measured viewport — scroll rows go canonical-EMPTY (§3.3
|
|
760
|
-
const degenerateBand = viewport.height > 0 && viewport.height - frozenHeight <= 0
|
|
960
|
+
/** Degenerate band: H_F + H_T fills the measured viewport — scroll rows go canonical-EMPTY (§3.3 縮退帯の縦対称; 3.5.0 で −H_T を一般化). / 縮退帯: H_F + H_T が実測ビューポートを食い尽くす — スクロール行は正準空窓 (§3.3 の縦対称。3.5.0 で −H_T を一般化)。 */
|
|
961
|
+
const degenerateBand = viewport.height > 0 && viewport.height - frozenHeight - trailingHeight <= 0
|
|
761
962
|
degenerateBandRef.current = degenerateBand
|
|
762
963
|
const hasExplicitViewport = explicitViewport !== null
|
|
763
964
|
useLayoutEffect(() => {
|
|
@@ -793,10 +994,31 @@ const VirtualGridInner = <T,>(
|
|
|
793
994
|
root.style.setProperty("--aqvs-grid-hx-residual", `${committedAnchorRef.current - hxRef.current}px`)
|
|
794
995
|
}, [])
|
|
795
996
|
|
|
997
|
+
/**
|
|
998
|
+
* Fresh scroll-band width from refs (v3.5.0 trailing-freeze plan §6.1-6 — the ONE ref-space
|
|
999
|
+
* source): viewport minus BOTH band extents. Replaces the three former inline
|
|
1000
|
+
* `max(0, viewport − W_F)` sites (applyHx / refreshColumns / resolveColumnAnchorHx) so a
|
|
1001
|
+
* W_T change can never go stale in just one of them. T = 0 reduces to the pre-trailing form.
|
|
1002
|
+
* ref 空間のスクロール帯幅 (3.5.0 末尾凍結プラン §6.1-6 — ref 面の単一源)。viewport から
|
|
1003
|
+
* 両端の帯寸を控除する。旧 3 箇所のインライン `max(0, viewport − W_F)` を置換し、W_T 変化が
|
|
1004
|
+
* 1 箇所だけ stale 化する余地を構造的に断つ。T = 0 は従来式へ恒等縮退。
|
|
1005
|
+
*/
|
|
1006
|
+
const scrollBandWidthRef = useCallback(() => Math.max(0, viewportRef.current.width - frozenWidthRef.current - trailingWidthRef.current), [])
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* The ONE extracted x clamp law (v3.6.0 §3.5-7 SSOT): `max(0, totalWidth − W_F − W_T −
|
|
1010
|
+
* bandWidth)`. Consumed by `applyHx` (pure refactor — behavior byte-identical) AND the tap
|
|
1011
|
+
* driver (boundary / pinned detection + `gridScrollable`). No second copy of the law exists.
|
|
1012
|
+
* 唯一抽出の x クランプ則 (v3.6.0 §3.5-7 SSOT)。`applyHx` (純リファクタ — 挙動バイト恒等)
|
|
1013
|
+
* とタップドライバ (境界 / ピン判定 + `gridScrollable`) の両方が消費する。第 2 の複製は
|
|
1014
|
+
* 存在しない。
|
|
1015
|
+
*/
|
|
1016
|
+
const getMaxHx = useCallback(() => Math.max(0, totalWidthRef.current - frozenWidthRef.current - trailingWidthRef.current - scrollBandWidthRef()), [scrollBandWidthRef])
|
|
1017
|
+
|
|
796
1018
|
// ---- 列窓の再計算 (setState は窓 / アンカー変化時のみ — key 比較で bail) ----
|
|
797
1019
|
const refreshColumns = useCallback(() => {
|
|
798
|
-
// 凍結帯 (§8-a): 窓探索は木座標 W_F + hx 始まり・帯幅 = viewport − W_F
|
|
799
|
-
const bandWidth =
|
|
1020
|
+
// 凍結帯 (§8-a): 窓探索は木座標 W_F + hx 始まり・帯幅 = viewport − W_F − W_T (3.5.0 一般化)
|
|
1021
|
+
const bandWidth = scrollBandWidthRef()
|
|
800
1022
|
const placement = computeColumnPlacement(frozenWidthRef.current + hxRef.current, bandWidth, overscanCols, colCount, validatedGetColWidth, colTree, totalWidthRef.current, columnWindowRef.current.colAnchor)
|
|
801
1023
|
// 幅 0 の凍結列があると indexAt(W_F + hx) が F 未満へ届く — スクロール窓に凍結列を
|
|
802
1024
|
// 再出現させない床クランプ (§8-c: 凍結帯の描画は凍結 subtree の専有)。床は F そのもの:
|
|
@@ -806,21 +1028,31 @@ const VirtualGridInner = <T,>(
|
|
|
806
1028
|
if (effectiveFrozenCols > 0) {
|
|
807
1029
|
placement.renderingColStart = Math.max(placement.renderingColStart, effectiveFrozenCols)
|
|
808
1030
|
placement.visibleColStart = Math.max(placement.visibleColStart, effectiveFrozenCols)
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1031
|
+
}
|
|
1032
|
+
// 末尾帯の ceiling clamp (3.5.0 プラン §6.1-6 — F 床クランプの鏡像・anti-#4454):
|
|
1033
|
+
// stale-bandWidth や幅 0 末尾列で窓末端が末尾帯へ届いても、スクロール窓に末尾列を
|
|
1034
|
+
// 再出現させない。天井は colCount − T_eff − 1 そのもの
|
|
1035
|
+
if (effectiveTrailingCols > 0) {
|
|
1036
|
+
const ceiling = colCount - effectiveTrailingCols - 1
|
|
1037
|
+
placement.renderingColEnd = Math.min(placement.renderingColEnd, ceiling)
|
|
1038
|
+
placement.visibleColEnd = Math.min(placement.visibleColEnd, ceiling)
|
|
1039
|
+
}
|
|
1040
|
+
// 空窓ガードの一般化 (3.5.0 プラン §6.1-6): F > 0 または T > 0 のとき、縮退帯
|
|
1041
|
+
// (帯幅 ≤ 0 — W_F + W_T ≥ viewport) と clamp の結果 start > end になった窓は
|
|
1042
|
+
// 全列凍結と同じ空窓正準形へ整形する (非空の可視窓を報告するとヘッダー同期 /
|
|
1043
|
+
// liveRegion 消費側がセル無き列を描く — R3 実証の非対称の解消。T のみ縮退の
|
|
1044
|
+
// phantom 窓もここで封じる)
|
|
1045
|
+
if ((effectiveFrozenCols > 0 || effectiveTrailingCols > 0) && (bandWidth <= 0 || placement.renderingColStart > placement.renderingColEnd)) {
|
|
1046
|
+
placement.renderingColStart = colCount
|
|
1047
|
+
placement.renderingColEnd = colCount - 1
|
|
1048
|
+
placement.visibleColStart = colCount
|
|
1049
|
+
placement.visibleColEnd = colCount - 1
|
|
818
1050
|
}
|
|
819
1051
|
const key = `${placement.renderingColStart}:${placement.renderingColEnd}:${placement.colAnchor}`
|
|
820
1052
|
if (key !== columnWindowRef.current.key) {
|
|
821
1053
|
setColumnWindow({ ...placement, key })
|
|
822
1054
|
}
|
|
823
|
-
}, [overscanCols, colCount, validatedGetColWidth, colTree, effectiveFrozenCols])
|
|
1055
|
+
}, [overscanCols, colCount, validatedGetColWidth, colTree, effectiveFrozenCols, effectiveTrailingCols, scrollBandWidthRef])
|
|
824
1056
|
|
|
825
1057
|
// ---- 横スクロールバーの追従 (rAF 合流の軽量 state — React 毎フレーム再レンダー回避) ----
|
|
826
1058
|
const [barHx, setBarHx] = useState(0)
|
|
@@ -866,9 +1098,10 @@ const VirtualGridInner = <T,>(
|
|
|
866
1098
|
// 見積り基点は現在の行窓先頭 (到達済みなら) — マウント前は初期アンカー行。
|
|
867
1099
|
// リサイズ時に初期アンカー基点のままだと現在位置と別領域の行高で見積もってしまう
|
|
868
1100
|
const basisRow = rowRangeRef.current?.renderingStartIndex ?? initialAnchorRow
|
|
869
|
-
//
|
|
870
|
-
|
|
871
|
-
|
|
1101
|
+
// 凍結行帯 (先頭 + 末尾) はスクロール行の見積り高から控除する (帯外バンド — 設計 §4-3、
|
|
1102
|
+
// 3.5.0 プラン §6.1-12: −H_T 欠落は高い末尾帯のマウントコミットで偽打切りを発火させる)
|
|
1103
|
+
return estimateRenderedRowCount(Math.max(0, viewport.height - frozenHeightRef.current - trailingHeightRef.current), validatedGetRowHeight, rowCount, basisRow, overscanRows)
|
|
1104
|
+
}, [viewport.height, validatedGetRowHeight, rowCount, initialAnchorRow, overscanRows, frozenHeightEpochKey, trailingHeightEpochKey])
|
|
872
1105
|
const effectiveRenderedRows = notifiedRowCount ?? estimatedRowCount
|
|
873
1106
|
|
|
874
1107
|
const buildRange = useCallback((): VirtualGridRange | null => {
|
|
@@ -889,8 +1122,9 @@ const VirtualGridInner = <T,>(
|
|
|
889
1122
|
scrollX: hxRef.current,
|
|
890
1123
|
scrollY: vyRef.current,
|
|
891
1124
|
totalWidth: totalWidthRef.current,
|
|
892
|
-
// 帯高は鮮度加算 (格納はスクロール木のみ — 帯行リサイズ後も総高が正しい。R1
|
|
893
|
-
|
|
1125
|
+
// 帯高は鮮度加算 (格納はスクロール木のみ — 帯行リサイズ後も総高が正しい。R1 是正。
|
|
1126
|
+
// 3.5.0: 末尾帯高 H_T も同じ鮮度規律で加算)
|
|
1127
|
+
totalHeight: rows.totalHeight + frozenHeightRef.current + trailingHeightRef.current,
|
|
894
1128
|
}
|
|
895
1129
|
}, [])
|
|
896
1130
|
|
|
@@ -904,10 +1138,10 @@ const VirtualGridInner = <T,>(
|
|
|
904
1138
|
// ---- applyHx — 横論理位置の唯一の書き口 (clamp → ref → 残差 var → 列窓 → 通知 → クランプ後値 return) ----
|
|
905
1139
|
const applyHx = useCallback(
|
|
906
1140
|
(next: number): number => {
|
|
907
|
-
// 凍結帯 (§8): スクロール帯幅 = viewport − W_F
|
|
908
|
-
// W_F < viewport のとき従来式 (総幅 − viewport)
|
|
909
|
-
|
|
910
|
-
const maxHx =
|
|
1141
|
+
// 凍結帯 (§8): スクロール帯幅 = viewport − W_F − W_T (3.5.0 一般化)。maxHx =
|
|
1142
|
+
// (総幅 − W_F − W_T) − 帯幅で、W_F + W_T < viewport のとき従来式 (総幅 − viewport)
|
|
1143
|
+
// と一致する (F = T = 0 で恒等)。クランプ則の所有者は getMaxHx (v3.6.0 SSOT)
|
|
1144
|
+
const maxHx = getMaxHx()
|
|
911
1145
|
const clamped = minmax(next, 0, maxHx)
|
|
912
1146
|
if (clamped !== hxRef.current) {
|
|
913
1147
|
hxRef.current = clamped
|
|
@@ -918,7 +1152,7 @@ const VirtualGridInner = <T,>(
|
|
|
918
1152
|
}
|
|
919
1153
|
return clamped
|
|
920
1154
|
},
|
|
921
|
-
[writeResidual, refreshColumns, scheduleBarSync],
|
|
1155
|
+
[writeResidual, refreshColumns, scheduleBarSync, getMaxHx],
|
|
922
1156
|
)
|
|
923
1157
|
const applyHxRef = useRef(applyHx)
|
|
924
1158
|
applyHxRef.current = applyHx
|
|
@@ -936,7 +1170,7 @@ const VirtualGridInner = <T,>(
|
|
|
936
1170
|
if (rowRangeRef.current !== null) {
|
|
937
1171
|
emitRangeChange()
|
|
938
1172
|
}
|
|
939
|
-
}, [frozenHeight, emitRangeChange])
|
|
1173
|
+
}, [frozenHeight, trailingHeight, emitRangeChange])
|
|
940
1174
|
|
|
941
1175
|
// ---- 保留列アンカー (行側 scrollToIndex のアンカー張りの列対): initialScrollAnchor 復元と
|
|
942
1176
|
// scrollToCell が張り、self-heal / 総幅同期 / リサイズで列左が動くたび paint 前に再ピン留め
|
|
@@ -951,13 +1185,14 @@ const VirtualGridInner = <T,>(
|
|
|
951
1185
|
const prefix = colTree.prefixSum(safeCol, { materializeOption: { materialize: false } })
|
|
952
1186
|
const colLeft = prefix.cumulative - prefix.currentValue
|
|
953
1187
|
const colWidth = prefix.currentValue
|
|
954
|
-
// 凍結帯 (§8): 整列先はスクロール帯 [W_F, viewport) — 列座標から W_F を引き、
|
|
955
|
-
//
|
|
956
|
-
|
|
1188
|
+
// 凍結帯 (§8): 整列先はスクロール帯 [W_F, viewport − W_T) — 列座標から W_F を引き、
|
|
1189
|
+
// 幅もスクロール帯幅 (3.5.0 一般化: −W_T 済み) で測る (F = T = 0 では従来式と恒等。
|
|
1190
|
+
// "end" は目標右端を viewport − W_T へ着地させる — §8 の VALUE 行がピンする形)
|
|
1191
|
+
const bandWidth = scrollBandWidthRef()
|
|
957
1192
|
const base = anchor.alignX === "end" ? colLeft + colWidth - frozenWidthRef.current - bandWidth : anchor.alignX === "center" ? colLeft + colWidth / 2 - frozenWidthRef.current - bandWidth / 2 : colLeft - frozenWidthRef.current
|
|
958
1193
|
return base + anchor.offsetX
|
|
959
1194
|
},
|
|
960
|
-
[colTree, colCount],
|
|
1195
|
+
[colTree, colCount, scrollBandWidthRef],
|
|
961
1196
|
)
|
|
962
1197
|
|
|
963
1198
|
// ---- 幅 self-heal (窓内の getColWidth と木の乖離をバッチ適用 — 行 memo の toUpdateHeights 双子)。
|
|
@@ -1023,6 +1258,159 @@ const VirtualGridInner = <T,>(
|
|
|
1023
1258
|
}
|
|
1024
1259
|
}, [frozenWidth, effectiveFrozenCols])
|
|
1025
1260
|
|
|
1261
|
+
// ---- 末尾列帯の列挙 (3.5.0 プラン §6.1-7 — 先頭帯 memo の接尾辞双子)。self-heal 差分は
|
|
1262
|
+
// 共有バッチへ append 合流し、暴走ガードは帯専用の独立カウンタ (T = 0 でエピソードを
|
|
1263
|
+
// 閉じる — 先頭帯の「越境リーク遮断」と同文) ----
|
|
1264
|
+
const trailingHealBurstRef = useRef({ count: 0, warned: false })
|
|
1265
|
+
const { trailingColumns, trailingWidth } = useMemo(() => {
|
|
1266
|
+
// widthEpoch は木内容バージョンの反応辺 (先頭帯 memo と同じ規律)
|
|
1267
|
+
void widthEpoch
|
|
1268
|
+
if (effectiveTrailingCols === 0) {
|
|
1269
|
+
// 末尾凍結解除は帯ガードのエピソードも閉じる (先頭帯の R3 実証と同文の遮断)
|
|
1270
|
+
trailingHealBurstRef.current.count = 0
|
|
1271
|
+
trailingHealBurstRef.current.warned = false
|
|
1272
|
+
return { trailingColumns: EMPTY_PLACED_COLUMNS, trailingWidth: 0 }
|
|
1273
|
+
}
|
|
1274
|
+
const { columns, widthUpdates, width } = collectTrailingColumns(colCount, effectiveTrailingCols, validatedGetColWidth, colTree)
|
|
1275
|
+
if (widthUpdates.length > 0) {
|
|
1276
|
+
const burst = trailingHealBurstRef.current
|
|
1277
|
+
if (burst.count >= WIDTH_HEAL_BURST_LIMIT) {
|
|
1278
|
+
if (!burst.warned) {
|
|
1279
|
+
burst.warned = true
|
|
1280
|
+
Logger.warn("[VirtualGrid] getColWidth keeps disagreeing with its own previous values inside the trailing band; width self-heal is suspended until a converged pass re-arms it (unstable accessor).", {
|
|
1281
|
+
trailingCols: effectiveTrailingCols,
|
|
1282
|
+
pendingUpdates: widthUpdates.length,
|
|
1283
|
+
})
|
|
1284
|
+
}
|
|
1285
|
+
} else {
|
|
1286
|
+
burst.count += 1
|
|
1287
|
+
// 共有バッチへ append 合流 (同 tick の先頭帯 / スクロール窓の治癒を上書きで落とさない)
|
|
1288
|
+
pendingWidthUpdatesRef.current = [...(pendingWidthUpdatesRef.current ?? []), ...widthUpdates]
|
|
1289
|
+
Promise.resolve().then(flushWidthUpdates)
|
|
1290
|
+
}
|
|
1291
|
+
} else {
|
|
1292
|
+
trailingHealBurstRef.current.count = 0
|
|
1293
|
+
trailingHealBurstRef.current.warned = false
|
|
1294
|
+
}
|
|
1295
|
+
return { trailingColumns: columns, trailingWidth: width }
|
|
1296
|
+
}, [effectiveTrailingCols, colCount, widthEpoch, validatedGetColWidth, colTree, flushWidthUpdates])
|
|
1297
|
+
trailingWidthRef.current = trailingWidth
|
|
1298
|
+
// 末尾幅 var の書き手 (3.5.0 プラン §6.1-8 — **単一書き手規律**: この effect だけが書く)。
|
|
1299
|
+
// 消費者はクリップ右端 (.aqvs-grid-row-scroll の right)・末尾クリップの max() 左端と inner 幅。
|
|
1300
|
+
// T = 0 は除去 (T = 0 恒等 — CSS 側 0px フォールバックで安全)
|
|
1301
|
+
useLayoutEffect(() => {
|
|
1302
|
+
if (effectiveTrailingCols > 0) {
|
|
1303
|
+
rootRef.current?.style.setProperty("--aqvs-grid-trailing-width", `${trailingWidth}px`)
|
|
1304
|
+
} else {
|
|
1305
|
+
rootRef.current?.style.removeProperty("--aqvs-grid-trailing-width")
|
|
1306
|
+
}
|
|
1307
|
+
}, [trailingWidth, effectiveTrailingCols])
|
|
1308
|
+
|
|
1309
|
+
// ---- 統合 2 軸タップサークル (v3.6.0 — ADR-27..32) ----
|
|
1310
|
+
// 埋め込み VirtualScroll へ転送するオプションのメモ化フォーク (§3.1): tapScrollCircleOptions
|
|
1311
|
+
// だけ enabled: false を強制し、兄弟フィールド (width 等) は必ず生存させる。両バーの
|
|
1312
|
+
// サークルはグリッド側だけで抑止し、単体 VirtualScroll / ScrollPane / ScrollBar は触らない
|
|
1313
|
+
const embeddedScrollBarOptions = useMemo<VirtualScrollScrollBarOptions>(() => ({ ...scrollBarOptions, tapScrollCircleOptions: { ...scrollBarOptions?.tapScrollCircleOptions, enabled: false } }), [scrollBarOptions])
|
|
1314
|
+
const rawTapScrollCircleOptions = scrollBarOptions?.tapScrollCircleOptions
|
|
1315
|
+
// hasCustomMaxSpeedMultiplier は**生**オプションからのピン (§8.3 — ScrollBar stepAutoScroll 内の
|
|
1316
|
+
// 導出の双子)。リゾルバ出力からの導出 (typeof resolved.maxSpeedMultiplier === "number") は
|
|
1317
|
+
// 常に true で、1,200 px/s 自動床を minSpeed へ無言反転させる T1 破壊 (MV-XY28)
|
|
1318
|
+
const hasCustomTapMaxSpeedMultiplier = typeof rawTapScrollCircleOptions?.maxSpeedMultiplier === "number"
|
|
1319
|
+
// 生オプションへ両オフセットのグリッド既定 (−200) を注入してから解決する (§8.4-5)。バー既定
|
|
1320
|
+
// −80/0 はバーアンカー則の符号でありコーナーアンカーでは無意味
|
|
1321
|
+
const gridTapOptions = useMemo<ScrollBarTapCircleOptions>(
|
|
1322
|
+
() => ({
|
|
1323
|
+
...rawTapScrollCircleOptions,
|
|
1324
|
+
offsetX: rawTapScrollCircleOptions?.offsetX === undefined ? GRID_TAP_CIRCLE_DEFAULT_OFFSET : rawTapScrollCircleOptions.offsetX,
|
|
1325
|
+
offsetY: rawTapScrollCircleOptions?.offsetY === undefined ? GRID_TAP_CIRCLE_DEFAULT_OFFSET : rawTapScrollCircleOptions.offsetY,
|
|
1326
|
+
}),
|
|
1327
|
+
[rawTapScrollCircleOptions],
|
|
1328
|
+
)
|
|
1329
|
+
// 軸別解決 (§8.4-5): resolveTapScrollCircleOptions を軸ごとに 1 回 (itemCount = 軸カウント —
|
|
1330
|
+
// 不変量 8 保存)。視覚フィールドはどちらの結果から読んでも同一 (構造上) — x 側から読む
|
|
1331
|
+
const resolvedTapX = useMemo(() => resolveTapScrollCircleOptions(gridTapOptions, Math.max(0, colCount - effectiveFrozenCols - effectiveTrailingCols)), [gridTapOptions, colCount, effectiveFrozenCols, effectiveTrailingCols])
|
|
1332
|
+
const resolvedTapY = useMemo(() => resolveTapScrollCircleOptions(gridTapOptions, scrollRowCount), [gridTapOptions, scrollRowCount])
|
|
1333
|
+
const tapCircleEnabled = resolvedTapX.enabled
|
|
1334
|
+
const tapMaxDistance = Math.max(resolvedTapX.maxVisualDistance, 1)
|
|
1335
|
+
// 軸別速度パラメータ (§4.1): 今日の 2 バー構成そのもの — x はスクロール帯幅 × 列カウント、
|
|
1336
|
+
// y は埋め込みペイン高 × スクロール行カウント。min/max/curve は共有 (1 オプションオブジェクト)
|
|
1337
|
+
const tapXSpeedParams: TapScrollAxisSpeedParams = {
|
|
1338
|
+
viewportSize: Math.max(0, viewport.width - frozenWidth - trailingWidth),
|
|
1339
|
+
minSpeedMultiplier: resolvedTapX.minSpeedMultiplier,
|
|
1340
|
+
maxSpeedMultiplier: resolvedTapX.maxSpeedMultiplier,
|
|
1341
|
+
hasCustomMaxSpeedMultiplier: hasCustomTapMaxSpeedMultiplier,
|
|
1342
|
+
curve: resolvedTapX.maxSpeedCurve,
|
|
1343
|
+
}
|
|
1344
|
+
const tapYSpeedParams: TapScrollAxisSpeedParams = {
|
|
1345
|
+
viewportSize: Math.max(0, viewport.height - frozenHeight - trailingHeight),
|
|
1346
|
+
minSpeedMultiplier: resolvedTapY.minSpeedMultiplier,
|
|
1347
|
+
maxSpeedMultiplier: resolvedTapY.maxSpeedMultiplier,
|
|
1348
|
+
hasCustomMaxSpeedMultiplier: hasCustomTapMaxSpeedMultiplier,
|
|
1349
|
+
curve: resolvedTapY.maxSpeedCurve,
|
|
1350
|
+
}
|
|
1351
|
+
const getHx = useCallback(() => hxRef.current, [])
|
|
1352
|
+
const getVy = useCallback(() => vyRef.current, [])
|
|
1353
|
+
/**
|
|
1354
|
+
* Fresh y extent through the embedded handle — the same freshness channel the vertical bar
|
|
1355
|
+
* uses (`contentSize − viewportSize` floored at 0). Never re-derived from grid-side trees.
|
|
1356
|
+
* 埋め込みハンドル経由の鮮度 y 延長 — vbar と同じ鮮度チャネル (`contentSize − viewportSize`
|
|
1357
|
+
* の 0 下支え)。グリッド側の木から再導出しない (§3.5-7)。
|
|
1358
|
+
*/
|
|
1359
|
+
const getMaxVy = useCallback(() => {
|
|
1360
|
+
const inner = scrollHandleRef.current
|
|
1361
|
+
if (inner === null) {
|
|
1362
|
+
return 0
|
|
1363
|
+
}
|
|
1364
|
+
return Math.max(0, Math.max(0, inner.getContentSize()) - Math.max(0, inner.getViewportSize()))
|
|
1365
|
+
}, [])
|
|
1366
|
+
/**
|
|
1367
|
+
* The y apply seam (§3.5-8): wraps the embedded `scrollBy` AND syncs `vyRef.current =
|
|
1368
|
+
* applied` before returning — the grid handle `scrollBy` y-half twin, symmetric with
|
|
1369
|
+
* `applyHxRef`. Without the sync, x-frame `onScroll({ x, y: vyRef.current })` notifications
|
|
1370
|
+
* during a diagonal drag carry y stale by up to the `callbackThrottleMs` window (MV-XY29).
|
|
1371
|
+
* y 適用シーム (§3.5-8): 埋め込み `scrollBy` をラップし、**返す前に** `vyRef.current =
|
|
1372
|
+
* applied` を同期する — グリッドハンドル `scrollBy` の y 半分の双子で `applyHxRef` と対称。
|
|
1373
|
+
* 同期が無いと対角ドラッグ中の x フレーム通知が stale y を運ぶ (MV-XY29)。
|
|
1374
|
+
*/
|
|
1375
|
+
const applyVy = useCallback((delta: number): number => {
|
|
1376
|
+
const inner = scrollHandleRef.current
|
|
1377
|
+
if (inner === null) {
|
|
1378
|
+
// ハンドル未接続時は現在値を返して動かない (グリッドハンドル scrollBy と同じ姿勢)
|
|
1379
|
+
return vyRef.current
|
|
1380
|
+
}
|
|
1381
|
+
const applied = inner.scrollBy(delta)
|
|
1382
|
+
vyRef.current = applied
|
|
1383
|
+
return applied
|
|
1384
|
+
}, [])
|
|
1385
|
+
// gridScrollable (§8.4-6): 動く軸が 1 つあれば十分 — y-only グリッドは x 成分が境界ピンの
|
|
1386
|
+
// まま動くサークルを保持する (MV-XY20/21)。バーを駆動するのと同じ鮮度チャネル (x:
|
|
1387
|
+
// columnWindow / 総幅、y: 縦レンジ state = notifiedRowCount の通知駆動再レンダー) で再計算される
|
|
1388
|
+
const gridScrollable = getMaxHx() > 0 || getMaxVy() > 0
|
|
1389
|
+
// 延長成長の再武装キー (§3.5-3): 保持中の境界パークを鮮度チャネルの成長で再開する
|
|
1390
|
+
const tapXExtentFreshness = `${totalWidth}:${frozenWidth}:${trailingWidth}:${viewport.width}:${columnWindow.key}`
|
|
1391
|
+
const tapYExtentFreshness = `${rowCount}:${notifiedRowCount ?? -1}:${frozenHeight}:${trailingHeight}:${viewport.height}`
|
|
1392
|
+
const { isTapActive, handleTapCircleDragChange, tapCircleHandleRef } = useGridTapScroll({
|
|
1393
|
+
enabled: tapCircleEnabled && gridScrollable,
|
|
1394
|
+
maxDistance: tapMaxDistance,
|
|
1395
|
+
xSpeedParams: tapXSpeedParams,
|
|
1396
|
+
ySpeedParams: tapYSpeedParams,
|
|
1397
|
+
applyHxRef,
|
|
1398
|
+
getHx,
|
|
1399
|
+
getMaxHx,
|
|
1400
|
+
applyVy,
|
|
1401
|
+
getVy,
|
|
1402
|
+
getMaxVy,
|
|
1403
|
+
pendingColAnchorRef,
|
|
1404
|
+
xExtentFreshness: tapXExtentFreshness,
|
|
1405
|
+
yExtentFreshness: tapYExtentFreshness,
|
|
1406
|
+
})
|
|
1407
|
+
// バーの 2 行 opacity memo の複製 (§8.4-5): インライン opacity の着地先はサークルルート
|
|
1408
|
+
// (props 経由) — 値と要素同一性はテストでピン (MV-XY23/27)
|
|
1409
|
+
const tapCircleOpacityValue = useMemo(() => {
|
|
1410
|
+
const baseOpacity = isTapActive ? 1 : 0.8
|
|
1411
|
+
return minmax(baseOpacity * resolvedTapX.opacity, 0, 1)
|
|
1412
|
+
}, [isTapActive, resolvedTapX.opacity])
|
|
1413
|
+
|
|
1026
1414
|
// ---- ビューポート / 総幅 / 幅エポック変化での再クランプ + 列窓追随。保留列アンカーが
|
|
1027
1415
|
// あれば位置はアンカーから再導出 (単なる再クランプでは推定空間の hx が残留する) ----
|
|
1028
1416
|
useLayoutEffect(() => {
|
|
@@ -1033,7 +1421,7 @@ const VirtualGridInner = <T,>(
|
|
|
1033
1421
|
applyHxRef.current(resolveColumnAnchorHx(pending))
|
|
1034
1422
|
}
|
|
1035
1423
|
refreshColumns()
|
|
1036
|
-
}, [viewport.width, totalWidth, widthEpoch, frozenWidth, refreshColumns, resolveColumnAnchorHx])
|
|
1424
|
+
}, [viewport.width, totalWidth, widthEpoch, frozenWidth, trailingWidth, refreshColumns, resolveColumnAnchorHx])
|
|
1037
1425
|
|
|
1038
1426
|
// ---- 初期アンカー復元 (横) — 生 px でなく {col, offsetX} (行と同一契約)。マウント時 1 回。
|
|
1039
1427
|
// 保留列アンカーとして張り、以後の self-heal による列左の実測化へ追随させる ----
|
|
@@ -1048,10 +1436,15 @@ const VirtualGridInner = <T,>(
|
|
|
1048
1436
|
if (targetCol < effectiveFrozenCols) {
|
|
1049
1437
|
return
|
|
1050
1438
|
}
|
|
1439
|
+
// 末尾凍結列狙いも同文の no-op (3.5.0 プラン §6.1-16 — appliedRef を先に立てる現行順序は
|
|
1440
|
+
// 仕様 §7 の登記済み未検証防御と同じ姿勢で維持)
|
|
1441
|
+
if (targetCol >= colCount - effectiveTrailingCols) {
|
|
1442
|
+
return
|
|
1443
|
+
}
|
|
1051
1444
|
const anchor = { col: targetCol, alignX: "start" as const, offsetX: Math.max(0, initialScrollAnchor.offsetX) }
|
|
1052
1445
|
pendingColAnchorRef.current = anchor
|
|
1053
1446
|
applyHxRef.current(resolveColumnAnchorHx(anchor))
|
|
1054
|
-
}, [initialScrollAnchor, colCount, resolveColumnAnchorHx, effectiveFrozenCols])
|
|
1447
|
+
}, [initialScrollAnchor, colCount, resolveColumnAnchorHx, effectiveFrozenCols, effectiveTrailingCols])
|
|
1055
1448
|
|
|
1056
1449
|
// ---- 縦軸の結線 ----
|
|
1057
1450
|
const handleVerticalScroll = useCallback((position: number) => {
|
|
@@ -1097,8 +1490,9 @@ const VirtualGridInner = <T,>(
|
|
|
1097
1490
|
}, [])
|
|
1098
1491
|
|
|
1099
1492
|
// ---- 描画列の列挙 (配置ごとに 1 回 — 行間で共有) + セル数上限 ----
|
|
1100
|
-
//
|
|
1101
|
-
|
|
1493
|
+
// 凍結行帯 (先頭 + 末尾) は全帯行 × 全描画列を具現化する — 予算分母に帯行数を算入
|
|
1494
|
+
// (行凍結設計 §4-7、3.5.0 プラン §6.1-13)
|
|
1495
|
+
const maxColsPerRow = resolveMaxColsPerRow(__maxRenderedCells, effectiveRenderedRows + frozenRows.length + trailingRows.length)
|
|
1102
1496
|
// self-heal の暴走ガード: 乖離→治癒→再乖離の非収束が連続する = getColWidth が自身の
|
|
1103
1497
|
// 過去値と不一致を繰り返す契約違反 (振動アクセサ)。放置すると治癒→エポック→再走査の
|
|
1104
1498
|
// マイクロタスクループが無限旋回するため、上限で治癒を停止し 1 回だけ警告する。
|
|
@@ -1108,9 +1502,9 @@ const VirtualGridInner = <T,>(
|
|
|
1108
1502
|
const { placedColumns, truncatedCols } = useMemo(() => {
|
|
1109
1503
|
// widthEpoch は木の内容バージョンの反応辺 (木は identity 安定のため値経由では失効しない)
|
|
1110
1504
|
void widthEpoch
|
|
1111
|
-
// 凍結帯 (§8): スクロール帯幅 ≤ 0 (W_F ≥ viewport の縮退)
|
|
1112
|
-
//
|
|
1113
|
-
const bandWidth = Math.max(0, viewport.width - frozenWidth)
|
|
1505
|
+
// 凍結帯 (§8): スクロール帯幅 ≤ 0 (W_F + W_T ≥ viewport の縮退 — 3.5.0 一般化) は
|
|
1506
|
+
// スクロール列を描かない — 凍結帯 / 末尾帯自体は独立 memo が描き続ける定義済み縮退
|
|
1507
|
+
const bandWidth = Math.max(0, viewport.width - frozenWidth - trailingWidth)
|
|
1114
1508
|
if (colCount === 0 || bandWidth <= 0) {
|
|
1115
1509
|
return { placedColumns: [] as PlacedColumn[], truncatedCols: 0 }
|
|
1116
1510
|
}
|
|
@@ -1118,8 +1512,8 @@ const VirtualGridInner = <T,>(
|
|
|
1118
1512
|
if (columnWindow.renderingColStart > columnWindow.renderingColEnd) {
|
|
1119
1513
|
return { placedColumns: [] as PlacedColumn[], truncatedCols: 0 }
|
|
1120
1514
|
}
|
|
1121
|
-
//
|
|
1122
|
-
const scrollColsBudget = Math.max(1, maxColsPerRow - frozenColumns.length)
|
|
1515
|
+
// セル予算は凍結列 + 末尾列ぶんを控除 (両帯セルも行ごとに具現化するため — 上限は行の総セル数)
|
|
1516
|
+
const scrollColsBudget = Math.max(1, maxColsPerRow - frozenColumns.length - trailingColumns.length)
|
|
1123
1517
|
const { columns, widthUpdates, truncated } = collectVisibleColumns(minmax(columnWindow.renderingColStart, 0, colCount - 1), minmax(columnWindow.renderingColEnd, 0, colCount - 1), validatedGetColWidth, colTree, scrollColsBudget)
|
|
1124
1518
|
if (widthUpdates.length > 0) {
|
|
1125
1519
|
const burst = healBurstRef.current
|
|
@@ -1147,7 +1541,10 @@ const VirtualGridInner = <T,>(
|
|
|
1147
1541
|
healBurstRef.current.warned = false
|
|
1148
1542
|
}
|
|
1149
1543
|
return { placedColumns: columns, truncatedCols: truncated }
|
|
1150
|
-
|
|
1544
|
+
// deps へ trailingWidth / trailingColumns.length を明示追加 (3.5.0 プラン §6.1-6 / F4):
|
|
1545
|
+
// bandWidth 式とスクロール列予算が同 memo 内で両値を読む — 追加を怠ると列窓キー不変の
|
|
1546
|
+
// T トグルで memo が stale 化し、予算が trailing 分だけ過大に残る
|
|
1547
|
+
}, [columnWindow, colCount, viewport.width, frozenWidth, trailingWidth, frozenColumns.length, trailingColumns.length, widthEpoch, validatedGetColWidth, colTree, maxColsPerRow, flushWidthUpdates])
|
|
1151
1548
|
|
|
1152
1549
|
// 打切りは無言にしない (設計 §3.2 — 行側の病的ガードと違い正当構成で到達し得る)
|
|
1153
1550
|
const lastTruncationKeyRef = useRef("")
|
|
@@ -1156,10 +1553,11 @@ const VirtualGridInner = <T,>(
|
|
|
1156
1553
|
const key = `${columnWindow.key}:${truncatedCols}`
|
|
1157
1554
|
if (key !== lastTruncationKeyRef.current) {
|
|
1158
1555
|
lastTruncationKeyRef.current = key
|
|
1159
|
-
|
|
1556
|
+
// 打切り分子は両帯セル / 両帯行込みの exactness を維持する (3.5.0 プラン §6.1-13)
|
|
1557
|
+
onRenderTruncatedRef.current?.({ renderedCells: (placedColumns.length + frozenColumns.length + trailingColumns.length) * (effectiveRenderedRows + frozenRows.length + trailingRows.length), droppedCols: truncatedCols })
|
|
1160
1558
|
}
|
|
1161
1559
|
}
|
|
1162
|
-
}, [truncatedCols, columnWindow.key, placedColumns.length, frozenColumns.length, effectiveRenderedRows, frozenRows.length])
|
|
1560
|
+
}, [truncatedCols, columnWindow.key, placedColumns.length, frozenColumns.length, trailingColumns.length, effectiveRenderedRows, frozenRows.length, trailingRows.length])
|
|
1163
1561
|
|
|
1164
1562
|
// ---- 行レンダラー (VirtualScroll children) — セル left は「絶対 − コミット対象アンカー」 ----
|
|
1165
1563
|
const colAnchor = columnWindow.colAnchor
|
|
@@ -1181,20 +1579,23 @@ const VirtualGridInner = <T,>(
|
|
|
1181
1579
|
</div>
|
|
1182
1580
|
)
|
|
1183
1581
|
})
|
|
1184
|
-
//
|
|
1185
|
-
if (effectiveFrozenCols === 0) {
|
|
1582
|
+
// 横帯なし (既定) は従来 DOM と恒等 — 行が残差 translate を運ぶ (中身はバイト恒等)
|
|
1583
|
+
if (effectiveFrozenCols === 0 && effectiveTrailingCols === 0) {
|
|
1186
1584
|
return (
|
|
1187
1585
|
<div {...rowProps} className={twMerge("aqvs-grid-row", rowProps?.className)} style={rowProps?.style}>
|
|
1188
1586
|
{scrollCells}
|
|
1189
1587
|
</div>
|
|
1190
1588
|
)
|
|
1191
1589
|
}
|
|
1192
|
-
//
|
|
1193
|
-
// −W_F 原点シフト付き残差 translate
|
|
1194
|
-
//
|
|
1195
|
-
//
|
|
1590
|
+
// 横帯モード (§8-c + 3.5.0 プラン §6.1-15): 行は静的化し、スクロールセルは左端 W_F ・
|
|
1591
|
+
// 右端 W_T の静的クリップ + −W_F 原点シフト付き残差 translate の内側へ。凍結セルは
|
|
1592
|
+
// 行直下の独立 subtree で木絶対 left の直接描画 — DOM は**前置** (論理列順 = 読み上げ /
|
|
1593
|
+
// フォーカス順)。末尾セルは右アンカー inner の帯ローカル left で**後置** (同じく論理列順)。
|
|
1594
|
+
// class はモードマーカー (描画トレイト) のまま、data-aqvs-frozen-row は F_eff > 0 のみ
|
|
1595
|
+
// (ADR-25 — 末尾のみのグリッドが凍結列ゼロで属性を出すフック面の嘘の封じ)。
|
|
1596
|
+
// 描画順は自由: クリップが帯への侵入を構造的に遮断するため重なり自体が無い
|
|
1196
1597
|
return (
|
|
1197
|
-
<div {...rowProps} data-aqvs-frozen-row="" className={twMerge("aqvs-grid-row aqvs-grid-row-frozen-host", rowProps?.className)} style={rowProps?.style}>
|
|
1598
|
+
<div {...rowProps} data-aqvs-frozen-row={effectiveFrozenCols > 0 ? "" : undefined} className={twMerge("aqvs-grid-row aqvs-grid-row-frozen-host", rowProps?.className)} style={rowProps?.style}>
|
|
1198
1599
|
{hidden
|
|
1199
1600
|
? null
|
|
1200
1601
|
: frozenColumns.map(({ col, left, width }) => {
|
|
@@ -1211,10 +1612,25 @@ const VirtualGridInner = <T,>(
|
|
|
1211
1612
|
<div className="aqvs-grid-row-scroll-inner">{scrollCells}</div>
|
|
1212
1613
|
</div>
|
|
1213
1614
|
)}
|
|
1615
|
+
{hidden || effectiveTrailingCols === 0 ? null : (
|
|
1616
|
+
<div className="aqvs-grid-row-trailing">
|
|
1617
|
+
<div className="aqvs-grid-row-trailing-inner">
|
|
1618
|
+
{trailingColumns.map(({ col, left, width }) => {
|
|
1619
|
+
const cellProps = getCellProps?.(rowIndex, col)
|
|
1620
|
+
const style: CSSProperties = { ...cellProps?.style, left, width }
|
|
1621
|
+
return (
|
|
1622
|
+
<div {...cellProps} data-aqvs-trailing-cell="" key={getCellKey ? getCellKey(rowIndex, col) : col} className={twMerge("aqvs-grid-cell", cellProps?.className)} style={style}>
|
|
1623
|
+
{children(getCell(rowIndex, col), rowIndex, col)}
|
|
1624
|
+
</div>
|
|
1625
|
+
)
|
|
1626
|
+
})}
|
|
1627
|
+
</div>
|
|
1628
|
+
</div>
|
|
1629
|
+
)}
|
|
1214
1630
|
</div>
|
|
1215
1631
|
)
|
|
1216
1632
|
},
|
|
1217
|
-
[placedColumns, frozenColumns, effectiveFrozenCols, colAnchor, getRowProps, getCellProps, getCellKey, getCell, children, validatedGetRowHeight],
|
|
1633
|
+
[placedColumns, frozenColumns, trailingColumns, effectiveFrozenCols, effectiveTrailingCols, colAnchor, getRowProps, getCellProps, getCellKey, getCell, children, validatedGetRowHeight],
|
|
1218
1634
|
)
|
|
1219
1635
|
|
|
1220
1636
|
// ---- liveRegion (2D — 文言は消費側所有、静定デバウンス) ----
|
|
@@ -1313,15 +1729,17 @@ const VirtualGridInner = <T,>(
|
|
|
1313
1729
|
if (inner !== null && rowCount > 0) {
|
|
1314
1730
|
const targetRow = minmax(Math.trunc(row), 0, rowCount - 1)
|
|
1315
1731
|
// 凍結行狙いの y は no-op (行凍結設計 §3 — 常時可視。列の §8 no-op と同文)。
|
|
1732
|
+
// 末尾帯行狙いも同文の縦 no-op (帯所属は**呼出し時**判定 — 3.5.0 プラン §3)。
|
|
1316
1733
|
// スクロール行はシフト空間へ −R 翻訳して委譲する
|
|
1317
|
-
if (targetRow >= effectiveFrozenRows) {
|
|
1734
|
+
if (targetRow >= effectiveFrozenRows && targetRow < rowCount - effectiveTrailingRows) {
|
|
1318
1735
|
inner.scrollToIndex(targetRow - effectiveFrozenRows, { align: options?.alignY ?? "top", offset: options?.offsetY })
|
|
1319
1736
|
}
|
|
1320
1737
|
}
|
|
1321
1738
|
if (colCount > 0) {
|
|
1322
1739
|
const targetCol = minmax(Math.trunc(col), 0, colCount - 1)
|
|
1323
|
-
// 凍結列狙いの x は no-op (§8 —
|
|
1324
|
-
|
|
1740
|
+
// 凍結列狙いの x は no-op (§8 — 常時可視)。末尾列狙いも横 no-op かつ既存の
|
|
1741
|
+
// 列アンカー残置 (分岐スキップのみ — pendingColAnchorRef は触らない。3.5.0 §3)
|
|
1742
|
+
if (targetCol >= effectiveFrozenCols && targetCol < colCount - effectiveTrailingCols) {
|
|
1325
1743
|
// 列アンカーを張る (行 scrollToIndex のアンカー張りと同義): 以後の self-heal で
|
|
1326
1744
|
// 列左が実測化されても狙った列に留まり続ける。手動横入力で解除
|
|
1327
1745
|
const anchor = { col: targetCol, alignX: options?.alignX ?? "start", offsetX: options?.offsetX ?? 0 }
|
|
@@ -1357,15 +1775,19 @@ const VirtualGridInner = <T,>(
|
|
|
1357
1775
|
return { row: rowAnchor.index + effectiveFrozenRows, col: found.index, offsetX: Math.max(0, bandStart - colLeft), offsetY: rowAnchor.offsetPx }
|
|
1358
1776
|
},
|
|
1359
1777
|
getViewportSize: () => ({ width: viewportRef.current.width, height: viewportRef.current.height }),
|
|
1360
|
-
// 縦はインセット込みの行側 getContentSize を単一情報源にする (range.totalHeight は木総高のみ)
|
|
1361
|
-
|
|
1778
|
+
// 縦はインセット込みの行側 getContentSize を単一情報源にする (range.totalHeight は木総高のみ)。
|
|
1779
|
+
// 3.5.0: height = H_F + 埋め込み + H_T
|
|
1780
|
+
getContentSize: () => ({ width: totalWidthRef.current, height: scrollHandleRef.current === null ? 0 : frozenHeightRef.current + trailingHeightRef.current + Math.max(0, scrollHandleRef.current.getContentSize()) }),
|
|
1362
1781
|
getRange: buildRange,
|
|
1363
1782
|
updateRowSize: (row, px) => {
|
|
1364
1783
|
validateTrackSize(px, "row", row)
|
|
1365
1784
|
// 凍結行はグリッド所有バンド — epoch 繰上げでアクセサ再読 (帯に第 2 の格納庫は
|
|
1366
|
-
// 無いため木更新は不要)
|
|
1785
|
+
// 無いため木更新は不要)。末尾帯行も同文の epoch 繰上げ (3.5.0 — 3 分岐。T = 0 は
|
|
1786
|
+
// 分岐自体が不成立で従来挙動と恒等)。スクロール行はシフト空間へ −R 翻訳して委譲する
|
|
1367
1787
|
if (Math.trunc(row) < effectiveFrozenRows) {
|
|
1368
1788
|
setFrozenRowEpoch((epoch) => epoch + 1)
|
|
1789
|
+
} else if (effectiveTrailingRows > 0 && Math.trunc(row) >= rowCount - effectiveTrailingRows) {
|
|
1790
|
+
setTrailingRowEpoch((epoch) => epoch + 1)
|
|
1369
1791
|
} else {
|
|
1370
1792
|
scrollHandleRef.current?.updateItemSize(Math.trunc(row) - effectiveFrozenRows, px)
|
|
1371
1793
|
}
|
|
@@ -1377,23 +1799,38 @@ const VirtualGridInner = <T,>(
|
|
|
1377
1799
|
// 窓キー不変の幅変更でもセル配置を失効させる (バッチループは React 自動バッチングで 1 コミット)
|
|
1378
1800
|
setWidthEpoch((epoch) => epoch + 1)
|
|
1379
1801
|
},
|
|
1380
|
-
getFrozenSize: () => ({
|
|
1802
|
+
getFrozenSize: () => ({
|
|
1803
|
+
cols: effectiveFrozenCols,
|
|
1804
|
+
width: frozenWidthRef.current,
|
|
1805
|
+
rows: effectiveFrozenRows,
|
|
1806
|
+
height: frozenHeightRef.current,
|
|
1807
|
+
trailingCols: effectiveTrailingCols,
|
|
1808
|
+
trailingRows: effectiveTrailingRows,
|
|
1809
|
+
trailingWidth: trailingWidthRef.current,
|
|
1810
|
+
trailingHeight: trailingHeightRef.current,
|
|
1811
|
+
// vis は単一 JS 源ヘルパーの呼び出しのみ (式の再導出禁止 — 3.5.0 プラン §2.1-4)
|
|
1812
|
+
trailingVisibleWidth: trailingVisibleSize(trailingWidthRef.current, frozenWidthRef.current, viewportRef.current.width),
|
|
1813
|
+
trailingVisibleHeight: trailingVisibleSize(trailingHeightRef.current, frozenHeightRef.current, viewportRef.current.height),
|
|
1814
|
+
}),
|
|
1381
1815
|
applyWheel: (event) => scrollHandleRef.current?.applyWheel(event) ?? false,
|
|
1382
1816
|
focusRowAtIndex: (row, options) => {
|
|
1383
|
-
// 凍結行はバンド行 DOM
|
|
1817
|
+
// 凍結行はバンド行 DOM へ直接フォーカス、末尾帯行も帯行 DOM へ直接フォーカス
|
|
1818
|
+
// (3.5.0 — 3 分岐。T = 0 は分岐不成立で従来挙動と恒等)、スクロール行は −R 翻訳して委譲
|
|
1384
1819
|
const targetRow = Math.trunc(row)
|
|
1385
1820
|
if (targetRow < effectiveFrozenRows) {
|
|
1386
1821
|
rootRef.current?.querySelector<HTMLElement>(`[data-aqvs-frozen-band-row="${targetRow}"]`)?.focus()
|
|
1822
|
+
} else if (effectiveTrailingRows > 0 && targetRow >= rowCount - effectiveTrailingRows) {
|
|
1823
|
+
rootRef.current?.querySelector<HTMLElement>(`[data-aqvs-trailing-band-row="${targetRow}"]`)?.focus()
|
|
1387
1824
|
} else {
|
|
1388
1825
|
scrollHandleRef.current?.focusItemAtIndex(targetRow - effectiveFrozenRows, options)
|
|
1389
1826
|
}
|
|
1390
1827
|
},
|
|
1391
1828
|
}),
|
|
1392
|
-
[rowCount, colCount, colTree, buildRange, commitTotalWidth, resolveColumnAnchorHx, effectiveFrozenCols, effectiveFrozenRows],
|
|
1829
|
+
[rowCount, colCount, colTree, buildRange, commitTotalWidth, resolveColumnAnchorHx, effectiveFrozenCols, effectiveFrozenRows, effectiveTrailingCols, effectiveTrailingRows],
|
|
1393
1830
|
)
|
|
1394
1831
|
|
|
1395
1832
|
return (
|
|
1396
|
-
<div ref={rootRef} className={twMerge("aqvs-grid", effectiveFrozenRows > 0 ? "aqvs-grid-has-frozen-rows" : "", className)} data-testid={testId}>
|
|
1833
|
+
<div ref={rootRef} className={twMerge("aqvs-grid", effectiveFrozenRows > 0 ? "aqvs-grid-has-frozen-rows" : "", effectiveTrailingRows > 0 ? "aqvs-grid-has-trailing-rows" : "", className)} data-testid={testId}>
|
|
1397
1834
|
{effectiveFrozenRows > 0 ? (
|
|
1398
1835
|
// 凍結行帯 (行凍結設計 §4-2): グリッド所有の帯外バンド — 行本体は renderRow を
|
|
1399
1836
|
// そのまま再利用する (F > 0 の凍結セル / クリップ / hx 残差 var は継承で無償追随
|
|
@@ -1421,10 +1858,10 @@ const VirtualGridInner = <T,>(
|
|
|
1421
1858
|
getItemKey={(i) => i + effectiveFrozenRows}
|
|
1422
1859
|
getItemHeight={scrollGetRowHeight}
|
|
1423
1860
|
overscanCount={overscanRows}
|
|
1424
|
-
viewportSize={viewport.height > 0 ? Math.max(0, viewport.height - frozenHeight) : undefined}
|
|
1861
|
+
viewportSize={viewport.height > 0 ? Math.max(0, viewport.height - frozenHeight - trailingHeight) : undefined}
|
|
1425
1862
|
callbackThrottleMs={callbackThrottleMs}
|
|
1426
1863
|
behaviorOptions={behaviorOptions}
|
|
1427
|
-
scrollBarOptions={
|
|
1864
|
+
scrollBarOptions={embeddedScrollBarOptions}
|
|
1428
1865
|
contentProps={contentProps}
|
|
1429
1866
|
background={background}
|
|
1430
1867
|
contentInsets={contentInsets}
|
|
@@ -1439,7 +1876,7 @@ const VirtualGridInner = <T,>(
|
|
|
1439
1876
|
? toggleAnchorRef.current !== null && toggleAnchorRef.current.row >= effectiveFrozenRows && scrollRowCount > 0
|
|
1440
1877
|
? { index: toggleAnchorRef.current.row - effectiveFrozenRows, offsetPx: toggleAnchorRef.current.offsetPx }
|
|
1441
1878
|
: undefined
|
|
1442
|
-
: initialScrollAnchor !== undefined && scrollRowCount > 0 && Math.trunc(initialScrollAnchor.row) >= effectiveFrozenRows
|
|
1879
|
+
: initialScrollAnchor !== undefined && scrollRowCount > 0 && Math.trunc(initialScrollAnchor.row) >= effectiveFrozenRows && Math.trunc(initialScrollAnchor.row) < rowCount - effectiveTrailingRows
|
|
1443
1880
|
? { index: Math.trunc(initialScrollAnchor.row) - effectiveFrozenRows, offsetPx: initialScrollAnchor.offsetY }
|
|
1444
1881
|
: undefined
|
|
1445
1882
|
}
|
|
@@ -1451,16 +1888,36 @@ const VirtualGridInner = <T,>(
|
|
|
1451
1888
|
{renderRow}
|
|
1452
1889
|
</VirtualScroll>
|
|
1453
1890
|
</div>
|
|
1454
|
-
|
|
1891
|
+
{effectiveTrailingRows > 0 ? (
|
|
1892
|
+
// 末尾行帯 (3.5.0 プラン §6.1-17): .aqvs-grid-main の**後**・hbar の**前**の兄弟
|
|
1893
|
+
// (AT には「最終行が最後」の DOM 順正しさ。縦バーは帯の上で自然に終端)。帯クリップ高は
|
|
1894
|
+
// インライン H_T_vis (単一 JS 源 — 帯寸の CSS 変数は幅 2 つのみで高さ変数は導入しない)、
|
|
1895
|
+
// inner はインライン木高 H_T の bottom 定着 — 帯上端 = extent − H_T_vis が全構成で
|
|
1896
|
+
// 成立し、重複縮退では帯の**末尾**行が可視に残る。行本体は renderRow 逐語再利用
|
|
1897
|
+
// (先頭列 / 末尾列の帯構造を継承し 4 象限コーナー全種が無償で成立)
|
|
1898
|
+
<div className="aqvs-grid-trailing-rows" style={{ height: trailingVisibleSize(trailingHeight, frozenHeight, viewport.height) }} data-testid={testId ? `${testId}-trailing-rows` : undefined}>
|
|
1899
|
+
<div className="aqvs-grid-trailing-rows-inner" style={{ height: trailingHeight }}>
|
|
1900
|
+
{trailingRows.map(({ row, top, height }) => (
|
|
1901
|
+
// 帯行は帯ローカル top + right: scrollBarWidth (縦バーコーナーの予約 — 先頭帯行と
|
|
1902
|
+
// 同文) + tabIndex −1 + onFocus フル行 index (focusRowAtIndex ↔ onRowFocus 往復)。
|
|
1903
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: role は消費側の複合ウィジェット契約の所有物 (先頭帯行と同文)
|
|
1904
|
+
<div key={row} data-aqvs-trailing-band-row={row} className="aqvs-grid-trailing-band-row" style={{ top, height, right: scrollBarWidth }} tabIndex={-1} onFocus={() => onRowFocus?.(row)}>
|
|
1905
|
+
{renderRow(row)}
|
|
1906
|
+
</div>
|
|
1907
|
+
))}
|
|
1908
|
+
</div>
|
|
1909
|
+
</div>
|
|
1910
|
+
) : null}
|
|
1911
|
+
<div className="aqvs-grid-hbar-strip" style={{ height: scrollBarWidth, paddingRight: effectiveTrailingCols > 0 ? scrollBarWidth + trailingVisibleSize(trailingWidth, frozenWidth, viewport.width) : scrollBarWidth }} data-testid={testId ? `${testId}-hbar` : undefined}>
|
|
1455
1912
|
<ScrollBar
|
|
1456
1913
|
horizontal
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
viewportSize={Math.max(0, viewport.width - frozenWidth)}
|
|
1914
|
+
contentSize={Math.max(0, totalWidth - frozenWidth - trailingWidth)}
|
|
1915
|
+
viewportSize={Math.max(0, viewport.width - frozenWidth - trailingWidth)}
|
|
1460
1916
|
scrollPosition={barHx}
|
|
1461
1917
|
// タップ速度の log10(colCount) 則 (設計 §3.3 — B7 の対策そのもの)。未配線だと
|
|
1462
|
-
// 兆列でも基礎倍率 2.2x
|
|
1463
|
-
|
|
1918
|
+
// 兆列でも基礎倍率 2.2x に縮退し、サム粒度の到達性が死ぬ。−F / −T 写像は
|
|
1919
|
+
// aria 面に露出せずピン不能 (仕様 §7 非ピン面 (3) の登記済み面)
|
|
1920
|
+
itemCount={Math.max(0, colCount - effectiveFrozenCols - effectiveTrailingCols)}
|
|
1464
1921
|
tapScrollCircleOptions={scrollBarOptions?.tapScrollCircleOptions}
|
|
1465
1922
|
onScroll={(request, previous) => {
|
|
1466
1923
|
// バー操作は手動横入力 — 列アンカーの張りを解除する
|
|
@@ -1470,6 +1927,32 @@ const VirtualGridInner = <T,>(
|
|
|
1470
1927
|
scrollBarWidth={scrollBarWidth}
|
|
1471
1928
|
/>
|
|
1472
1929
|
</div>
|
|
1930
|
+
{tapCircleEnabled && gridScrollable && (
|
|
1931
|
+
// 統合 2 軸タップサークル (v3.6.0 — ADR-30 の 3 アーム則): (C) 可視上限
|
|
1932
|
+
// `100% − (sbw + size)` / (R) 到達性 `100% − sbw + off` / (Q) 遠象限床
|
|
1933
|
+
// `75% − size/2`。全アームが root 寸・sbw・size・オフセットのみに依存 —
|
|
1934
|
+
// 帯幅非依存 (凍結トグル不変)。wrapper は純配置で transition を持たない
|
|
1935
|
+
// (フェードはサークルルートのインライン opacity — §8.7)。hbar strip の後・
|
|
1936
|
+
// live region の前 = 共有 z-50 層の DOM 後勝ちで最前
|
|
1937
|
+
<div
|
|
1938
|
+
className="aqvs-grid-tap-circle-wrapper"
|
|
1939
|
+
data-aqvs-grid-tap-circle=""
|
|
1940
|
+
style={{
|
|
1941
|
+
left: `min(calc(100% - ${scrollBarWidth + resolvedTapX.size}px), max(calc(100% - ${scrollBarWidth}px + ${resolvedTapX.offsetX}px), calc(75% - ${resolvedTapX.size / 2}px)))`,
|
|
1942
|
+
top: `min(calc(100% - ${scrollBarWidth + resolvedTapX.size}px), max(calc(100% - ${scrollBarWidth}px + ${resolvedTapX.offsetY}px), calc(75% - ${resolvedTapX.size / 2}px)))`,
|
|
1943
|
+
}}>
|
|
1944
|
+
<TapScrollCircle
|
|
1945
|
+
axis="xy"
|
|
1946
|
+
ref={tapCircleHandleRef}
|
|
1947
|
+
className={twMerge("aqvs-grid-tap-circle", resolvedTapX.className)}
|
|
1948
|
+
size={resolvedTapX.size}
|
|
1949
|
+
maxVisualDistance={tapMaxDistance}
|
|
1950
|
+
opacity={tapCircleOpacityValue}
|
|
1951
|
+
renderVisual={resolvedTapX.renderVisual}
|
|
1952
|
+
onDragChange={handleTapCircleDragChange}
|
|
1953
|
+
/>
|
|
1954
|
+
</div>
|
|
1955
|
+
)}
|
|
1473
1956
|
{liveRegion !== undefined ? (
|
|
1474
1957
|
<div className="aqvs-grid-live-region" aria-live="polite" data-testid={testId ? `${testId}-live` : undefined}>
|
|
1475
1958
|
{liveMessage}
|