@aiquants/virtualscroll 3.5.0 → 3.6.1
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 +66 -0
- package/README.md +11 -1
- 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 +11 -4
- package/dist/TapScrollCircle.d.ts +11 -4
- package/dist/TapScrollCircle.d.ts.map +1 -1
- 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 +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2160 -1961
- 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 +41 -9
- package/src/VirtualGrid.tsx +181 -8
- package/src/computeTapScrollVelocity.ts +150 -0
- package/src/index.ts +1 -0
- package/src/styles/virtualscroll.css +31 -0
- 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
|
|
|
@@ -997,6 +1005,16 @@ const VirtualGridInner = <T,>(
|
|
|
997
1005
|
*/
|
|
998
1006
|
const scrollBandWidthRef = useCallback(() => Math.max(0, viewportRef.current.width - frozenWidthRef.current - trailingWidthRef.current), [])
|
|
999
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
|
+
|
|
1000
1018
|
// ---- 列窓の再計算 (setState は窓 / アンカー変化時のみ — key 比較で bail) ----
|
|
1001
1019
|
const refreshColumns = useCallback(() => {
|
|
1002
1020
|
// 凍結帯 (§8-a): 窓探索は木座標 W_F + hx 始まり・帯幅 = viewport − W_F − W_T (3.5.0 一般化)
|
|
@@ -1122,9 +1140,8 @@ const VirtualGridInner = <T,>(
|
|
|
1122
1140
|
(next: number): number => {
|
|
1123
1141
|
// 凍結帯 (§8): スクロール帯幅 = viewport − W_F − W_T (3.5.0 一般化)。maxHx =
|
|
1124
1142
|
// (総幅 − W_F − W_T) − 帯幅で、W_F + W_T < viewport のとき従来式 (総幅 − viewport)
|
|
1125
|
-
// と一致する (F = T = 0 で恒等)
|
|
1126
|
-
const
|
|
1127
|
-
const maxHx = Math.max(0, totalWidthRef.current - frozenWidthRef.current - trailingWidthRef.current - bandWidth)
|
|
1143
|
+
// と一致する (F = T = 0 で恒等)。クランプ則の所有者は getMaxHx (v3.6.0 SSOT)
|
|
1144
|
+
const maxHx = getMaxHx()
|
|
1128
1145
|
const clamped = minmax(next, 0, maxHx)
|
|
1129
1146
|
if (clamped !== hxRef.current) {
|
|
1130
1147
|
hxRef.current = clamped
|
|
@@ -1135,7 +1152,7 @@ const VirtualGridInner = <T,>(
|
|
|
1135
1152
|
}
|
|
1136
1153
|
return clamped
|
|
1137
1154
|
},
|
|
1138
|
-
[writeResidual, refreshColumns, scheduleBarSync,
|
|
1155
|
+
[writeResidual, refreshColumns, scheduleBarSync, getMaxHx],
|
|
1139
1156
|
)
|
|
1140
1157
|
const applyHxRef = useRef(applyHx)
|
|
1141
1158
|
applyHxRef.current = applyHx
|
|
@@ -1289,6 +1306,137 @@ const VirtualGridInner = <T,>(
|
|
|
1289
1306
|
}
|
|
1290
1307
|
}, [trailingWidth, effectiveTrailingCols])
|
|
1291
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
|
+
/**
|
|
1353
|
+
* Fresh y position through the embedded handle — the SAME authority `applyVy`'s `scrollBy`
|
|
1354
|
+
* resolves its updater against (the pane's internal position ref), symmetric with
|
|
1355
|
+
* `getMaxVy`'s freshness channel and with the x side, where `getHx` reads the very cell
|
|
1356
|
+
* `applyHx` writes. NEVER read `vyRef` here: that mirror has a second writer — the
|
|
1357
|
+
* throttled `onScroll` relay (`handleVerticalScroll`) — which regresses it by up to the
|
|
1358
|
+
* `callbackThrottleMs` window mid-drive; a driver base read from the mirror overstates
|
|
1359
|
+
* `actualDelta` by the staleness gap, drives the axis residual negative, and the driver
|
|
1360
|
+
* then issues REAL backward `scrollBy` writes (the 3.6.0 y-axis tap yo-yo — v3.6.1).
|
|
1361
|
+
* 埋め込みハンドル経由の鮮度 y 位置 — `applyVy` の `scrollBy` が updater を解決するのと
|
|
1362
|
+
* **同一の権威** (ペイン内部 ref)。`getMaxVy` の鮮度チャネル、および「`getHx` が `applyHx`
|
|
1363
|
+
* の書き先セルそのものを読む」x 側と対称。ここで `vyRef` を読んではならない: あの鏡像には
|
|
1364
|
+
* 第 2 の書き手 (スロットル済み `onScroll` 中継 = `handleVerticalScroll`) が居て、駆動中に
|
|
1365
|
+
* 最大 `callbackThrottleMs` 窓ぶん巻き戻す。鏡像基準の `actualDelta` は staleness ギャップ
|
|
1366
|
+
* ぶん過大になり、軸残差が負へ落ちて、ドライバ自身が**実後退**の `scrollBy` を発行する
|
|
1367
|
+
* (3.6.0 の y 軸タップヨーヨー — v3.6.1)。
|
|
1368
|
+
*/
|
|
1369
|
+
const getVy = useCallback(() => {
|
|
1370
|
+
const inner = scrollHandleRef.current
|
|
1371
|
+
if (inner === null) {
|
|
1372
|
+
// ハンドル未接続時は applyVy と同じ「動かない」姿勢 — 同じ座標 (vyRef) を基準に返す
|
|
1373
|
+
return vyRef.current
|
|
1374
|
+
}
|
|
1375
|
+
const position = inner.getScrollPosition()
|
|
1376
|
+
// ペイン未接続の番兵 (-1) も applyVy の不動姿勢と同じ座標へ落とす (負位置は実在しない)
|
|
1377
|
+
return position < 0 ? vyRef.current : position
|
|
1378
|
+
}, [])
|
|
1379
|
+
/**
|
|
1380
|
+
* Fresh y extent through the embedded handle — the same freshness channel the vertical bar
|
|
1381
|
+
* uses (`contentSize − viewportSize` floored at 0). Never re-derived from grid-side trees.
|
|
1382
|
+
* 埋め込みハンドル経由の鮮度 y 延長 — vbar と同じ鮮度チャネル (`contentSize − viewportSize`
|
|
1383
|
+
* の 0 下支え)。グリッド側の木から再導出しない (§3.5-7)。
|
|
1384
|
+
*/
|
|
1385
|
+
const getMaxVy = useCallback(() => {
|
|
1386
|
+
const inner = scrollHandleRef.current
|
|
1387
|
+
if (inner === null) {
|
|
1388
|
+
return 0
|
|
1389
|
+
}
|
|
1390
|
+
return Math.max(0, Math.max(0, inner.getContentSize()) - Math.max(0, inner.getViewportSize()))
|
|
1391
|
+
}, [])
|
|
1392
|
+
/**
|
|
1393
|
+
* The y apply seam (§3.5-8): wraps the embedded `scrollBy` AND syncs `vyRef.current =
|
|
1394
|
+
* applied` before returning — the grid handle `scrollBy` y-half twin, symmetric with
|
|
1395
|
+
* `applyHxRef`. Without the sync, x-frame `onScroll({ x, y: vyRef.current })` notifications
|
|
1396
|
+
* during a diagonal drag carry y stale by up to the `callbackThrottleMs` window (MV-XY29).
|
|
1397
|
+
* y 適用シーム (§3.5-8): 埋め込み `scrollBy` をラップし、**返す前に** `vyRef.current =
|
|
1398
|
+
* applied` を同期する — グリッドハンドル `scrollBy` の y 半分の双子で `applyHxRef` と対称。
|
|
1399
|
+
* 同期が無いと対角ドラッグ中の x フレーム通知が stale y を運ぶ (MV-XY29)。
|
|
1400
|
+
*/
|
|
1401
|
+
const applyVy = useCallback((delta: number): number => {
|
|
1402
|
+
const inner = scrollHandleRef.current
|
|
1403
|
+
if (inner === null) {
|
|
1404
|
+
// ハンドル未接続時は現在値を返して動かない (グリッドハンドル scrollBy と同じ姿勢)
|
|
1405
|
+
return vyRef.current
|
|
1406
|
+
}
|
|
1407
|
+
const applied = inner.scrollBy(delta)
|
|
1408
|
+
vyRef.current = applied
|
|
1409
|
+
return applied
|
|
1410
|
+
}, [])
|
|
1411
|
+
// gridScrollable (§8.4-6): 動く軸が 1 つあれば十分 — y-only グリッドは x 成分が境界ピンの
|
|
1412
|
+
// まま動くサークルを保持する (MV-XY20/21)。バーを駆動するのと同じ鮮度チャネル (x:
|
|
1413
|
+
// columnWindow / 総幅、y: 縦レンジ state = notifiedRowCount の通知駆動再レンダー) で再計算される
|
|
1414
|
+
const gridScrollable = getMaxHx() > 0 || getMaxVy() > 0
|
|
1415
|
+
// 延長成長の再武装キー (§3.5-3): 保持中の境界パークを鮮度チャネルの成長で再開する
|
|
1416
|
+
const tapXExtentFreshness = `${totalWidth}:${frozenWidth}:${trailingWidth}:${viewport.width}:${columnWindow.key}`
|
|
1417
|
+
const tapYExtentFreshness = `${rowCount}:${notifiedRowCount ?? -1}:${frozenHeight}:${trailingHeight}:${viewport.height}`
|
|
1418
|
+
const { isTapActive, handleTapCircleDragChange, tapCircleHandleRef } = useGridTapScroll({
|
|
1419
|
+
enabled: tapCircleEnabled && gridScrollable,
|
|
1420
|
+
maxDistance: tapMaxDistance,
|
|
1421
|
+
xSpeedParams: tapXSpeedParams,
|
|
1422
|
+
ySpeedParams: tapYSpeedParams,
|
|
1423
|
+
applyHxRef,
|
|
1424
|
+
getHx,
|
|
1425
|
+
getMaxHx,
|
|
1426
|
+
applyVy,
|
|
1427
|
+
getVy,
|
|
1428
|
+
getMaxVy,
|
|
1429
|
+
pendingColAnchorRef,
|
|
1430
|
+
xExtentFreshness: tapXExtentFreshness,
|
|
1431
|
+
yExtentFreshness: tapYExtentFreshness,
|
|
1432
|
+
})
|
|
1433
|
+
// バーの 2 行 opacity memo の複製 (§8.4-5): インライン opacity の着地先はサークルルート
|
|
1434
|
+
// (props 経由) — 値と要素同一性はテストでピン (MV-XY23/27)
|
|
1435
|
+
const tapCircleOpacityValue = useMemo(() => {
|
|
1436
|
+
const baseOpacity = isTapActive ? 1 : 0.8
|
|
1437
|
+
return minmax(baseOpacity * resolvedTapX.opacity, 0, 1)
|
|
1438
|
+
}, [isTapActive, resolvedTapX.opacity])
|
|
1439
|
+
|
|
1292
1440
|
// ---- ビューポート / 総幅 / 幅エポック変化での再クランプ + 列窓追随。保留列アンカーが
|
|
1293
1441
|
// あれば位置はアンカーから再導出 (単なる再クランプでは推定空間の hx が残留する) ----
|
|
1294
1442
|
useLayoutEffect(() => {
|
|
@@ -1739,7 +1887,7 @@ const VirtualGridInner = <T,>(
|
|
|
1739
1887
|
viewportSize={viewport.height > 0 ? Math.max(0, viewport.height - frozenHeight - trailingHeight) : undefined}
|
|
1740
1888
|
callbackThrottleMs={callbackThrottleMs}
|
|
1741
1889
|
behaviorOptions={behaviorOptions}
|
|
1742
|
-
scrollBarOptions={
|
|
1890
|
+
scrollBarOptions={embeddedScrollBarOptions}
|
|
1743
1891
|
contentProps={contentProps}
|
|
1744
1892
|
background={background}
|
|
1745
1893
|
contentInsets={contentInsets}
|
|
@@ -1789,7 +1937,6 @@ const VirtualGridInner = <T,>(
|
|
|
1789
1937
|
<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}>
|
|
1790
1938
|
<ScrollBar
|
|
1791
1939
|
horizontal
|
|
1792
|
-
enableHorizontalTapCircle
|
|
1793
1940
|
contentSize={Math.max(0, totalWidth - frozenWidth - trailingWidth)}
|
|
1794
1941
|
viewportSize={Math.max(0, viewport.width - frozenWidth - trailingWidth)}
|
|
1795
1942
|
scrollPosition={barHx}
|
|
@@ -1806,6 +1953,32 @@ const VirtualGridInner = <T,>(
|
|
|
1806
1953
|
scrollBarWidth={scrollBarWidth}
|
|
1807
1954
|
/>
|
|
1808
1955
|
</div>
|
|
1956
|
+
{tapCircleEnabled && gridScrollable && (
|
|
1957
|
+
// 統合 2 軸タップサークル (v3.6.0 — ADR-30 の 3 アーム則): (C) 可視上限
|
|
1958
|
+
// `100% − (sbw + size)` / (R) 到達性 `100% − sbw + off` / (Q) 遠象限床
|
|
1959
|
+
// `75% − size/2`。全アームが root 寸・sbw・size・オフセットのみに依存 —
|
|
1960
|
+
// 帯幅非依存 (凍結トグル不変)。wrapper は純配置で transition を持たない
|
|
1961
|
+
// (フェードはサークルルートのインライン opacity — §8.7)。hbar strip の後・
|
|
1962
|
+
// live region の前 = 共有 z-50 層の DOM 後勝ちで最前
|
|
1963
|
+
<div
|
|
1964
|
+
className="aqvs-grid-tap-circle-wrapper"
|
|
1965
|
+
data-aqvs-grid-tap-circle=""
|
|
1966
|
+
style={{
|
|
1967
|
+
left: `min(calc(100% - ${scrollBarWidth + resolvedTapX.size}px), max(calc(100% - ${scrollBarWidth}px + ${resolvedTapX.offsetX}px), calc(75% - ${resolvedTapX.size / 2}px)))`,
|
|
1968
|
+
top: `min(calc(100% - ${scrollBarWidth + resolvedTapX.size}px), max(calc(100% - ${scrollBarWidth}px + ${resolvedTapX.offsetY}px), calc(75% - ${resolvedTapX.size / 2}px)))`,
|
|
1969
|
+
}}>
|
|
1970
|
+
<TapScrollCircle
|
|
1971
|
+
axis="xy"
|
|
1972
|
+
ref={tapCircleHandleRef}
|
|
1973
|
+
className={twMerge("aqvs-grid-tap-circle", resolvedTapX.className)}
|
|
1974
|
+
size={resolvedTapX.size}
|
|
1975
|
+
maxVisualDistance={tapMaxDistance}
|
|
1976
|
+
opacity={tapCircleOpacityValue}
|
|
1977
|
+
renderVisual={resolvedTapX.renderVisual}
|
|
1978
|
+
onDragChange={handleTapCircleDragChange}
|
|
1979
|
+
/>
|
|
1980
|
+
</div>
|
|
1981
|
+
)}
|
|
1809
1982
|
{liveRegion !== undefined ? (
|
|
1810
1983
|
<div className="aqvs-grid-live-region" aria-live="polite" data-testid={testId ? `${testId}-live` : undefined}>
|
|
1811
1984
|
{liveMessage}
|
|
@@ -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"
|
|
@@ -526,3 +526,34 @@
|
|
|
526
526
|
white-space: nowrap;
|
|
527
527
|
border: 0;
|
|
528
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
|
+
}
|