@aiquants/spreadsheet 0.8.0 → 0.10.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 +104 -0
- package/README.md +160 -13
- package/dist/index.cjs +13 -13
- package/dist/index.d.cts +156 -7
- package/dist/index.d.ts +156 -7
- package/dist/index.js +4508 -4057
- package/dist/styles/spreadsheet.css +1 -1
- package/dist/styles/spreadsheet.standalone.css +1 -1
- package/package.json +5 -5
- package/src/Spreadsheet.tsx +350 -34
- package/src/clipboard/tiling.ts +106 -16
- package/src/clipboard/useClipboard.ts +120 -7
- package/src/commands/useCommands.ts +226 -42
- package/src/editor/useCellEditor.ts +62 -13
- package/src/grid/GridBody.tsx +45 -3
- package/src/header/ColumnHeader.tsx +84 -12
- package/src/header/RowHeader.tsx +79 -11
- package/src/header/resizeStrips.ts +86 -1
- package/src/header/useHeaderResize.ts +88 -20
- package/src/header/useHeaderSelection.ts +28 -9
- package/src/index.ts +5 -1
- package/src/keyboard/ensureCellVisible.ts +40 -17
- package/src/keyboard/useGridKeyboard.ts +34 -7
- package/src/model/dataAdapter.ts +121 -2
- package/src/model/frozenPartition.ts +183 -59
- package/src/model/labels.ts +8 -2
- package/src/overlay/SelectionOverlay.tsx +599 -116
- package/src/overlay/useFillHandle.ts +100 -13
- package/src/popover/CommentPopover.tsx +14 -2
- package/src/popover/LinkPopover.tsx +15 -3
- package/src/popover/NotePopover.tsx +24 -6
- package/src/popover/usePopovers.ts +128 -18
- package/src/selection/useAutoScroller.ts +58 -15
- package/src/selection/useSelectionEngine.ts +27 -10
- package/src/styles/spreadsheet.css +32 -0
package/src/grid/GridBody.tsx
CHANGED
|
@@ -19,6 +19,15 @@
|
|
|
19
19
|
* identical style objects (two-tier intern: paint identity, then canonical JSON for the
|
|
20
20
|
* per-call paints of edge / folded cells).
|
|
21
21
|
*
|
|
22
|
+
* v0.9 trailing freeze (plan §4.15): `frozenTrailingColumns` / `frozenTrailingRows` are
|
|
23
|
+
* engine-validated pass-throughs onto VirtualGrid's `frozenTrailingCols` / `frozenTrailingRows`
|
|
24
|
+
* — validation SSOT is the engine's fail-fast contract, no second layer here (§1.1).
|
|
25
|
+
*
|
|
26
|
+
* v0.10 read-only cells (plan §6.5, S1): `getCellProps` conditionally stamps painted read-only
|
|
27
|
+
* cells with `aria-readonly="true"` + `data-aqsp-readonly=""` through the `isReadOnlyCell`
|
|
28
|
+
* helper (ADR-31 — no package CSS; capability absent ⇒ O(1) false ⇒ keys absent ⇒ the DOM
|
|
29
|
+
* stays byte-identical).
|
|
30
|
+
*
|
|
22
31
|
* グリッド本体。VirtualGrid 結線 (両軸) とセルレンダラーを所有する。v0.4 から横軸は VirtualGrid
|
|
23
32
|
* の所有 (量子化列アンカー + 残差 transform + 横バー) — v0.3 の列スタック (useColumnWindow /
|
|
24
33
|
* --aqsp-hx CSS 変数 / 軸原点絶対 left / 自前 HScrollBar) は設計計画 §7-1 に従い削除。
|
|
@@ -33,16 +42,24 @@
|
|
|
33
42
|
* 全読みは能力分岐 (`adapter.x === undefined` ⇒ v0.1 の属性なしセル)、条件付き書式索引は root の宣言能力 (未指定 ⇒
|
|
34
43
|
* base 書式のみ)、無変化セルは同一の style object を保つ (2 段 intern: paint identity → 端 / 畳込み
|
|
35
44
|
* セルの呼出毎 paint は正準 JSON)。
|
|
45
|
+
*
|
|
46
|
+
* v0.9 末尾凍結 (計画 §4.15): `frozenTrailingColumns` / `frozenTrailingRows` は VirtualGrid の
|
|
47
|
+
* `frozenTrailingCols` / `frozenTrailingRows` へのエンジン検証素通し — 検証 SSOT はエンジンの
|
|
48
|
+
* fail-fast 契約で、ここに第二検証層は置かない (§1.1)。
|
|
49
|
+
*
|
|
50
|
+
* v0.10 読み取り専用セル (計画 §6.5、S1): `getCellProps` が塗られた読み取り専用セルへ
|
|
51
|
+
* `aria-readonly="true"` + `data-aqsp-readonly=""` を `isReadOnlyCell` ヘルパ経由で条件スタンプ
|
|
52
|
+
* する (ADR-31 — パッケージ CSS なし。能力未宣言 = O(1) false = キー不在 = DOM バイト同一)。
|
|
36
53
|
*/
|
|
37
54
|
|
|
38
|
-
import { VirtualGrid, type VirtualGridHandle, type VirtualGridRange } from "@aiquants/virtualscroll"
|
|
55
|
+
import { type ScrollBarTapCircleOptions, VirtualGrid, type VirtualGridHandle, type VirtualGridRange } from "@aiquants/virtualscroll"
|
|
39
56
|
import { type HTMLAttributes, memo, type ProfilerOnRenderCallback, type ReactNode, type RefObject, useCallback, useMemo, useRef, useSyncExternalStore } from "react"
|
|
40
57
|
import { type AxisModel, nextVisibleIndex } from "../model/axisModel.ts"
|
|
41
58
|
import { normalizeAdapterHyperlink } from "../model/cellAnnotations.ts"
|
|
42
59
|
import { alignToJustify, type CellAlign, type CellValueFormat, formatCellValue } from "../model/cellFormat.ts"
|
|
43
60
|
import { type CellPaint, resolveCellPaint } from "../model/cellPaint.ts"
|
|
44
61
|
import { type CfIndex, type CfMemo, createCfMemo, resolvePaintedFormat } from "../model/conditionalFormat.ts"
|
|
45
|
-
import { lazyCellText, resolveCellAlign, type SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
62
|
+
import { isReadOnlyCell, lazyCellText, resolveCellAlign, type SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
46
63
|
import { isVisiblyInvalid } from "../model/dataValidation.ts"
|
|
47
64
|
import { SCROLLBAR_WIDTH, type ViewStore } from "../model/viewStore.ts"
|
|
48
65
|
import { CellView, type RenderCellContext } from "./CellView.tsx"
|
|
@@ -257,6 +274,10 @@ type GridBodyProps = {
|
|
|
257
274
|
frozenColumns: number
|
|
258
275
|
/** Frozen leading rows (v0.6 — VirtualGrid frozenLeadingRows pass-through; engine-validated). / 先頭凍結行数 (v0.6 — エンジン検証の素通し)。 */
|
|
259
276
|
frozenRows: number
|
|
277
|
+
/** Trailing frozen columns (v0.9 — VirtualGrid frozenTrailingCols engine-validated pass-through; no second validation layer — §1.1 SSOT). / 末尾凍結列数 (v0.9 — VirtualGrid frozenTrailingCols のエンジン検証素通し。第二検証層なし — §1.1 SSOT)。 */
|
|
278
|
+
frozenTrailingColumns: number
|
|
279
|
+
/** Trailing frozen rows (v0.9 — VirtualGrid frozenTrailingRows engine-validated pass-through; the row twin). / 末尾凍結行数 (v0.9 — frozenTrailingRows のエンジン検証素通し。行双子)。 */
|
|
280
|
+
frozenTrailingRows: number
|
|
260
281
|
renderCell?: (ctx: RenderCellContext) => ReactNode
|
|
261
282
|
/** 2D scroll notification from the grid handle (the truth — §7-4). / グリッドハンドル発の 2 軸スクロール通知 (§7-4 の真実側)。 */
|
|
262
283
|
onScroll: (position: { x: number; y: number }) => void
|
|
@@ -287,6 +308,16 @@ type GridBodyProps = {
|
|
|
287
308
|
* 焦点回収 — §4.2.11)。未指定 ⇒ v0.7 のゲート。
|
|
288
309
|
*/
|
|
289
310
|
isPointing?: () => boolean
|
|
311
|
+
/**
|
|
312
|
+
* Tap-scroll circle tuning (v0.10 — the engine `scrollBarOptions.tapScrollCircleOptions`
|
|
313
|
+
* engine-validated pass-through; the engine's `resolveTapScrollCircleOptions` is the
|
|
314
|
+
* validation SSOT). Undefined rides the memo as an explicit key = the engine's declared
|
|
315
|
+
* absence signal (engine defaults), never a value fallback here.
|
|
316
|
+
* タップサークル調整 (v0.10 — エンジン `scrollBarOptions.tapScrollCircleOptions` の
|
|
317
|
+
* エンジン検証素通し。検証の SSOT はエンジンの `resolveTapScrollCircleOptions`)。undefined は
|
|
318
|
+
* キー同乗のままエンジン宣言の不在信号 (エンジン既定) — この層での値フォールバックなし。
|
|
319
|
+
*/
|
|
320
|
+
tapScrollCircleOptions?: ScrollBarTapCircleOptions
|
|
290
321
|
}
|
|
291
322
|
|
|
292
323
|
/**
|
|
@@ -303,6 +334,8 @@ const GridBodyInner = ({
|
|
|
303
334
|
defaultColumnWidth,
|
|
304
335
|
frozenColumns,
|
|
305
336
|
frozenRows,
|
|
337
|
+
frozenTrailingColumns,
|
|
338
|
+
frozenTrailingRows,
|
|
306
339
|
renderCell,
|
|
307
340
|
onScroll,
|
|
308
341
|
onRangeChange,
|
|
@@ -314,6 +347,7 @@ const GridBodyInner = ({
|
|
|
314
347
|
cfIndexRef,
|
|
315
348
|
cfMemoRef,
|
|
316
349
|
isPointing,
|
|
350
|
+
tapScrollCircleOptions,
|
|
317
351
|
}: GridBodyProps): ReactNode => {
|
|
318
352
|
const internalBodyRef = useRef<HTMLDivElement | null>(null)
|
|
319
353
|
// 外部 ref (エンジン結線用) と内部 ref の合成
|
|
@@ -343,7 +377,8 @@ const GridBodyInner = ({
|
|
|
343
377
|
// (§7-8 — サンプリング省略の Fenwick baseValue)。pointerDragInputs はペン / タッチのみ
|
|
344
378
|
// (マウスドラッグは選択エンジンの所有)
|
|
345
379
|
const behaviorOptions = useMemo(() => ({ enableKeyboardNavigation: false, pointerDragInputs: ["pen", "touch"] as const, resetOnGetItemHeightChange: true, resetOnGetColWidthChange: true, defaultColWidth: defaultColumnWidth }), [defaultColumnWidth])
|
|
346
|
-
|
|
380
|
+
// タップサークル素通し (v0.10 §2.1): 未指定は undefined キー同乗 = エンジン宣言の不在信号
|
|
381
|
+
const scrollBarOptions = useMemo(() => ({ width: SCROLLBAR_WIDTH, tapScrollCircleOptions }), [tapScrollCircleOptions])
|
|
347
382
|
// 表示値書式の掛け金 (getCellProps が書き、同じセルの children が直後に消費 — 番兵は届かない座標)
|
|
348
383
|
const displayLatchRef = useRef<DisplayLatch>({ row: -1, col: -1, value: undefined })
|
|
349
384
|
/**
|
|
@@ -367,6 +402,11 @@ const GridBodyInner = ({
|
|
|
367
402
|
"data-row": row,
|
|
368
403
|
"data-col": col,
|
|
369
404
|
className: "aqsp-cell",
|
|
405
|
+
// S1 (v0.10 §6.5 / 設計 ADR-31): 読み取り専用スタンプ — 閲覧モード / a11y ツリー用の
|
|
406
|
+
// aria-readonly と data-* フック (パッケージ CSS なし)。ヘルパ経由が規範 (member 名の
|
|
407
|
+
// 単一居住 — layer-2 census)。未宣言 = O(1) false = キー不在 = v0.9 と DOM バイト同一、
|
|
408
|
+
// 宣言ホストは塗りセル毎 1 述語読み (ADR-31 の登記コスト)
|
|
409
|
+
...(isReadOnlyCell(adapter, row, col) ? { "aria-readonly": "true", "data-aqsp-readonly": "" } : {}),
|
|
370
410
|
}
|
|
371
411
|
const rich = resolveRichCellProps(adapter, rowAxis, colAxis, cf, row, col)
|
|
372
412
|
// 掛け金は毎セル更新 (v0.1 恒等の null でも座標を進める — children が消費するのは書式能力下だけ)
|
|
@@ -467,6 +507,8 @@ const GridBodyInner = ({
|
|
|
467
507
|
colCount={adapter.colCount}
|
|
468
508
|
frozenLeadingCols={frozenColumns}
|
|
469
509
|
frozenLeadingRows={frozenRows}
|
|
510
|
+
frozenTrailingCols={frozenTrailingColumns}
|
|
511
|
+
frozenTrailingRows={frozenTrailingRows}
|
|
470
512
|
getRowHeight={getRowHeight}
|
|
471
513
|
getColWidth={getColWidth}
|
|
472
514
|
getCell={getCell}
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
* v0.5 adds the two-band frozen structure (plan §3-2): static frozen labels [0, F) at direct
|
|
10
10
|
* offsetOf lefts, plus a static clip (left = W_F) that hosts the existing scroll track with
|
|
11
11
|
* a floor-clamped enumeration (max(colStart, F)); F = 0 renders the v0.4-identical DOM.
|
|
12
|
+
* v0.9 adds the trailing band (v0.9 plan §4.12): the scroll enumeration gains the ceiling
|
|
13
|
+
* clamp min(colEnd, count − 1 − T_eff); a trailing static subtree (T_eff > 0 only) hosts a
|
|
14
|
+
* right-anchored inner with band-local labels and the ADR-26 near-edge strips/markers
|
|
15
|
+
* (`data-aqsp-resize-anchor="trailing"`); the scroll clip gains the registered calc() right
|
|
16
|
+
* inset (ADR-19 exception 4 — axis-model values only, NEVER a handle read: refs are null on
|
|
17
|
+
* first mount and an inline px snapshot has no heal channel on renderless resize); the two
|
|
18
|
+
* header divider segments (x-leading / x-trailing — §2.4) render count-driven inside the
|
|
19
|
+
* scroll clip; T = 0 renders no trailing DOM and no inline right (v0.8 identity).
|
|
12
20
|
*
|
|
13
21
|
* 列ヘッダー帯 (viewStore 購読 2)。v0.4 から RowHeader 方式の完全対称 (§7-5): VirtualGrid の
|
|
14
22
|
* onRangeChange (setColRange) が供給する描画列レンジのみを**窓原点相対**の小座標で描画し、
|
|
@@ -16,7 +24,12 @@
|
|
|
16
24
|
* paint 前に公開する (commit ゲート)。v0.3 の --aqsp-hx CSS 変数と useColumnWindow は削除:
|
|
17
25
|
* 軸原点の生 left は深部でアンカー済みセルと desync する (2^25 壁)。v0.5 で 2 帯凍結構造を追加
|
|
18
26
|
* (計画 §3-2): 静的凍結ラベル [0, F) は offsetOf 直接値、スクロール track は静的クリップ
|
|
19
|
-
* (left = W_F) 内で床クランプ列挙 (max(colStart, F)) — F = 0 は v0.4 恒等 DOM。
|
|
27
|
+
* (left = W_F) 内で床クランプ列挙 (max(colStart, F)) — F = 0 は v0.4 恒等 DOM。v0.9 で末尾帯を
|
|
28
|
+
* 追加 (v0.9 計画 §4.12): スクロール列挙に天井クランプ min(colEnd, count − 1 − T_eff)、末尾
|
|
29
|
+
* 静的サブツリー (T_eff > 0 のみ — 右詰めインナー + band-local ラベル + ADR-26 近端ストリップ /
|
|
30
|
+
* マーカー)、スクロールクリップの登記 calc() right インセット (ADR-19 例外 4 — 軸モデル値のみ、
|
|
31
|
+
* ハンドル読み厳禁: 初回マウント null / renderless resize で陳腐化)、ヘッダー分割線 2 セグメント
|
|
32
|
+
* (件数駆動 — §2.4)。T = 0 は末尾 DOM もインセットも描かない (v0.8 恒等)。
|
|
20
33
|
*
|
|
21
34
|
* 逸脱 (ADR-9): ヘッダー帯の contextmenu は非対応 — ネイティブメニューへ素通しし、
|
|
22
35
|
* 列選択も onCellContextMenu も発火しない (§6.8-3。README 逸脱表に記載)。
|
|
@@ -26,16 +39,18 @@ import { useWheelBridge, type VirtualGridHandle } from "@aiquants/virtualscroll"
|
|
|
26
39
|
import { type ReactNode, type RefObject, useCallback, useLayoutEffect, useSyncExternalStore } from "react"
|
|
27
40
|
import { type AxisModel, visibleIndicesInRange } from "../model/axisModel.ts"
|
|
28
41
|
import type { CellRange } from "../model/cellCoord.ts"
|
|
29
|
-
import { frozenWidthOf } from "../model/frozenPartition.ts"
|
|
30
|
-
import type
|
|
42
|
+
import { frozenWidthOf, trailingWidthOf } from "../model/frozenPartition.ts"
|
|
43
|
+
import { SCROLLBAR_WIDTH, type ViewStore } from "../model/viewStore.ts"
|
|
31
44
|
import { columnIndexToName } from "./columnName.ts"
|
|
32
|
-
import { buildResizeStrips } from "./resizeStrips.ts"
|
|
45
|
+
import { buildResizeStrips, buildTrailingResizeStrips } from "./resizeStrips.ts"
|
|
33
46
|
|
|
34
47
|
type ColumnHeaderProps = {
|
|
35
48
|
viewStore: ViewStore
|
|
36
49
|
colAxis: AxisModel
|
|
37
50
|
/** Frozen leading columns (v0.5 — mirrors the grid's engine-validated value; render-surface W_F derives from colAxis). / 先頭凍結列数 (v0.5 — エンジン検証値の写像。render 面の W_F は colAxis から導出)。 */
|
|
38
51
|
frozenCols: number
|
|
52
|
+
/** Trailing frozen columns (v0.9 — raw prop mirror; the effective count re-derives the engine clamp byte-identically and W_T derives from colAxis — the two-value discipline, §2.2). / 末尾凍結列数 (v0.9 — 素の prop 写像。実効件数はエンジンクランプを byte 一致で再導出し、W_T は colAxis から導出 — 二値系規律、§2.2)。 */
|
|
53
|
+
frozenTrailingCols: number
|
|
39
54
|
/** Wheel-bridge target (§6.8-1 — the grid handle satisfies WheelBridgeTarget). / ホイール橋渡し先 (グリッドハンドルがブリッジ契約適合)。 */
|
|
40
55
|
handleRef: RefObject<VirtualGridHandle | null>
|
|
41
56
|
/** Band element registration for the header selection machine (§6.8-1). / 選択機械の帯登録先。 */
|
|
@@ -50,6 +65,10 @@ type ColumnHeaderProps = {
|
|
|
50
65
|
selectionRanges: readonly CellRange[]
|
|
51
66
|
/** §11 opt-out: false suppresses strip/marker DOM entirely (§7.1). / §11 の opt-out (false は DOM 生成ごと抑止)。 */
|
|
52
67
|
enableResizeUI: boolean
|
|
68
|
+
/** Registration target for the x-leading divider segment (repaintColHeaders' display heal reads it — mount-cached, never a per-notification querySelector; v0.9 §2.4). / x-leading 分割線セグメントの登録先 (repaintColHeaders の display heal がマウント時キャッシュとして読む — 毎通知 querySelector の排除; v0.9 §2.4)。 */
|
|
69
|
+
dividerLeadingRef: RefObject<HTMLDivElement | null>
|
|
70
|
+
/** Registration target for the x-trailing divider segment (the dividerLeadingRef twin; v0.9 §2.4). / x-trailing 分割線セグメントの登録先 (dividerLeadingRef の双子; v0.9 §2.4)。 */
|
|
71
|
+
dividerTrailingRef: RefObject<HTMLDivElement | null>
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
/**
|
|
@@ -57,7 +76,7 @@ type ColumnHeaderProps = {
|
|
|
57
76
|
* OUTSIDE role="grid" (§10) and therefore aria-hidden.
|
|
58
77
|
* 描画ウィンドウの可視列名セルを描画。role="grid" 外の視覚補助のため aria-hidden。
|
|
59
78
|
*/
|
|
60
|
-
export const ColumnHeader = ({ viewStore, colAxis, frozenCols, handleRef, bandRef, colTrackRef, windowOriginXRef, repaintColHeaders, selectionRanges, enableResizeUI }: ColumnHeaderProps): ReactNode => {
|
|
79
|
+
export const ColumnHeader = ({ viewStore, colAxis, frozenCols, frozenTrailingCols, handleRef, bandRef, colTrackRef, windowOriginXRef, repaintColHeaders, selectionRanges, enableResizeUI, dividerLeadingRef, dividerTrailingRef }: ColumnHeaderProps): ReactNode => {
|
|
61
80
|
const bridgeRef = useWheelBridge(handleRef)
|
|
62
81
|
// 帯要素は選択機械 (bandRef) と wheel bridge の 2 消費者を持つ — マージ ref で合成
|
|
63
82
|
const mergedBandRef = useCallback(
|
|
@@ -99,10 +118,21 @@ export const ColumnHeader = ({ viewStore, colAxis, frozenCols, handleRef, bandRe
|
|
|
99
118
|
// 凍結ラベルは静的 subtree — 木絶対 left の直接値 (W_F ≤ viewport で LayoutUnit 安全)
|
|
100
119
|
const frozenVisibleCols = effectiveFrozenCols > 0 ? visibleIndicesInRange(colAxis, 0, effectiveFrozenCols - 1) : []
|
|
101
120
|
|
|
121
|
+
// 末尾凍結帯 (v0.9 計画 §2.2): 実効 T = min(max(0, T), count − F_eff) — エンジンの動的
|
|
122
|
+
// クランプと byte 一致 (leading が件数空間に勝つ)。W_T は colAxis から導出する render 面双子
|
|
123
|
+
const effectiveTrailingCols = Math.min(Math.max(0, frozenTrailingCols), colAxis.count - effectiveFrozenCols)
|
|
124
|
+
const trailingWidth = trailingWidthOf(colAxis, effectiveTrailingCols)
|
|
125
|
+
// 帯原点 (木座標) = totalSize − W_T (導出: W_T = totalSize − offsetOf(count − T_eff) — §2.1。
|
|
126
|
+
// 帯先頭の非表示トラックは 0 寄与のため offsetOf(count − T_eff) と常に等しい)
|
|
127
|
+
const trailingOrigin = colAxis.totalSize() - trailingWidth
|
|
128
|
+
const trailingVisibleCols = effectiveTrailingCols > 0 ? visibleIndicesInRange(colAxis, colAxis.count - effectiveTrailingCols, colAxis.count - 1) : []
|
|
129
|
+
|
|
102
130
|
// §5.4: Fenwick ジャンプ列挙 — 幅 0 列はヘッダーセル DOM を生成しない。配置は窓原点相対
|
|
103
131
|
// (生 offset は深部グリッドで数百万 px になり LayoutUnit 壁を踏む — §7-5)。スクロール側は
|
|
104
|
-
// 凍結列を再出現させない床クランプ (エンジン §8-a と同文 — 空窓 start > end は空列挙)
|
|
105
|
-
|
|
132
|
+
// 凍結列を再出現させない床クランプ (エンジン §8-a と同文 — 空窓 start > end は空列挙) +
|
|
133
|
+
// 末尾列を再出現させない天井クランプ min(colEnd, count − 1 − T_eff) (v0.9 §4.12 — 床の末尾
|
|
134
|
+
// 双子。Math.max(0, count − 1) は v0.8 出荷済みの空軸ガードで、T_eff = 0 では byte 恒等)
|
|
135
|
+
const visibleCols = visibleIndicesInRange(colAxis, Math.max(colStart, effectiveFrozenCols), Math.min(colEnd, Math.max(0, colAxis.count - 1) - effectiveTrailingCols))
|
|
106
136
|
const cells: ReactNode[] = []
|
|
107
137
|
for (const col of visibleCols) {
|
|
108
138
|
cells.push(
|
|
@@ -118,11 +148,16 @@ export const ColumnHeader = ({ viewStore, colAxis, frozenCols, handleRef, bandRe
|
|
|
118
148
|
const rawStrips = enableResizeUI ? buildResizeStrips(colAxis, visibleCols) : null
|
|
119
149
|
// 凍結境界の重複 unhide 遮断 (R3 是正): スクロール側先頭マーカーの復元対象が凍結列
|
|
120
150
|
// (index < F) かつ凍結帯が実在 (W_F > 0 — 帯側の分割マーカーが同じランを覆う) なら落とす。
|
|
121
|
-
// 全凍結列非表示 (W_F = 0)
|
|
122
|
-
|
|
151
|
+
// 全凍結列非表示 (W_F = 0) は帯側が何も描けないため、スクロール側が唯一の復元導線として残る。
|
|
152
|
+
// 末尾双子 (v0.9 §4.12): 末尾列 (index ≥ count − T_eff) 宛のマーカーは T_px > 0 で落とす
|
|
153
|
+
// (帯側の近端マーカーが同じランを覆う — §2.6 の発行が前提)。全末尾列非表示 (T_px = 0) は
|
|
154
|
+
// スクロール側マーカーが唯一の復元路として残る
|
|
155
|
+
const strips = rawStrips === null ? null : rawStrips.filter((strip) => !((strip.kind === "unhide" && strip.index < effectiveFrozenCols && frozenWidth > 0) || (strip.kind === "unhide" && strip.index >= colAxis.count - effectiveTrailingCols && trailingWidth > 0)))
|
|
123
156
|
|
|
124
157
|
// 凍結帯のストリップ (リサイズ / unhide) — 静的配置 (木絶対 left 直接値)
|
|
125
158
|
const frozenStrips = enableResizeUI && effectiveFrozenCols > 0 ? buildResizeStrips(colAxis, frozenVisibleCols) : null
|
|
159
|
+
// 末尾帯のストリップ / マーカー — 近端転置ビルダ (帯ローカル px、§2.6)
|
|
160
|
+
const trailingStrips = enableResizeUI && effectiveTrailingCols > 0 ? buildTrailingResizeStrips(colAxis, trailingVisibleCols) : null
|
|
126
161
|
const track = (
|
|
127
162
|
<div ref={colTrackRef} className="aqsp-col-header-track">
|
|
128
163
|
{cells}
|
|
@@ -140,8 +175,10 @@ export const ColumnHeader = ({ viewStore, colAxis, frozenCols, handleRef, bandRe
|
|
|
140
175
|
{/* track transform は repaintColHeaders (命令パス) の専有物 — React は書かない (§7-5)。
|
|
141
176
|
pointerdown (フォーカス回収 + 選択機械) は useHeaderSelection が帯へ結線する (§6.8-1)。
|
|
142
177
|
v0.5: F > 0 はスクロール track を静的クリップ (left = W_F) で包み、凍結ラベルを
|
|
143
|
-
帯直下の静的 subtree に置く (エンジン §8-b/c
|
|
144
|
-
|
|
178
|
+
帯直下の静的 subtree に置く (エンジン §8-b/c のヘッダー同文)。v0.9: T > 0 は末尾
|
|
179
|
+
静的サブツリー (右詰めインナー) を追加し、スクロールクリップに登記 calc() right
|
|
180
|
+
インセットを与える (§4.12)。F = 0 ∧ T = 0 は従来 DOM 恒等 */}
|
|
181
|
+
{effectiveFrozenCols === 0 && effectiveTrailingCols === 0 ? (
|
|
145
182
|
track
|
|
146
183
|
) : (
|
|
147
184
|
<>
|
|
@@ -157,9 +194,44 @@ export const ColumnHeader = ({ viewStore, colAxis, frozenCols, handleRef, bandRe
|
|
|
157
194
|
<div key={`fu${strip.index}`} className="aqsp-unhide-marker" data-aqsp-unhide={strip.index} style={{ left: strip.start, width: strip.length }} />
|
|
158
195
|
),
|
|
159
196
|
)}
|
|
160
|
-
|
|
197
|
+
{/* T_eff > 0 の inline right = 登記 calc() 形 #4 (ADR-19 例外レジスタ — 等価:
|
|
198
|
+
SCROLLBAR_WIDTH + W_T_vis の CSS 衣装。W_T_vis = min(W_T, max(0, 100% −
|
|
199
|
+
SCROLLBAR_WIDTH − W_F)) — 100% の再解決がブラウザ自身の heal チャネル。
|
|
200
|
+
軸モデル値のみ補間しハンドルは読まない。T = 0 は inline right 不在 = v0.8 恒等) */}
|
|
201
|
+
<div className="aqsp-col-header-scroll" data-aqsp-col-header-scroll="" style={{ left: frozenWidth, right: effectiveTrailingCols > 0 ? `calc(${SCROLLBAR_WIDTH}px + min(${trailingWidth}px, max(0px, 100% - ${SCROLLBAR_WIDTH + frozenWidth}px)))` : undefined }}>
|
|
161
202
|
{track}
|
|
203
|
+
{/* ヘッダー分割線セグメント (§2.4 — 存在 = 実効件数駆動の render 双子、可視 =
|
|
204
|
+
repaintColHeaders の等値抑制 display heal、幾何 = クリップ任せの静的値。
|
|
205
|
+
x-leading は境界 W_F = クリップ左端 (left: 0)、x-trailing は中央帯右端 =
|
|
206
|
+
クリップ右端 (right: 0) — 分割線は常に SCROLL 帯の縁画素を覆う) */}
|
|
207
|
+
{effectiveFrozenCols > 0 ? <div ref={dividerLeadingRef} className="aqsp-freeze-divider aqsp-freeze-divider-v" data-aqsp-freeze-divider="x-leading" style={{ left: 0, top: 0, bottom: 0 }} /> : null}
|
|
208
|
+
{effectiveTrailingCols > 0 ? <div ref={dividerTrailingRef} className="aqsp-freeze-divider aqsp-freeze-divider-v" data-aqsp-freeze-divider="x-trailing" style={{ right: 0, top: 0, bottom: 0 }} /> : null}
|
|
162
209
|
</div>
|
|
210
|
+
{effectiveTrailingCols > 0 ? (
|
|
211
|
+
/* 末尾クリップ (§4.12): inline left = 登記 calc() 形 #3 (ADR-19 例外レジスタ —
|
|
212
|
+
等価: クリップ left = extent − W_T_vis の CSS 衣装。max() は leading 被覆
|
|
213
|
+
画素を隠すだけでアンカーを動かさない)。right = SCROLLBAR_WIDTH (帯右端の
|
|
214
|
+
縦バー帯ぶん)。クリップ箱の描画特性は scroll クリップと同一のため同クラスを
|
|
215
|
+
再利用 (フックは data-aqsp-col-header-trailing) */
|
|
216
|
+
<div className="aqsp-col-header-scroll" data-aqsp-col-header-trailing="" style={{ left: `max(${frozenWidth}px, calc(100% - ${trailingWidth + SCROLLBAR_WIDTH}px))`, right: SCROLLBAR_WIDTH }}>
|
|
217
|
+
{/* 右詰めインナー (width = W_T の木寸法箱): overlap ではクリップが左縁を隠し、
|
|
218
|
+
右アンカーが帯の遠端整列を保つ (エンジン末尾クリップの右詰めインナー同文) */}
|
|
219
|
+
<div style={{ position: "absolute", top: 0, height: "100%", right: 0, width: trailingWidth }}>
|
|
220
|
+
{trailingVisibleCols.map((col) => (
|
|
221
|
+
<div key={col} className="aqsp-col-header-cell" data-col={col} data-aqsp-trailing-header="" data-aqsp-in-selection={isInSelection(col) ? "" : undefined} style={{ left: colAxis.offsetOf(col) - trailingOrigin, width: colAxis.sizeOf(col) }}>
|
|
222
|
+
{columnIndexToName(col)}
|
|
223
|
+
</div>
|
|
224
|
+
))}
|
|
225
|
+
{trailingStrips?.map((strip) =>
|
|
226
|
+
strip.kind === "resize" ? (
|
|
227
|
+
<div key={`tr${strip.index}`} className="aqsp-col-resize-strip" data-aqsp-col-resize={strip.index} data-aqsp-resize-anchor="trailing" style={{ left: strip.start, width: strip.length }} />
|
|
228
|
+
) : (
|
|
229
|
+
<div key={`tu${strip.index}`} className="aqsp-unhide-marker" data-aqsp-unhide={strip.index} data-aqsp-resize-anchor="trailing" style={{ left: strip.start, width: strip.length }} />
|
|
230
|
+
),
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
) : null}
|
|
163
235
|
</>
|
|
164
236
|
)}
|
|
165
237
|
</div>
|
package/src/header/RowHeader.tsx
CHANGED
|
@@ -2,19 +2,32 @@
|
|
|
2
2
|
* @fileoverview Row header band (viewStore subscription #3). Renders only the rendering-range
|
|
3
3
|
* rows (overscan-inclusive) with window-origin-relative coordinates; the track's residual
|
|
4
4
|
* translate is owned EXCLUSIVELY by the imperative repaintHeaders path (§6.8-2).
|
|
5
|
+
* v0.9 adds the trailing band as the strict transpose of ColumnHeader §4.12 (v0.9 plan §4.13):
|
|
6
|
+
* ceiling clamp min(rowEnd, count − 1 − T_eff) on the scroll enumeration; a trailing static
|
|
7
|
+
* subtree (T_eff > 0 only) with a bottom-anchored inner, band-local labels and the ADR-26
|
|
8
|
+
* near-edge strips/markers at each trailing track's TOP boundary; the scroll clip gains the
|
|
9
|
+
* registered calc() bottom inset (ADR-19 exception 5 — no gutter term: the row-header box
|
|
10
|
+
* already excludes the hbar strip via its marginBottom); the two y divider segments render
|
|
11
|
+
* count-driven inside the scroll clip. T = 0 renders no trailing DOM (v0.8 identity).
|
|
5
12
|
*
|
|
6
13
|
* 行ヘッダー帯 (viewStore 購読 3)。描画レンジ (オーバースキャン込み) の行のみを
|
|
7
14
|
* ウィンドウ原点基準の小座標で描画。track の残差 translate は命令パス (repaintHeaders) の
|
|
8
|
-
* 専有物 — React レンダーは書かない (§6.8-2 の二重書き手禁止)。
|
|
15
|
+
* 専有物 — React レンダーは書かない (§6.8-2 の二重書き手禁止)。v0.9 で末尾帯を ColumnHeader
|
|
16
|
+
* §4.12 の厳密転置として追加 (v0.9 計画 §4.13): スクロール列挙の天井クランプ
|
|
17
|
+
* min(rowEnd, count − 1 − T_eff)、末尾静的サブツリー (T_eff > 0 のみ — 下詰めインナー +
|
|
18
|
+
* band-local ラベル + 各末尾トラック TOP 境界の ADR-26 近端ストリップ / マーカー)、スクロール
|
|
19
|
+
* クリップの登記 calc() bottom インセット (ADR-19 例外 5 — ガター項なし: 行ヘッダー箱は
|
|
20
|
+
* marginBottom で横バー帯を既に除外)、y 分割線 2 セグメント (件数駆動)。T = 0 は末尾 DOM を
|
|
21
|
+
* 描かない (v0.8 恒等)。
|
|
9
22
|
*/
|
|
10
23
|
|
|
11
24
|
import { useWheelBridge, type VirtualGridHandle } from "@aiquants/virtualscroll"
|
|
12
25
|
import { type ReactNode, type RefObject, useCallback, useLayoutEffect, useSyncExternalStore } from "react"
|
|
13
26
|
import { type AxisModel, visibleIndicesInRange } from "../model/axisModel.ts"
|
|
14
27
|
import type { CellRange } from "../model/cellCoord.ts"
|
|
15
|
-
import { frozenHeightOf } from "../model/frozenPartition.ts"
|
|
28
|
+
import { frozenHeightOf, trailingHeightOf } from "../model/frozenPartition.ts"
|
|
16
29
|
import { SCROLLBAR_WIDTH, type ViewStore } from "../model/viewStore.ts"
|
|
17
|
-
import { buildResizeStrips } from "./resizeStrips.ts"
|
|
30
|
+
import { buildResizeStrips, buildTrailingResizeStrips } from "./resizeStrips.ts"
|
|
18
31
|
|
|
19
32
|
type RowHeaderProps = {
|
|
20
33
|
viewStore: ViewStore
|
|
@@ -34,13 +47,19 @@ type RowHeaderProps = {
|
|
|
34
47
|
enableResizeUI: boolean
|
|
35
48
|
/** Frozen leading rows (v0.6 — drives the two-band split; the ColumnHeader two-band transpose). / 先頭凍結行数 (v0.6 — 2 帯分割の構造判定。ColumnHeader 2 帯の転置)。 */
|
|
36
49
|
frozenRows: number
|
|
50
|
+
/** Trailing frozen rows (v0.9 — raw prop mirror; the effective count re-derives the engine clamp byte-identically and H_T derives from rowAxis — the two-value discipline, §2.2). / 末尾凍結行数 (v0.9 — 素の prop 写像。実効件数はエンジンクランプを byte 一致で再導出し、H_T は rowAxis から導出 — 二値系規律、§2.2)。 */
|
|
51
|
+
frozenTrailingRows: number
|
|
52
|
+
/** Registration target for the y-leading divider segment (repaintHeaders' display heal reads it — mount-cached, never a per-notification querySelector; v0.9 §2.4). / y-leading 分割線セグメントの登録先 (repaintHeaders の display heal がマウント時キャッシュとして読む — 毎通知 querySelector の排除; v0.9 §2.4)。 */
|
|
53
|
+
dividerLeadingRef: RefObject<HTMLDivElement | null>
|
|
54
|
+
/** Registration target for the y-trailing divider segment (the dividerLeadingRef twin; v0.9 §2.4). / y-trailing 分割線セグメントの登録先 (dividerLeadingRef の双子; v0.9 §2.4)。 */
|
|
55
|
+
dividerTrailingRef: RefObject<HTMLDivElement | null>
|
|
37
56
|
}
|
|
38
57
|
|
|
39
58
|
/**
|
|
40
59
|
* Renders row-number labels for the rendering window.
|
|
41
60
|
* 描画ウィンドウの行番号ラベルを描画 (1 始まり表示)。
|
|
42
61
|
*/
|
|
43
|
-
export const RowHeader = ({ viewStore, rowAxis, handleRef, bandRef, selectionRanges, rowTrackRef, windowOriginYRef, repaintHeaders, enableResizeUI, frozenRows }: RowHeaderProps): ReactNode => {
|
|
62
|
+
export const RowHeader = ({ viewStore, rowAxis, handleRef, bandRef, selectionRanges, rowTrackRef, windowOriginYRef, repaintHeaders, enableResizeUI, frozenRows, frozenTrailingRows, dividerLeadingRef, dividerTrailingRef }: RowHeaderProps): ReactNode => {
|
|
44
63
|
const bridgeRef = useWheelBridge(handleRef)
|
|
45
64
|
// 帯要素は選択機械 (bandRef) と wheel bridge の 2 消費者 — マージ ref で合成
|
|
46
65
|
const mergedBandRef = useCallback(
|
|
@@ -83,8 +102,18 @@ export const RowHeader = ({ viewStore, rowAxis, handleRef, bandRef, selectionRan
|
|
|
83
102
|
// 凍結ラベルは静的 subtree — 木絶対 top の直接値 (H_F ≤ viewport で LayoutUnit 安全)
|
|
84
103
|
const frozenVisibleRows = effectiveFrozenRows > 0 ? visibleIndicesInRange(rowAxis, 0, effectiveFrozenRows - 1) : []
|
|
85
104
|
|
|
86
|
-
//
|
|
87
|
-
|
|
105
|
+
// 末尾凍結帯 (v0.9 計画 §2.2 の行双子): 実効 T = min(max(0, T), count − R_eff) — エンジンの
|
|
106
|
+
// 動的クランプと byte 一致 (leading 勝ち)。H_T は rowAxis から導出する render 面双子
|
|
107
|
+
const effectiveTrailingRows = Math.min(Math.max(0, frozenTrailingRows), rowAxis.count - effectiveFrozenRows)
|
|
108
|
+
const trailingHeight = trailingHeightOf(rowAxis, effectiveTrailingRows)
|
|
109
|
+
// 帯原点 (木座標) = totalSize − H_T (導出: H_T = totalSize − offsetOf(count − T_eff) — §2.1)
|
|
110
|
+
const trailingOrigin = rowAxis.totalSize() - trailingHeight
|
|
111
|
+
const trailingVisibleRows = effectiveTrailingRows > 0 ? visibleIndicesInRange(rowAxis, rowAxis.count - effectiveTrailingRows, rowAxis.count - 1) : []
|
|
112
|
+
|
|
113
|
+
// スクロールラベルは床クランプ max(rowStart, R) — 凍結行を再出現させない (空窓 start > end は
|
|
114
|
+
// 空列挙) + 天井クランプ min(rowEnd, count − 1 − T_eff) — 末尾行を再出現させない (v0.9 §4.13。
|
|
115
|
+
// Math.max(0, count − 1) は v0.8 出荷済みの空軸ガードで、T_eff = 0 では byte 恒等)
|
|
116
|
+
const visibleRows = visibleIndicesInRange(rowAxis, Math.max(rowStart, effectiveFrozenRows), Math.min(rowEnd, Math.max(0, rowAxis.count - 1) - effectiveTrailingRows))
|
|
88
117
|
const labels: ReactNode[] = []
|
|
89
118
|
for (const row of visibleRows) {
|
|
90
119
|
labels.push(
|
|
@@ -93,11 +122,15 @@ export const RowHeader = ({ viewStore, rowAxis, handleRef, bandRef, selectionRan
|
|
|
93
122
|
</div>,
|
|
94
123
|
)
|
|
95
124
|
}
|
|
96
|
-
// §7.1: ストリップ / マーカーは track 末尾の後方兄弟。凍結境界の重複 unhide 遮断 (R3 是正の縦版)
|
|
125
|
+
// §7.1: ストリップ / マーカーは track 末尾の後方兄弟。凍結境界の重複 unhide 遮断 (R3 是正の縦版)。
|
|
126
|
+
// 末尾双子 (v0.9 §4.13): 末尾行宛マーカーは T_px > 0 で落とす (帯側近端マーカーが同じランを
|
|
127
|
+
// 覆う)。全末尾行非表示 (T_px = 0) はスクロール側が唯一の復元路
|
|
97
128
|
const rawStrips = enableResizeUI ? buildResizeStrips(rowAxis, visibleRows) : null
|
|
98
|
-
const strips = rawStrips === null ? null : rawStrips.filter((strip) => !(strip.kind === "unhide" && strip.index < effectiveFrozenRows && frozenHeight > 0))
|
|
129
|
+
const strips = rawStrips === null ? null : rawStrips.filter((strip) => !((strip.kind === "unhide" && strip.index < effectiveFrozenRows && frozenHeight > 0) || (strip.kind === "unhide" && strip.index >= rowAxis.count - effectiveTrailingRows && trailingHeight > 0)))
|
|
99
130
|
// 凍結帯のストリップ (静的配置 — 木絶対 top 直接値)
|
|
100
131
|
const frozenStrips = enableResizeUI && effectiveFrozenRows > 0 ? buildResizeStrips(rowAxis, frozenVisibleRows) : null
|
|
132
|
+
// 末尾帯のストリップ / マーカー — 近端転置ビルダ (帯ローカル px、各末尾トラック TOP 境界 — §2.6)
|
|
133
|
+
const trailingStrips = enableResizeUI && effectiveTrailingRows > 0 ? buildTrailingResizeStrips(rowAxis, trailingVisibleRows) : null
|
|
101
134
|
const track = (
|
|
102
135
|
<div ref={rowTrackRef} className="aqsp-row-header-track">
|
|
103
136
|
{labels}
|
|
@@ -117,8 +150,10 @@ export const RowHeader = ({ viewStore, rowAxis, handleRef, bandRef, selectionRan
|
|
|
117
150
|
<div ref={mergedBandRef} className="aqsp-row-header" aria-hidden="true" data-aqsp-row-header="" style={{ marginBottom: SCROLLBAR_WIDTH }}>
|
|
118
151
|
{/* track transform は repaintHeaders (命令パス) の専有物 — React は書かない (§6.8-2 (b))。
|
|
119
152
|
v0.6: R > 0 はスクロール track を静的クリップ (top = H_F) で包み、凍結ラベルを帯直下の
|
|
120
|
-
静的 subtree に置く (ColumnHeader の 2
|
|
121
|
-
|
|
153
|
+
静的 subtree に置く (ColumnHeader の 2 帯を転置)。v0.9: T > 0 は末尾静的サブツリー
|
|
154
|
+
(下詰めインナー) を追加し、スクロールクリップに登記 calc() bottom インセットを与える
|
|
155
|
+
(§4.13)。R = 0 ∧ T = 0 は従来 DOM 恒等 */}
|
|
156
|
+
{effectiveFrozenRows === 0 && effectiveTrailingRows === 0 ? (
|
|
122
157
|
track
|
|
123
158
|
) : (
|
|
124
159
|
<>
|
|
@@ -134,9 +169,42 @@ export const RowHeader = ({ viewStore, rowAxis, handleRef, bandRef, selectionRan
|
|
|
134
169
|
<div key={`fu${strip.index}`} className="aqsp-unhide-marker" data-aqsp-unhide={strip.index} style={{ top: strip.start, height: strip.length }} />
|
|
135
170
|
),
|
|
136
171
|
)}
|
|
137
|
-
|
|
172
|
+
{/* T_eff > 0 の inline bottom = 登記 calc() 形 #5 (ADR-19 例外レジスタ — 等価:
|
|
173
|
+
H_T_vis = min(H_T, max(0, 100% − H_F)) の CSS 衣装。ガター項なし — 行ヘッダー箱は
|
|
174
|
+
marginBottom で横バー帯を既に除外。100% の再解決が heal チャネル。T = 0 は
|
|
175
|
+
inline bottom 不在でクラスの bottom: 0 = v0.8 恒等) */}
|
|
176
|
+
<div className="aqsp-row-header-scroll" data-aqsp-row-header-scroll="" style={{ top: frozenHeight, bottom: effectiveTrailingRows > 0 ? `calc(min(${trailingHeight}px, max(0px, 100% - ${frozenHeight}px)))` : undefined }}>
|
|
138
177
|
{track}
|
|
178
|
+
{/* y ヘッダー分割線セグメント (§2.4 の転置 — 存在 = 件数駆動、可視 = repaintHeaders の
|
|
179
|
+
等値抑制 display heal、幾何 = クリップ任せ。y-leading は境界 H_F = クリップ上端、
|
|
180
|
+
y-trailing は中央帯下端 = クリップ下端) */}
|
|
181
|
+
{effectiveFrozenRows > 0 ? <div ref={dividerLeadingRef} className="aqsp-freeze-divider aqsp-freeze-divider-h" data-aqsp-freeze-divider="y-leading" style={{ top: 0, left: 0, right: 0 }} /> : null}
|
|
182
|
+
{effectiveTrailingRows > 0 ? <div ref={dividerTrailingRef} className="aqsp-freeze-divider aqsp-freeze-divider-h" data-aqsp-freeze-divider="y-trailing" style={{ bottom: 0, left: 0, right: 0 }} /> : null}
|
|
139
183
|
</div>
|
|
184
|
+
{effectiveTrailingRows > 0 ? (
|
|
185
|
+
/* 末尾クリップ (§4.13): inline top = 登記 calc() 形 #2 (ADR-19 例外レジスタ —
|
|
186
|
+
等価: クリップ top = extent − H_T_vis の CSS 衣装、ガター項なし)。bottom: 0 は
|
|
187
|
+
クラス由来。クリップ箱の描画特性は scroll クリップと同一のため同クラスを再利用
|
|
188
|
+
(フックは data-aqsp-row-header-trailing) */
|
|
189
|
+
<div className="aqsp-row-header-scroll" data-aqsp-row-header-trailing="" style={{ top: `max(${frozenHeight}px, calc(100% - ${trailingHeight}px))` }}>
|
|
190
|
+
{/* 下詰めインナー (height = H_T の木寸法箱): overlap ではクリップが上縁を隠し、
|
|
191
|
+
下アンカーが帯の遠端整列を保つ (エンジン末尾行帯インナーの転置同文) */}
|
|
192
|
+
<div style={{ position: "absolute", left: 0, right: 0, bottom: 0, height: trailingHeight }}>
|
|
193
|
+
{trailingVisibleRows.map((row) => (
|
|
194
|
+
<div key={row} className="aqsp-row-header-cell" data-row={row} data-aqsp-trailing-row-header="" data-aqsp-in-selection={isInSelection(row) ? "" : undefined} style={{ top: rowAxis.offsetOf(row) - trailingOrigin, height: rowAxis.sizeOf(row) }}>
|
|
195
|
+
{row + 1}
|
|
196
|
+
</div>
|
|
197
|
+
))}
|
|
198
|
+
{trailingStrips?.map((strip) =>
|
|
199
|
+
strip.kind === "resize" ? (
|
|
200
|
+
<div key={`tr${strip.index}`} className="aqsp-row-resize-strip" data-aqsp-row-resize={strip.index} data-aqsp-resize-anchor="trailing" style={{ top: strip.start, height: strip.length }} />
|
|
201
|
+
) : (
|
|
202
|
+
<div key={`tu${strip.index}`} className="aqsp-unhide-marker" data-aqsp-unhide={strip.index} data-aqsp-resize-anchor="trailing" style={{ top: strip.start, height: strip.length }} />
|
|
203
|
+
),
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
) : null}
|
|
140
208
|
</>
|
|
141
209
|
)}
|
|
142
210
|
</div>
|
|
@@ -8,13 +8,19 @@
|
|
|
8
8
|
* (the affordance never disappears — §7.1 recovery guarantee). A leading hidden run gets a
|
|
9
9
|
* 3px unhide marker on the first visible track's leading edge. Unhide markers are emitted
|
|
10
10
|
* AFTER all resize strips so DOM order stacks them on top (degenerate priority rule).
|
|
11
|
+
* v0.9 adds `buildTrailingResizeStrips` — the NEAR-EDGE transpose of the same three-variant +
|
|
12
|
+
* marker system for the trailing frozen band (v0.9 plan §2.6 / ADR-26); the leading builder
|
|
13
|
+
* is neither reused (its far-boundary output contradicts the near-edge contract) nor modified
|
|
14
|
+
* (leading byte-identity gate).
|
|
11
15
|
*
|
|
12
16
|
* §7.1 のリサイズストリップ / unhide マーカーの純幾何。ストリップは視覚とヒット判定のための
|
|
13
17
|
* DOM のみ (リスナーなし — 帯の wire-once capture リスナーが closest() でディスパッチ)。通常
|
|
14
18
|
* トラック (幅 ≥ 12) は境界の内側 6px、非表示ラン境界は同じ 6px を 3px リサイズ + 3px unhide
|
|
15
19
|
* に分割 (構造的排他)、縮退トラック (幅 < 12) は境界アンカー 3px の隣接 overhang (アフォー
|
|
16
20
|
* ダンスは消えない — 復旧保証)。先頭非表示ランは最初の可視トラックの先頭端 3px。unhide は
|
|
17
|
-
* リサイズ全件の後に出力し DOM 順で上層に積む (縮退時のマーカー優先則)。
|
|
21
|
+
* リサイズ全件の後に出力し DOM 順で上層に積む (縮退時のマーカー優先則)。v0.9 で
|
|
22
|
+
* `buildTrailingResizeStrips` を追加 — 末尾凍結帯向けの同 3 変種 + マーカー系の**近端**転置
|
|
23
|
+
* (v0.9 計画 §2.6 / ADR-26)。leading ビルダは再利用も改変もしない (byte 恒等ゲート)。
|
|
18
24
|
*/
|
|
19
25
|
|
|
20
26
|
import { type AxisModel, nextVisibleIndex } from "../model/axisModel.ts"
|
|
@@ -77,3 +83,82 @@ export const buildResizeStrips = (axis: AxisModel, visible: readonly number[]):
|
|
|
77
83
|
// マーカーを後置 — DOM 順の後勝ち積層で復元アフォーダンスを上層へ (縮退時の優先則)
|
|
78
84
|
return [...strips, ...markers]
|
|
79
85
|
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Builds the NEAR-EDGE strip/marker descriptors for the trailing frozen band (v0.9 plan §2.6 /
|
|
89
|
+
* ADR-26 — the full near-edge transpose of the leading three-variant + marker system).
|
|
90
|
+
* `visible` = the effective trailing indices [count − T_eff, count) filtered by sizeOf > 0
|
|
91
|
+
* (callers pass visibleIndicesInRange over the band). `start` is BAND-LOCAL px inside the
|
|
92
|
+
* trailing clip's inner. Variants (b = band-local near boundary of the owning track):
|
|
93
|
+
* normal track (size >= 12) resize [b, b+6) — the band-first strip is [0, 6), fully inside the
|
|
94
|
+
* clip; hidden-run-BEFORE split (axis-wide previous track hidden) [b, b+3) unhide restoring the
|
|
95
|
+
* boundary-nearest run index (track − 1) + [b+3, b+6) resize (the shipped 3px/3px permanent
|
|
96
|
+
* exclusivity, near-edge); degenerate track (size < 12) near-anchored 3px [b, b+3) overhanging
|
|
97
|
+
* only the FAR-side neighbor (recovery guarantee — the mirror of the leading LEFT overhang);
|
|
98
|
+
* band-tail marker [bandEnd − 3, bandEnd) restoring lastVisible + 1 when the axis's final
|
|
99
|
+
* tracks are hidden (the mirror of the leading window-head marker). Strips are emitted in
|
|
100
|
+
* REVERSE (far-to-near) order — a degenerate track's 3px overhangs its FAR neighbor, so DOM
|
|
101
|
+
* last-wins must place the degenerate after that neighbor for it to keep the overhang (the
|
|
102
|
+
* near-edge transpose of the ledger R5(B) precedence the leading builder gets for free from
|
|
103
|
+
* ascending order). Markers are appended AFTER
|
|
104
|
+
* strips (shared DOM last-wins precedence). Emitting band-side markers is the §4.12 dedup
|
|
105
|
+
* premise: scroll-side markers targeting trailing indices are dropped at T_px > 0 without
|
|
106
|
+
* orphaning any hidden run, and a run straddling count − T_eff emits on BOTH sides (each side
|
|
107
|
+
* restores its own boundary-nearest index).
|
|
108
|
+
*
|
|
109
|
+
* 末尾凍結帯の**近端**ストリップ / マーカー記述子を構築 (v0.9 計画 §2.6 / ADR-26 — 出荷済み
|
|
110
|
+
* 3 変種 + マーカー系の完全な近端転置)。`visible` は [count − T_eff, count) を sizeOf > 0 で
|
|
111
|
+
* 濾した実効末尾 index 群、`start` は末尾クリップのインナー内の帯ローカル px。変種 (b = 所有
|
|
112
|
+
* トラックの帯ローカル近端境界): 通常 (size ≥ 12) は [b, b+6) — 帯先頭ストリップは [0, 6) で
|
|
113
|
+
* クリップ内 / 非表示ラン直前分割は [b, b+3) unhide (復元対象 = 境界最近ラン track − 1) +
|
|
114
|
+
* [b+3, b+6) resize (3px/3px 恒久排他の近端転置) / 縮退 (size < 12) は近端 3px [b, b+3) の
|
|
115
|
+
* 遠側隣専用張出し (復旧保証) / 帯末尾マーカー [bandEnd − 3, bandEnd) は軸最終ラン非表示時に
|
|
116
|
+
* lastVisible + 1 を復元 (先頭マーカーの鏡像)。ストリップは遠側 → 近側の**逆順**出力 — 縮退の
|
|
117
|
+
* 張出しは遠側隣へ向かうため、DOM 後勝ちで縮退が overhang を保持するには隣より後が必要
|
|
118
|
+
* (leading が昇順で無償に得る台帳 R5(B) 優先則の近端転置)。マーカーは昇順のままストリップ後置
|
|
119
|
+
* (DOM 後勝ち)。帯側
|
|
120
|
+
* マーカー発行は §4.12 dedup の前提 — 跨ぎランは両側発行 (各側が自側の境界最近を復元)。
|
|
121
|
+
*/
|
|
122
|
+
export const buildTrailingResizeStrips = (axis: AxisModel, visible: readonly number[]): ResizeStripDescriptor[] => {
|
|
123
|
+
const strips: ResizeStripDescriptor[] = []
|
|
124
|
+
const markers: ResizeStripDescriptor[] = []
|
|
125
|
+
if (visible.length === 0) {
|
|
126
|
+
return strips
|
|
127
|
+
}
|
|
128
|
+
// 帯原点 (木座標) = offsetOf(先頭可視)。帯先頭の非表示トラックは 0 寄与のため
|
|
129
|
+
// offsetOf(count − T_eff) と常に等しい (導出: §2.1 の接頭和算術)
|
|
130
|
+
const origin = axis.offsetOf(visible[0])
|
|
131
|
+
// 帯ローカル終端 bandEnd = W_T = totalSize − origin (導出: W_T = totalSize − offsetOf(count − T) — §2.1)
|
|
132
|
+
const bandEnd = axis.totalSize() - origin
|
|
133
|
+
for (const track of visible) {
|
|
134
|
+
const size = axis.sizeOf(track)
|
|
135
|
+
// 近端境界の帯ローカル位置 b = offsetOf(track) − origin (導出: §2.6 — 帯ローカル px)
|
|
136
|
+
const b = axis.offsetOf(track) - origin
|
|
137
|
+
// 直前トラックの非表示判定は軸全体 — 帯先頭では scroll 側 index も見る (跨ぎランの両側発行)
|
|
138
|
+
const hiddenBefore = track > 0 && axis.sizeOf(track - 1) === 0
|
|
139
|
+
if (hiddenBefore) {
|
|
140
|
+
// 近端分割 [b, b+3) unhide + [b+3, b+6) resize (恒久排他の近端転置)。復元対象は
|
|
141
|
+
// 境界に近い側 = ランの末尾 index (track − 1)
|
|
142
|
+
markers.push({ kind: "unhide", index: track - 1, start: b, length: STRIP_HALF })
|
|
143
|
+
strips.push({ kind: "resize", index: track, start: b + STRIP_HALF, length: STRIP_HALF })
|
|
144
|
+
} else if (size >= 2 * STRIP_FULL) {
|
|
145
|
+
// 通常トラック: 近端の内側 6px (帯先頭は [0, 6) — クリップ内・非クリップのフル幅)
|
|
146
|
+
strips.push({ kind: "resize", index: track, start: b, length: STRIP_FULL })
|
|
147
|
+
} else {
|
|
148
|
+
// 縮退トラック: 近端アンカー 3px (自トラックに収まらない分は遠側隣へのみ張出し — 復旧保証)
|
|
149
|
+
strips.push({ kind: "resize", index: track, start: b, length: STRIP_HALF })
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// 帯末尾マーカー: 軸最終ラン非表示 (帯被覆により last より後の末尾トラックは定義上すべて非表示 —
|
|
153
|
+
// sizeOf 検査は先頭マーカーと同文の防御的対称)。[bandEnd − 3, bandEnd) が lastVisible + 1 を復元
|
|
154
|
+
const last = visible[visible.length - 1]
|
|
155
|
+
if (last < axis.count - 1 && axis.sizeOf(last + 1) === 0) {
|
|
156
|
+
markers.push({ kind: "unhide", index: last + 1, start: bandEnd - STRIP_HALF, length: STRIP_HALF })
|
|
157
|
+
}
|
|
158
|
+
// ストリップは遠側 → 近側の**逆順**へ反転 (台帳 R5(B) 優先則の近端転置): 縮退トラックの
|
|
159
|
+
// 近端 3px は遠側隣へ張出すため、昇順のままだと隣の DOM 後勝ちが overhang [b+size, b+3) を
|
|
160
|
+
// 奪い実効ヒット域が size px に潰れる (leading ビルダは昇順が同じ「縮退が overhang を勝つ」
|
|
161
|
+
// 優先則を無償で与える — 張出し方向が逆のため)。マーカーは昇順のまま後置 — DOM 順の後勝ち
|
|
162
|
+
// 積層で復元アフォーダンスを最上層へ (leading と共有の優先則)
|
|
163
|
+
return [...strips.reverse(), ...markers]
|
|
164
|
+
}
|