@aiquants/virtualscroll 3.0.0 → 3.1.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 +47 -0
- package/README.md +68 -0
- package/dist/ScrollBar.d.ts.map +1 -1
- package/dist/ScrollPane.d.ts.map +1 -1
- package/dist/VirtualScroll.d.cts +88 -0
- package/dist/VirtualScroll.d.ts +88 -0
- package/dist/VirtualScroll.d.ts.map +1 -1
- 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 +1497 -1429
- package/dist/residualQuantizer.d.cts +95 -0
- package/dist/residualQuantizer.d.ts +96 -0
- package/dist/residualQuantizer.d.ts.map +1 -0
- package/dist/styles/virtualscroll.css +1 -1
- package/dist/styles/virtualscroll.standalone.css +1 -1
- package/dist/utils.d.cts +15 -0
- package/dist/utils.d.ts +15 -0
- package/dist/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ScrollBar.tsx +3 -30
- package/src/ScrollPane.tsx +16 -3
- package/src/VirtualScroll.tsx +216 -3
- package/src/index.ts +3 -0
- package/src/residualQuantizer.ts +133 -0
- package/src/styles/virtualscroll.css +15 -0
- package/src/utils.ts +42 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module residualQuantizer
|
|
3
|
+
* @description Residual-carrying delta quantizer for consumers that snap scroll positions to a
|
|
4
|
+
* fixed quantum (row height, column width). High-resolution trackpads emit many sub-quantum
|
|
5
|
+
* deltas; a consumer that rounds each delta independently rounds every one of them to zero and
|
|
6
|
+
* the list never moves. This accumulator carries the sub-quantum remainder across events and
|
|
7
|
+
* emits whole quanta as soon as they accumulate.
|
|
8
|
+
*
|
|
9
|
+
* @description スクロール位置を固定の量子 (行高・列幅) へスナップする消費側のための、
|
|
10
|
+
* 残差持ち越し型のデルタ量子化器。高解像度トラックパッドは 1 量子未満のデルタを大量に発行するため、
|
|
11
|
+
* イベントごとに独立して丸める消費側では**すべてが 0 に丸められ、ゆっくり撫でると 1px も動かない**。
|
|
12
|
+
* 本量子化器は量子未満の余りをイベントを跨いで持ち越し、貯まった分だけ量子単位で放出する。
|
|
13
|
+
*
|
|
14
|
+
* ❗ **時間は扱わない (意図的)。** アイドルタイムアウトでの残差破棄は「いつ捨てるか」の方針が
|
|
15
|
+
* 消費側の UI ごとに異なり、内蔵すると幽霊スクロール (古い残差が後から発火) か不感帯のどちらかを
|
|
16
|
+
* パッケージが押し付けることになる。時間起点の破棄が要る消費側は、自前のタイマーから
|
|
17
|
+
* {@link ResidualQuantizer.reset} を呼ぶこと。既定の防御は**方向反転での残差破棄**のみで、
|
|
18
|
+
* これは時間に依存せず「逆へ動かし始めたのに古い残差が先に順方向へ発火する」事故だけを塞ぐ。
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Options for {@link createResidualQuantizer}.
|
|
22
|
+
* {@link createResidualQuantizer} のオプション。
|
|
23
|
+
*/
|
|
24
|
+
export type ResidualQuantizerOptions = {
|
|
25
|
+
/**
|
|
26
|
+
* Quantum (px) each emission is a multiple of. Must be finite and positive — anything else
|
|
27
|
+
* throws at creation time (fail fast; a silent default would hide the caller's bug).
|
|
28
|
+
* 放出量の単位となる量子 (px)。有限かつ正であること — それ以外は生成時に throw する
|
|
29
|
+
* (Fail Fast。既定値へ黙って読み替えると呼び出し側のバグを隠す)。
|
|
30
|
+
*/
|
|
31
|
+
quantum: number;
|
|
32
|
+
/**
|
|
33
|
+
* Drop the carried residue when the incoming delta's direction opposes it (default: `true`).
|
|
34
|
+
* Without this, scrolling +20px (quantum 24, nothing emitted) and then reversing emits the
|
|
35
|
+
* first reverse quantum "late" — the stale forward residue eats part of the reverse motion.
|
|
36
|
+
* 入力デルタの向きが持ち越し残差と逆のとき残差を捨てる (既定 `true`)。捨てないと、
|
|
37
|
+
* +20px (量子 24 で未放出) の後に反転したとき、古い順方向残差が逆方向の動きを食い、
|
|
38
|
+
* 最初の逆方向量子の発火が「遅れて」体感される。
|
|
39
|
+
*/
|
|
40
|
+
resetOnDirectionChange?: boolean;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* A quantizer instance. One instance per quantized axis — sharing one across axes mixes residues.
|
|
44
|
+
* 量子化器インスタンス。量子化する軸ごとに 1 つ持つこと — 軸間で共有すると残差が混線する。
|
|
45
|
+
*/
|
|
46
|
+
export type ResidualQuantizer = {
|
|
47
|
+
/**
|
|
48
|
+
* Accumulates `delta` and returns the emission: a multiple of the quantum (possibly 0),
|
|
49
|
+
* signed toward the accumulated direction. Non-finite deltas AND deltas whose magnitude
|
|
50
|
+
* exceeds `Number.MAX_SAFE_INTEGER` emit 0 and leave the residue untouched — beyond 2^53
|
|
51
|
+
* the float product stops being an EXACT multiple of the quantum, which is precisely the
|
|
52
|
+
* property a grid-snapping consumer relies on.
|
|
53
|
+
* `delta` を蓄積し、放出量 (量子の倍数。0 のこともある) を蓄積方向の符号で返す。
|
|
54
|
+
* 非有限のデルタ、および絶対値が `Number.MAX_SAFE_INTEGER` を超えるデルタは 0 を返し
|
|
55
|
+
* 残差を汚さない — 2^53 超では浮動小数点積が量子の**正確な**倍数でなくなり、グリッド
|
|
56
|
+
* スナップ消費側が依拠する性質そのものが壊れるため。
|
|
57
|
+
*
|
|
58
|
+
* @param delta - Incoming delta (px) / 入力デルタ (px)
|
|
59
|
+
* @returns Emitted amount (multiple of the quantum) / 放出量 (量子の倍数)
|
|
60
|
+
*/
|
|
61
|
+
push: (delta: number) => number;
|
|
62
|
+
/**
|
|
63
|
+
* Returns the carried residue without mutating it (|residue| < quantum).
|
|
64
|
+
* 持ち越し中の残差を変更せずに返す (|残差| < 量子)。
|
|
65
|
+
*
|
|
66
|
+
* @returns Current residue (px) / 現在の残差 (px)
|
|
67
|
+
*/
|
|
68
|
+
peekResidue: () => number;
|
|
69
|
+
/**
|
|
70
|
+
* Clears the residue. Call from consumer-side idle timers, on pointer leave, or when the
|
|
71
|
+
* quantized target (row/column layout) changes under the accumulator.
|
|
72
|
+
* 残差を破棄する。消費側のアイドルタイマー・ポインタ離脱・量子化対象 (行/列レイアウト) の
|
|
73
|
+
* 変更時に呼ぶ。
|
|
74
|
+
*/
|
|
75
|
+
reset: () => void;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Creates a residual-carrying delta quantizer.
|
|
79
|
+
* 残差持ち越し型のデルタ量子化器を生成する処理。
|
|
80
|
+
*
|
|
81
|
+
* ```ts
|
|
82
|
+
* const quantizer = createResidualQuantizer({ quantum: rowHeight })
|
|
83
|
+
* const handleWheelVertical = (deltaY: number) => {
|
|
84
|
+
* const emitted = quantizer.push(deltaY)
|
|
85
|
+
* if (emitted !== 0) {
|
|
86
|
+
* snapScrollBy(emitted) // 常に量子の倍数
|
|
87
|
+
* }
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* @param options - Quantizer options / 量子化器のオプション
|
|
92
|
+
* @returns A new quantizer instance / 新しい量子化器インスタンス
|
|
93
|
+
* @throws {TypeError} When `quantum` is not a finite positive number / `quantum` が有限の正数でない場合
|
|
94
|
+
*/
|
|
95
|
+
export declare const createResidualQuantizer: (options: ResidualQuantizerOptions) => ResidualQuantizer;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module residualQuantizer
|
|
3
|
+
* @description Residual-carrying delta quantizer for consumers that snap scroll positions to a
|
|
4
|
+
* fixed quantum (row height, column width). High-resolution trackpads emit many sub-quantum
|
|
5
|
+
* deltas; a consumer that rounds each delta independently rounds every one of them to zero and
|
|
6
|
+
* the list never moves. This accumulator carries the sub-quantum remainder across events and
|
|
7
|
+
* emits whole quanta as soon as they accumulate.
|
|
8
|
+
*
|
|
9
|
+
* @description スクロール位置を固定の量子 (行高・列幅) へスナップする消費側のための、
|
|
10
|
+
* 残差持ち越し型のデルタ量子化器。高解像度トラックパッドは 1 量子未満のデルタを大量に発行するため、
|
|
11
|
+
* イベントごとに独立して丸める消費側では**すべてが 0 に丸められ、ゆっくり撫でると 1px も動かない**。
|
|
12
|
+
* 本量子化器は量子未満の余りをイベントを跨いで持ち越し、貯まった分だけ量子単位で放出する。
|
|
13
|
+
*
|
|
14
|
+
* ❗ **時間は扱わない (意図的)。** アイドルタイムアウトでの残差破棄は「いつ捨てるか」の方針が
|
|
15
|
+
* 消費側の UI ごとに異なり、内蔵すると幽霊スクロール (古い残差が後から発火) か不感帯のどちらかを
|
|
16
|
+
* パッケージが押し付けることになる。時間起点の破棄が要る消費側は、自前のタイマーから
|
|
17
|
+
* {@link ResidualQuantizer.reset} を呼ぶこと。既定の防御は**方向反転での残差破棄**のみで、
|
|
18
|
+
* これは時間に依存せず「逆へ動かし始めたのに古い残差が先に順方向へ発火する」事故だけを塞ぐ。
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Options for {@link createResidualQuantizer}.
|
|
22
|
+
* {@link createResidualQuantizer} のオプション。
|
|
23
|
+
*/
|
|
24
|
+
export type ResidualQuantizerOptions = {
|
|
25
|
+
/**
|
|
26
|
+
* Quantum (px) each emission is a multiple of. Must be finite and positive — anything else
|
|
27
|
+
* throws at creation time (fail fast; a silent default would hide the caller's bug).
|
|
28
|
+
* 放出量の単位となる量子 (px)。有限かつ正であること — それ以外は生成時に throw する
|
|
29
|
+
* (Fail Fast。既定値へ黙って読み替えると呼び出し側のバグを隠す)。
|
|
30
|
+
*/
|
|
31
|
+
quantum: number;
|
|
32
|
+
/**
|
|
33
|
+
* Drop the carried residue when the incoming delta's direction opposes it (default: `true`).
|
|
34
|
+
* Without this, scrolling +20px (quantum 24, nothing emitted) and then reversing emits the
|
|
35
|
+
* first reverse quantum "late" — the stale forward residue eats part of the reverse motion.
|
|
36
|
+
* 入力デルタの向きが持ち越し残差と逆のとき残差を捨てる (既定 `true`)。捨てないと、
|
|
37
|
+
* +20px (量子 24 で未放出) の後に反転したとき、古い順方向残差が逆方向の動きを食い、
|
|
38
|
+
* 最初の逆方向量子の発火が「遅れて」体感される。
|
|
39
|
+
*/
|
|
40
|
+
resetOnDirectionChange?: boolean;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* A quantizer instance. One instance per quantized axis — sharing one across axes mixes residues.
|
|
44
|
+
* 量子化器インスタンス。量子化する軸ごとに 1 つ持つこと — 軸間で共有すると残差が混線する。
|
|
45
|
+
*/
|
|
46
|
+
export type ResidualQuantizer = {
|
|
47
|
+
/**
|
|
48
|
+
* Accumulates `delta` and returns the emission: a multiple of the quantum (possibly 0),
|
|
49
|
+
* signed toward the accumulated direction. Non-finite deltas AND deltas whose magnitude
|
|
50
|
+
* exceeds `Number.MAX_SAFE_INTEGER` emit 0 and leave the residue untouched — beyond 2^53
|
|
51
|
+
* the float product stops being an EXACT multiple of the quantum, which is precisely the
|
|
52
|
+
* property a grid-snapping consumer relies on.
|
|
53
|
+
* `delta` を蓄積し、放出量 (量子の倍数。0 のこともある) を蓄積方向の符号で返す。
|
|
54
|
+
* 非有限のデルタ、および絶対値が `Number.MAX_SAFE_INTEGER` を超えるデルタは 0 を返し
|
|
55
|
+
* 残差を汚さない — 2^53 超では浮動小数点積が量子の**正確な**倍数でなくなり、グリッド
|
|
56
|
+
* スナップ消費側が依拠する性質そのものが壊れるため。
|
|
57
|
+
*
|
|
58
|
+
* @param delta - Incoming delta (px) / 入力デルタ (px)
|
|
59
|
+
* @returns Emitted amount (multiple of the quantum) / 放出量 (量子の倍数)
|
|
60
|
+
*/
|
|
61
|
+
push: (delta: number) => number;
|
|
62
|
+
/**
|
|
63
|
+
* Returns the carried residue without mutating it (|residue| < quantum).
|
|
64
|
+
* 持ち越し中の残差を変更せずに返す (|残差| < 量子)。
|
|
65
|
+
*
|
|
66
|
+
* @returns Current residue (px) / 現在の残差 (px)
|
|
67
|
+
*/
|
|
68
|
+
peekResidue: () => number;
|
|
69
|
+
/**
|
|
70
|
+
* Clears the residue. Call from consumer-side idle timers, on pointer leave, or when the
|
|
71
|
+
* quantized target (row/column layout) changes under the accumulator.
|
|
72
|
+
* 残差を破棄する。消費側のアイドルタイマー・ポインタ離脱・量子化対象 (行/列レイアウト) の
|
|
73
|
+
* 変更時に呼ぶ。
|
|
74
|
+
*/
|
|
75
|
+
reset: () => void;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Creates a residual-carrying delta quantizer.
|
|
79
|
+
* 残差持ち越し型のデルタ量子化器を生成する処理。
|
|
80
|
+
*
|
|
81
|
+
* ```ts
|
|
82
|
+
* const quantizer = createResidualQuantizer({ quantum: rowHeight })
|
|
83
|
+
* const handleWheelVertical = (deltaY: number) => {
|
|
84
|
+
* const emitted = quantizer.push(deltaY)
|
|
85
|
+
* if (emitted !== 0) {
|
|
86
|
+
* snapScrollBy(emitted) // 常に量子の倍数
|
|
87
|
+
* }
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* @param options - Quantizer options / 量子化器のオプション
|
|
92
|
+
* @returns A new quantizer instance / 新しい量子化器インスタンス
|
|
93
|
+
* @throws {TypeError} When `quantum` is not a finite positive number / `quantum` が有限の正数でない場合
|
|
94
|
+
*/
|
|
95
|
+
export declare const createResidualQuantizer: (options: ResidualQuantizerOptions) => ResidualQuantizer;
|
|
96
|
+
//# sourceMappingURL=residualQuantizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"residualQuantizer.d.ts","sourceRoot":"","sources":["../src/residualQuantizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;GAGG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACnC;;;;;OAKG;IACH,OAAO,EAAE,MAAM,CAAA;IACf;;;;;;;OAOG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAA;CACnC,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC5B;;;;;;;;;;;;;OAaG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IAC/B;;;;;OAKG;IACH,WAAW,EAAE,MAAM,MAAM,CAAA;IACzB;;;;;OAKG;IACH,KAAK,EAAE,MAAM,IAAI,CAAA;CACpB,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,uBAAuB,GAAI,SAAS,wBAAwB,KAAG,iBAmC3E,CAAA"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
|
|
2
|
-
@layer components{.aqvs-scroll-pane{display:flex;position:relative}.aqvs-scroll-pane[data-self-measured=true]{height:100%}.aqvs-scroll-pane-content{box-sizing:border-box;flex:1;height:100%;position:relative;overflow:hidden}.aqvs-tap-scroll-circle{touch-action:none;-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;transition-property:transform;transition-duration:.1s;transition-timing-function:cubic-bezier(0,0,.2,1);display:flex;position:relative}.aqvs-tap-scroll-circle-visual-outer{border-radius:9999px;position:absolute;inset:0}.aqvs-tap-scroll-circle-gradient{background:linear-gradient(to bottom right,#1d4ed899,#60a5fa8c,#bfdbfe66);border:1px solid #fff6;border-radius:9999px;position:absolute;inset:0;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-tap-scroll-circle-inner{border-radius:9999px;position:absolute;inset:18%}.aqvs-sample-visual-pupil{box-sizing:border-box;background-color:#ffffffd9;border:1px solid #ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-highlight{background-color:#fffc;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-rod{background-color:#ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-scrollbar{z-index:50;cursor:default;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;touch-action:none;background-color:#fff;position:relative}.aqvs-scrollbar-horizontal{flex-direction:row;align-items:stretch;display:flex}.aqvs-scrollbar-vertical{flex-direction:column;align-items:stretch;display:flex}.aqvs-scrollbar[data-visible=false],.aqvs-scrollbar-thumb-wrapper[data-visible=false]{opacity:0;pointer-events:none}.aqvs-scrollbar-tap-circle-wrapper{pointer-events:auto;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);position:absolute}.aqvs-scrollbar-arrow-button{color:#313131;letter-spacing:normal;background-color:#e0e0e0;border:0 solid;justify-content:center;align-items:center;padding:0;font-family:system-ui,sans-serif;font-size:.75rem;font-weight:400;line-height:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:flex}.aqvs-scrollbar-arrow-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scrollbar-arrow-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scrollbar-arrow-button:disabled{cursor:not-allowed;opacity:.5}.aqvs-scrollbar-arrow-button:enabled:hover{background-color:#d4d4d4}.aqvs-scrollbar-track{background-color:#f5f5f5;flex:1;position:relative}.aqvs-scrollbar-overlay{pointer-events:none;position:absolute;inset:0}.aqvs-scrollbar-thumb-wrapper{touch-action:none;position:absolute}.aqvs-scrollbar-thumb{transform-origin:50%;background-color:#7f7f7f;transition:transform 80ms ease-out;position:absolute}.aqvs-scrollbar-thumb[data-thumb-state=disabled]{transition:none;transform:none}.aqvs-scrollbar-thumb[data-thumb-state=hover]{background-color:#5f5f5f}.aqvs-scrollbar-thumb[data-thumb-state=dragging]{background-color:#4f4f4f;transition:transform 60ms ease-out}.aqvs-scrollbar-thumb-horizontal{inset:1.5px 0}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=hover]{top:-.5px;bottom:-.5px;transform:scaleY(1.06)}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=dragging]{top:-.5px;bottom:-.5px;transform:scaleY(1.12)}.aqvs-scrollbar-thumb-vertical{inset:0 1.5px}.aqvs-scrollbar-thumb-vertical[data-thumb-state=hover]{left:-.5px;right:-.5px;transform:scaleX(1.06)}.aqvs-scrollbar-thumb-vertical[data-thumb-state=dragging]{left:-.5px;right:-.5px;transform:scaleX(1.12)}.aqvs-scroll-to-edge-button{pointer-events:auto;cursor:pointer;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;color:#fff;text-transform:uppercase;letter-spacing:.05em;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background-color:#1f2937cc;border:0 solid;border-radius:9999px;padding:.25rem 3rem;font-family:inherit;font-size:10px;font-weight:500;line-height:1.5;transition-property:transform,background-color;transition-duration:.15s;transform:none;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-scroll-to-edge-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scroll-to-edge-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scroll-to-edge-button:hover{background-color:#374151}.aqvs-scroll-to-edge-button:active{transform:scale(.95)}.aqvs-scroll-to-edge-overlay{pointer-events:none;z-index:10;transition-property:opacity;transition-duration:.5s;position:absolute;inset:0}.aqvs-scroll-to-edge-overlay[data-visible=true]{opacity:1}.aqvs-scroll-to-edge-overlay[data-visible=false]{opacity:0}.aqvs-scroll-to-edge-overlay[data-visible=false] .aqvs-scroll-to-edge-button{pointer-events:none}.aqvs-scroll-to-edge-button-container{justify-content:center;display:flex;position:absolute;left:0;right:0}.aqvs-scroll-to-edge-button-container-top{top:.5rem}.aqvs-scroll-to-edge-button-container-bottom{bottom:.5rem}.aqvs-no-items-container{width:100%;position:absolute;top:0}.aqvs-no-items-text{text-align:center;color:#6b7280}.aqvs-item-container,.aqvs-bottom-inset,.aqvs-items-wrapper{width:100%;position:absolute}}
|
|
2
|
+
@layer components{.aqvs-scroll-pane{display:flex;position:relative}.aqvs-scroll-pane[data-self-measured=true]{height:100%}.aqvs-scroll-pane-content{box-sizing:border-box;flex:1;height:100%;position:relative;overflow:hidden}.aqvs-tap-scroll-circle{touch-action:none;-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;transition-property:transform;transition-duration:.1s;transition-timing-function:cubic-bezier(0,0,.2,1);display:flex;position:relative}.aqvs-tap-scroll-circle-visual-outer{border-radius:9999px;position:absolute;inset:0}.aqvs-tap-scroll-circle-gradient{background:linear-gradient(to bottom right,#1d4ed899,#60a5fa8c,#bfdbfe66);border:1px solid #fff6;border-radius:9999px;position:absolute;inset:0;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-tap-scroll-circle-inner{border-radius:9999px;position:absolute;inset:18%}.aqvs-sample-visual-pupil{box-sizing:border-box;background-color:#ffffffd9;border:1px solid #ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-highlight{background-color:#fffc;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-rod{background-color:#ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-scrollbar{z-index:50;cursor:default;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;touch-action:none;background-color:#fff;position:relative}.aqvs-scrollbar-horizontal{flex-direction:row;align-items:stretch;display:flex}.aqvs-scrollbar-vertical{flex-direction:column;align-items:stretch;display:flex}.aqvs-scrollbar[data-visible=false],.aqvs-scrollbar-thumb-wrapper[data-visible=false]{opacity:0;pointer-events:none}.aqvs-scrollbar-tap-circle-wrapper{pointer-events:auto;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);position:absolute}.aqvs-scrollbar-arrow-button{color:#313131;letter-spacing:normal;background-color:#e0e0e0;border:0 solid;justify-content:center;align-items:center;padding:0;font-family:system-ui,sans-serif;font-size:.75rem;font-weight:400;line-height:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:flex}.aqvs-scrollbar-arrow-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scrollbar-arrow-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scrollbar-arrow-button:disabled{cursor:not-allowed;opacity:.5}.aqvs-scrollbar-arrow-button:enabled:hover{background-color:#d4d4d4}.aqvs-scrollbar-track{background-color:#f5f5f5;flex:1;position:relative}.aqvs-scrollbar-overlay{pointer-events:none;position:absolute;inset:0}.aqvs-scrollbar-thumb-wrapper{touch-action:none;position:absolute}.aqvs-scrollbar-thumb{transform-origin:50%;background-color:#7f7f7f;transition:transform 80ms ease-out;position:absolute}.aqvs-scrollbar-thumb[data-thumb-state=disabled]{transition:none;transform:none}.aqvs-scrollbar-thumb[data-thumb-state=hover]{background-color:#5f5f5f}.aqvs-scrollbar-thumb[data-thumb-state=dragging]{background-color:#4f4f4f;transition:transform 60ms ease-out}.aqvs-scrollbar-thumb-horizontal{inset:1.5px 0}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=hover]{top:-.5px;bottom:-.5px;transform:scaleY(1.06)}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=dragging]{top:-.5px;bottom:-.5px;transform:scaleY(1.12)}.aqvs-scrollbar-thumb-vertical{inset:0 1.5px}.aqvs-scrollbar-thumb-vertical[data-thumb-state=hover]{left:-.5px;right:-.5px;transform:scaleX(1.06)}.aqvs-scrollbar-thumb-vertical[data-thumb-state=dragging]{left:-.5px;right:-.5px;transform:scaleX(1.12)}.aqvs-scroll-to-edge-button{pointer-events:auto;cursor:pointer;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;color:#fff;text-transform:uppercase;letter-spacing:.05em;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background-color:#1f2937cc;border:0 solid;border-radius:9999px;padding:.25rem 3rem;font-family:inherit;font-size:10px;font-weight:500;line-height:1.5;transition-property:transform,background-color;transition-duration:.15s;transform:none;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-scroll-to-edge-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scroll-to-edge-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scroll-to-edge-button:hover{background-color:#374151}.aqvs-scroll-to-edge-button:active{transform:scale(.95)}.aqvs-scroll-to-edge-overlay{pointer-events:none;z-index:10;transition-property:opacity;transition-duration:.5s;position:absolute;inset:0}.aqvs-scroll-to-edge-overlay[data-visible=true]{opacity:1}.aqvs-scroll-to-edge-overlay[data-visible=false]{opacity:0}.aqvs-scroll-to-edge-overlay[data-visible=false] .aqvs-scroll-to-edge-button{pointer-events:none}.aqvs-scroll-to-edge-button-container{justify-content:center;display:flex;position:absolute;left:0;right:0}.aqvs-scroll-to-edge-button-container-top{top:.5rem}.aqvs-scroll-to-edge-button-container-bottom{bottom:.5rem}.aqvs-no-items-container{width:100%;position:absolute;top:0}.aqvs-no-items-text{text-align:center;color:#6b7280}.aqvs-item-container,.aqvs-bottom-inset,.aqvs-items-wrapper{width:100%;position:absolute}.aqvs-live-region{clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
|
|
2
2
|
/*! @aiquants/virtualscroll standalone CSS — 非 Tailwind ホスト専用。ホストの Tailwind ビルドと混在させないこと */
|
|
3
|
-
@layer theme,base;@layer components{.aqvs-scroll-pane{display:flex;position:relative}.aqvs-scroll-pane[data-self-measured=true]{height:100%}.aqvs-scroll-pane-content{box-sizing:border-box;flex:1;height:100%;position:relative;overflow:hidden}.aqvs-tap-scroll-circle{touch-action:none;-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;transition-property:transform;transition-duration:.1s;transition-timing-function:cubic-bezier(0,0,.2,1);display:flex;position:relative}.aqvs-tap-scroll-circle-visual-outer{border-radius:9999px;position:absolute;inset:0}.aqvs-tap-scroll-circle-gradient{background:linear-gradient(to bottom right,#1d4ed899,#60a5fa8c,#bfdbfe66);border:1px solid #fff6;border-radius:9999px;position:absolute;inset:0;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-tap-scroll-circle-inner{border-radius:9999px;position:absolute;inset:18%}.aqvs-sample-visual-pupil{box-sizing:border-box;background-color:#ffffffd9;border:1px solid #ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-highlight{background-color:#fffc;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-rod{background-color:#ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-scrollbar{z-index:50;cursor:default;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;touch-action:none;background-color:#fff;position:relative}.aqvs-scrollbar-horizontal{flex-direction:row;align-items:stretch;display:flex}.aqvs-scrollbar-vertical{flex-direction:column;align-items:stretch;display:flex}.aqvs-scrollbar[data-visible=false],.aqvs-scrollbar-thumb-wrapper[data-visible=false]{opacity:0;pointer-events:none}.aqvs-scrollbar-tap-circle-wrapper{pointer-events:auto;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);position:absolute}.aqvs-scrollbar-arrow-button{color:#313131;letter-spacing:normal;background-color:#e0e0e0;border:0 solid;justify-content:center;align-items:center;padding:0;font-family:system-ui,sans-serif;font-size:.75rem;font-weight:400;line-height:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:flex}.aqvs-scrollbar-arrow-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scrollbar-arrow-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scrollbar-arrow-button:disabled{cursor:not-allowed;opacity:.5}.aqvs-scrollbar-arrow-button:enabled:hover{background-color:#d4d4d4}.aqvs-scrollbar-track{background-color:#f5f5f5;flex:1;position:relative}.aqvs-scrollbar-overlay{pointer-events:none;position:absolute;inset:0}.aqvs-scrollbar-thumb-wrapper{touch-action:none;position:absolute}.aqvs-scrollbar-thumb{transform-origin:50%;background-color:#7f7f7f;transition:transform 80ms ease-out;position:absolute}.aqvs-scrollbar-thumb[data-thumb-state=disabled]{transition:none;transform:none}.aqvs-scrollbar-thumb[data-thumb-state=hover]{background-color:#5f5f5f}.aqvs-scrollbar-thumb[data-thumb-state=dragging]{background-color:#4f4f4f;transition:transform 60ms ease-out}.aqvs-scrollbar-thumb-horizontal{inset:1.5px 0}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=hover]{top:-.5px;bottom:-.5px;transform:scaleY(1.06)}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=dragging]{top:-.5px;bottom:-.5px;transform:scaleY(1.12)}.aqvs-scrollbar-thumb-vertical{inset:0 1.5px}.aqvs-scrollbar-thumb-vertical[data-thumb-state=hover]{left:-.5px;right:-.5px;transform:scaleX(1.06)}.aqvs-scrollbar-thumb-vertical[data-thumb-state=dragging]{left:-.5px;right:-.5px;transform:scaleX(1.12)}.aqvs-scroll-to-edge-button{pointer-events:auto;cursor:pointer;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;color:#fff;text-transform:uppercase;letter-spacing:.05em;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background-color:#1f2937cc;border:0 solid;border-radius:9999px;padding:.25rem 3rem;font-family:inherit;font-size:10px;font-weight:500;line-height:1.5;transition-property:transform,background-color;transition-duration:.15s;transform:none;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-scroll-to-edge-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scroll-to-edge-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scroll-to-edge-button:hover{background-color:#374151}.aqvs-scroll-to-edge-button:active{transform:scale(.95)}.aqvs-scroll-to-edge-overlay{pointer-events:none;z-index:10;transition-property:opacity;transition-duration:.5s;position:absolute;inset:0}.aqvs-scroll-to-edge-overlay[data-visible=true]{opacity:1}.aqvs-scroll-to-edge-overlay[data-visible=false]{opacity:0}.aqvs-scroll-to-edge-overlay[data-visible=false] .aqvs-scroll-to-edge-button{pointer-events:none}.aqvs-scroll-to-edge-button-container{justify-content:center;display:flex;position:absolute;left:0;right:0}.aqvs-scroll-to-edge-button-container-top{top:.5rem}.aqvs-scroll-to-edge-button-container-bottom{bottom:.5rem}.aqvs-no-items-container{width:100%;position:absolute;top:0}.aqvs-no-items-text{text-align:center;color:#6b7280}.aqvs-item-container,.aqvs-bottom-inset,.aqvs-items-wrapper{width:100%;position:absolute}}@layer utilities;
|
|
3
|
+
@layer theme,base;@layer components{.aqvs-scroll-pane{display:flex;position:relative}.aqvs-scroll-pane[data-self-measured=true]{height:100%}.aqvs-scroll-pane-content{box-sizing:border-box;flex:1;height:100%;position:relative;overflow:hidden}.aqvs-tap-scroll-circle{touch-action:none;-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;transition-property:transform;transition-duration:.1s;transition-timing-function:cubic-bezier(0,0,.2,1);display:flex;position:relative}.aqvs-tap-scroll-circle-visual-outer{border-radius:9999px;position:absolute;inset:0}.aqvs-tap-scroll-circle-gradient{background:linear-gradient(to bottom right,#1d4ed899,#60a5fa8c,#bfdbfe66);border:1px solid #fff6;border-radius:9999px;position:absolute;inset:0;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-tap-scroll-circle-inner{border-radius:9999px;position:absolute;inset:18%}.aqvs-sample-visual-pupil{box-sizing:border-box;background-color:#ffffffd9;border:1px solid #ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-highlight{background-color:#fffc;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-rod{background-color:#ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-scrollbar{z-index:50;cursor:default;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;touch-action:none;background-color:#fff;position:relative}.aqvs-scrollbar-horizontal{flex-direction:row;align-items:stretch;display:flex}.aqvs-scrollbar-vertical{flex-direction:column;align-items:stretch;display:flex}.aqvs-scrollbar[data-visible=false],.aqvs-scrollbar-thumb-wrapper[data-visible=false]{opacity:0;pointer-events:none}.aqvs-scrollbar-tap-circle-wrapper{pointer-events:auto;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);position:absolute}.aqvs-scrollbar-arrow-button{color:#313131;letter-spacing:normal;background-color:#e0e0e0;border:0 solid;justify-content:center;align-items:center;padding:0;font-family:system-ui,sans-serif;font-size:.75rem;font-weight:400;line-height:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:flex}.aqvs-scrollbar-arrow-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scrollbar-arrow-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scrollbar-arrow-button:disabled{cursor:not-allowed;opacity:.5}.aqvs-scrollbar-arrow-button:enabled:hover{background-color:#d4d4d4}.aqvs-scrollbar-track{background-color:#f5f5f5;flex:1;position:relative}.aqvs-scrollbar-overlay{pointer-events:none;position:absolute;inset:0}.aqvs-scrollbar-thumb-wrapper{touch-action:none;position:absolute}.aqvs-scrollbar-thumb{transform-origin:50%;background-color:#7f7f7f;transition:transform 80ms ease-out;position:absolute}.aqvs-scrollbar-thumb[data-thumb-state=disabled]{transition:none;transform:none}.aqvs-scrollbar-thumb[data-thumb-state=hover]{background-color:#5f5f5f}.aqvs-scrollbar-thumb[data-thumb-state=dragging]{background-color:#4f4f4f;transition:transform 60ms ease-out}.aqvs-scrollbar-thumb-horizontal{inset:1.5px 0}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=hover]{top:-.5px;bottom:-.5px;transform:scaleY(1.06)}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=dragging]{top:-.5px;bottom:-.5px;transform:scaleY(1.12)}.aqvs-scrollbar-thumb-vertical{inset:0 1.5px}.aqvs-scrollbar-thumb-vertical[data-thumb-state=hover]{left:-.5px;right:-.5px;transform:scaleX(1.06)}.aqvs-scrollbar-thumb-vertical[data-thumb-state=dragging]{left:-.5px;right:-.5px;transform:scaleX(1.12)}.aqvs-scroll-to-edge-button{pointer-events:auto;cursor:pointer;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;color:#fff;text-transform:uppercase;letter-spacing:.05em;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background-color:#1f2937cc;border:0 solid;border-radius:9999px;padding:.25rem 3rem;font-family:inherit;font-size:10px;font-weight:500;line-height:1.5;transition-property:transform,background-color;transition-duration:.15s;transform:none;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-scroll-to-edge-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scroll-to-edge-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scroll-to-edge-button:hover{background-color:#374151}.aqvs-scroll-to-edge-button:active{transform:scale(.95)}.aqvs-scroll-to-edge-overlay{pointer-events:none;z-index:10;transition-property:opacity;transition-duration:.5s;position:absolute;inset:0}.aqvs-scroll-to-edge-overlay[data-visible=true]{opacity:1}.aqvs-scroll-to-edge-overlay[data-visible=false]{opacity:0}.aqvs-scroll-to-edge-overlay[data-visible=false] .aqvs-scroll-to-edge-button{pointer-events:none}.aqvs-scroll-to-edge-button-container{justify-content:center;display:flex;position:absolute;left:0;right:0}.aqvs-scroll-to-edge-button-container-top{top:.5rem}.aqvs-scroll-to-edge-button-container-bottom{bottom:.5rem}.aqvs-no-items-container{width:100%;position:absolute;top:0}.aqvs-no-items-text{text-align:center;color:#6b7280}.aqvs-item-container,.aqvs-bottom-inset,.aqvs-items-wrapper{width:100%;position:absolute}.aqvs-live-region{clip-path:inset(50%);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}}@layer utilities;
|
package/dist/utils.d.cts
CHANGED
|
@@ -12,3 +12,18 @@
|
|
|
12
12
|
* @returns The clamped value. / クランプされた値。
|
|
13
13
|
*/
|
|
14
14
|
export declare const minmax: (value: number, min: number, max: number) => number;
|
|
15
|
+
/**
|
|
16
|
+
* Computes an element's CSS transform scale along one axis (visual px / layout px).
|
|
17
|
+
*
|
|
18
|
+
* 要素の指定軸方向 CSS transform スケール (視覚px / レイアウトpx) を算出。
|
|
19
|
+
* 祖先に transform: scale(...) があると client 座標 (視覚px) がレイアウトpx とズレるため、
|
|
20
|
+
* その比率でドラッグ量・クリック座標を補正する。分母には整数丸めされる offsetWidth/offsetHeight
|
|
21
|
+
* ではなく、transform の影響を受けず小数精度を持つ getComputedStyle のレイアウト寸法
|
|
22
|
+
* (content + padding + border) を優先して使い、transform なしなら厳密に 1 を返す。
|
|
23
|
+
* 取得不能時 (jsdom 等) は offset 寸法へフォールバックし、それも 0 なら 1 を返す。
|
|
24
|
+
*
|
|
25
|
+
* @param element The element to measure. / 計測対象要素。
|
|
26
|
+
* @param axis The axis to measure along ("x" = width, "y" = height). / 計測軸。
|
|
27
|
+
* @returns The visual/layout scale ratio (1 when unmeasurable). / 視覚/レイアウト比 (計測不能時 1)。
|
|
28
|
+
*/
|
|
29
|
+
export declare const getAxisScale: (element: HTMLElement, axis: "x" | "y") => number;
|
package/dist/utils.d.ts
CHANGED
|
@@ -12,4 +12,19 @@
|
|
|
12
12
|
* @returns The clamped value. / クランプされた値。
|
|
13
13
|
*/
|
|
14
14
|
export declare const minmax: (value: number, min: number, max: number) => number;
|
|
15
|
+
/**
|
|
16
|
+
* Computes an element's CSS transform scale along one axis (visual px / layout px).
|
|
17
|
+
*
|
|
18
|
+
* 要素の指定軸方向 CSS transform スケール (視覚px / レイアウトpx) を算出。
|
|
19
|
+
* 祖先に transform: scale(...) があると client 座標 (視覚px) がレイアウトpx とズレるため、
|
|
20
|
+
* その比率でドラッグ量・クリック座標を補正する。分母には整数丸めされる offsetWidth/offsetHeight
|
|
21
|
+
* ではなく、transform の影響を受けず小数精度を持つ getComputedStyle のレイアウト寸法
|
|
22
|
+
* (content + padding + border) を優先して使い、transform なしなら厳密に 1 を返す。
|
|
23
|
+
* 取得不能時 (jsdom 等) は offset 寸法へフォールバックし、それも 0 なら 1 を返す。
|
|
24
|
+
*
|
|
25
|
+
* @param element The element to measure. / 計測対象要素。
|
|
26
|
+
* @param axis The axis to measure along ("x" = width, "y" = height). / 計測軸。
|
|
27
|
+
* @returns The visual/layout scale ratio (1 when unmeasurable). / 視覚/レイアウト比 (計測不能時 1)。
|
|
28
|
+
*/
|
|
29
|
+
export declare const getAxisScale: (element: HTMLElement, axis: "x" | "y") => number;
|
|
15
30
|
//# sourceMappingURL=utils.d.ts.map
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,MAAM,GAAI,OAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,KAAG,MAEhE,CAAA"}
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,MAAM,GAAI,OAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,KAAG,MAEhE,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,GAAI,SAAS,WAAW,EAAE,MAAM,GAAG,GAAG,GAAG,KAAG,MA0BpE,CAAA"}
|
package/package.json
CHANGED
package/src/ScrollBar.tsx
CHANGED
|
@@ -8,7 +8,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr
|
|
|
8
8
|
import { twMerge } from "tailwind-merge"
|
|
9
9
|
import { Logger } from "./logger.ts"
|
|
10
10
|
import { TapScrollCircle, type TapScrollCircleDragState, type TapScrollCircleHandle, type TapScrollCircleRenderProps } from "./TapScrollCircle.tsx"
|
|
11
|
-
import { minmax } from "./utils.ts"
|
|
11
|
+
import { getAxisScale, minmax } from "./utils.ts"
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Configuration for scrollbar orientation (vertical or horizontal).
|
|
@@ -1055,35 +1055,8 @@ export const ScrollBar = ({
|
|
|
1055
1055
|
* (content + padding + border) を優先して使い、transform なしなら厳密に 1 を返す。
|
|
1056
1056
|
* 取得不能時 (jsdom 等) は offset 寸法へフォールバックし、それも 0 なら 1 を返す。
|
|
1057
1057
|
*/
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
const rect = element.getBoundingClientRect()
|
|
1061
|
-
const visualSize = horizontal ? rect.width : rect.height
|
|
1062
|
-
// computed style の値 (px 文字列) を数値化する。解釈不能なら 0 とみなす。
|
|
1063
|
-
const parseSize = (value: string) => {
|
|
1064
|
-
const parsed = Number.parseFloat(value)
|
|
1065
|
-
return Number.isFinite(parsed) ? parsed : 0
|
|
1066
|
-
}
|
|
1067
|
-
const computed = window.getComputedStyle(element)
|
|
1068
|
-
const contentSize = parseSize(horizontal ? computed.width : computed.height)
|
|
1069
|
-
// rect (border-box) と同系になるよう content + padding + border を合成する。
|
|
1070
|
-
const computedLayoutSize =
|
|
1071
|
-
contentSize > 0
|
|
1072
|
-
? contentSize +
|
|
1073
|
-
parseSize(horizontal ? computed.paddingLeft : computed.paddingTop) +
|
|
1074
|
-
parseSize(horizontal ? computed.paddingRight : computed.paddingBottom) +
|
|
1075
|
-
parseSize(horizontal ? computed.borderLeftWidth : computed.borderTopWidth) +
|
|
1076
|
-
parseSize(horizontal ? computed.borderRightWidth : computed.borderBottomWidth)
|
|
1077
|
-
: 0
|
|
1078
|
-
// computed が取れない環境 (jsdom 等) では従来の offset 寸法へフォールバックする。
|
|
1079
|
-
const layoutSize = computedLayoutSize > 0 ? computedLayoutSize : horizontal ? element.offsetWidth : element.offsetHeight
|
|
1080
|
-
if (layoutSize <= 0 || visualSize <= 0) {
|
|
1081
|
-
return 1
|
|
1082
|
-
}
|
|
1083
|
-
return visualSize / layoutSize
|
|
1084
|
-
},
|
|
1085
|
-
[horizontal],
|
|
1086
|
-
)
|
|
1058
|
+
// 本体は共有ヘルパ getAxisScale (utils.ts — ScrollPane のポインタパンと同一 SSOT)
|
|
1059
|
+
const getMainAxisScale = useCallback((element: HTMLElement) => getAxisScale(element, horizontal ? "x" : "y"), [horizontal])
|
|
1087
1060
|
|
|
1088
1061
|
/**
|
|
1089
1062
|
* Pointer move handler for thumb dragging.
|
package/src/ScrollPane.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayo
|
|
|
2
2
|
import { twMerge } from "tailwind-merge"
|
|
3
3
|
import { Logger } from "./logger.ts"
|
|
4
4
|
import { ScrollBar, type ScrollBarTapCircleOptions, type ScrollBarThumbOverlayRenderProps, TAP_SCROLL_CANCEL_EVENT } from "./ScrollBar.tsx"
|
|
5
|
-
import { minmax } from "./utils.ts"
|
|
5
|
+
import { getAxisScale, minmax } from "./utils.ts"
|
|
6
6
|
import { resolveWheelAxes } from "./wheelAxes.ts"
|
|
7
7
|
import { markWheelConsumed, wasWheelConsumed } from "./wheelConsumption.ts"
|
|
8
8
|
|
|
@@ -231,6 +231,8 @@ type DragState = {
|
|
|
231
231
|
shouldCancelNextClick: boolean
|
|
232
232
|
clickResetTimer: number | null
|
|
233
233
|
velocitySamples: DragSample[]
|
|
234
|
+
/** press 時に採取した縦軸の視覚/レイアウト比 (祖先 transform scale — 補正の分母)。 */
|
|
235
|
+
scale: number
|
|
234
236
|
}
|
|
235
237
|
|
|
236
238
|
/**
|
|
@@ -989,6 +991,7 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
989
991
|
shouldCancelNextClick: false,
|
|
990
992
|
clickResetTimer: null,
|
|
991
993
|
velocitySamples: [],
|
|
994
|
+
scale: 1,
|
|
992
995
|
})
|
|
993
996
|
|
|
994
997
|
const enablePointerDragRef = useRef(enablePointerDrag)
|
|
@@ -1084,6 +1087,7 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
1084
1087
|
state.isDragging = false
|
|
1085
1088
|
state.shouldCancelNextClick = shouldKeepClickSuppression
|
|
1086
1089
|
state.velocitySamples = []
|
|
1090
|
+
state.scale = 1
|
|
1087
1091
|
// 状態を確定させてから解放する (同期 lostpointercapture に引き継ぎを壊させない)
|
|
1088
1092
|
if (element?.hasPointerCapture(releasedPointerId)) {
|
|
1089
1093
|
element.releasePointerCapture(releasedPointerId)
|
|
@@ -1107,6 +1111,7 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
1107
1111
|
state.startScroll = 0
|
|
1108
1112
|
state.isDragging = false
|
|
1109
1113
|
state.velocitySamples = []
|
|
1114
|
+
state.scale = 1
|
|
1110
1115
|
}
|
|
1111
1116
|
|
|
1112
1117
|
const clearClickTimer = () => {
|
|
@@ -1239,7 +1244,9 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
1239
1244
|
|
|
1240
1245
|
pushVelocitySample(event.clientY)
|
|
1241
1246
|
|
|
1242
|
-
|
|
1247
|
+
// ÷scale: 視覚 px の指移動をレイアウト (論理) px へ (発動閾値 6px は指の物理
|
|
1248
|
+
// 移動量の判別なので視覚 px のまま — 意図的非対称)
|
|
1249
|
+
const deltaY = (event.clientY - state.startClientY) / (state.scale || 1)
|
|
1243
1250
|
const nextPosition = state.startScroll - deltaY
|
|
1244
1251
|
scrollToRef.current(nextPosition)
|
|
1245
1252
|
|
|
@@ -1309,7 +1316,8 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
1309
1316
|
if (lastSample && firstSample && lastSample.time !== firstSample.time) {
|
|
1310
1317
|
const deltaClientY = lastSample.clientY - firstSample.clientY
|
|
1311
1318
|
const deltaTime = lastSample.time - firstSample.time
|
|
1312
|
-
|
|
1319
|
+
// ÷scale: フリック速度も視覚 px/ms のままだと慣性走行距離が z 倍に歪む
|
|
1320
|
+
inertiaVelocity = -(deltaClientY / deltaTime) / (state.scale || 1)
|
|
1313
1321
|
}
|
|
1314
1322
|
}
|
|
1315
1323
|
|
|
@@ -1405,6 +1413,11 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
1405
1413
|
state.pointerType = event.pointerType
|
|
1406
1414
|
state.startClientY = event.clientY
|
|
1407
1415
|
state.startScroll = scrollPositionRef.current
|
|
1416
|
+
// 祖先 transform scale の押下時採取 (ScrollBar サム / トラックと同じ press キャッシュ
|
|
1417
|
+
// 規約): 視覚 px の指移動をレイアウト px へ換算しないと scale z 下でコンテンツが
|
|
1418
|
+
// 指速の z 倍で動く。move 毎の gBCR + getComputedStyle は 60-120Hz 熱経路の
|
|
1419
|
+
// 強制レイアウトになるため押下時 1 回に固定する
|
|
1420
|
+
state.scale = getAxisScale(element, "y")
|
|
1408
1421
|
state.isDragging = false
|
|
1409
1422
|
state.shouldCancelNextClick = false
|
|
1410
1423
|
state.velocitySamples = []
|