@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.
@@ -39,9 +39,10 @@
39
39
  * openPopoverRef / hideNoteTipRef / pasteRef / pendingPasteOptionsRef / ポップオーバーアンカー) を所有する。
40
40
  */
41
41
 
42
- import type { VirtualGridHandle, VirtualGridRange } from "@aiquants/virtualscroll"
42
+ import type { ScrollBarTapCircleOptions, VirtualGridHandle, VirtualGridRange } from "@aiquants/virtualscroll"
43
43
  import type { ProfilerOnRenderCallback } from "react"
44
44
  import { forwardRef, type ReactNode, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"
45
+ import type { ClipChannel } from "./clipboard/clipPayload.ts"
45
46
  import type { PasteOptions } from "./clipboard/pasteSpecial.ts"
46
47
  import { type ClipInfo, type PasteOutcome, type PasteSheet, useClipboard } from "./clipboard/useClipboard.ts"
47
48
  import { type BorderPreset, type CommandRunner, useCommands } from "./commands/useCommands.ts"
@@ -62,7 +63,7 @@ import { type CellCoord, type CellRange, clampCoord, makeRange, rangeSize, type
62
63
  import { type CellBorder, type CellFormat, type CellFormatPatch, formatCellValue } from "./model/cellFormat.ts"
63
64
  import { type EditPaint, IDLE_EDIT_PAINT, normalizeColor, resolveEditPaint } from "./model/cellPaint.ts"
64
65
  import { buildCfIndex, type CfIndex, type CfMemo, createCfMemo, resolvePaintedFormat } from "./model/conditionalFormat.ts"
65
- import { type CellEdit, type EditContext, type EditRejection, type EditSource, lazyCellText, resolveCellAlign, type SpreadsheetDataAdapter } from "./model/dataAdapter.ts"
66
+ import { type CellEdit, type EditContext, type EditRejection, type EditSource, isReadOnlyCell, lazyCellText, type ReadOnlyRejection, readOnlyDeclared, resolveCellAlign, type SpreadsheetDataAdapter } from "./model/dataAdapter.ts"
66
67
  import { type DataValidationRule, type ValidationVerdict, validateCellText } from "./model/dataValidation.ts"
67
68
  import type { ReferenceAxis } from "./model/formulaRefs.ts"
68
69
  import { colToViewportX, rowToViewportY } from "./model/frozenPartition.ts"
@@ -177,6 +178,45 @@ export type SpreadsheetProps = {
177
178
  * 位置に依らず常時可視で、可視化の縦成分は no-op。飛行中の変更は全ドラッグを取り消す。
178
179
  */
179
180
  frozenRows?: number
181
+ /**
182
+ * Freezes the LAST N columns at the right edge (v0.9 — the VirtualGrid frozenTrailingCols
183
+ * pass-through; LIVE prop, default 0). Validation SSOT is the engine's fail-fast contract
184
+ * (integer in [0, 128] — RangeError otherwise; the count clamp is leading-wins:
185
+ * effectiveTrailing = min(T, colCount − effectiveLeading)). Trailing columns stay visible at
186
+ * any horizontal scroll — with ONE registered exception: when the leading + trailing extent
187
+ * exceeds the viewport, occluded trailing cells cannot be scrolled into view and keyboard
188
+ * focus paints no visible frame (ADR-19-1). scrollToCell / ensure-visible treat a trailing
189
+ * target as a horizontal no-op. A mid-flight change cancels every in-flight drag without
190
+ * restore (the frozenColumns cancel seam).
191
+ * 末尾 N 列の右端凍結 (v0.9 — VirtualGrid frozenTrailingCols の素通し。ライブ prop、既定 0)。
192
+ * 検証 SSOT はエンジンの fail-fast 契約 ([0, 128] 整数、それ以外 RangeError。件数クランプは
193
+ * leading 勝ち: min(T, colCount − effectiveLeading))。末尾列は任意の横スクロールで可視 —
194
+ * ただし leading + trailing の extent が viewport を超える overlap では遮蔽された末尾セルを
195
+ * スクロールで可視化できず、キーボードフォーカスは可視フレームを描かない (ADR-19-1 の登記済み
196
+ * 帰結)。scrollToCell / ensure-visible は末尾ターゲットを横 no-op として扱う。飛行中の変更は
197
+ * 全ドラッグを復元なしで取り消す (frozenColumns と同じシーム)。
198
+ */
199
+ frozenTrailingColumns?: number
200
+ /**
201
+ * Freezes the LAST N rows at the bottom (v0.9 — the VirtualGrid frozenTrailingRows
202
+ * pass-through; LIVE prop, default 0 — the row twin of frozenTrailingColumns, same engine
203
+ * fail-fast contract and cancel seam). The typical use is a pinned totals row.
204
+ * 末尾 N 行の下端凍結 (v0.9 — frozenTrailingRows の素通し。ライブ prop、既定 0 —
205
+ * frozenTrailingColumns の行双子。エンジン契約・取消シームは同文)。典型は固定合計行。
206
+ */
207
+ frozenTrailingRows?: number
208
+ /**
209
+ * Tap-scroll circle tuning (v0.10 — the VirtualGrid `scrollBarOptions.tapScrollCircleOptions`
210
+ * engine-validated pass-through; LIVE prop): position and enable/disable of the touch
211
+ * tap-scroll circle. The engine's `resolveTapScrollCircleOptions` is the validation SSOT —
212
+ * undefined is the engine's declared absence signal (engine defaults), never a value
213
+ * fallback in this package.
214
+ * タップサークル調整 (v0.10 — VirtualGrid `scrollBarOptions.tapScrollCircleOptions` の
215
+ * エンジン検証素通し。ライブ prop): タッチのタップスクロールサークルの位置・有効 / 無効の
216
+ * 調整口。検証の SSOT はエンジンの `resolveTapScrollCircleOptions` — undefined はエンジン
217
+ * 宣言の不在信号 (エンジン既定) で、本パッケージでの値フォールバックなし。
218
+ */
219
+ tapScrollCircleOptions?: ScrollBarTapCircleOptions
180
220
  /**
181
221
  * Identity-stable contract (same as adapter): an inline arrow here defeats the CellView memo
182
222
  * and turns every host re-render into a full visible re-slice (~800 cells at 16k columns).
@@ -228,6 +268,39 @@ export type SpreadsheetProps = {
228
268
  * Escape は WCAG 2.1.2 のフォーカス脱出)。
229
269
  */
230
270
  onEditRejected?: (error: unknown, rejection: EditRejection) => void
271
+ /**
272
+ * Read-only refusal notice (v0.10): fired once per refused gesture — the adapter was never
273
+ * called, so this is neither an EditRejection nor an operation limit. Never fired when the
274
+ * adapter does not declare `isCellReadOnly`; omit for silence.
275
+ * 読み取り専用拒否通知 (v0.10)。拒否されたジェスチャ毎に 1 回発火 — adapter は呼ばれて
276
+ * いないため EditRejection でも操作上限でもない。`isCellReadOnly` 未宣言では発火せず、
277
+ * 省略時は通知なし。
278
+ */
279
+ onReadOnlyRejected?: (rejection: ReadOnlyRejection) => void
280
+ /**
281
+ * AT announcement text for a read-only refusal (v0.10), routed through the live region;
282
+ * omit for silence (no built-in strings). Inherits the announcement debounce + rAF collapse,
283
+ * which is what keeps autorepeat / IME refusal storms from spamming the live region.
284
+ * 読み取り専用拒否の AT 読み上げ文言 (v0.10) — live region 経由。省略で無音 (組み込み文言
285
+ * なし)。読み上げのデバウンス + rAF 合流を継承し、autorepeat / IME の拒否連打でも live
286
+ * region は溢れない。
287
+ */
288
+ formatReadOnlyAnnouncement?: (rejection: ReadOnlyRejection) => string
289
+ /**
290
+ * Paste outcome summary (v0.10 plan §3.1): the menu channel mirrors `paste(options)`'s
291
+ * resolve value 1:1 (read refusals and the write-time re-gate included); the event channel
292
+ * fires once per gesture that reaches the paste pipeline — the drag-freeze consumption and a
293
+ * null `clipboardData` stay deliberately silent, and an empty grid attaches no listener at
294
+ * all. Cause-specific callbacks (`onClipboardUnavailable` / `onOperationRejected` /
295
+ * `onReadOnlyRejected`) fire FIRST — `"rejected"` can be the trailing summary of one of
296
+ * them. Omit = never fired.
297
+ * 貼り付け結果の要約 (v0.10 計画 §3.1)。menu チャネルは `paste(options)` の resolve 値の
298
+ * 1:1 鏡 (読み拒否・書込時再ゲート含む)、event チャネルは貼り付けパイプラインに到達した
299
+ * ゲスチャ毎 1 回 — ドラッグ凍結消費と `clipboardData` null は意図的無言、空グリッドは
300
+ * リスナ自体が無い。原因特定系 callback が先に発火し、`"rejected"` はその後追いの要約に
301
+ * なり得る。省略時は発火なし。
302
+ */
303
+ onPasteResolved?: (outcome: PasteOutcome, channel: ClipChannel) => void
231
304
  /** Change notice for out-of-package writes (undo / collaboration); each increment repaints visible cells (§6.9-2). / パッケージ外経路のデータ変更通知。 */
232
305
  dataVersion?: number
233
306
  /** Operation cell-count ceiling, default 100,000 (ADR-12). / 操作セル数上限。 */
@@ -254,12 +327,19 @@ export type SpreadsheetProps = {
254
327
  * AT announcement text for the active cell; omit for silence (no built-in strings). `text`
255
328
  * is the RAW canonical getCellText; `displayText` is what a sighted user sees (the
256
329
  * getCellFormat-formatted string — equals `text` when the cell has no format), so AT can
257
- * hear "87.0%" for a cell whose raw value is "0.87".
330
+ * hear "87.0%" for a cell whose raw value is "0.87". `readOnly` (v0.10) is
331
+ * declaration-gated three-valued: the key is ABSENT when the adapter does not declare
332
+ * `isCellReadOnly` (payload byte-identical to v0.9), `false` = declared editable, `true` =
333
+ * declared read-only — the navigation-time read-only channel (DOM focus stays on the sink,
334
+ * so `aria-readonly` is inert during Arrow navigation).
258
335
  * AT 向け読み上げ文言 (省略で無音)。`text` は素の正準 getCellText、`displayText` は目視と同じ
259
336
  * 表示文字列 (getCellFormat 整形後 — 書式なしセルでは text と同値)。素値 "0.87" の表示
260
- * "87.0%" をそのまま読み上げられる。
337
+ * "87.0%" をそのまま読み上げられる。`readOnly` (v0.10) は宣言ゲートの 3 値: `isCellReadOnly`
338
+ * 未宣言はキー自体なし (payload は v0.9 とバイト同一)、`false` = 宣言済み編集可、`true` =
339
+ * 宣言済み読み取り専用 — 矢印航行時の読み取り専用チャネル (フォーカスはシンク常駐のため
340
+ * `aria-readonly` は航行中の AT に届かない)。
261
341
  */
262
- formatCellAnnouncement?: (ctx: { row: number; col: number; text: string; displayText: string; columnName: string }) => string
342
+ formatCellAnnouncement?: (ctx: { row: number; col: number; text: string; displayText: string; columnName: string; readOnly?: boolean }) => string
263
343
  /** AT announcement for the selection extent ("A1 to C3, 3 rows 3 cols"); omit for silence. / AT 向け選択範囲読み上げ。 */
264
344
  formatSelectionAnnouncement?: (ctx: { range: CellRange; anchor: CellCoord; active: CellCoord; rangeRowCount: number; rangeColCount: number; rangeCount: number }) => string
265
345
  /**
@@ -417,6 +497,17 @@ const stepZoom = (value: number, direction: 1 | -1): number => (direction > 0 ?
417
497
  type SpreadsheetTestProps = {
418
498
  /** Test-only CellView re-render counter; never passed in production. / テスト専用カウンタ。 */
419
499
  __onCellRender?: ProfilerOnRenderCallback
500
+ /**
501
+ * Test-only virtual-keyboard raise spy (v0.10 plan §6.4 / RC-G2t): called synchronously by
502
+ * the single `raiseVirtualKeyboard` funnel, so spy count ≡ raise executions — a React-batched
503
+ * raise-then-lower mutates no DOM (`inputmode` observer-blind) yet still counts. Never
504
+ * passed in production (zero calls when unset — the `__onCellRender` conduct).
505
+ * テスト専用の仮想キーボード招待スパイ (v0.10 計画 §6.4 / RC-G2t)。単一漏斗
506
+ * `raiseVirtualKeyboard` が同期で呼ぶため、計数 ≡ raise 実行 — React バッチの
507
+ * raise-then-lower は DOM 無変異 (`inputmode` の観測は盲目) でも計数には映る。production
508
+ * では渡さない (未指定 = 呼出しゼロ — `__onCellRender` と同じ品行)。
509
+ */
510
+ __onVirtualKeyboardRaise?: () => void
420
511
  }
421
512
 
422
513
  const DEFAULT_ROW_HEIGHT = 24
@@ -525,6 +616,9 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
525
616
  defaultColumnWidth = DEFAULT_COLUMN_WIDTH,
526
617
  frozenColumns = 0,
527
618
  frozenRows = 0,
619
+ frozenTrailingColumns = 0,
620
+ frozenTrailingRows = 0,
621
+ tapScrollCircleOptions,
528
622
  renderCell,
529
623
  onSelectionChange,
530
624
  onCellContextMenu,
@@ -533,6 +627,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
533
627
  onCopyMultiRange,
534
628
  onOperationRejected,
535
629
  onEditRejected,
630
+ onReadOnlyRejected,
631
+ formatReadOnlyAnnouncement,
536
632
  dataVersion,
537
633
  maxOperationCells = DEFAULT_MAX_OPERATION_CELLS,
538
634
  enableFillHandle = true,
@@ -555,6 +651,7 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
555
651
  onValidationRejected,
556
652
  onPasteSpecialRequest,
557
653
  onClipboardUnavailable,
654
+ onPasteResolved,
558
655
  now = Date.now,
559
656
  zoom,
560
657
  defaultZoom,
@@ -562,6 +659,7 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
562
659
  testId,
563
660
  className,
564
661
  __onCellRender,
662
+ __onVirtualKeyboardRaise,
565
663
  }: SpreadsheetProps & SpreadsheetTestProps,
566
664
  ref,
567
665
  ): ReactNode {
@@ -747,6 +845,16 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
747
845
  const windowOriginYRef = useRef(0)
748
846
  const colTrackRef = useRef<HTMLDivElement | null>(null)
749
847
  const windowOriginXRef = useRef(0)
848
+ // ヘッダー帯要素 (選択機械 / オートスクローラーの結線先 — 従来はオートスクローラー節で宣言)
849
+ const colHeaderRef = useRef<HTMLDivElement | null>(null)
850
+ const rowHeaderRef = useRef<HTMLDivElement | null>(null)
851
+ // ヘッダー分割線セグメントのマウント時キャッシュ (v0.9 §2.4 / §4.9 census #12/#13):
852
+ // ヘッダー再描画は同期横パス上の毎通知呼びのため、querySelector 走査ではなく JSX ref で
853
+ // 装着済みノードを直読みする (存在は件数駆動 render 双子 — アンマウントで React が null へ戻す)
854
+ const dividerXLeadingRef = useRef<HTMLDivElement | null>(null)
855
+ const dividerXTrailingRef = useRef<HTMLDivElement | null>(null)
856
+ const dividerYLeadingRef = useRef<HTMLDivElement | null>(null)
857
+ const dividerYTrailingRef = useRef<HTMLDivElement | null>(null)
750
858
  const liveRegionRef = useRef<HTMLDivElement | null>(null)
751
859
 
752
860
  // ---- 選択状態 (React state + 命令読み ref — §6.9-1) ----
@@ -873,11 +981,42 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
873
981
  }, [])
874
982
 
875
983
  /**
876
- * Repaints ONLY the row-header track residual transform. Callers: onScroll, resizeRow, and
984
+ * Equality-suppressed display heal of one header divider segment (v0.9 §4.9 census #12/#13):
985
+ * visibility = band TREE px > 0; existence is the header's count-driven render twin and the
986
+ * geometry stays clip-owned, so this display flip is the ONLY divider write the header
987
+ * repaint path ever makes. The segment arrives through the mount-cached JSX ref (never a
988
+ * per-notification querySelector traversal — the synchronous horizontal pass must not pay
989
+ * a selector scan, least of all a guaranteed miss at F = T = 0). A same-value pass writes
990
+ * nothing — the §5.7 zero-write invariant observes both header repaint writers.
991
+ * ヘッダー分割線セグメント 1 つの等値抑制 display heal (v0.9 §4.9 census #12/#13)。可視 =
992
+ * 帯 TREE px > 0。存在はヘッダーの件数駆動 render 双子、幾何はクリップ所有のままで、ヘッダー
993
+ * 再描画が分割線へ行う書込はこの display フリップだけ。セグメントはマウント時キャッシュの
994
+ * JSX ref 経由 (毎通知の querySelector 走査は同期横パスに載せない — F = T = 0 では全走査
995
+ * ミスが保証される)。同値パスは無書込 — §5.7 のゼロ書込不変量が両書き手を観測する。
996
+ */
997
+ const healHeaderDivider = useCallback((segment: HTMLElement | null, shown: boolean) => {
998
+ // 件数 0 の家族はセグメント未マウント (ref は null) — 無仕事で戻る
999
+ if (segment === null) {
1000
+ return
1001
+ }
1002
+ const next = shown ? "" : "none"
1003
+ // 等値抑制 — 定常スクロール wheel 窓中の分割線 style 書込 = 0 の不変量 (§2.4 / §5.7)
1004
+ if (segment.style.display !== next) {
1005
+ segment.style.display = next
1006
+ }
1007
+ }, [])
1008
+
1009
+ /**
1010
+ * Repaints the row-header track residual transform. Callers: onScroll, resizeRow, and
877
1011
  * RowHeader's layout effect. vy comes from the grid handle (the truth — §7-4; the v0.6 −H_F
878
- * frozen-band term makes this the exact twin of repaintColHeaders).
879
- * 行 track の残差 transform のみを書く命令再描画。vy はグリッドハンドル (§7-4 の真実) から読む
880
- * (v0.6 の −H_F 凍結帯項で repaintColHeaders の完全双子)。
1012
+ * frozen-band term makes this the exact twin of repaintColHeaders). v0.9 adds the
1013
+ * equality-suppressed display heal of the two y header divider segments (§4.9 census #12 —
1014
+ * NO geometry write: the scroll-clip insets are the registered calc() forms that re-resolve
1015
+ * through 100% by themselves).
1016
+ * 行 track の残差 transform を書く命令再描画。vy はグリッドハンドル (§7-4 の真実) から読む
1017
+ * (v0.6 の −H_F 凍結帯項で repaintColHeaders の完全双子)。v0.9 で y ヘッダー分割線 2 セグメント
1018
+ * の等値抑制 display heal を追加 (§4.9 census #12 — 幾何書込なし: scroll クリップのインセットは
1019
+ * 登記 calc() 形が 100% 再解決で自癒する)。
881
1020
  */
882
1021
  const repaintHeaders = useCallback(() => {
883
1022
  const track = rowTrackRef.current
@@ -888,14 +1027,27 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
888
1027
  const frozenHeight = handleRef.current?.getFrozenSize().height ?? 0
889
1028
  track.style.transform = `translateY(${windowOriginYRef.current - vy - frozenHeight}px)`
890
1029
  }
891
- }, [])
1030
+ // v0.9 (§4.9 census #12): y ヘッダー分割線の可視 heal — 可視 = 帯 TREE px > 0 (H_F / H_T の
1031
+ // ハンドルフィールド)。ハンドル未装着は無書込 (JSX 初期状態のまま = 件数 render が真実)。
1032
+ // 両セグメント未マウント (R = T = 0) は getFrozenSize 読みごと省く — 同期横パスの無仕事保証
1033
+ const handle = handleRef.current
1034
+ if (handle !== null && (dividerYLeadingRef.current !== null || dividerYTrailingRef.current !== null)) {
1035
+ const frozen = handle.getFrozenSize()
1036
+ healHeaderDivider(dividerYLeadingRef.current, frozen.height > 0)
1037
+ healHeaderDivider(dividerYTrailingRef.current, frozen.trailingHeight > 0)
1038
+ }
1039
+ }, [healHeaderDivider])
892
1040
 
893
1041
  /**
894
- * Repaints ONLY the column-header track residual transform (§7-5 — the row repaintHeaders
1042
+ * Repaints the column-header track residual transform (§7-5 — the row repaintHeaders
895
1043
  * twin; the v0.3 --aqsp-hx CSS variable is retired). hx comes from the grid handle (the
896
- * truth — §7-4). Callers: onScroll, resize tails, and ColumnHeader's layout effect.
897
- * 列 track の残差 transform のみを書く命令再描画 (§7-5 — 行 repaintHeaders の双子。v0.3 の
898
- * --aqsp-hx CSS 変数は退役)。hx はグリッドハンドル (§7-4 の真実) から読む。
1044
+ * truth — §7-4). Callers: onScroll, resize tails, and ColumnHeader's layout effect. v0.9
1045
+ * adds the equality-suppressed display heal of the two x header divider segments (§4.9
1046
+ * census #13 — NO geometry write, the exact twin of repaintHeaders).
1047
+ * 列 track の残差 transform を書く命令再描画 (§7-5 — 行 repaintHeaders の双子。v0.3 の
1048
+ * --aqsp-hx CSS 変数は退役)。hx はグリッドハンドル (§7-4 の真実) から読む。v0.9 で x ヘッダー
1049
+ * 分割線 2 セグメントの等値抑制 display heal を追加 (§4.9 census #13 — 幾何書込なし、
1050
+ * repaintHeaders の完全双子)。
899
1051
  */
900
1052
  const repaintColHeaders = useCallback(() => {
901
1053
  const track = colTrackRef.current
@@ -906,7 +1058,61 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
906
1058
  const frozenWidth = handleRef.current?.getFrozenSize().width ?? 0
907
1059
  track.style.transform = `translateX(${windowOriginXRef.current - hx - frozenWidth}px)`
908
1060
  }
909
- }, [])
1061
+ // v0.9 (§4.9 census #13): x ヘッダー分割線の可視 heal (W_F / W_T の TREE px 駆動 — 行と同文。
1062
+ // 両セグメント未マウント (F = T = 0) は getFrozenSize 読みごと省く)
1063
+ const handle = handleRef.current
1064
+ if (handle !== null && (dividerXLeadingRef.current !== null || dividerXTrailingRef.current !== null)) {
1065
+ const frozen = handle.getFrozenSize()
1066
+ healHeaderDivider(dividerXLeadingRef.current, frozen.width > 0)
1067
+ healHeaderDivider(dividerXTrailingRef.current, frozen.trailingWidth > 0)
1068
+ }
1069
+ }, [healHeaderDivider])
1070
+
1071
+ // ---- 分割線可視の post-commit heal (v0.9 §2.4 / §2.7) ----
1072
+ const dividerHealRafRef = useRef<number | null>(null)
1073
+ /**
1074
+ * Schedules ONE post-commit divider-visibility heal after a track-resize seam (v0.9 §2.7 —
1075
+ * "T_px = 0 ⇒ no divider" must hold without an interposed scroll). The seam's synchronous
1076
+ * repaint reads the engine's render-captured getFrozenSize (a registered per-render mapping)
1077
+ * BEFORE the engine has re-rendered the new track size, so the divider display would
1078
+ * otherwise lag one geometry transition until the next write pass. One coalesced rAF
1079
+ * re-runs the three imperative writers from the committed getFrozenSize; the equality
1080
+ * suppression keeps this a zero-write pass unless a visibility flip is actually due, and
1081
+ * the F = T = 0 configuration schedules nothing (v0.8 identity — no reservation either).
1082
+ * トラックリサイズシーム後の分割線可視 post-commit heal を 1 回予約 (v0.9 §2.7 —
1083
+ * 「T_px = 0 ⇒ 分割線なし」はスクロール介在なしで成立が要件)。シームの同期 repaint は
1084
+ * エンジンの render 捕捉 getFrozenSize (登記済み毎レンダー写像) をエンジン再レンダー**前**に
1085
+ * 読むため、放置すると分割線 display が次の書込パスまで 1 遷移遅れる。合流 rAF 1 回で
1086
+ * commit 済み getFrozenSize から命令書き手 3 つを再実行 — 等値抑制によりフリップ不要なら
1087
+ * 無書込パス、F = T = 0 構成は予約ごと省く (v0.8 恒等)。
1088
+ */
1089
+ const scheduleDividerHeal = useCallback(() => {
1090
+ // 分割線セグメントが 1 本も存在しない構成 (F = T = 0) は heal 対象なし — 予約も残さない
1091
+ if (dividerXLeadingRef.current === null && dividerXTrailingRef.current === null && dividerYLeadingRef.current === null && dividerYTrailingRef.current === null) {
1092
+ return
1093
+ }
1094
+ // 合流 (バッチリサイズの多発呼びでも 1 フレーム 1 heal)
1095
+ if (dividerHealRafRef.current !== null) {
1096
+ return
1097
+ }
1098
+ dividerHealRafRef.current = requestAnimationFrame(() => {
1099
+ dividerHealRafRef.current = null
1100
+ // ヘッダー 2 面 + overlay (胴体セグメントは layersFor が同パスで heal する) — commit 済み
1101
+ // getFrozenSize からの再実行。残差 transform / 幾何は同値再書込 (エンジンが既に確定済み)
1102
+ repaintHeaders()
1103
+ repaintColHeaders()
1104
+ repaintOverlay()
1105
+ })
1106
+ }, [repaintHeaders, repaintColHeaders, repaintOverlay])
1107
+ // アンマウント時に予約中の heal rAF を回収 (本パッケージの全タイマ / rAF 回収規律)
1108
+ useEffect(
1109
+ () => () => {
1110
+ if (dividerHealRafRef.current !== null) {
1111
+ cancelAnimationFrame(dividerHealRafRef.current)
1112
+ }
1113
+ },
1114
+ [],
1115
+ )
910
1116
 
911
1117
  // ensureCellVisible: §6.6.3 の自前 nearest (1 回構築して keyboard / Shift+F10 へ配布 — §6.9-7)。
912
1118
  // 軸 identity が変わったら作り直す (クロージャが旧軸を掴む stale を防ぐ)
@@ -961,6 +1167,28 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
961
1167
  [],
962
1168
  )
963
1169
 
1170
+ /**
1171
+ * The single read-only refusal funnel (v0.10 plan §6.4 — the SSOT of RC-ANN / RC-B): every
1172
+ * gate (G1..G16) delivers its rejection here, which relays the callback AND the announcement
1173
+ * (`formatReadOnlyAnnouncement` → `announce()` — the debounce is inherited). Both props
1174
+ * omitted = silence = byte-identical; the read-only refusal never rides `onEditRejected` /
1175
+ * `onOperationRejected` (channel purity).
1176
+ * 読み取り専用拒否の単一漏斗 (v0.10 計画 §6.4 — RC-ANN / RC-B の SSOT)。全ゲート (G1..G16)
1177
+ * の拒否はここへ集まり、callback と読み上げ (`formatReadOnlyAnnouncement` → `announce()` —
1178
+ * デバウンス継承) の両方を運ぶ。両 prop 省略 = 無音 = バイト恒等。読み取り専用拒否を
1179
+ * `onEditRejected` / `onOperationRejected` へ流用しない (チャネル純度)。
1180
+ */
1181
+ const rejectReadOnly = useCallback(
1182
+ (rejection: ReadOnlyRejection) => {
1183
+ onReadOnlyRejected?.(rejection)
1184
+ // 読み上げは prop 宣言時だけ (省略 = 無音 — 組み込み文言なしの家訓)
1185
+ if (formatReadOnlyAnnouncement !== undefined) {
1186
+ announce(formatReadOnlyAnnouncement(rejection))
1187
+ }
1188
+ },
1189
+ [onReadOnlyRejected, formatReadOnlyAnnouncement, announce],
1190
+ )
1191
+
964
1192
  /** The 3-tier announcement rule (§6.9-9, evaluated top-down). / 3 段機械判定 (上から順)。 */
965
1193
  const evaluateAnnouncement = useCallback(
966
1194
  (prev: SelectionState, next: SelectionState) => {
@@ -978,7 +1206,18 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
978
1206
  const raw = adapter.getCellText(a.row, a.col)
979
1207
  // displayText は目視と同一の整形後文字列 (塗り書式 = base ⊕ CF — getCellFormat 未宣言は raw と同値)
980
1208
  const painted = adapter.getCellFormat === undefined ? undefined : paintedFormatOf(a.row, a.col, () => raw)
981
- announce(formatCellAnnouncement({ row: a.row, col: a.col, text: raw, displayText: formatCellValue(raw, painted === undefined ? undefined : painted.value), columnName: columnIndexToName(a.col) }))
1209
+ // S2 (v0.10 §6.4): readOnly キーは宣言ゲート — isCellReadOnly 未宣言はキー自体を出さず
1210
+ // payload は v0.9 とバイト同一、宣言時は正負両真理のライブ回答 (RC-S2)
1211
+ announce(
1212
+ formatCellAnnouncement({
1213
+ row: a.row,
1214
+ col: a.col,
1215
+ text: raw,
1216
+ displayText: formatCellValue(raw, painted === undefined ? undefined : painted.value),
1217
+ columnName: columnIndexToName(a.col),
1218
+ ...(readOnlyDeclared(adapter) ? { readOnly: isReadOnlyCell(adapter, a.row, a.col) } : {}),
1219
+ }),
1220
+ )
982
1221
  }
983
1222
  }
984
1223
  // (1) 単一 1×1 レンジ → セル読み上げ (素のクリック / 矢印移動)
@@ -1092,14 +1331,37 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
1092
1331
  // OnTextInputTypeChanged で進行中の変換を強制終了する: 未編集 → IME 1 文字 → Enter が
1093
1332
  // 落ちた利用者報告の根因 — セッション基準の切替は compositionstart の中で属性を変えていた)
1094
1333
  const [virtualKeyboard, setVirtualKeyboard] = useState(false)
1334
+ /**
1335
+ * The ONE invitation-raising funnel (v0.10 plan §6.4 / RC-G2t): `setVirtualKeyboard(true)`
1336
+ * has exactly this production site, so the test seam's spy count ≡ raise executions by
1337
+ * construction — a React-batched raise-then-lower mutates no DOM yet still counts. The two
1338
+ * LOWERING sites (`onRootBlur`, the non-mouse root-capture pointerdown) call
1339
+ * `setVirtualKeyboard(false)` directly. The uniqueness invariant is machine-sealed by the
1340
+ * side-(iv) census of `readOnlyCensus.spec.ts` (a second raise site cannot land silently).
1341
+ * 招待 raise の単一漏斗 (v0.10 計画 §6.4 / RC-G2t)。`setVirtualKeyboard(true)` の production
1342
+ * 出現はここだけ — テストシームのスパイ計数 ≡ raise 実行が構成的に成立し、React バッチの
1343
+ * raise-then-lower も DOM 無変異のまま計数に映る。下ろし 2 サイト (`onRootBlur` / 非 mouse の
1344
+ * root capture pointerdown) は `setVirtualKeyboard(false)` を直呼びのまま。単一性の不変条件は
1345
+ * `readOnlyCensus.spec.ts` の側 (iv) census が機械封印 (第 2 raise サイトは無音で着地できない)。
1346
+ */
1347
+ const raiseVirtualKeyboard = useCallback(() => {
1348
+ __onVirtualKeyboardRaise?.()
1349
+ setVirtualKeyboard(true)
1350
+ }, [__onVirtualKeyboardRaise])
1095
1351
  const onRequestEdit = useCallback(
1096
- (_cell: CellCoord, caretIndex?: number) => {
1097
- // ポインタで開くエディタだけが仮想キーボードを招待する (キーボード無し端末で開ける唯一の経路)。
1098
- // 変換は進行中でない (dblclick 押下面が先に requestCommit 済み) — 属性変更は安全
1099
- setVirtualKeyboard(true)
1352
+ (cell: CellCoord, caretIndex?: number) => {
1353
+ // G2 (v0.10 §6.4 / 設計 ADR-29 v3.1 — 招待前置が規範): raise は startEdit より先に走る
1354
+ // ため、読み取り専用セルでは招待自体をスキップする。G1 だけでは OSK が拒否より先に
1355
+ // 上がってしまう (キーボード無し端末のフラッシュ)。skip でも startEdit は呼ぶ —
1356
+ // 拒否発火サイトは G1 の 1 箇所 (SSOT)。能力未宣言は従来どおり無条件 raise (バイト恒等)
1357
+ if (!isReadOnlyCell(adapter, cell.row, cell.col)) {
1358
+ // ポインタで開くエディタだけが仮想キーボードを招待する (キーボード無し端末で開ける唯一の経路)。
1359
+ // 変換は進行中でない (dblclick 押下面が先に requestCommit 済み) — 属性変更は安全
1360
+ raiseVirtualKeyboard()
1361
+ }
1100
1362
  startEdit("edit", caretIndex)
1101
1363
  },
1102
- [startEdit],
1364
+ [adapter, raiseVirtualKeyboard, startEdit],
1103
1365
  )
1104
1366
  const getClipRef = useRef<(() => ClipInfo | null) | null>(null)
1105
1367
  const getClip = useCallback(() => getClipRef.current?.() ?? null, [])
@@ -1265,6 +1527,9 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
1265
1527
  viewStore.bumpColVersion()
1266
1528
  repaintColHeaders()
1267
1529
  repaintOverlay()
1530
+ // 同期 repaint はエンジン再レンダー前の stale getFrozenSize — 分割線可視は
1531
+ // post-commit heal が commit 済み値で整合させる (§2.7 — スクロール介在なし)
1532
+ scheduleDividerHeal()
1268
1533
  return
1269
1534
  }
1270
1535
  for (const entry of entries) {
@@ -1276,8 +1541,10 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
1276
1541
  viewStore.bumpRowGeometry()
1277
1542
  repaintHeaders()
1278
1543
  repaintOverlay()
1544
+ // 行側も同文 (H_F / H_T の分割線可視は commit 済み値で heal — §2.7)
1545
+ scheduleDividerHeal()
1279
1546
  },
1280
- [rowAxis, colAxis, viewStore, repaintColHeaders, repaintHeaders, repaintOverlay],
1547
+ [rowAxis, colAxis, viewStore, repaintColHeaders, repaintHeaders, repaintOverlay, scheduleDividerHeal],
1281
1548
  )
1282
1549
 
1283
1550
  /** Pointer contextmenu relay to onCellContextMenu (§6.4.3-4 sole channel). / pointer 起点の横流し。 */
@@ -1290,8 +1557,6 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
1290
1557
 
1291
1558
  // ---- オートスクローラー 3 種 (セル both / 列ヘッダー x / 行ヘッダー y — §6.4.4 / §6.8) ----
1292
1559
  const cellAutoScroller = useAutoScroller({ handleRef, rowAxis, colAxis, bodyRef, onDragFrame, axis: "both", zoomRef })
1293
- const colHeaderRef = useRef<HTMLDivElement | null>(null)
1294
- const rowHeaderRef = useRef<HTMLDivElement | null>(null)
1295
1560
  const colAutoScroller = useAutoScroller({ handleRef, rowAxis, colAxis, bodyRef, onDragFrame, axis: "x", zoomRef })
1296
1561
  const rowAutoScroller = useAutoScroller({ handleRef, rowAxis, colAxis, bodyRef, onDragFrame, axis: "y", zoomRef })
1297
1562
  // fill 専用の軸別 2 インスタンス (§8.2 — writer も axis も構築時固定。v0.3 のボックス方式では
@@ -1455,6 +1720,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
1455
1720
  commitEdits,
1456
1721
  maxOperationCells,
1457
1722
  onOperationRejected,
1723
+ // v0.10: G7 の拒否漏斗 (§6.4)
1724
+ rejectReadOnly,
1458
1725
  writeFillPreview: onFillFrame,
1459
1726
  repaintOverlay,
1460
1727
  xAutoScroller: fillXAutoScroller,
@@ -1545,6 +1812,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
1545
1812
  rearmSink,
1546
1813
  discardClip,
1547
1814
  isDragging: isAnyDragging,
1815
+ // v0.10: G1 / G16 の拒否漏斗 (callback + 読み上げの SSOT — §6.4)
1816
+ rejectReadOnly,
1548
1817
  // Excel 同様、編集中もセルの水平揃えを保つ (数値セルは右揃えのまま編集)。getCellFormat 未宣言は
1549
1818
  // null = v0.1 恒等 (textarea の text-align を触らない)。宣言時は塗り書式 (base ⊕ CF) で判定し
1550
1819
  // text は遅延 1 回読み (v0.8 §4.2.13)
@@ -1606,6 +1875,9 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
1606
1875
  onClipChange,
1607
1876
  onClipboardUnavailable,
1608
1877
  onEditRejected,
1878
+ // v0.10: G4 / G5 / G6a / G6b の拒否漏斗 (§6.4) + 貼り付け結果の要約 (§3.1)
1879
+ rejectReadOnly,
1880
+ onPasteResolved,
1609
1881
  })
1610
1882
  getClipRef.current = clipboard.getClip
1611
1883
  discardClipRef.current = clipboard.discardClip
@@ -1635,25 +1907,31 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
1635
1907
  onHyperlinkOpen,
1636
1908
  anchors: popoverAnchorsRef,
1637
1909
  repaintOverlay,
1910
+ // v0.10: G14a / G14b / G14c の拒否漏斗 (§6.4)
1911
+ rejectReadOnly,
1638
1912
  })
1639
1913
  popoversRef.current = popovers
1640
1914
  openPopoverRef.current = popovers.open
1641
1915
  hideNoteTipRef.current = popovers.hideTip
1642
1916
 
1643
1917
  // ---- コマンド実行 (v0.8 §4.1.13 / ADR-17) ----
1644
- /** The active cell's viewport rect in visual px — the keyboard-origin cellRect recipe (onCellContextMenu twin) for onPasteSpecialRequest. / active セルの視覚 px 矩形 (keyboard 起点 cellRect のレシピ — onCellContextMenu と同式)。 */
1918
+ /** The active cell's viewport rect in visual px via the three-way forward map (v0.9 §4.9 census #11 — the keyboard-origin cellRect recipe, onCellContextMenu twin) for onPasteSpecialRequest. / active セルの視覚 px 矩形 — 3 分岐順写像 (v0.9 §4.9 census #11。keyboard 起点 cellRect のレシピ、onCellContextMenu と同式)。 */
1645
1919
  const keyboardCellRect = useCallback(() => {
1646
1920
  const active = selectionRef.current.active
1647
1921
  // 画面外座標へメニューを出さない (§6.6.1) — 可視化してからハンドルの同期読み (§7-4)
1648
1922
  ensureCellVisible(active)
1649
1923
  const handle = handleRef.current
1650
1924
  const position = handle === null ? { x: 0, y: 0 } : handle.getScrollPosition()
1651
- const frozen = handle === null ? { rows: 0, cols: 0 } : handle.getFrozenSize()
1925
+ // 帯件数と extent も同じ判断で読む (v0.9 計画 §4.9、census #11 — AxisForwardCtx はこの場で
1926
+ // インライン構築、§2.1 規則 1)。detached ハンドルは件数 0 / extent 0 の縮退 (末尾分岐不活性 = v0.8 恒等)
1927
+ const frozen = handle === null ? { rows: 0, cols: 0, trailingRows: 0, trailingCols: 0 } : handle.getFrozenSize()
1928
+ const viewport = handle === null ? { width: 0, height: 0 } : handle.getViewportSize()
1652
1929
  // 両軸とも番兵クランプ (detached の {-1, -1} を座標へ流さない)、ズーム境界で視覚 px へ
1653
1930
  const z = zoomRef.current
1654
1931
  return {
1655
- top: rowToViewportY(rowAxis, active.row, Math.max(0, position.y), frozen.rows) * z,
1656
- left: colToViewportX(colAxis, active.col, Math.max(0, position.x), frozen.cols) * z,
1932
+ // 行 / 列とも凍結パーティション 3 分岐順写像 (末尾は extent アンカー — v0.9 §2.1)。×zoom は写像後 (契約不変)
1933
+ top: rowToViewportY(rowAxis, active.row, { scroll: Math.max(0, position.y), frozenCount: frozen.rows, trailingCount: frozen.trailingRows, extent: viewport.height }) * z,
1934
+ left: colToViewportX(colAxis, active.col, { scroll: Math.max(0, position.x), frozenCount: frozen.cols, trailingCount: frozen.trailingCols, extent: viewport.width }) * z,
1657
1935
  width: colAxis.sizeOf(active.col) * z,
1658
1936
  height: rowAxis.sizeOf(active.row) * z,
1659
1937
  }
@@ -1690,6 +1968,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
1690
1968
  announce,
1691
1969
  formatCommandAnnouncement,
1692
1970
  keyboardCellRect,
1971
+ // v0.10: G8–G13 / G15 の拒否漏斗 (§6.4)
1972
+ rejectReadOnly,
1693
1973
  })
1694
1974
  commandsRef.current = commands
1695
1975
  executeCommandRef.current = commands.executeCommand
@@ -1724,6 +2004,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
1724
2004
  commitEdits,
1725
2005
  maxOperationCells,
1726
2006
  onOperationRejected,
2007
+ // v0.10: G3 (Delete / Backspace) の拒否漏斗 (§6.4)
2008
+ rejectReadOnly,
1727
2009
  getClip,
1728
2010
  discardClip,
1729
2011
  isDragging: isDraggingAny,
@@ -2072,12 +2354,18 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
2072
2354
  // 同じ stale 座標遮断だが、座標系は同一グリッドのため選択と編集セッションは保持する
2073
2355
  const prevFrozenColsRef = useRef(frozenColumns)
2074
2356
  const prevFrozenRowsRef = useRef(frozenRows)
2357
+ // v0.9 (§4.9 census #21 — FZ46 教訓): 末尾 2 props も同じ取消 OR に参加する (4 props の
2358
+ // どれかの変化 = 旧 3 分岐パーティションの押下時アンカーが全て陳腐化する)
2359
+ const prevTrailingColsRef = useRef(frozenTrailingColumns)
2360
+ const prevTrailingRowsRef = useRef(frozenTrailingRows)
2075
2361
  useLayoutEffect(() => {
2076
- // 列 / 行いずれの凍結変更も飛行中ドラッグを復元なしで取り消す (R2 是正の縦拡張 —
2077
- // 押下時アンカーは旧パーティション座標系。混成ヒットで誤 fill 確定を防ぐ)
2078
- if (prevFrozenColsRef.current !== frozenColumns || prevFrozenRowsRef.current !== frozenRows) {
2362
+ // 列 / 行・先頭 / 末尾いずれの凍結変更も飛行中ドラッグを復元なしで取り消す (R2 是正の縦拡張 +
2363
+ // v0.9 の末尾拡張 — 押下時アンカーは旧パーティション座標系。混成ヒットで誤 fill 確定を防ぐ)
2364
+ if (prevFrozenColsRef.current !== frozenColumns || prevFrozenRowsRef.current !== frozenRows || prevTrailingColsRef.current !== frozenTrailingColumns || prevTrailingRowsRef.current !== frozenTrailingRows) {
2079
2365
  prevFrozenColsRef.current = frozenColumns
2080
2366
  prevFrozenRowsRef.current = frozenRows
2367
+ prevTrailingColsRef.current = frozenTrailingColumns
2368
+ prevTrailingRowsRef.current = frozenTrailingRows
2081
2369
  engine.cancelDrag()
2082
2370
  colHeaderMachine.cancelDrag()
2083
2371
  rowHeaderMachine.cancelDrag()
@@ -2088,7 +2376,19 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
2088
2376
  // (cancelDrag は abortDrag と違いシンク断面を復元しない)
2089
2377
  pointDrag.cancelDrag()
2090
2378
  }
2091
- }, [frozenColumns, frozenRows, engine, colHeaderMachine, rowHeaderMachine, headerResizeMachine, fillMachine, pointDrag])
2379
+ }, [frozenColumns, frozenRows, frozenTrailingColumns, frozenTrailingRows, engine, colHeaderMachine, rowHeaderMachine, headerResizeMachine, fillMachine, pointDrag])
2380
+
2381
+ // ---- 凍結 props フリップの分割線可視整合 (v0.9 §2.4 / §2.7) ----
2382
+ // フリップの同一 commit 内でエンジンは新 props で render 済み (毎レンダー写像の getFrozenSize は
2383
+ // layout effect 時点で確定値)。非表示トラックを含む帯へのフリップ (例: 全 track 幅 0 の列を F = 1 で
2384
+ // 凍結) では新規マウントの分割線セグメントが JSX 初期 display "" のまま px = 0 になるため、
2385
+ // paint 前にここで heal する — スクロール介在なしで「px = 0 ⇒ 分割線なし」を成立させる (§2.7)。
2386
+ // F = T = 0 では両ヘッダー書き手が getFrozenSize 読みごと省き、残差 transform の同値再書込のみ
2387
+ useLayoutEffect(() => {
2388
+ repaintHeaders()
2389
+ repaintColHeaders()
2390
+ repaintOverlay()
2391
+ }, [frozenColumns, frozenRows, frozenTrailingColumns, frozenTrailingRows, repaintHeaders, repaintColHeaders, repaintOverlay])
2092
2392
 
2093
2393
  // ---- dataVersion (外部書込通知 §6.9-2 (b) — Undo / 協調編集) もクリップの破棄条件 ----
2094
2394
  // adapter 外で値が書き戻ると cut 時行列のまま照合が通り、古い指紋の move が外部値を "" で
@@ -2129,6 +2429,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
2129
2429
  // §6.8-2 の順序契約の設計内猶予。自己修復するため実害なし)
2130
2430
  repaintHeaders()
2131
2431
  repaintOverlay()
2432
+ // 分割線可視は commit 済み getFrozenSize の post-commit heal で整合 (§2.7)
2433
+ scheduleDividerHeal()
2132
2434
  },
2133
2435
  resizeColumn: (col: number, widthPx: number) => {
2134
2436
  // 対呼び出し (§3.8): colAxis (SSOT) を先に更新し、グリッドの幅木へ通知する。
@@ -2141,6 +2443,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
2141
2443
  viewStore.bumpColVersion()
2142
2444
  repaintColHeaders()
2143
2445
  repaintOverlay()
2446
+ // 分割線可視は commit 済み getFrozenSize の post-commit heal で整合 (§2.7)
2447
+ scheduleDividerHeal()
2144
2448
  },
2145
2449
  scrollToCell: (cell: CellCoord, options?: ScrollToCellOptions) => {
2146
2450
  // エンジンのセル狙いジャンプへ素通し (§7-4 の真実。選択は変えない — 純 viewport 移動)。
@@ -2175,7 +2479,7 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
2175
2479
  openPopoverRef.current(kind)
2176
2480
  },
2177
2481
  }),
2178
- [rowAxis, colAxis, viewStore, repaintColHeaders, repaintHeaders, repaintOverlay, focusSink, isEditing, rearmSink, adapter, paintedFormatOf, startEdit, requireCommands],
2482
+ [rowAxis, colAxis, viewStore, repaintColHeaders, repaintHeaders, repaintOverlay, scheduleDividerHeal, focusSink, isEditing, rearmSink, adapter, paintedFormatOf, startEdit, requireCommands],
2179
2483
  )
2180
2484
 
2181
2485
  // ---- 空グリッド分岐 (§6.9-1: フックの後・結線の前) ----
@@ -2184,7 +2488,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
2184
2488
  }
2185
2489
 
2186
2490
  // live region は読み上げ prop のどれか 1 つでも宣言されたときだけ描く (コマンド通知も同じ領域へ)
2187
- const hasAnnouncement = formatCellAnnouncement !== undefined || formatSelectionAnnouncement !== undefined || formatCommandAnnouncement !== undefined
2491
+ // v0.10: formatReadOnlyAnnouncement も live region の存在条件 (拒否読み上げは announce() 経由 — §6.4)
2492
+ const hasAnnouncement = formatCellAnnouncement !== undefined || formatSelectionAnnouncement !== undefined || formatCommandAnnouncement !== undefined || formatReadOnlyAnnouncement !== undefined
2188
2493
 
2189
2494
  return (
2190
2495
  // biome-ignore lint/a11y/useSemanticElements: fieldset はフォーム意味論であり不適。role="group" は Escape 脱出の着地点に現在地名を与えるための選択 (§6.9-7 / §10-4)
@@ -2231,6 +2536,7 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
2231
2536
  viewStore={viewStore}
2232
2537
  colAxis={colAxis}
2233
2538
  frozenCols={frozenColumns}
2539
+ frozenTrailingCols={frozenTrailingColumns}
2234
2540
  handleRef={handleRef}
2235
2541
  bandRef={colHeaderRef}
2236
2542
  colTrackRef={colTrackRef}
@@ -2238,6 +2544,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
2238
2544
  repaintColHeaders={repaintColHeaders}
2239
2545
  selectionRanges={selection.ranges}
2240
2546
  enableResizeUI={enableResizeUI}
2547
+ dividerLeadingRef={dividerXLeadingRef}
2548
+ dividerTrailingRef={dividerXTrailingRef}
2241
2549
  />
2242
2550
  <RowHeader
2243
2551
  viewStore={viewStore}
@@ -2250,6 +2558,9 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
2250
2558
  repaintHeaders={repaintHeaders}
2251
2559
  enableResizeUI={enableResizeUI}
2252
2560
  frozenRows={frozenRows}
2561
+ frozenTrailingRows={frozenTrailingRows}
2562
+ dividerLeadingRef={dividerYLeadingRef}
2563
+ dividerTrailingRef={dividerYTrailingRef}
2253
2564
  />
2254
2565
  <div className="aqsp-body-stack" data-aqsp-body-stack="">
2255
2566
  <GridBody
@@ -2263,6 +2574,9 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
2263
2574
  defaultColumnWidth={defaultColumnWidth}
2264
2575
  frozenColumns={frozenColumns}
2265
2576
  frozenRows={frozenRows}
2577
+ frozenTrailingColumns={frozenTrailingColumns}
2578
+ frozenTrailingRows={frozenTrailingRows}
2579
+ tapScrollCircleOptions={tapScrollCircleOptions}
2266
2580
  renderCell={renderCell}
2267
2581
  onScroll={onScroll}
2268
2582
  onRangeChange={onRangeChange}
@@ -2294,6 +2608,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
2294
2608
  enableFillHandle={enableFillHandle}
2295
2609
  frozenCols={frozenColumns}
2296
2610
  frozenRows={frozenRows}
2611
+ trailingCols={frozenTrailingColumns}
2612
+ trailingRows={frozenTrailingRows}
2297
2613
  sinkAriaLabel={resolvedLabels.sinkAriaLabel}
2298
2614
  onSinkMouseDown={onSinkMouseDown}
2299
2615
  editPaintRef={editPaintRef}