@aiquants/spreadsheet 0.8.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +1103 -0
  2. package/LICENSE.md +155 -0
  3. package/README.md +817 -0
  4. package/dist/index.cjs +21 -0
  5. package/dist/index.d.cts +1960 -0
  6. package/dist/index.d.ts +1960 -0
  7. package/dist/index.js +7711 -0
  8. package/dist/styles/spreadsheet.css +2 -0
  9. package/dist/styles/spreadsheet.standalone.css +3 -0
  10. package/package.json +94 -0
  11. package/src/Spreadsheet.tsx +2313 -0
  12. package/src/clipboard/clipPayload.ts +1242 -0
  13. package/src/clipboard/fixtures/excel.fixture.ts +301 -0
  14. package/src/clipboard/fixtures/html.fixture.ts +90 -0
  15. package/src/clipboard/fixtures/sheets.fixture.ts +83 -0
  16. package/src/clipboard/htmlInterop.ts +1694 -0
  17. package/src/clipboard/numberFormatCodec.ts +870 -0
  18. package/src/clipboard/pasteSpecial.ts +608 -0
  19. package/src/clipboard/tiling.ts +370 -0
  20. package/src/clipboard/tsv.ts +186 -0
  21. package/src/clipboard/useClipboard.ts +1154 -0
  22. package/src/commands/selfCalc.ts +279 -0
  23. package/src/commands/useCommands.ts +946 -0
  24. package/src/editor/useCellEditor.ts +708 -0
  25. package/src/grid/CellView.tsx +51 -0
  26. package/src/grid/GridBody.tsx +489 -0
  27. package/src/header/ColumnHeader.tsx +167 -0
  28. package/src/header/RowHeader.tsx +144 -0
  29. package/src/header/columnName.ts +31 -0
  30. package/src/header/resizeGates.ts +146 -0
  31. package/src/header/resizeStrips.ts +79 -0
  32. package/src/header/useHeaderResize.ts +598 -0
  33. package/src/header/useHeaderSelection.ts +454 -0
  34. package/src/index.ts +122 -0
  35. package/src/keyboard/compositionGuard.ts +53 -0
  36. package/src/keyboard/ensureCellVisible.ts +77 -0
  37. package/src/keyboard/shortcuts.ts +718 -0
  38. package/src/keyboard/useGridKeyboard.ts +940 -0
  39. package/src/model/axisModel.ts +186 -0
  40. package/src/model/cellAnnotations.ts +92 -0
  41. package/src/model/cellCoord.ts +114 -0
  42. package/src/model/cellFormat.ts +562 -0
  43. package/src/model/cellPaint.ts +537 -0
  44. package/src/model/conditionalFormat.ts +805 -0
  45. package/src/model/dataAdapter.ts +478 -0
  46. package/src/model/dataValidation.ts +542 -0
  47. package/src/model/formulaRefs.ts +984 -0
  48. package/src/model/frozenPartition.ts +100 -0
  49. package/src/model/labels.ts +228 -0
  50. package/src/model/viewStore.ts +124 -0
  51. package/src/overlay/SelectionOverlay.tsx +1425 -0
  52. package/src/overlay/useFillHandle.ts +432 -0
  53. package/src/popover/CommentPopover.tsx +109 -0
  54. package/src/popover/LinkPopover.tsx +108 -0
  55. package/src/popover/ListPopover.tsx +161 -0
  56. package/src/popover/MessagePopover.tsx +36 -0
  57. package/src/popover/NotePopover.tsx +106 -0
  58. package/src/popover/NoteTip.tsx +26 -0
  59. package/src/popover/PopoverLayer.tsx +210 -0
  60. package/src/popover/placement.ts +138 -0
  61. package/src/popover/usePopovers.ts +642 -0
  62. package/src/selection/selectionState.ts +239 -0
  63. package/src/selection/useAutoScroller.ts +226 -0
  64. package/src/selection/usePointDrag.ts +273 -0
  65. package/src/selection/useSelectionEngine.ts +687 -0
  66. package/src/styles/components.entry.css +10 -0
  67. package/src/styles/spreadsheet.css +1029 -0
  68. package/src/styles/standalone.entry.css +9 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,1103 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@aiquants/spreadsheet` are documented here.
4
+
5
+ ## 0.8.0 (2026-09-14)
6
+
7
+ v0.8 はリッチセル — 宣言式のセル書式 / 注釈 / 検証 / 条件付き書式、Excel / Google Sheets クリップボード相互運用、
8
+ 再バインド可能コマンド、ポップオーバー層、強調帯、数式参照ポインティング。統治文書は
9
+ [v0.8 実装計画](<./docs/plans/2026.09.04 [AI] spreadsheet-v0.8-rich-cells-plan.md>) (以下「計画 §N」)、横断仕様は
10
+ 新設の分冊 [08-rich-cells](<./docs/specs/2026.09.05 [AI] 08-rich-cells.md>)。未宣言能力 + 既定 props は v0.7 と
11
+ **DOM byte 同一** (値フォールバックではなく `adapter.x === undefined` の能力分岐 — `richCells.spec.tsx` /
12
+ `popovers.spec.tsx` / `highlightBands.spec.tsx` の whole-root 封印)、後方互換 shim は置かない (クリーンカットオーバー —
13
+ 下記「破壊的」)。
14
+
15
+ - **新機能 — リッチセル (ADR-13)**: `SpreadsheetDataAdapter` の任意能力を v0.7 の `getCellFormat` + `findDataEdge` から
16
+ 8 能力読み (`getCellFormat` / `getCellFormula` / `getCellNote` / `getCellComments` / `getCellHyperlink` /
17
+ `getCellValidation` / `getConditionalFormats` / `getRangeExtrema` + `evaluateFormula`) へ拡張。`aspectCapabilities(adapter)`
18
+ が 7 フラグの能力分岐へ写し、`resolveRichCellProps` (`GridBody.tsx`) がセル毎**最大 8 読み** (塗り書式 / 右・下の可視隣接
19
+ base 書式 / note / comments / hyperlink / validation / 遅延共有 text) でインライン style + `data-aqsp-*` 状態属性へ塗る。
20
+ (1) **書式モデル** (`cellFormat.ts` 拡張): `CellFormat` を 13 キー (`value` / `align` / `verticalAlign` / `wrap` /
21
+ `background` / `color` / `bold` / `italic` / `underline` / `strikethrough` / `fontFamily` / `fontSize` (pt) / `borders` 4 辺 ×
22
+ `{ style: thin | medium | thick | dashed | dotted | double | hair; color? }`、既定色 `BORDER_DEFAULT_COLOR = "#000000"`) へ、
23
+ `canonicalizeFormat` (undefined / false / 空 borders / 未知キーを落とし全階層のキーを辞書順に固定 = 決定的 intern キー) /
24
+ `mergeFormat` (`CellFormatPatch` の `null` 消去、`borders` は集合置換、**無変化は base を identity で返す** — RC1) /
25
+ `formatEquals` を公開。`CellEdit.format` は常に完全な正準書式か `null`。(2) **塗り** (`cellPaint.ts` 新設): React
26
+ camelCase の style キー、罫線は inline の `border*` 短縮形 (Excel 所有規則 — 自セルの right / bottom、右隣の left / 下隣の
27
+ top を畳込み、自セル優先、軸原点セルだけ left / top を直接描く。非表示の列 0 の次の可視列が左端所有者)、解釈不能な線種 /
28
+ 色はその辺だけスキップ、`WeakMap` intern + 正準 JSON の 2 段 style intern (`STYLE_INTERN_LIMIT` 4,096)。エディタは
29
+ `resolveEditPaint` (文字装飾 + `lineBoxPx = round(pt × 4 / 3)` + `verticalAlign`) をシンクへ写し `sinkPaddingTop` で行箱を
30
+ 合わせる。(3) **注釈** (`cellAnnotations.ts` 新設): `CellComment` (読み) / `CellCommentInput` (書き — `id` ありは verbatim
31
+ 保持、なしは新規で consumer が id / author / createdAt を採番、パッケージは一切採番しない)、`normalizeHyperlink`
32
+ (`http:` / `https:` / `mailto:` / `tel:` のみ、WHATWG 直列化、それ以外 null) と `normalizeAdapterHyperlink` (アダプタ読みの
33
+ ADR-10 信頼境界 — `getCellHyperlink` の全読み口 (開通 3 シーム / 指標 / `readClipCell` / `readFillSource`) を通り、許可外
34
+ スキームはリンク無し扱い: 航行せず、クリップフレーバーへ書き出さず、`CellPatch.hyperlink` へ再確定しない)、指標
35
+ `data-aqsp-note` / `data-aqsp-comment` (空スレッドは出さない) / `data-aqsp-link` (正規化を生き残るリンクだけ)。(4) **データ検証** (`dataValidation.ts` 新設): 7 規則型 (`list` / `number` /
36
+ `textLength` / `date` / `text` / `checkbox` / `custom`) × `strict` / `inputMessage` / `errorMessage`、`validateCellText` は
37
+ 空文字常時有効・正確 10 進比較 (double 非経由)・純暦の日付タプル比較・不完全規則は不活性・語彙外は `RangeError`、
38
+ `isVisiblyInvalid` は塗り時に `custom` を決して評価しない (RC24)、list の Set は規則 object 毎に intern。確定時は
39
+ 変更のある commit と編集列の Ctrl+Enter (`fillSelection` — 対象セル毎) だけ `validateCommit` — `strict` 拒否は `onValidationRejected` + message ポップオーバーで
40
+ `applyEdits` 不達、非 strict は受理して `data-aqsp-invalid` を灯す。(5) **条件付き書式** (`conditionalFormat.ts` 新設):
41
+ 6 条件 (`cellValue` / `text` / `empty` / `notEmpty` / `colorScale` / `customFormula`) × Sheets 部分集合の style、
42
+ **2 次元 256 × 256 バケット索引 + 列スライス** (触れたバケットだけ遅延実体化、バケット内は触れた列毎に列スライスを遅延実体化 —
43
+ 256 列以下のシート (列帯 1 つ) でも全列規則を全セルで走査しない、バケット内は大域規則順 — RC31)、containment を評価より先、
44
+ 成立した全規則が寄与し同一プロパティは先勝ち + `stopIfTrue` 打ち切り、`getRangeExtrema` / `evaluateFormula` を
45
+ renderVersion 毎に memo (上限 4,096)、painted 書式を base object 毎に intern (無変化は base identity)、
46
+ `getConditionalFormats` を `getCellFormat` なしで宣言するとマウント時 `RangeError`、autofit は `widthRelevantRules`
47
+ (bold / italic 設定規則と `stopIfTrue` 規則 — 障壁を残して走査を塗りと同じ位置で止める) で事前抽出しフォントキー毎に `ctx.font` を 1 回。(6) **書込語彙 (ADR-16)**:
48
+ `applyEdits(edits, context)` — `CellEdit = { row, col } & CellPatch` (省略 = 不変、`null` = 消去、重複はキー単位の後勝ち)、
49
+ `EditContext = PlainEditContext | PasteEditContext` (`transaction` はグリッド実体毎に厳密増加、呼び出し前に採番 — 拒否も
50
+ id を消費、空トランザクションはアダプタ不達 — RC9)、`SpreadsheetOperation` / `EditSource` / `EditRejection.source` に
51
+ `format` / `calc` / `annotate` を追加 (4 面の型パリティガードと定義隣接シール `plainContextNeverPaste` が `tsc` で封印)。
52
+ (7) **handle**: `executeCommand` / `applyFormat` / `setBorders` / `setHyperlink` / `setNote` / `addComment` /
53
+ `setValidation` / `paste` / `getSelection` / `getBaseFormat` / `getPaintedFormat` / `startEdit` / `openPopover` / `focus`
54
+ (計画 §1.10 — 未宣言能力は `RangeError`)。
55
+ - **新機能 — クリップボード相互運用 (ADR-14)**: copy / cut は 3 フレーバー — `text/plain` (素の TSV)、`text/html`
56
+ (Excel / Sheets が読む相互運用表: Sheets origin ラッパー + `data-sheets-value` / `-formula` / `-numberformat` /
57
+ `-hyperlink` / `-note`、Excel の `x:num` / `x:str` / `x:fmla` / `mso-number-format`、可視文字列は表示文字列、`<table>` の
58
+ **先頭属性** `data-aqsp-clip='…'` に lossless JSON)、paste イベント専用のカスタム MIME `application/x-aqsp-clip` (同一
59
+ JSON)。`clipPayload.ts` 新設 — wire format v1 (先頭 4 キー固定 `{"v":1,"rows","cols","clipId"}`、書式 / 検証の辞書、
60
+ cell `{ t, f? (R1C1 相対), fmt?, val?, note?, comments? (id なし), link? }`、原点相対 CF)、全域検証の `parseClipPayload`
61
+ (不正入力は全て null — 同じ検証器を `serializeClipPayload` が走らせ `RangeError`)、`newClipId` = `crypto.getRandomValues`
62
+ 128 bit (能力分岐もカウンタ代替もなし — 不在は `RangeError`、RC27)、`readClipHeader` の 512 code unit 窓、
63
+ `detectClipSource` (event: カスタム MIME ⇒ `aqsp` / 先頭 16 KiB マーカー `data-aqsp-clip=` ⇒ `aqsp`、
64
+ `<google-sheets-html-origin>` + `data-sheets-root="1"` ⇒ `sheets`、Excel マーカー ⇒ `excel`、全文 `<table` ⇒ `html` (RC30)、
65
+ 非空 `text/plain` ⇒ `tsv` — RC4 の順序、検出は reader を試さない)、`estimateHtmlTableCells` (ASCII 大小を畳んだ 1 パス —
66
+ DOM と同じ読み、全カーソル単調前進で O(bytes)、rows × 行毎 (rowspan 持越し + Σcolspan) の最大 = 上限値、`<track` / `<thead` /
67
+ `<textarea` を数えない — RC6 / RC29)、copy の 2 段 intern (identity → 正準 JSON —
68
+ fresh-but-equal 書式 10,000 個で辞書 1 件、RC3)。`htmlInterop.ts` 新設 (`htmlTable.ts` を置換 — 分冊 03 §3):
69
+ `serializeHtmlTable(payload, clipJson)` / `parseHtmlTable(html, kind, { currency?, locale? }, maxCells)` (Sheets / Excel /
70
+ 汎用 HTML の全域 reader — 結果は `payload` / `limit` / `none` の `HtmlTableResult`、reader 自身が各実体化の前に `maxCells` で止まり (行は兄弟リンクの遅延列挙 — 停止は行の走査も終える)
71
+ ADR-12 の第 2 の壁 — class 規則 / インライン style / 全体 run 要素の cascade (全体 run の判定は run 毎 O(1) の窓検査 —
72
+ 空白正規化キー一致と等価で、深い入れ子 × 長文の敵対セルも O(深さ × 文字数) にならない)、`colspan` / `rowspan` は anchor + 空白、
73
+ `mso-number-format` の `\0022` 復号、型付き値と日付シリアル、100 乱数 payload の往復 property test。writer の書式由来文字列
74
+ (style / `data-sheets-numberformat`) は辞書 index 毎に 1 回だけ導出 — セル毎再計算は 100k copy の HTML 直列化の約半分)、
75
+ `numberFormatCodec.ts` 新設 (Excel パターン ⇄ `CellValueFormat`、LCID 22 行、`numberDisplayToCanonical`)。
76
+ `useClipboard.ts`: paste は検出した**唯一の**ソースを読み parse 失敗は `onClipboardUnavailable("unreadable")` (別 reader へ
77
+ 滑らない — RC5)、ADR-12 の見積もりは実体化前 (自前ヘッダ寸法 × タイル式 / HTML 上限値 (`DOMParser` は上限内でしか構築
78
+ せず、reader 自身も `maxOperationCells` で止まる) / TSV は `measureTsv` の計数走査 (同一文法・行列実体化なし — `parseTsv`
79
+ の行列はゲート通過後だけ。改行だけの巨大クリップボードが拒否より先に OOM しない)、parse 後は実寸をタイル込みで再ゲート)、**cut-move は `clipId`
80
+ 同一性** (両経路 — event はカスタム MIME ヘッダ、menu は属性ヘッダ; identity ヒットは装填 payload を parse なしで再利用 —
81
+ RC7 / RC28。cut 記録はレンジへの**参照**で paste 時に元セルを再読 — Excel 同様、Ctrl+X 後に温存された `format` /
82
+ `annotate` の commit が move に同伴し、copy は snapshot のまま。装填後の行 / 列削除で元レンジが元アダプタの**今の**軸から
83
+ 外れたら遅延読みの前に staleness 検査が `"bounds"` で拒否する — 素の move も identity ヒットの Paste Special 再読も、
84
+ 跨ぎでは表示中でなく元アダプタの現在のエクステントを読む・クリップ維持で軸復元後は move 成立 — 分冊 03 §5.5 手順 3)、跨ぎ cut
85
+ (対象 commit → 同一 transaction id で元 clear、`sameSheet: false`、元の CF は動かさない — RC8 / RC10)、`onAdapterSwitch`
86
+ (copy 破棄 / cut は `foreign: true` で保持)、`discardClipOnCommit(source)` (`format` / `annotate` の commit は温存)、
87
+ **メニュー経路** `handle.paste(options)` (`navigator.clipboard.read({ unsanitized: ["text/html"] })` 1 回、在るフレーバーの
88
+ `blob.text()` を検出前に実体化、4 原因 `insecure-origin` / `permission-denied` / `empty` / `unreadable` は全て `"rejected"` —
89
+ RC36、`"applied"` はアダプタ呼出時だけ、`"empty"` は無書込)、`onClipChange` / `onClipboardUnavailable` / `formats` prop。
90
+ **Paste Special** (`pasteSpecial.ts` 新設): 9 モード × 10 側面の `MODE_ASPECTS` 表 (分冊 08 §7.2)、`applyOperation`
91
+ (Excel 算術 — 空ターゲット = 0、非数ターゲット skip、÷0 不変、数式ターゲットはラップ、空ソースは × ÷ で不変、15 有効桁)、
92
+ `transposePayload` (完全相対参照だけ回転、寸法は運ばない)、`stripUndeclaredAspects` (タイル前の書込ゲート —
93
+ v0.7 アダプタは `{ text }` だけ)、`patchFromClipCell` (`skipBlanks` / `injectIntoEmpty` / `noBorders` の既存罫線保持)、
94
+ `translateConditionalFormats` (タイル毎のレンジ平行移動を規則 1 入力へ統合)。`tiling.ts` は generic `tileCells<T>` /
95
+ `expandRichPaste` (公開 3 関数は不変、タイル原点を返す)。`Ctrl+Shift+V` = 値のみ (次の paste イベント 1 回)、
96
+ `Ctrl+Alt+V` = `onPasteSpecialRequest` または書式のみ貼り付け。
97
+ - **新機能 — ポップオーバー層 (ADR-15)**: `usePopovers.ts` / `PopoverLayer.tsx` / `placement.ts` + 既定コンポーネント 6 種
98
+ (`NotePopover` / `NoteTip` / `CommentPopover` / `LinkPopover` / `ListPopover` / `MessagePopover` — 全て公開、
99
+ `renderPopover` で差替え)。層は `layerEnabled = ∃K: popovers[K] !== false ∧ capability(K)` のときだけマウント
100
+ (v0.7 アダプタ + 既定 props は DOM 0 — RC20)、overlay が配置する 2 つの常設アンカー (`interactive` / `tip`、閉じている間
101
+ `inert`) の上に遅延マウント、`placePopover` は below-right → below-left → above-right → above-left → 全高で収まらなければアンカー脇の広い方の空きへ高さを縮める (`POPOVER_MIN_HEIGHT` 64 以上、アンカーを覆わない) → ピン (8, 8) の 6 分岐で、幅 (`popoverWidth`) は高さ計測の前にインライン書き (340px 未満の層でも実高で配置)。アンカーの非表示 (viewport 外 /
102
+ 凍結帯の下) は `clip-path: inset(100%)` で `display` / `visibility` に触れない — 開いたノートエディタの下書きと焦点を保つ
103
+ hide-and-restore (post-C X3)。部品クラス 27 (`aqsp-popover-*` — 枠 / 見出し / 入力 / ボタン / スレッド / 候補 / 拒否アクセント)
104
+ は `spreadsheet.css` に描画規則を持ち (角丸 8px / 影 / 1px 罫 / kind ルート余白 / フレーム幅 320 = `POPOVER_MAX_WIDTH`、
105
+ forced-colors 版込み — post-C X2)、`src/styles/popoverStyles.spec.ts` 9 件が CSS を機械検査する。
106
+ note (Escape = 変更があれば保存、Ctrl+Enter = 再試行、拒否状態は `noteRejectedHint` を `aria-describedby` へ差し替えて
107
+ 開いたまま) / ホバー note tip (rAF 合流ヒットテスト、`shouldShowNoteTip` はどのドラッグ飛行中も false — RC34、150 ms
108
+ 離脱猶予 — tip 上ではブラウザの実順序 (body `pointerleave` と tip `pointerenter` が同一タスク) でも `tipHoveredRef` で残る、ヒットテストは rAF 内でフレーム 1 回 —、Escape チェーン step 0 で消灯 — RC21、ホバーセルが active の間シンクへ `aria-describedby`) / comment
109
+ (返信は `[...existing (id つき), { text }]`) / link (Open ボタンは `onHyperlinkOpen` 宣言時だけ — フック `data-aqsp-link-open`、
110
+ 正規化不能 URL は保存不可) / list (`Alt+ArrowDown`、list 規則のセルだけ、大文字小文字無視の前方一致、Enter は
111
+ `source: "edit"`) / message (受動、`inputMessage` を active 変化で表示、strict 拒否は `rejected`、開いているエディタは
112
+ 置換しない)。全 keydown に IME ガード (`compositionGuard.ts` 新設 — `useGridKeyboard` から抽出した 50 ms 窓の SSOT)。
113
+ `labels` prop (`labels.ts` 新設 — 24 キー、`DEFAULT_LABELS` / `resolveLabel` 公開、`sinkAriaLabel` に "Escape" 必須 —
114
+ RC22)。DOM フックは `data-aqsp-popover-layer` / `-popover-anchor` / `-popover-frame` / `-popover` / `-popover-open` /
115
+ `-popover-close` / `-popover-save` / `-popover-post` / `-popover-hint` / `-popover-rejected` / `-popover-autofocus` と
116
+ 各 kind の入力 (`data-aqsp-note-input` / `-comment-*` / `-link-*` / `-list-*`)。
117
+ - **新機能 — 再バインド可能コマンド (ADR-17)**: `shortcuts.ts` 新設 — `SpreadsheetCommand` **70 リテラル**、単一表
118
+ `BUILTIN_ROWS` (**100 行** = 非再バインド組込み行 65 (`Ctrl+Alt+V` = `pasteSpecial` 行とズーム空間 `Ctrl+Alt(+Shift)` × 6
119
+ トークン込み) + 再バインド可コマンド行 35)、`DEFAULT_SHORTCUTS` (既定束縛 34 コマンド / 36 チョード、未束縛 36 =
120
+ `hideColumns` + 拡張コマンド 35)、`SPREADSHEET_RESERVED_SHORTCUTS` (idle 列の非再バインド行の正確なチョード 63 本 —
121
+ 族は予約しない: `Alt+ArrowDown` / `Alt+Enter` / `Ctrl+Enter` / `Shift+F2` は正当な既定、RC15)、
122
+ `EDITING_COLUMN_COMMANDS` 16。文法 `[Ctrl+][Alt+][Shift+]KEY` (正準綴りのみ — `parseChord`)、VK 規則 (文字は生成文字
123
+ 優先で code の Key 文字へ、数字 / 記号 / 機能キーは code — US / JIS / DE / AZERTY / RU / macOS-Option の行列テスト)、
124
+ `AltGraph` 状態の Alt 行は不請求 (RC14)、`validateShortcuts` (render 中 `RangeError`: 非正準 / 修飾なし / 単一 string
125
+ (RC26) / 未知コマンドキー / `pasteSpecial` の上書き / 列を意識した予約行との一致 (RC25) / 二重束縛 (RC16))。
126
+ `useCommands.ts` 新設 — 70 コマンド全件の `executeCommand` と handle の書込群、`isCommandAvailable` (能力ゲート = 行の
127
+ 存在)、書式パッチは ACTIVE セルの base から 1 ジェスチャ 1 回 + base identity 毎の `mergeFormat` memo (10k セル bold で
128
+ `format` identity 1 — RC2 / 混在選択は active に従う — RC32)、`bordersOuter` / 単一辺プリセットは位置依存 (各セル自身の辺 ⊕ レンジ外周の該当辺 — Excel / Sheets のレンジ辺意味論、分冊 08 §11 D-35)、`clearFormatting`
129
+ は無書式セルに edit を出さない、色トグルは正規化比較 (名前つきプリセットは塗り淡色 `#FFFF99` / `#CCFFFF` / `#CCFFCC` / `#FFCCCC` と文字原色 `#FFFF00` / `#0000FF` / `#00FF00` / `#FF0000` の別表 — 設計 v3 §7)、`fillDown` / `fillRight` / `fillSelection` (comments 除外、数式は
130
+ `shiftFormula` — fill ハンドルドラッグも同じ `readFillSource` / `fillSourceEdit` (`tiling.ts`) を通り、数式の対象毎シフトと宣言済み側面を運ぶ)、`insertDate` / `insertTime` (idle は active セルだけ — RC35、編集中はキャレット挿入、`now` prop)、
131
+ hide / unhide (`resizeGates.ts` 新設 — `useHeaderResize.applyEntries` の 3 ゲートを `gateResizeEntries` へ抽出、RC23。
132
+ 最終認証是正: 丸めパスは各サイズを `MAX_TRACK_SIZE_PX = 10_000` へクランプし、同一上限を wire 検証器 `isSize` /
133
+ HTML reader `trackSize` / copy writer `buildClipPayload` が共有 — 敵対的貼り付け寸法 (例 1e24 px) が非 undo の
134
+ 格子幾何を復旧不能に破壊する経路を信頼境界で封鎖)、
135
+ `selfCalc.ts` 新設 (正確な桁シフト / 符号反転、数式は釣り合った括弧の逆ラッパだけ剥離)、`formatCommandAnnouncement`。
136
+ キーマップ (`useGridKeyboard.ts`): merge 済み表を列ごとに 1 回参照 (idle: ドラッグ凍結の後・Arrow 系の前 / 編集列:
137
+ ポインティング行の後・`EDITING_COLUMN_COMMANDS` に限る)、Delete は `getCellHyperlink` 宣言時 `hyperlink: null`、
138
+ Escape チェーンは「ドラッグ中断 > ノート tip 消灯 > strict 拒否 message 消灯 > クリップ破棄 > root へ脱出」。ネイティブ文字行 (Ctrl+A / Ctrl+Z /
139
+ Ctrl+Shift+Z / Ctrl+Y / Ctrl+Shift+V) は表と同じ VK 規則 (`matchChord`) で照合 — RU / EL 等の非ラテン配列でも成立し
140
+ (ラテン配列では生成文字照合と同一)、Ctrl+Shift+V の武装が抜けて続く貼り付けが書式を消す経路を塞ぐ。`openHyperlink`
141
+ (Alt+Enter) は `onHyperlinkOpen` 宣言時にセル依存の行 — リンク無しの active は行不在 (consume せず素通し)、未宣言は
142
+ Ctrl+K 同等に link ポップオーバー。
143
+ - **新機能 — 強調帯**: `highlight` / `defaultHighlight` / `onHighlightChange` (`HighlightState`、zoom と同型の制御 / 非制御、
144
+ 既定全 off = 帯ノード 0) と `highlightColors` (`HighlightColors`、`DEFAULT_HIGHLIGHT_COLORS` = `#0084FF` / `#42FF4F` /
145
+ `#E1FF00` / 0.2 を公開)。確定レンジ毎 (末尾 `BAND_RANGE_CAP` 16) に象限層ごと 5 ノード
146
+ (`data-aqsp-band="row" | "column" | "intersection"` + `data-aqsp-band-edge="left" | "right" | "above" | "below"`)、
147
+ **入力分割** (セグメント主軸サイズは層のクランプ幅 / 高さで heal パスだけが書き、毎フレームは `transform` のみ — RC19、
148
+ 画面外へスクロールしたレンジでも viewport 全幅の帯)、全列 / 全行スパンは行帯 / 列帯の両族を省略して交点だけ (交差軸の帯が反対側の viewport 全面を染めない — 拡張機能の全軸選択 = 矩形 0 とパリティ)、**単一族 (行 XOR 列) だけ on + intersection off の交点ノードはその族の色で選択矩形を埋める** (族の `--aqsp-band-*` を参照するインライン `background`・tint 変化時だけ書く — 拡張は片族 off なら選択を貫く全幅帯 1 本を描くため、両脇セグメントだけではレンジ位置で帯色が途切れる。両族 on は穴のまま・全軸スパンでは点灯しない — 分冊 05 §8.8)、add / subtract ドラッグは帯を
149
+ 伴い参照ドラッグ (`data-aqsp-drag-mode="point"`) は伴わない、`--aqsp-band-*` は layout effect だけが書く
150
+ (`resolveOverlayPalette`: 未宣言 + 全 off は何も書かず overlay の style は v0.7 と byte 同一、帯 on で既定パレット、
151
+ 全 off へ戻すと `removeProperty`)、forced-colors は `Highlight` の枠。
152
+ - **新機能 — 数式参照ポインティング**: `formulaRefs.ts` 新設 (`tokenizeFormula` / `shiftFormula` / `toR1C1` / `fromR1C1` /
153
+ `cycleAnchor` / `cycleAnchorAt` / `findReferenceAt` / `formatRangeReference` / `isPointingPrefix` / `createFormulaMemo` —
154
+ 文字列リテラル不透過、シート修飾、`A:C` / `2:5` の全軸、11 字 / 15 桁の名前境界、400 数式コーパスの R1C1 往復)。
155
+ ポインティングは**モードではない**: 純述語 `isPointingPrefix(value, caret)` を各判断点で live シンク上で評価 (キャッシュ
156
+ したフラグを判断根拠にしない — RC37)、状態は `pointingArmed` (セッション開始で非武装 — F2 / dblclick で開いた既存数式
157
+ `=A1+B1` は Excel の Edit モードで最初の矢印はキャレット移動、キャレットを契機の直後に置く入力で武装 = Sheets の演算子契機 /
158
+ Excel の Enter モード、F2 で反転 — 分冊 04 §5 D-17) と指しトークンの**伸長アンカー** (最後に座標挿入した角を origin に固定し
159
+ Shift+Arrow は対角を動かす — 上 / 左方向の連鎖でもレンジが畳まれない Excel / Sheets のキーボードポインティング。手打ち
160
+ トークンは始端アンカー — 分冊 04 §3.20)、`data-aqsp-pointing` は漸進的な視覚ヒント。
161
+ セル押下は `usePointDrag` (新設 — 参照ドラッグの独立所有者: 自前 capture / `buildRange` / 6 番目の autoscroller) を
162
+ commit ゲートより**先**に諮り (RC13)、選択 / active / シンク位置は不変で `setSelection` シームを持たない (型で封印)、
163
+ Escape はドラッグだけ中断して `restoreSink` (セッション継続、`cancelEdit` 0 回 — RC33)、列 / 行ヘッダー押下は `A:A` /
164
+ `2:2`、コーナー / リサイズストリップ / fill ハンドル / バー帯 / ズーム死空間は consume + `focusSink`、Arrow / Shift+Arrow /
165
+ Ctrl+Arrow は参照カーソル (`findDataEdge` の fail-fast 検証は idle と共有)、F4 は `$` 循環、`getCellFormula` 未宣言は
166
+ v0.7 恒等 (`=` + ArrowDown は確定 + 移動 — RC12)。数式セッションは `resolveEditText` が数式ソースを開き (無変更判定も
167
+ 数式基準)、数式セルはクリック位置キャレットを計算しない。
168
+ - **破壊的** (0.x — clean cut-over、shim なし):
169
+ 1. `applyEdits(edits, context)` — 第 2 引数 `EditContext` を追加。既存の 1 引数実装は型適合するが `context.transaction` /
170
+ `source` / `sameSheet` / `conditionalFormats` を読まない限り挙動は不変。
171
+ 2. `CellEdit` のパッチ化 — `text` が任意 (`{ row, col } & CellPatch`)。`edit.text` を `string` として扱っていたストアは
172
+ `edit.text === undefined` (書式 / 注釈だけの edit) を「不変」として分岐すること (README の Basic Usage / `/edit` デモ)。
173
+ 3. `sinkAriaLabel` / `rootAriaLabel` prop を削除し `labels={{ sinkAriaLabel, rootAriaLabel, … }}` へ畳込み (既定文言は
174
+ byte 同一、Escape 告知規則は不変で違反は render 中 `RangeError`)。
175
+ 4. `SpreadsheetOperation` / `EditRejection.source` / `EditSource` に `"format" | "calc" | "annotate"` を追加 —
176
+ `onOperationRejected` / `onEditRejected` のコールバック引数が広がる (反変性)。デモ `SOURCE_LABEL`
177
+ (`demo/src/components/editRejection.ts` が export する唯一の表、`Record<EditSource, string>` の 7 鍵) が型で新行を強制。
178
+ 5. クリップ状態の通知形 — `onClipChange(clip: ClipInfo | null)` の `ClipInfo = { kind; range; foreign }`。シート切替
179
+ (adapter identity) は copy を破棄し **cut を `foreign: true` で保持**する (v0.7 は両方破棄): 切替先での `Ctrl+V` は
180
+ 跨ぎ move (対象書込 → 元 clear、同一 transaction id)。シート切替 / 凍結変更の effect は `fillMachine` / `editor` /
181
+ `pointDrag` など 6 機械を機械取消し (`pointDrag.cancelDrag()` — post-C X3、分冊 05 §2.5)。
182
+ 6. **cut-move はパッケージ自前フレーバーを要する — cut 後の `text/plain` だけの paste は通常貼り付け** (v0.7 の TSV 行列
183
+ 指紋照合は全廃、`clipId` 同一性へ)。literal 引用対の往復あいまい性 (v0.7 分冊 03 §2.6 の cut 退化) は消滅。
184
+ `htmlTable.ts` (+ spec) は削除し `serializeHtmlTable` は `htmlInterop.ts` から同名で公開 (出力は Sheets origin
185
+ ラッパー + `data-aqsp-clip` 先頭属性つき — `<table>` の exact 文字列ピンは構造ピンへ)。
186
+ - **デモ**: 新ルート `/styles` (`demo/src/pages/StylesApp.tsx` + `demo/src/pages/styles/` の `richStore.ts` / `seed.ts` /
187
+ `formulaEvaluator.ts` / `toolbar.tsx` / `panels.tsx` / `PasteSpecialMenu.tsx` / `fixtures.ts`、`testId="styles-sheet"`、300 × 26 — 全 8 能力を宣言する
188
+ パッチ適用ストア + トランザクション単位の逆パッチ Undo (跨ぎ cut の 2 書込を 1 Undo に) + consumer 採番のコメント id、
189
+ 数式ミニ評価器、`handle.executeCommand` のツールバー (全ボタン `onMouseDown preventDefault` でフォーカスはシンクに残る)、
190
+ 色ピッカー / 罫線 / 数値書式プリセット、リンク / ノート / コメント / 検証 / CF エディタ、`onPasteSpecialRequest` で開く
191
+ Paste Special メニュー (Excel アクセラレータ — モード A / F / V / T / C / N / X / W / R、演算 O / D / S / M / I、フラグ B / E / J)、強調帯設定、ショートカット再バインド表
192
+ (`SPREADSHEET_RESERVED_SHORTCUTS` の予約警告)、クリップ情報チップ、Sheet A / B トグル、Excel / Sheets フィクスチャボタン —
193
+ 計画 §4.4)。`/format` の全ボタンにフォーカス修正 (`onMouseDown preventDefault` + 適用後 `focus({ preventScroll: true })`)、
194
+ `/a11y` は `labels` へ、`/edit` / `/render` / `/tune` のストアは `CellEdit` をパッチとして扱う、`components/editRejection.ts` の
195
+ `SOURCE_LABEL` を 7 鍵 (`edit: "セル編集"` 込みの `Record<EditSource, string>`) で export し `format` / `calc` / `annotate`
196
+ (「書式」/「計算」/「注釈」) を追加 — `/edit` / `/styles` の書込拒否トーストと `/` / `/format` / `/a11y` の読み取り専用通知は
197
+ この 1 表を import で共有する (独自表なし — `demo/src/components/editRejection.spec.ts` 6 の census が封印。`vitest.config.ts` の
198
+ include に `demo/src/**/*.spec.{ts,tsx}` を追加)。README のルート表は「Nine routes」。
199
+ - **公開面 — 到達型の名指し公開 (2026-09-06 是正)**: 公開宣言から到達するのに barrel に無かった 24 型を `export type` で公開 —
200
+ `PasteOutcome` (`handle.paste` の戻り)、`PopoverRect` (6 つの `*PopoverContext` の `anchorRect`)、`CfTextOp` / `ValidationCompareOp` /
201
+ `ValidationTextOp` (op 語彙)、`ClipFlavours` / `ClipChannel` (`detectClipSource` の引数)、`HtmlTableKind` / `NumberFormatDecodeOptions`
202
+ (`parseHtmlTable` の引数)、`ValidationEvaluate` (`validateCellText` の評価器)、`CfCondition` の 6 つの腕 (`CellValueCondition` /
203
+ `TextCondition` / `EmptyCondition` / `NotEmptyCondition` / `ColorScaleCondition` / `CustomFormulaCondition`)、`DataValidationRule` の
204
+ 7 つの腕 (`ListValidationRule` / `NumberValidationRule` / `TextLengthValidationRule` / `DateValidationRule` / `TextValidationRule` /
205
+ `CheckboxValidationRule` / `CustomValidationRule`) + `ValidationRuleBase`。api-extractor は未公開の参照型を `dist/index.d.ts` に
206
+ ローカル `declare type` として同梱するためビルドは緑のまま、consumer 側だけ TS2459 で `Awaited<ReturnType<SpreadsheetHandle["paste"]>>`
207
+ / `PopoverContext["anchorRect"]` / `Extract<DataValidationRule, { type: "number" }>["op"]` の抽出を強いられていた。型のみの追加で
208
+ 非破壊。新規 `src/index.spec.ts` (5 件) が実 TypeScript チェッカーで barrel の到達型を走査し、未公開の到達型 (例外はテスト専用
209
+ シーム `SpreadsheetTestProps` のみ) と README / CHANGELOG / 分冊 00 §5・§5.1 / 分冊 08 §12 の export 数を CI で red にする (分冊 00 §5)。
210
+ - **検証**: 単体 / 統合 **72 ファイル 3,456 テスト** 全緑 (`pnpm exec vitest run` — v0.7 の 31 ファイル 638 から +41 ファイル
211
+ (新規 42、`htmlTable.spec.ts` 4 件の退役 1) / +2,818 (post-C 是正 X1〜X3 / Y1 の +16、CHANGELOG 参照 census +2、post-C 後の reader 上限 `maxCells` +7 / docs census +7 / popover 単体 3、2026-09-06 のゲート修復の壁時計 `it` 分離 +8、2026-09-11 の最終スイープ是正 +84 (新規 `useClipboardGates` 4 + 既存 20 ファイル +80 — ポインティング伸長アンカーの `pointing` +3 / `useCellEditor` +2 込み。
212
+ 下記内訳は是正後の実測)、同日の census 追補 +1 (`hxCensus` — 分冊 07 §1 ループ登録対 ↔ ソース実測 / §2 台帳 / 本増分行)、最終認証ラウンドの是正 +32 (是正パス込み — `cellFormat` +2 / `cellAnnotations` +1 / `conditionalFormat` +3 / `formulaRefs` +2 / `clipPayload` +4 / `htmlInterop` +3 / `numberFormatCodec` +1 / `tiling` +1 / `resizeGates` +2 / `useCommands` +1 / `usePopovers` +1 / `useClipboard` +2 / `useCellEditor` +1 / `GridBody` +2 / `SelectionOverlay` +3 / `richCells` +3 — 分冊 07 §2 の行別「最終認証」注記が正)、同ラウンドの内訳 census 追補 +3 (`hxCensus` — 検証内訳・分冊 00 §7 現況ブロックの全対 ↔ 分冊 07 §2 台帳、
213
+ 638 + 増分 = 総数の算術) と分冊間引用 census 追補 +5 (`hxCensus` — 分冊 00 §7 内訳・分冊 01 / 02 / 03 / 08 の件数引用 ↔ 分冊 07 §2 台帳、CfIndex / CfMemo 署名・キー綴りピン)、同ラウンド R5 の popover 群是正 +9 (`ListPopover` +4 — 描画上限 `LIST_POPOVER_MAX_OPTIONS` 500 と `labels.listMore` 超過行 / `labels` +1 / `usePopovers` +2・`popovers` +1 — シンク `aria-describedby` の「active の間」不変条件を `syncTipAriaDescribedBy` が active 変化で両方向に再評価 / `PopoverLayer` +1 — 同セル開き直しの初期フォーカス)、
214
+ 最終認証 FIX のクリップボードゲート腕 +3 (`useClipboardGates` — cut-move staleness ガード 2 本の `endCol >= colCount` 列軸双子 2 / `measureTsv` を迂回する素の改行事前カウントを誤 limit 拒否で殺す TSV 文法ゲート陰性ピン 1、tsv 側は行爆弾ピンを `Array.prototype.push` 0 回の構造オラクル化) 込み): 純関数 `cellFormat` 52 / `cellPaint` 34 / `cellAnnotations` 25 / `dataValidation` 35 / `conditionalFormat` 95 /
215
+ `formulaRefs` 81 / `labels` 21 / `dataAdapter` 37 / `clipPayload` 296 / `htmlInterop` 145 / `numberFormatCodec` 179 /
216
+ `pasteSpecial` 68 / `tsv` 21 (最終スイープ +3) / `tiling` 55 / `shortcuts` 729 (6 レイアウト行列) / `compositionGuard` 19 / `resizeGates` 23 /
217
+ `selfCalc` 35 / `placement` 25、フック `useCommands` 144 / `usePopovers` 51 / `PopoverLayer` 17 + 既定コンポーネント 43 /
218
+ `useClipboard` 47 / `useClipboardGates` 7 (跨ぎ staleness / Paste Special 同ガード / copy snapshot / 改行行爆弾 — 最終スイープ新規、最終認証 FIX の列軸双子 + TSV 文法ゲート陰性ピン +3) / `useCellEditor` 41 / `useGridKeyboard` 43 / `useSelectionEngine` 20 / `usePointDrag` 14 /
219
+ `useHeaderSelection` 7 / `useHeaderResize` 12 / `useFillHandle` 5 / `GridBody` 30 (最終認証是正 +1 — v0.1 恒等の早期判定は塗り 5 側面のメンバー直接読みで caps スナップショットをセル毎に組まない) / `SelectionOverlay` 34 (最終認証是正 +3 — 分割記述子キャッシュのスクロール鮮度 / 幾何無効化 / 同値抑止ピン)、統合
220
+ `richCells` 45 (§5.6 予算 3 行を構造ピン `it` + 壁時計 `it` に分離 +3、最終認証の cellValue 変種予算行 +2) / `clipboardRich` 43 / `commands` 141 / `popovers` 30 (post-C X3 の hide-and-restore +1) / `highlightBands` 18 / `pointing` 26 /
221
+ `Spreadsheet` 58 (post-C X3 のシート切替 / 凍結変更 `pointDrag.cancelDrag()` +2) / `phase3Integration` 96 (退役ピン 23 件を
222
+ 陽性ピンへ置換 — 計画 §5.2) / `hxCensus` 47 (`??` 連鎖と `getData("text/plain")` 無条件呼びの機械 census 5 件 +
223
+ `SpreadsheetCommand` ↔ README キーマップ ↔ 分冊 04 §3.2 のコマンド census 4 件 — post-C Y1 + CHANGELOG の `data-aqsp-*` インベントリ参照 ↔ 分冊 05 §10 / 分冊 06 §4 の見出し番号の文書参照 census 2 件 + 検証見出し・分冊 07 §2 全数一覧・正条文アンカー等の docs census — 最終スイープ増分と census 追補 (分冊 07 §1 ループ登録対)、最終認証ラウンドの内訳 census 8 件 (検証内訳・分冊 00 §7 現況ブロック・分冊 01〜03 / 08 の件数引用 ↔ 分冊 07 §2 台帳・638 + 増分 = 総数・CfIndex / CfMemo 署名ピン) と分冊間引用 census 5 件 (分冊 00 §7 内訳・分冊 01 / 02 / 03 / 08 の件数引用 ↔ 分冊 07 §2 台帳、分冊 01 §8.2 署名・CfMemo キー綴りの正条文) 込み) / `pasteBudget` 8 /
224
+ `styles/popoverStyles` 9 (post-C X2))、`tsc --noEmit` 0 (パッケージ + demo)、`biome check src tests demo/src` 0、
225
+ `pnpm run build` / `pnpm run demo:build` exit 0 (post-C X1 — 下記「ビルド」)。公開面は `index.ts` の値 39 + 型 94 = 133 export (`src/index.spec.ts` 5 件が到達型の名指し可能性と文書の export 数を census)、`data-aqsp-*` 属性は 37 → 73
226
+ (新規 36 — 分冊 05 §10 / 分冊 06 §4)。リテラル census の 73 種は dataset 直書きの同値抑止署名キー 4 種
227
+ (`data-aqsp-anchor-sig` / `data-aqsp-clip-kind-last` / `data-aqsp-outset-sig` / `data-aqsp-drag-mode-last` — 分冊 05 §10) を
228
+ 含まず、DOM 属性名の総数は 77。変異検証は wave ごとに機械適用 (backup → 1 変異 → 対象 spec → byte 同一復元) し
229
+ 各ガードに red 実測の killer を持つ — RC1〜RC37 の計画骨格と実装追加分の正準台帳は分冊 07 §5.9。予算 (計画 §5.6):
230
+ 自前 JSON 100k paste < 100 ms (solo 実測 ≈ 90 ms、全異数式 ≈ 220 ms / 300 ms)、copy 100k `html.length ≤ 160 × cells` +
231
+ fresh-but-equal 書式で辞書 1 件、ラグ表拒否で `DOMParser` 0、塗り 1,200 セル × 全列規則 1,000 件は読み 8 / セル +
232
+ 2 次元バケット分の `ranges` 参照 (jsdom seam 中央値 ≈ 18 ms、8 ms は Chromium 行)、26 隣接列 (256 列以下 = 列帯 1 つ) は
233
+ 列スライスの厳密会計 71,200 (中央値 ≈ 28 ms)、検証指標 1,200 セル ≈ 1.4 ms /
234
+ `evaluateFormula` 0、`getRangeExtrema` = 値欠落 colorScale 規則のレンジ数 / renderVersion、帯 60 フレームは `transform`
235
+ のみ。jsdom の壁時計行は全て構造ピンとは別 `it` に置く (2026-09-06 — ベンチは memo で 1 回だけ走り 2 つの `it` が読む):
236
+ 塗りベンチ 2 行 / 検証指標 / TSV 100k / 自前 JSON 100k 2 行は二段予算 (`AQSP_BENCH_SOLO=1` で solo 目標 40 / 40 / 3 / 100 / 100 / 300 ms、
237
+ スイート内 100 / 100 / 15 / 300 / 500 / 900 ms)、copy 100k / writer 100k / reader 100k / serialize + parse 100k / バイト線形見積もりは
238
+ 文書化した床 × 緩衝 5 の単段 (5,000 / 1,500 / 60,000 / 1,000 / 250 ms — 共有ホストの負荷 22 で旧単段 2,000 / 30,000 ms が偽赤になった
239
+ copy 100k / reader 100k を含む)。負荷で赤になるのは壁時計 `it` だけで構造ピンは緑のまま。reader は行を兄弟リンク
240
+ (`firstElementChild` / `nextElementSibling`) で遅延列挙する — live `HTMLCollection` の添字走査は jsdom で O(添字) のため 65,534 行の
241
+ 持越し爆弾で二次 (855 s の実測) になっていた退行を是正し、爆弾ピンは `nextElementSibling` の読み ≤ 110 を一次ゲートに置く。E2E は **20 ファイル 217 件** 全緑 (`AQSP_E2E_PORT=5199 pnpm run test:e2e` — 本番ビルド + preview、1.9 m):
242
+ v0.7 の 19 ファイル 173 件に `tests/e2e/styles.spec.ts` 40 (`/styles` — 計画 §5.7 の 19 ゲート + 計画外 8 + post-C の CSS
243
+ 構造ピン 1 + ホバー可能 tip の実ポインタ順序 / 狭い層の幅先書き 2) / `editing.spec.ts` +2 (`/edit` の書式拒否 — post-C Y1) / `format.spec.ts` +1 (§5.7-19 フォーカス追記) /
244
+ `v2b.spec.ts` +1 (`ctx.font` 回数ゲート = RC18 — post-C Y1) = +42、`editing.spec.ts` のシート切替ピン (cut は foreign で
245
+ 保持 → 跨ぎ move) と `clipboard.spec.ts` 2 件は改題 (件数不変)。E2E 予算 (solo `AQSP_BENCH_SOLO=1`、`/styles` は
246
+ 300 × 26 = 7,800 セルのため「100k」行は全シート + 外来 99,900 セルの着地形 — 分冊 07 §8 D12): 自前 copy 30.4 ms
247
+ (目標 150) / 自前 Ctrl+V 27.1 ms (120) / メニュー paste 54.7 ms (350) / 外来 Sheets 99,900 セル 1,133.9 ms (1,500) /
248
+ 帯 repaint p90 1.30 ms (4)。
249
+ - **ビルド (post-C X1)**: `vite.config.ts` の `dts()` に `rollupOptions.typescriptCompilerFolder` を追加 — vite-plugin-dts が
250
+ `typescript` を `process.cwd()` 起点で解決するとモノレポ root の TypeScript 7 (ネイティブ、`lib.*.d.ts` 無し) に落ち、
251
+ api-extractor が v0.8 の `handle.paste(): Promise<PasteOutcome>` で「Unable to follow symbol for Promise」を出して
252
+ `pnpm run build` が赤になっていた。`createRequire(import.meta.url).resolve("typescript/package.json")` のディレクトリ
253
+ (パッケージローカルの TypeScript 5) を明示して解消 (分冊 06 §8 / §12)。ソース / 成果物の変更なし。
254
+ - **リリース義務**: 公開ペアは virtualscroll **3.4.0** (0.6.0 / 0.7.0 と同一 — v0.8 はエンジン側の変更なし。`package.json`
255
+ の `peerDependencies` は `workspace:^` で、publish 時に**本 worktree のローカル** `packages/virtualscroll/package.json` の
256
+ version (3.4.0) から `^3.4.0` へ展開される)。2 段 publish 振付とローカル同期は virtualscroll 現状仕様 §8 が正準。
257
+ `feat/develop` への統合は `--no-ff` merge (publish は別途指示があるまで行わない)。
258
+
259
+ ## 0.7.0 (未リリース)
260
+
261
+ - **新機能 — 組み込みセル表示形式 (`getCellFormat` 能力)**: 水平揃え (左 / 中央 / 右・未指定は値による自動) と
262
+ 簡易な値書式 (数値 / パーセント / 通貨 / 指数 / 日付 / 文字) をグリッドが直接適用する。アダプタの任意能力
263
+ `getCellFormat(row, col): CellFormat | undefined` (findDataEdge と同型の能力宣言) で宣言し、未宣言は
264
+ v0.1 の素テキスト左寄せ描画に厳密恒等 (値フォールバックではない)。新規純関数モジュール
265
+ `src/model/cellFormat.ts` に型 (`CellAlign` / `CellValueFormat` 判別共用体 / `CellFormat`) と React 非依存
266
+ エンジンを実装し、`CellAlign` / `CellFormat` / `CellValueFormat` / `formatCellValue` をパッケージルートから公開
267
+ (揃えの内部共有判定 `resolveCellAlign` は barrel 非公開)。
268
+ - **値書式エンジン — 契約 6 条項** (spec 07 §5.8 の変異検証で各条項を封印):
269
+ 1. **正確十進**: 厳密 10 進文法 (`parseNumber` — 16 / 2 / 8 進 `"0x10"`・区切り済み `"1,234"`・空文字・
270
+ 指数溢れ `"1e999"` は非数値扱い) で検証した素文字列を `Intl.NumberFormat` へ**文字列のまま**渡す
271
+ (NumberFormat v3 mathematical value — 二進 double を経由しない)。percent `scale:false` の ÷100 も
272
+ 文字列指数シフトで行い、丸め境界 (`decimals: 1` 指定時 `"1.45"` → `"1.5%"`) と 2^53 超の整数桁を正確に保つ。
273
+ 2. **interned フォーマッタ**: `Intl.NumberFormat` はオプションタプル毎にモジュール Map へインターンし
274
+ (JSON null 番兵キー)、**構築失敗も null でインターン** — 壊れた列の throw は組ごとに 1 回きり。
275
+ 上限 256 到達で全消去の安全弁。1,200 セル級スライスの整形 ~50ms → ~2ms (約 25 倍)、autofit 2.5 万セル
276
+ 計測 1.45s → 32ms の実測改善。
277
+ 3. **負ゼロ抑止は `signDisplay:"negative"`**: ゼロへ丸まる値は全 locale で符号を出さない
278
+ (`"-0"` → `"0"` — 非 ASCII 数字体系 ar-EG 等でも成立。追加整形コストゼロ)。
279
+ 4. **全経路 fail-soft**: 数値書式の非数値・無効日付・範囲外 `Intl` オプション (不正な `decimals` /
280
+ `currency` / `locale`) はいずれも素の text を無変更で返す (ヘッダー / ラベルが壊れない)。
281
+ `{type:"text"}` / 空文字も素通し。通貨コードは必須 (地域既定を仮定しないホスト非依存設計)。
282
+ 5. **真に TZ 非依存の日付 — 純暦計算**: 解析 / 検証 / 整形の全段が純整数の暦計算で **`Date` を一切
283
+ 構築しない** (閏年規則 ÷4 / ÷100 / ÷400 内蔵)。ロールオーバー `"2026-02-30"`・末尾ゴミ・
284
+ **混在区切り `"2026-09/02"`** (両側同一区切りの後方参照)・時刻範囲外 (時 24+ / 分 60+ / 秒 60+) は
285
+ 素通し。ホスト TZ の DST ギャップ日 (例: America/Santiago の `"2026-09-06"`) や年 0001-0099 も正しく
286
+ 整形でき、年は 4 桁 0 詰め。`{type:"date"}` の死オプション `locale?` は型から削除。
287
+ 6. **`number` の `decimals` 省略は入力桁を保持**: `maximumFractionDigits: 20` (Intl 既定の最大 3 桁は
288
+ `"0.000001"` を `"0"` に見せる偽ゼロ丸め — Excel の標準 (General) パリティ)。
289
+ 数値系 `switch` は網羅の `never` ガードで将来の値型追加を型エラー検出。
290
+ - **自動揃えは値による (Excel / Google Sheets の描画時既定)**: `align` 未指定は「左」でなく**実際の値による
291
+ 自動** — 宣言された書式の下で実際に解釈できる文字列 (数値系は `parseNumber`・日付は `parseDate`・書式なしは
292
+ 厳密 10 進) だけが右、ラベルは左。通貨列の `"Total"` は左寄せの素テキストで、右寄せの偽装数値にならない
293
+ (fail-soft と鏡映)。明示 `align` は常に優先。揃えは永続しない描画時の既定。
294
+ - **描画結線 (新 DOM ノードなし)**: 揃えは共有判定 `resolveCellAlign` 経由で `getCellProps` のインライン
295
+ `justify-content` + `data-aqsp-align` 属性としてグリッド所有の `.aqsp-cell` (flex) へ乗る。整形表示は
296
+ `GridBody` の `children` で計算し、`renderCell` は組み込み**値書式**にのみ**優先** (素の text がカスタム
297
+ レンダラーへ、解決済み**揃え**は renderCell と独立に効く)。編集 (F2) は素の `getCellText` を維持し
298
+ (Excel 同型)、**コピー / TSV も素値を運ぶ (Excel は表示文字列を写すため意図的逸脱 — 素値が数値往復を保つ。
299
+ README 逸脱表に登記)**。
300
+ - **列 autofit は整形後の表示文字列を計測**: dblclick autofit の `measureText` 対象を画面表示と同一の
301
+ `formatCellValue` 整形結果へ (素値計測は通貨 "¥203,428" 列を素値幅へ潰し、右寄せセルが左クリップされて
302
+ 最上位桁ごと欠ける誤読事故 — 実機再現済み)。書式なしセルは素通しで従来と同一。
303
+ - **ED-1 クリック位置キャレットはセル単位ゲート**: エンジン param `caretFromPointEnabled` を
304
+ `(cell) => boolean` の関数へ改訂し、Spreadsheet は「`renderCell` 未宣言 **かつ** そのセルの表示テキスト ===
305
+ 素の正準文字列」で true を返す (dblclick 毎 O(1) の整形比較 1 回)。書式シートでも text 型 / ラベル /
306
+ 書式なしセルはクリック位置キャレットを**保ち**、表示が乖離するセル (`"¥1,234"` の offset は素値 `"1234"` へ
307
+ 写像不能) だけ末尾へ落ちる。`CellView.tsx` は不変。
308
+ - **編集エディタはセルの揃えを引き継ぐ (Excel のその場編集パリティ)**: `useCellEditor` の新 param
309
+ `resolveEditAlign` (実体は `resolveCellAlign`) が `startEdit` で編集 textarea の `text-align` へセルの実効
310
+ 揃えを適用し、セッション終端で退役する。能力なし (null) は `text-align` に触れない v0.1 恒等。
311
+ - **読み上げ ctx に `displayText` 追加**: `formatCellAnnouncement` の ctx が素の `text` に加えて**目視と同一の
312
+ 整形後表示文字列** `displayText` (書式なしセルでは `text` と同値) を運ぶ — AT が「87.0%」を「0.87」でなく
313
+ 目視どおり読み上げられる。
314
+ - **デモ — 新ルート `/format`**: 各列が `getCellFormat` で書式を宣言する型付きシート (数値 / 通貨¥ /
315
+ パーセント / 指数 / 日付 / 中央寄せ状態列)。renderCell を一切使わず、renderCell ショーケースの `/render` と
316
+ 対。読み取り専用アダプタの標準結線を兼ねる: `enableFillHandle={false}` (死んだ操作子を見せない) +
317
+ `onEditRejected` の可視リードアウト `data-demo-edit-rejected` (Enter が無言の死キーにならない)。
318
+ - **実行環境フロア**: tsconfig の `lib` を ES2022 → **ES2023** へ (`signDisplay:"negative"` と `format(string)`
319
+ の型)。ランタイム下限は `Intl.NumberFormat` v3 — **Chrome / Edge 106+・Firefox 116+・Safari 15.4+**
320
+ (床未満では値書式が fail-soft の素 text 表示へ退化)。
321
+ - **UX 追補 (実機レビュー第 3 巡)**: (1) **選択範囲の外周枠** — 確定塗り `.aqsp-selection-fill` に
322
+ 1px solid の外周罫 (Excel / Sheets の「選択範囲の枠」パリティ。border-box で幾何不変)。
323
+ (2) **/edit の値自動揃え** — アダプタの `getCellFormat: () => undefined` 宣言だけで
324
+ 数値入力セルが右揃えになるショーケース。(3) **/format の対話型書式ツールバー** — 選択
325
+ セル / 範囲へ値書式と揃えを個別適用・既定へ復元 (公開は文書化された `dataVersion` 増分 —
326
+ 更新契約の実演。上書きは適用時に完全形へ合成して interned 保存)。(4) 収まりコンテンツの
327
+ 横バー `aria-valuemax` が 0 になる virtualscroll 側クランプ (#2506) の消費者ゲート。
328
+ - **UX 追補 (実機レビュー第 4 巡) — `onEditRejected` の拒否文脈 `EditRejection` と、グリッド外
329
+ フォーカスアウト拒否の誤案内是正**: `onEditRejected` を `(error, rejection: EditRejection) => void` の
330
+ 2 引数へ拡張 (第 2 引数の追加 — 旧 1 引数コールバックはそのまま型適合する非破壊追加)。新 export 型
331
+ `EditRejection` はパッケージルートから公開の判別 union
332
+ `{ source: "edit"; editSession: "kept" | "discarded" } | { source: "paste" | "fill" | "delete" }` —
333
+ 拒否された書込経路 (非編集経路は `onOperationRejected` と同じ `SpreadsheetOperation` の書込語彙 — cut-move は Ctrl+V の
334
+ ジェスチャで `"paste"`) と、セル内確定なら
335
+ セッションの行方 (kept = グリッド内確定でセッション維持・フォーカスもシンクに留まり Escape で取消か修正可 /
336
+ discarded = グリッド外 focusout — この拒否の完了と同時にグリッドが閉じる: コールバックが返った時点で入力値は消え、
337
+ シンクは再武装、フォーカスはグリッド外のまま) を消費者へ届け、通知文言を文脈から組めるようにした。
338
+ **動機 (利用者実証)**: 読み取り専用シートでセルを編集し値を変えてグリッド外へフォーカスを移すと、拒否は
339
+ focusout 経路で発火しセッションは既に破棄済み・フォーカスはグリッド外にあるのに、デモの通知は「Escape で
340
+ 編集を終了してください」と案内していた。案内どおりグリッドへ戻って Escape を押すと待機中の Escape =
341
+ WCAG 2.1.2 のフォーカス脱出が起き、「シートのフォーカスが外れてセルを非編集状態へ滑らかに戻せない」と
342
+ 読めた。さらに隠しシンクには打った文字列が次のグリッド再進入まで残留していた。
343
+ **是正**: (1) `useCellEditor` の `finishEdit` に `onReject: "keep" | "discard"` を追加 — グリッド外
344
+ focusout の拒否は `finishEdit` 内で**セッション破棄 + 編集 text-align の退役 + シンクの即時再武装**
345
+ (`closeSession`) を一括で担い (focusout ハンドラは単一呼びへ)、入力残留を即座に掃除する。グリッド内
346
+ 経路 (`requestCommit` — Enter / Tab / クリック / ヘッダー / バー帯) は従来どおりセッション維持。
347
+ (2) `rearmSink` を**フォーカス中立**へ (`value = " "` + `setSelectionRange(0, 1)` — `select()` は
348
+ ブラウザによりフォーカスを伴うため置換): 破棄経路の再武装がユーザーの選んだ外部要素からフォーカスを
349
+ 奪わない。(3) `commitEdits` ラッパーは `(edits, rejection)` で呼び手宣言の文脈を中継するだけ
350
+ (単一発火点は不変 — useCellEditor `{ source: "edit", editSession }` / useClipboard 貼り付け
351
+ `{ source: "paste" }`・cut-move も `{ source: "paste" }` (Ctrl+V の同一ジェスチャ — R2 是正) / useFillHandle `{ source: "fill" }` /
352
+ useGridKeyboard Delete・Backspace `{ source: "delete" }`)。(4) デモは共通文言
353
+ `demo/src/components/editRejection.ts` (`describeEditRejection`) — kept は「Escape で編集を取り消せます
354
+ (入力値は保持中)」、discarded は「グリッド外へ移動したため編集内容は破棄されました」、非編集経路は
355
+ 経路名で「〜は拒否されました」 — を `/` と `/a11y` (トースト) / `/format` (status リードアウトに
356
+ `data-demo-rejection-source` / `data-demo-rejection-session` を併載) で共有する。
357
+ - **レビュー是正 (第 4 巡 R1)**: (1) **語彙統一** — 非編集経路の `source` を `SpreadsheetOperation` の書込語彙
358
+ (`"paste" | "fill" | "delete"` — api-extractor が `Extract<SpreadsheetOperation, …>` を d.ts ロールアップできず `pnpm run build` が
359
+ 落ちるため明示リテラル union で宣言し、`SpreadsheetOperation` の書込要素 (`"copy" | "cut" | "resize"` を除いた集合) との**完全一致**は
360
+ spec 水準の型パリティガード `model/dataAdapter.spec.ts` (非分配 `[A] extends [B]` の双方向断定 — R2 是正で書込集合との集合等値へ
361
+ 厳格化) が必須ゲート `tsc -p .` で封印) に揃え、Delete / Backspace は `"clear"` でなく
362
+ **`"delete"`** (`onOperationRejected("delete", …)` と同名 — 消費者は表示名マップを 1 つで共有できる。デモ `SOURCE_LABEL`
363
+ のキーも `delete`)。(2) **`finishEdit` オプションの判別 union 化** —
364
+ `{ rearm: boolean } & ({ commit: false } | { commit: true; onReject: "keep" | "discard" })`: `onReject` は commit 経路にだけ
365
+ 存在し、`cancelEdit` は `{ rearm: true, commit: false }` (commit しない経路の `onReject` は型で排除)。
366
+ (3) **拒否されたグリッド内押下確定の既定動作抑止 (既存バグ — 「kept ⇒ Escape で取消」契約で顕在化)**: 編集中の
367
+ 別セル押下 (`useSelectionEngine` の `onPointerDown`) と列 / 行ヘッダー押下 (`useHeaderSelection` の `onPointerDown`) で
368
+ `requestCommit()` が拒否されたとき、`focusSink()` の前に `event.preventDefault()` を呼ぶ。無いと mousedown の既定動作が
369
+ 最近傍 focusable の root `role="group"` へフォーカスを移し、セッション生存中は `onRootFocus` が再武装しないため
370
+ 「セッションは維持されているのに Escape も打鍵もシンクへ届かない」宙吊りになっていた。他のゲートサイト (GridBody
371
+ バー帯 / コーナー / リサイズストリップ / 縦バー死空間) は元から無条件 preventDefault 済み。(4) `rearmSink` の docstring を
372
+ 実装どおり `value=" "` + `setSelectionRange(0, 1)` — focus-neutral へ。
373
+ - **レビュー是正 (第 4 巡 R2)**: (1) **右ボタン押下がキーボードフォーカスを宙吊りにする既存バグ (MAJOR)** —
374
+ `rearmSink` のフォーカス中立化で露出した既存欠陥: mouse 非主ボタンの `pointerdown` は全押下面で `preventDefault()` なしに
375
+ 無視されていたため、mousedown の既定動作がフォーカスをシンク (`tabIndex=-1`) から root `role="group"` (`tabIndex=0`) へ移し、
376
+ `onRootFocus` は in-root 移動 (relatedTarget = シンク) として無視、セル `contextmenu` の成功経路も `focusSink()` を呼ばず —
377
+ v0.7 まではこの回収を `setSelection` 段 5 の `rearmSink` にあった `select()` の副作用が Blink / WebKit で偶然担っていた。
378
+ 結果、あらゆる右クリック (選択外セル / 選択内セル / ヘッダー / バー帯 / 編集セッションを確定する右クリック) の後、次の
379
+ 左クリックまで矢印キー・打鍵・Escape が死んでいた。Chromium 実機で「右ボタン `pointerdown` を cancel しても `contextmenu`
380
+ イベントとネイティブメニューは届く」ことを実証した上で、全押下面に同一のゲートを敷いた: `pointerType === "mouse" &&
381
+ button !== 0` は `event.preventDefault()` して return (stopPropagation なし・選択不変・ネイティブメニュー不干渉) —
382
+ `useSelectionEngine` `onPointerDown` のセル分岐 (`button !== 0` ゲート) と非セル分岐 (縦バー / ペイン死空間 —
383
+ `if (pointerType === "mouse") { preventDefault(); if (button !== 0) return; … }`)、`GridBody` `onPointerDownCapture`
384
+ (バー帯)、`useHeaderSelection` `onPointerDown` (列 / 行ヘッダー — ADR-9 のネイティブメニュー素通しは不変)、`useHeaderResize`
385
+ `makePointerDown` (リサイズストリップ)、`Spreadsheet.tsx` のコーナー `onPointerDown`、`useFillHandle` `onPointerDown`。
386
+ 加えて `useSelectionEngine` `onContextMenu` は編集中 `requestCommit` ゲートの直後 (ヒットテスト前) に `p.focusSink()` を
387
+ 呼び、グリッド外にフォーカスがある状態からの右クリックでもキーボードをグリッドへ呼び込む (HEAD の挙動を `select()` 副作用
388
+ でなく明示に — 後段で自前要素へ focus する consumer メニューはそちらが勝つ)。touch / pen の長押しは不変 (mouse 限定ゲートは
389
+ 従来どおり)。
390
+ (2) **型パリティガードを集合の完全一致へ厳格化** (`src/model/dataAdapter.spec.ts`): 左辺の比較先が `SpreadsheetOperation`
391
+ 全体だったため、`"copy"` / `"resize"` が `EditRejection.source` へ混入しても `tsc` が緑のままだった。
392
+ `WriteOperation = Exclude<SpreadsheetOperation, "copy" | "cut" | "resize">` を定義し、`Extends<RejectionSource, WriteOperation>`
393
+ と `Extends<WriteOperation, RejectionSource>` (非分配 `[A] extends [B]`) の両方を `true` リテラルで断定 — 書込要素の欠落・
394
+ 外来名・非書込要素の混入はいずれも `tsc -p .` が赤 (変異実証: union へ `"copy"` を足すと red)。
395
+ (3) **cut-move の拒否文脈は `{ source: "paste" }`** (旧 `"cut"`): 同じ Ctrl+V の cut-move ジェスチャで 2 つの拒否チャネルが
396
+ 食い違っていた — limit / bounds ゲートは `onOperationRejected("paste", …)` を報告するのに `applyEdits` throw は `"cut"` を
397
+ 報告し、しかも `onOperationRejected("cut", …)` は書込に到達しない Ctrl+X の直列化上限を意味する。是正後は同一ジェスチャ ⇒
398
+ 両コールバックで同一名。`EditRejection` は
399
+ `{ source: "edit"; editSession: "kept" | "discarded" } | { source: "paste" | "fill" | "delete" }` — `"cut"` / `"copy"` は
400
+ 書かず (直列化のみ)、`"resize"` は幾何。デモ `SOURCE_LABEL` (`demo/src/components/editRejection.ts`) から `cut`
401
+ (「切り取りの移動」) を撤去し、cut-move の拒否は「貼り付けは拒否されました」と読む。
402
+ (4) **リサイズストリップ dblclick ゲートは拒否済み確定を再試行しない** (`useHeaderResize` `makeDblClick`):
403
+ `if (p.isEditing() && !p.requestCommit())` → `if (p.isEditing())` + `focusSink()` + return。同一ダブルクリックジェスチャの
404
+ 2 押下ゲートが既に確定を試みており、dblclick 時点で生きているセッションはそこで拒否されたもの — 再試行は同一ジェスチャで
405
+ `onEditRejected` を 3 発目として重複発火させるだけだった。不変条件 (未確定セッションを跨ぐ構造変異 = autofit / unhide を
406
+ 作らない) は維持。規則: **1 押下 = 1 拒否** (セル / ストリップのダブルクリックは 2 回、3 回にはならない)。第 2 押下の
407
+ ポインタ水準 dedupe は不可能 — Chromium の `pointerdown.detail` は常に 0 (クリック回数は `mousedown` だけが運ぶ) を実証。
408
+ (5) **マウントでフォーカスを奪わない (挙動変更の明文化)**: HEAD のマウント時 `rearmSink()` は `select()` を呼び Blink /
409
+ WebKit でシンクを自動フォーカスしていた (偶然の副作用 — WCAG 3.2.1 / 2.4.3 のアンチパターン)。フォーカス中立の再武装により
410
+ グリッドはマウントでページのフォーカスを奪わず、キーボード進入は Tab / クリックの再進入経路 (`onRootFocus`)。
411
+ `tests/e2e/a11y-demo.spec.ts` の新規 E2E がピン。
412
+ - **レビュー是正 (第 4 巡 R3)**: R2 の押下ゲート (非主ボタン `pointerdown` の `preventDefault()`) では見えない 3 経路に同じ
413
+ フォーカス宙吊りが残り、加えて Linux で 1 件のデータ破壊経路が開いていた — 全て是正。
414
+ (1) **`onRootFocus` のフォーカス不変条件** (`src/Spreadsheet.tsx`): 「シンクを離れて root group に座れるのは Escape の脱出だけ」。
415
+ 主ボタンドラッグ中 (セル / ヘッダー / fill / リサイズの各機械) の chord 押下 (第 2 ボタン) は `pointerdown` でなく `pointermove`
416
+ として届くため押下ゲートが一切走らず、Chromium の mousedown 既定がフォーカスを root に置き去りにする。シンク保持中の touch /
417
+ ペンのタップ (押下ゲートはモバイル OSK 回避で意図的に mouse 限定) も、所有面の無い素の面の押下も同じ。是正後は root **自身**に
418
+ フォーカスイベントが着地し (`event.target === root`)、`relatedTarget` が root 内で、Escape 脱出の旗が立っていなければ
419
+ `onRootFocus` が `focusSink()` する (`rearmSink()` は呼ばない — 編集セッションは値ごと生き残る)。root 内の他要素 (consumer の
420
+ `renderCell` ボタン等) への着地は触らない。Escape の脱出は `Spreadsheet.tsx` 所有の旗つきコールバック `handOffFocusToRoot()`
421
+ (同期的な `root.focus()` の間だけ `escapeHandOffRef` が true) となり、`useGridKeyboard` へ新パラメータ `handOffFocusToRoot` として
422
+ 渡される (キーマップは `root.focus()` を直接呼ばない)。Chromium 実機で、ドラッグ中の中ボタン chord 押下後もドラッグは B3:E5 で
423
+ 確定しフォーカスはシンクへ戻ること、touch タップでもシンクがフォーカスを保つことを実証。
424
+ (2) **中ボタンリリースの抑止 (MAJOR)** (`src/Spreadsheet.tsx` `onRootPointerDownCapture` + `cancelMiddleRelease`): Linux Chromium
425
+ の X11 PRIMARY selection 貼り付けは中ボタン**リリース**の既定動作。R2 で中ボタン押下中もフォーカスがシンクに留まるため、paste
426
+ イベントがグリッドの paste ハンドラへ届き、シンク自身が選択中の `" "` (または直近に他所で選択した文字列) がアクティブセルへ
427
+ — 範囲選択中は選択範囲全体へタイル状に — 静かに書き込まれていた。実証: 中ボタン `pointerup` を cancel すると貼り付けは完全に
428
+ 止まる。是正後はグリッド内 (root capture) の中ボタン `pointerdown` が 1 回限りの window capture `pointerup` リスナーを武装し、
429
+ button 1 のリリースを `preventDefault()` する (グリッド外でのリリースも止める)。編集中の textarea 自身への中クリックは除外
430
+ (エディタへのネイティブ PRIMARY 貼り付けはプラットフォーム慣習として残す)。unmount でリスナーを撤去。
431
+ (3) **ズームラッパーの死空間ゲート (§6.8-3 の第 8 サイト)** (`src/Spreadsheet.tsx` `onZoomPointerDown`): 行ヘッダー帯の下の
432
+ 64×12 px の余白 (`RowHeader` の `marginBottom = SCROLLBAR_WIDTH`) には所有者が無かった。`.aqsp-zoom` ラッパーが、所有面が cancel
433
+ していない mouse 押下 (`nativeEvent.defaultPrevented` が false。シンク target と touch / ペンは素通し) を受けて `preventDefault()`
434
+ し、非主ボタンはそこで return、主ボタンは `isEditing() && !requestCommit()` ゲート (拒否 = `focusSink()` + セッション維持) を経て
435
+ `focusSink()` — コーナーと同型。
436
+ (4) **テスト**: `src/selectionIntegration.spec.tsx` describe「WCAG 2.1.2 フォーカストポロジ (§10-4)」+2 (「内部から root 自身に
437
+ 着地した迷子フォーカスはシンクへ戻す — Escape の脱出 (旗つき) だけが root に留まる (R3 是正)」/「編集セッション中の迷子フォーカス
438
+ 回収は再武装しない (入力値とセッションを守る)」)。`src/phase3Integration.spec.tsx` describe「onEditRejected の EditRejection 文脈」:
439
+ 右ボタン掃引テストを「mouse 右ボタン押下は全押下面で既定動作 (フォーカス移動) だけ止め、選択機械には入らない — 層ごとの
440
+ preventDefault 回数で各サイトを個別に封印」へ改題し、面ごとの `Event.prototype.preventDefault` 呼出回数 (セル 1 / 列ヘッダー 2 /
441
+ リサイズストリップ 2 (トポロジ番兵) / コーナー 1 / fill ハンドル 1 / バー帯 2 / ズーム死空間 1) を断定 — 帯の各層を個別に殺せる
442
+ (単層を外す変異 4 件が red を実証)。+2 新設:「ズーム死空間 (行ヘッダー帯下の余白) の主ボタン押下は既定動作を止めフォーカスを
443
+ 回収し、編集中は requestCommit ゲート (拒否 = kept)」/「中ボタン押下はリリースの既定動作 (Linux の PRIMARY 貼り付け) を 1 回限りで
444
+ 止める — グリッド外リリースも止め、編集中のシンク自身は素通し」。cut-move テストは記録してから throw するアダプタで 8 件の
445
+ 単一トランザクション (2×2 ソースの clear 4 + 移動先の書込 4) を断定し cut-move 分岐そのものを証明。E2E: `tests/e2e/selection.spec.ts`
446
+ +3 (「ドラッグ中の chord 押下 (中ボタン — pointerdown を経ない pointermove 経路) でも焦点はシンクを離れずキーボードが生きている」
447
+ (R5 是正で改題 — R4 以降は監視の chord `pointermove` 抑止がフォーカス移動を先取りし、不変条件は第 2 層) /「行ヘッダー帯下の余白
448
+ (所有面の無い死空間) の押下でもフォーカスはシンクに留まる (ズームラッパーの第 8 ゲート)」/
449
+ `hasTouch` の入れ子 `test.describe("touch")`「シンク保持中の touch タップ (mouse 限定ゲートは素通し) でも焦点は root に迷子に
450
+ ならずキーボードが生きている」)、`tests/e2e/editing.spec.ts` +1 (「中クリックはセルへ何も貼り付けない (Linux Chromium の PRIMARY
451
+ selection 貼り付けを中ボタンリリースの抑止で止める)」)。
452
+ なお R2 の押下ゲートは mouse の**全**非主ボタン (中 / 戻る / 進むボタンも) に効き、第 2 ボタンでは `contextmenu` イベントと
453
+ ネイティブメニューが従来どおり届く。
454
+ - **レビュー是正 (第 4 巡 R4)**: R3 の是正後も同じクラスが **chord ボタン** (主ボタン保持中の 2 本目のボタンの押下 / 解放) 経由で
455
+ 残っていた — Pointer Events 仕様で chord の押下 / 解放は `pointerdown` / `pointerup` でなく `buttons` の変わる **`pointermove`**
456
+ として届くため、R2 の押下ゲートにも R3 の中ボタンリリース抑止 (押下で武装) にも一切見えない。Chromium 実機で再現した帰結を全て是正。
457
+ (1) **chord 中ボタン解放が PRIMARY をセルへ貼り付けていた (MAJOR)** (`src/Spreadsheet.tsx`): R3 のフォーカス不変条件がフォーカスを
458
+ シンクへ戻すため、非ドラッグ面 (死空間 / コーナー / バー帯 / Escape 中断後のドラッグ) での chord 中ボタン解放は Linux Chromium の
459
+ PRIMARY selection 貼り付けをシンクへ届け、`" "` (または直近に選択した文字列) をアクティブセル — 範囲選択中は選択範囲全体 (コーナー
460
+ 保持なら 50,000 セル) にタイル状 — へ静かに書き込んでいた。是正後は root capture のネイティブポインタ監視 (`pointerdown` /
461
+ `pointerup` / `pointermove`、mouse 限定) が直近に観測した `buttons` を追跡し、`buttons` の変わった `pointermove` = chord 遷移を
462
+ `event.preventDefault()` する (Chromium 実証: compat mousedown のフォーカス移動と compat mouseup の PRIMARY 貼り付けの両方が
463
+ 止まる)。中ボタンのビット (4) を得た遷移では 1 回限りの中ボタンリリース抑止を武装し、主ボタン先行解放後に残る実 `pointerup`
464
+ (button 1) も止める。抑止 (`armMiddleRelease` / `disarmMiddleRelease`、`middleReleaseRef`) は任意ボタンの次の `pointerup` と
465
+ `pointercancel` (R5 是正で mouse ポインタ限定) で解除する (従来は取り残された 1 回分が後日の正当な中ボタンリリースを他所で止め得た)。素の中ボタン押下は従来どおり
466
+ `pointerdown` で武装。編集中の textarea 自身だけ免除 (エディタへのネイティブ PRIMARY 貼り付け = プラットフォーム慣習) — 待機中の
467
+ シンクは免除しない (ピン)。R3 の `onRootPointerDownCapture` + `cancelMiddleRelease` はこの監視へ置換。
468
+ (2) **chord 保持中の主ボタン先行解放が全ドラッグを中断していた (v0.2 からの既存バグ — 4 機械全て)**: 主ボタンの解放は主ビットの
469
+ 消えた `pointermove` として届き、唯一の `pointerup` は副ボタンのため `button !== 0` の確定ゲートが落とし、暗黙 capture 解放の
470
+ `lostpointercapture` → abort で**押下前**の選択へ巻き戻していた (fill は無書込、リサイズは無適用)。是正は `useSelectionEngine.ts` /
471
+ `useHeaderSelection.ts` / `useHeaderResize.ts` / `useFillHandle.ts` の 4 機械同型: 押下で `pressButtonsRef = event.buttons` を記録し、
472
+ move ハンドラ (`onPointerMove` / `onWindowPointerMove`) は先に move を適用 (`applyPointerMove` / `applyWindowPointerMove`) してから、
473
+ 押下が主ビットを報告しかつこの own move に主ビットが無ければ共有の確定 (`confirmAt` (選択 2 機械) / `confirmResize` /
474
+ `confirmFill` — `onPointerUp` と同じ本体) を呼ぶ — セル / ヘッダー / リサイズはその位置で、fill は最終プレビュー帯 (R5 是正で
475
+ 明記)。Chromium 実証: 中 / 右 chord の左先行解放で B3:E5 が確定、フォーカスはシンク、
476
+ 後続の中 `pointerup` は抑止、貼り付けなし。
477
+ (3) **フォーカス不変条件をシンク起点に限定** (`onRootFocus`): R3 は root 自身へ着地した root 内 `relatedTarget` の旗なしフォーカスを
478
+ **全て**回収していたため、キーボード巡回まで捕まえていた — グリッド内の最初の tabbable (virtualscroll のスクロールバー矢印ボタン /
479
+ `renderCell` のフォーカス要素) からの Shift+Tab がシンクへ跳ね返され、次の Shift+Tab がセル巡回として消費される (後方脱出は Escape
480
+ 経由のみ)。是正後は `relatedTarget` がシンクのとき (`related === sinkRef.current`) だけ回収する: シンクはキーボードでは root へ
481
+ フォーカスを渡さない (Tab は巡回として消費、Escape は旗つき `handOffFocusToRoot`) ため、旗なしのシンク → root は常に押下既定動作の
482
+ 迷子。root 内の他要素からの着地は後方巡回の休止点として root に留まる。スクロールバー矢印の chord ケース (矢印ボタン上で主ボタン
483
+ 保持 + 中 chord がフォーカスをボタンへ置く) は (1) が閉じる — chord `pointermove` が cancel されフォーカス移動自体が起きない。
484
+ (4) **待機中のシンクは仮想キーボードを呼ばない**: シンク textarea に待機中は `inputMode="none"` (`SelectionOverlay.tsx` — R5 是正で
485
+ 編集セッション中だけ `"text"` へ動的化、R7 追補で仮想キーボード招待フラグの描画へ — 下記)。touch / ペンは
486
+ スクロール専用 (タッチ編集は非対応 — touch タップは選択せず、touch ダブルタップはエディタを開かず、touch 長押しは
487
+ `onCellContextMenu` を呼ばない — 全押下面が `pointerType === "mouse"` ゲート。R6 是正で文言訂正) のため、従来文書化
488
+ していた「touch はシンクをフォーカスしない (OSK 回避)」の意図 — 待機中グリッドへのタップが外部進入の再武装 (relatedTarget null)
489
+ で破っていた — を、フォーカスの行方によらず属性水準で保証する。ハードウェアキーボードとデスクトップ IME は無影響 (`inputmode` は
490
+ 仮想キーボードのヒントのみ)。
491
+ (5) **死空間ゲートは自身のピクセルだけを所有** (`onZoomPointerDown`): R3 の形 (`nativeEvent.defaultPrevented` なら素通し) は所有面の
492
+ ゲート欠落を無言で肩代わりしていた (セル / コーナー / fill ハンドルの非主ボタン `preventDefault` を外しても緑のまま — ズームゲートが
493
+ 期待 1 回分を供給)。是正後は `event.target !== event.currentTarget → return` (行ヘッダー帯下の余白の target は `.aqsp-zoom` 要素
494
+ そのもの — `elementFromPoint` で実証) とし、各所有面のゲートを個別に観測可能にした。touch / ペンの素通しは維持 (OSK 回避は (4) の
495
+ `inputMode` が担う)。
496
+ (6) **テスト**: 単体 **633 / 633** (31 ファイル — 626 + 7。R6 是正の `frozenColumns.spec.tsx` +1 と R7 追補の
497
+ `selectionIntegration.spec.tsx` +4 で現在 638)。
498
+ `src/phase3Integration.spec.tsx` 93 (+3) — 新 describe「chord 遷移
499
+ (主ボタン保持中の 2 本目のボタン — R4 是正)」に「chord 遷移の pointermove は root capture で既定動作を止め (buttons 不変の move は
500
+ 止めない)、chord 中ボタン押下は主ボタン先行解放後の実 pointerup を止める」/「chord 中の主ボタン解放 (buttons から主ビットが消える
501
+ move) はセルドラッグを解放位置で確定する — 押下前状態へ巻き戻さない」/「chord 中の主ボタン解放は列ヘッダードラッグも解放位置で
502
+ 確定する」。中ボタン抑止テストは「中ボタン押下はリリースの既定動作 (Linux の PRIMARY 貼り付け) を 1 回限りで止める — グリッド外
503
+ リリースも止め、待機中のシンク自身も止め、編集中のシンク自身だけ素通し」へ改題 (pointercancel の解除と F2 前の待機シンク陽性
504
+ コントロールを追加)、`dispatchPointer` ヘルパに `buttons` を追加。`src/v2bIntegration.spec.tsx` 53 (+2):「chord 中の主ボタン解放
505
+ (buttons から主ビットが消える move) はリサイズを解放位置で確定する (R4 是正)」(100px → chord 解放位置の 163px) /「chord 中の
506
+ 主ボタン解放 (buttons から主ビットが消える move) は fill を帯どおりに確定する (R4 是正)」(3 編集の copy fill が applyEdits 1 回)。
507
+ `src/selectionIntegration.spec.tsx` 70 (+2) — describe「WCAG 2.1.2 フォーカストポロジ (§10-4)」に「root 内の他要素 (renderCell の
508
+ フォーカス要素) からの root 着地はキーボード後方巡回の休止点として root に留まる (R4 是正)」/「シンクの inputMode は待機中 none
509
+ (仮想キーボードを呼ばない — touch / ペンはスクロール専用)、編集セッション中だけ text (キーボード無し端末の OSK)」(R5 是正で
510
+ 改題、R6 是正で touch 文言を再改題、R7 追補で撤去 — 招待 describe 5 件へ置換)。E2E **168** (19 ファイル — 165 + 3。R7 追補の
511
+ `tests/e2e/editing.spec.ts` +3 と R8 の同 +2 (是正の touch 1 + テスト品質レビューの脱出脚ゲート 1) で現在 173): `tests/e2e/selection.spec.ts` 26 (+2)
512
+ 「ドラッグ中の chord 押下で主ボタンを先に離してもドラッグは解放位置で確定する (中 / 右ボタンの chord — 巻き戻さない)」/
513
+ 「グリッド内の tabbable (スクロールバー矢印) からの Shift+Tab は root に休止し、もう一度 Shift+Tab でグリッドを出る (後方巡回 —
514
+ R4 是正)」、行ヘッダー帯下の死空間テストは「行ヘッダー帯下の余白 (所有面の無い死空間) の押下でもフォーカスはシンクに留まり
515
+ キーボードが生きている」へ改題 (ゲートと不変条件の両方が満たす利用者帰結をピン — ゲートの形は単体がピン)。
516
+ `tests/e2e/editing.spec.ts` 14 (+1)「主ボタン保持中の chord 中ボタン (押下ゲートに見えない pointermove 経路) でも PRIMARY 貼り付けは
517
+ セルに届かない — 死空間 / 範囲選択 / 主ボタン先行解放」。`tests/e2e/a11y-demo.spec.ts` 6 (アクセシブルネームのテストが
518
+ `inputmode="none"` も断定)、`format.spec.ts` 14。R4 変異 14 件の red を実証 (chord preventDefault / chord 武装 / pointercancel 解除 /
519
+ シンク免除の限定子 / 4 機械の確定 / 不変条件の範囲 / ズームゲートの形 + コーナー・セル・fill の単層サイト / inputMode)。
520
+ - **レビュー是正 (第 4 巡 R5)**: blocking / major 0 — minor / nit のみ。
521
+ (1) **シンクの `inputMode` を動的化** (`src/overlay/SelectionOverlay.tsx`): `inputMode={editing ? "text" : "none"}`。R4 の静的
522
+ `"none"` は仮想キーボードを恒久に封じており、ポインタが `mouse` として届くキーボード無し端末 (ハードウェアキーボードの無い iPad +
523
+ トラックパッド / マウス、Windows / ChromeOS のタブレットモード + マウス) では mouse 経路がセッションを開く (ダブルクリック) のに
524
+ エディタが OSK を呼べず、ポインタ駆動の編集が不可能だった。是正後は待機中 `"none"` (touch / ペンのタップは仮想キーボードを呼ばない —
525
+ 従来の保証は不変)、編集セッション中 `"text"` (プラットフォームは OSK を出し**得る** — フォーカス済みのエディタで属性が変わったとき
526
+ 出るか否かは UA が `inputmode` を再評価するかに依る: WebKit (iPad + トラックパッド) はフォーカス済み要素の変更を再評価して OSK を
527
+ 出す、Chromium (Android + マウス / Windows タブレットモード + マウス) は変更をプラットフォーム IME へ転送するが OSK を出す判断は
528
+ フォーカス駆動の更新に限られ、素の属性変更では出さない — 既知の制限 (R6 レビューで実装読解により確定、R8 レビューで転送と
529
+ 表示判断を分離して訂正。回避の blur / 再 focus は focusout の破棄経路を通るため採らない))。ハードウェアキーボードと
530
+ デスクトップ IME は無影響。**R7 追補で撤回** — セッション基準の切替は IME で開くセッションの 1 文字目の変換中に属性を変え、
531
+ Windows TSF が進行中の変換を強制終了して未編集からの IME 1 文字入力を落としていた (下記 R7 — 属性は招待フラグの描画へ)。
532
+ (2) **中ボタンリリース抑止の解除側を mouse 限定に** (`src/Spreadsheet.tsx` `armMiddleRelease`): 1 回限りの `pointerup` /
533
+ `pointercancel` リスナーは mouse 以外のポインタを無視する — 中ボタン保持中の touch タップ (別ポインタ) が武装を解除すると本物の
534
+ 中ボタンリリースが素通りし、タッチスクリーン + マウス機で PRIMARY 貼り付けの穴が再開していた。
535
+ (3) **fill の chord 確定の文言訂正** (`src/overlay/useFillHandle.ts` コメント + 文書): chord 中の主ボタン解放が確定するのは**最終
536
+ プレビューボックス** (`liveBandRef` — move rAF / スクローラー tick だけが書き、`cleanupDrag` が保留 rAF を取り消す) で `pointerup`
537
+ 経路と同一 (§8.2「up で再ヒットテストしない」)。chord 解放 move 自身の位置は反映しない — 「その位置で確定」はエンジン / ヘッダー
538
+ (イベント位置の `hitTest`) とリサイズ (move が同期更新する `pendingSize`) だけ。README / 分冊 05 の「4 機械がその位置で確定」を訂正。
539
+ (4) **E2E 改題** (`tests/e2e/selection.spec.ts`): R3 の chord 押下テストを「ドラッグ中の chord 押下 (中ボタン — pointerdown を経ない
540
+ pointermove 経路) でも焦点はシンクを離れずキーボードが生きている」へ — R4 以降は root capture 監視の chord `pointermove`
541
+ `preventDefault()` がフォーカス移動そのものを先取りするため、`onRootFocus` の跳ね返しはこのテストでは第 2 層にすぎない (跳ね返しの
542
+ 直接 E2E ゲートは `hasTouch` の touch タップテスト + 単体ピン)。
543
+ (5) **`onEditRejected` prop docstring 訂正** (`src/Spreadsheet.tsx` — `dist/index.d.ts` へロールアップ): `"discarded"` 拒否後の
544
+ シンク再武装は**フォーカスを動かさない** — フォーカスは focusout が残した場所にある (ユーザーが選んだ要素、またはウィンドウ blur
545
+ (alt-tab) なら隠し入力のまま — `relatedTarget` null も同じ破棄経路)。README の props 表と「Focus leaving the grid」も同旨へ揃えた。
546
+ (6) **テスト (件数不変 — 単体 633 / 633、E2E 168)**: `src/selectionIntegration.spec.tsx` の inputMode ピンを「シンクの inputMode は
547
+ 待機中 none (仮想キーボードを呼ばない — touch / ペンはスクロール専用)、編集セッション中だけ text (キーボード無し端末の OSK)」へ
548
+ 改題 (none → F2 → text → Escape → none を断定。`a11y-demo.spec.ts` の待機中 `inputmode="none"` 断定は不変。touch 文言は
549
+ R6 是正で「スクロール専用」へ再改題 — 上記は R6 時点の題。R7 追補で撤去し招待 describe 5 件へ置換)。
550
+ `src/phase3Integration.spec.tsx`「chord 遷移の pointermove は root capture で既定動作を止め…」を拡張: 解放後のホバー move
551
+ (`pointerup` 後の `buttons: 0` は止めない — どのテストも覆っていなかった `pointerup` の `lastButtons` 更新をピン) と中ボタン保持中の
552
+ touch タップ (解除しない — 本物の中ボタンリリースは止まる)。
553
+ - **レビュー是正 (第 4 巡 R6)**: blocking / major 0 — nit と文書精度のみ。
554
+ (1) **`EditRejection` 型 docstring 訂正** (`src/model/dataAdapter.ts` — 英日両方): `"discarded"` 拒否後のシンク再武装は
555
+ **フォーカスを動かさない** — フォーカスはユーザーが選んだ要素、またはウィンドウ blur (alt-tab — `relatedTarget` null も同じ経路)
556
+ なら隠し入力のまま。旧文「フォーカスはグリッド外に留まる」はウィンドウ blur 分岐で偽だったため、`Spreadsheet.tsx` の
557
+ `onEditRejected` prop docstring (R5 (5)) と一致させた。README の「Focus leaving the grid」と props 表は既に同旨 (不変)。
558
+ (2) **touch / ペンは「スクロール専用」** (文書訂正): touch はセルを選択せず、エディタを開かず、touch 長押しは `onCellContextMenu`
559
+ を呼ばない (全押下面と contextmenu ハンドラが `pointerType === "mouse"` ゲート — `tests/e2e/selection.spec.ts` の `hasTouch`
560
+ テストがタップの選択不変を実証)。従来の「選択」込みの文言は過大主張だったため README / CHANGELOG / 分冊 05 §8.1 / 分冊 07 を
561
+ 訂正し、`src/selectionIntegration.spec.tsx` の inputMode ピンを「シンクの inputMode は待機中 none (仮想キーボードを呼ばない —
562
+ touch / ペンはスクロール専用)、編集セッション中だけ text (キーボード無し端末の OSK)」へ再改題 (R7 追補で撤去)。
563
+ (3) **README のフォーカス不変条件の起動経路**: 「所有者の無いピクセルの押下」を削除 — R4 以降はズームラッパーの死空間ゲート
564
+ (`onZoomPointerDown`) が自身のピクセルへの mouse 押下を全て cancel するため、死空間の mouse 押下でフォーカスが root へ着地する
565
+ ことはなく、着地するのは touch / ペンのタップだけ (押下ゲートは死空間ゲート込みで mouse 限定)。不変条件はゲートの背後の第 2 層
566
+ (分冊 05 §2.7 (c))。
567
+ (4) **`inputMode="none"` の OSK 抑止文は待機中限定へ** (分冊 05 §2.7 (a) / §2.10、分冊 04 §4 ズーム行): 迷子の touch / ペン
568
+ フォーカスをシンクへ戻したとき OSK が出ないのは待機中の `"none"` の話で、編集セッション中は `"text"` (R5) のため回収先で OSK が
569
+ 出得る (R5 の意図どおり — UA 依存)。R7 追補で再訂正 — 待機中の touch / ペン押下は招待を下ろすため回収先の属性は `"none"`、
570
+ ポインタで開いたセッション中の押下は下ろさず (R8 是正) 回収先は `"text"` のまま — OSK で入力中の利用者からキーボードを
571
+ 奪わない (分冊 05 §2.7 (iii))。
572
+ (5) **テスト**: 単体 **634 / 634** (31 ファイル — 633 + 1)。`src/frozenColumns.spec.tsx` 27 (+1)「編集セッション中に active が
573
+ 凍結帯の下へ潜ってもシンクの inputMode は text のまま (セッション基準 — 可視化フラグ基準ではない)」— `inputMode` は編集セッション
574
+ (`editing` = `editor.editing`) 基準であって `data-aqsp-editing` の可視化フラグ基準ではない: セッション中に active が凍結帯の下へ
575
+ 潜るとエディタは降りる (`data-aqsp-editing` 消失) が `inputmode` は `"text"` のまま、Escape で `"none"` (キーボード無し端末が
576
+ セッション途中で OSK を失わない — R7 追補で「ポインタで開いたセッション中に active が凍結帯の下へ潜っても仮想キーボード招待
577
+ (inputmode text) は維持される — 招待は可視化フラグに依らない」へ改題: 招待フラグ基準で Escape でも `"text"`)。
578
+ `src/phase3Integration.spec.tsx` 93 (件数不変)「chord 遷移の pointermove は root capture で
579
+ 既定動作を止め…」を拡張: 中ボタン保持中の touch `pointercancel` (ブラウザに奪われたパン) も武装を解除しない — 解除は mouse の
580
+ cancel だけ。変異 2 件の red を実証 (`inputMode` を `editorVisible` (凍結帯潜りの可視化フラグ) に結ぶ変異 / cancel ハンドラの
581
+ `pointerType` ガードを外す変異)。E2E **168** (19 ファイル — 不変)。R7 追補 + R8 是正 / テスト品質レビューで現在 単体 638 / E2E 173 (下記)。
582
+ - **UX 追補 R7 — 未編集からの IME 1 文字入力の回帰是正 (仮想キーボード招待)**: 利用者報告 — `/edit` で未編集セルから日本語 IME で
583
+ 「あ」と打って Enter しても確定しない (「ああ」は通り、F2 / ダブルクリックで開いたセッションはどの長さでも通る)。
584
+ (1) **根因 (R5 の `inputMode={editing ? "text" : "none"}`)**: IME で開くセッションは `compositionstart` の中で始まるため、属性が
585
+ 1 文字目の変換の最中に `none → text` へ変わっていた (変異は最初の `compositionupdate` より前に着地 — 実測)。Chromium は
586
+ `inputmode` の変更を text-input-state の変更として扱い (`TextInputManager::ShouldUpdateTextInputState` が `mode` を比較)、Aura
587
+ ビューが `OnTextInputTypeChanged` として転送し、Windows では `InputMethodWinTSF::OnTextInputTypeChanged` が 3 つの早期 return
588
+ ガード (TSFBridge の存在 / 当該クライアントがフォーカス中の text-input クライアント / ウィンドウがフォーカス中 — 報告状況では
589
+ 全て成立) を抜けると、変換の進行中か否か・変わったのが `inputmode` だけか否かを見ずに `TSFBridge::CancelComposition()` を
590
+ 要求する — 無条件なのはこの要求だけで、`TSFTextStore::CancelComposition` はドキュメントロック (編集セッション) 中や client 不在では
591
+ bail し、`ITfContextOwnerCompositionServices::TerminateComposition` の COM 呼び出しは composition 範囲が存在するときだけ発行される
592
+ (変換が進行中でない変更は連鎖の末端で no-op) — グリッドがセッションを開いた瞬間に、IME の進行中の変換を
593
+ ブラウザが強制終了していた。1 文字入力は終了させられた変換の中に丸ごと収まって消え、2 文字入力は 1 文字目だけ失って 2 文字目は
594
+ 属性が既に `text` の新しい変換で始まり、F2 / ダブルクリックは変換の無い時点で属性を変えるため無傷 — 利用者の観察と一致。
595
+ Linux headless Chromium (IME バックエンド無し) は反応しないため E2E スイートが見えず、macOS はビューへ type と flags しか渡さない。
596
+ (2) **是正 — 仮想キーボード招待**: `src/Spreadsheet.tsx` が boolean state `virtualKeyboard` を 1 つ所有し、シンクの `inputmode` は
597
+ その純粋な描画 (`src/overlay/SelectionOverlay.tsx` — 新 prop `virtualKeyboard`、`inputMode={virtualKeyboard ? "text" : "none"}`)。
598
+ **立てる**のはポインタでエディタを開いたとき (`onRequestEdit` = ダブルクリックチャネル — キーボード無し端末がエディタを開ける
599
+ 唯一の経路。押下ゲートが先に `requestCommit` 済みのため変換は進行中でない) だけ。**下ろす**のはフォーカスが本当にグリッド外へ
600
+ 出たとき (root `onBlur` = `onRootBlur` — root 内の `relatedTarget` は脱出でなく、`requestCommit` の composition 確定の内部 `blur()`
601
+ は新設の内部シーム `CellEditor.isFocusoutSuppressed()` で除外 — その blur は直後の `focusSink()` で戻る) と**セッション外**の
602
+ touch / ペンの `pointerdown` (root capture 監視 — スクロール意図。タップでシンクへ焦点が戻っても OSK を出さない。セッション中
603
+ (変換中を含む) の押下は下ろさない — 下記 (3))。Enter / Tab の確定と Escape では下ろさない — キーボード無しのポインタ端末は
604
+ 「入力 → Enter → 入力」で OSK を失わない。性質: キーボード / IME の利用者には属性は常に `none` で一度も変わらず、属性が変わるのは
605
+ ポインタとフォーカスのイベントだけで、セッションを持つ変換の中では決して変わらない (厳密化 — R8 コードレビューの info 指摘:
606
+ セッションを持たない変換はドラッグ飛行中に始まった変換だけ — `useCellEditor` の `startEdit` は `isDragging` で bail する — で、
607
+ その変換は待機シンクの自己修復 (`useGridKeyboard` の `onInput` — 編集中でない `input` ごとに `rearmSink`) が捨てるため、
608
+ 利用者が保持し得る変換への影響は無い)。OSK の実際の表示は従来どおり UA 依存 (Chromium も Windows / Linux / ChromeOS (Aura) と
609
+ Android ではフォーカス済み要素の `inputmode` 変更を毎回読んでプラットフォーム IME へ転送する (macOS は mode だけの変更を無視する)
610
+ — だからこそ Windows TSF が変換を止める — が、
611
+ OSK を**出す**判断はフォーカス駆動 (またはタッチタップ駆動) の更新に限られ、レンダラーは素の属性変更では `show_ime_if_needed` を
612
+ 立てないため OSK を呼べない。WebKit はフォーカス済み要素を再評価して出し得る)。`useCellEditor` は `isFocusoutSuppressed` の
613
+ 公開以外は不変。
614
+ (3) **第 4 巡 R8 レビューで是正 — touch / ペンの撤回はセッション外限定** (`src/Spreadsheet.tsx` root capture 監視): R7 初出の
615
+ 監視は mouse 以外の `pointerdown` で常に招待を下ろしており、ポインタで開いたセッション中 (変換中を含む) の touch / ペン押下でも
616
+ 属性が変わった — ハイブリッド機 (マウスのダブルクリックで招待 → ハードウェア IME で変換中 → touch スクロール) では「変換中に
617
+ 属性が変わる」経路そのもの (本是正が排除すべきクラス) が残っていた。是正後、mouse 以外の分岐は `isEditing()` が false のときだけ
618
+ `setVirtualKeyboard(false)` する。根拠: 変換は必ずセッションの中にあるため、この 1 条件で「変換中に属性は変わらない」が構造的に
619
+ 成立する (グリッド脱出の撤回は blur が変換を先に確定してから focusout が届くため安全)。セッション中の touch 押下はキャレット
620
+ 配置 / スクロールであり、OSK で入力中の利用者からキーボードを奪わない — 撤回はセッション終了後の次の touch / ペン押下で起きる。
621
+ 同 R8 のテスト品質レビューは、この「blur が変換を先に確定する」脱出脚がどの層にもゲートを持たない (jsdom は blur で変換を終えず、
622
+ 実ブラウザだけが模せる) と指摘 — E2E「変換中のグリッド外クリック (focusout 確定): blur が変換を先に確定し、招待の解除 (none) は compositionend の後に届く — 変換中の属性変化は 0」
623
+ を新設し、この脚を実ブラウザで固定した (下記 (4))。
624
+ (4) **テスト**: 単体 **638 / 638** (31 ファイル — 634 + 4)。`src/selectionIntegration.spec.tsx` 74 (+4): R5 の inputMode ピン
625
+ (待機中 none / 編集セッション中 text の断定 — R4 (6) / R5 (6) / R6 (2)) を**撤去**し、新 describe「仮想キーボード招待 — シンクの
626
+ inputmode (v0.7 UX 追補 R7)」に 5 件を追加 (R3 是正の「編集セッション中の迷子フォーカス回収は再武装しない」は describe「WCAG 2.1.2
627
+ フォーカストポロジ (§10-4)」の最終 `it` のまま — 同 describe は基本 2 + R3 2 + R4 1 の 5 件、招待 describe は新設 5 件のみ):
628
+ 「キーボードで開くセッション (F2 / 印字 / IME) は招待しない — 属性は none のまま一度も変化しない (変換中の属性変更を
629
+ 構造的に排除)」/「ポインタで開くセッション (dblclick) は招待し、Enter の確定と続く未編集からの IME 入力を跨いで維持する」/
630
+ 「セッション外の touch / ペン押下で招待を下ろす (スクロール意図) — セッション中 (変換中を含む) の押下は下ろさない」(R8 是正で
631
+ 改題 — ポインタで開いたセッション中の touch / ペン押下で `text` 維持、`compositionstart` 中の押下でも `inputmode` 変異 0、Escape
632
+ 後の待機中押下で `none`) /「グリッド外への focusout で
633
+ 招待を下ろす — 待機中の脱出でもセッション中の脱出でも」/「composition 確定の内部 blur (変換中の別セル押下) は脱出でない — 招待を
634
+ 維持する」(MutationObserver でキーボード / IME 経路の `inputmode` 変異 0 を断定)。`src/frozenColumns.spec.tsx` 27 (件数不変):
635
+ R6 のピンを「ポインタで開いたセッション中に active が凍結帯の下へ潜っても仮想キーボード招待 (inputmode text) は維持される —
636
+ 招待は可視化フラグに依らない」へ改題 (dblclick で開き、Escape でも `text` のまま)。E2E **173** (19 ファイル — 168 + 5):
637
+ `tests/e2e/editing.spec.ts` 19 (+5) — 入れ子 describe「仮想キーボード招待 × IME (v0.7 UX 追補 R7 — 変換中に inputmode を
638
+ 変えない)」:「未編集から IME「あ」→ Enter: 変換中に inputmode は一度も変わらず、1 文字が確定する (利用者報告の回帰ゲート)」/
639
+ 「F2 → IME「あ」→ Enter も同じく無変化で確定する」/「ダブルクリックは招待 (text)、Enter の確定と続く未編集からの IME を跨いで
640
+ 維持し、グリッド外クリックで下ろす — 変換中の変化は 0」/「変換中のグリッド外クリック (focusout 確定): blur が変換を先に確定し、
641
+ 招待の解除 (none) は compositionend の後に届く — 変換中の属性変化は 0」(R8 テスト品質レビュー — ダブルクリックで `text` → CDP で
642
+ 「あ」の変換を開いたまま確定せずグリッド外をクリック → blur が変換を確定 → focusout の commit で「あ」が確定 → 脱出の撤回で
643
+ `none`。変換中 0、総変異 2) / `hasTouch` の入れ子 `test.describe("touch")`「セッション中の touch
644
+ タップは招待を下ろさない (OSK で入力中の利用者からキーボードを奪わない)、セッション外のタップで下ろす」(R8 是正 — マウスの
645
+ ダブルクリックで `text` → CDP で IME「あ」→ セッション中に別セルを実 touch タップしても `text` とセッションを維持 → Enter で
646
+ 「あ」が確定しても `text` → 待機中のタップで `none`。総変異 2、変換中 0)。CDP (`Input.imeSetComposition` /
647
+ `Input.insertText`) で Chromium の実
648
+ composition 経路を駆動し、capture 段の `compositionstart` リスナー + MutationObserver で「変換中の属性変更」という有害事象
649
+ そのものを実 IME 無しで観測する — 是正前のツリーでは未編集経路が変異 1、F2 経路が 0 を数え、利用者の観察を写す。
650
+ `tests/e2e/a11y-demo.spec.ts` の待機中 `inputmode="none"` 断定は不変。変異 7 件の red を実証 (招待を立てない / R5 のセッション形へ
651
+ 戻す / touch 撤回の削除 / グリッド脱出撤回の削除 / suppressed-blur ガードの削除 / 静的 none / セッション終端でのリセット)。
652
+ - **検証**: 単体 **638/638** (31 ファイル — v0.6 の 564 に +74: `cellFormat.spec.ts` 純関数 34 +
653
+ `cellFormat.spec.tsx` 統合 12 新設 + `useSelectionEngine.spec.ts` セル単位ゲート +1 + `Spreadsheet.spec.tsx` 型パリティガード +1 +
654
+ `model/dataAdapter.spec.ts` 型パリティガード +1 新設 (`EditRejection.source` × `SpreadsheetOperation` 書込要素の集合完全一致 —
655
+ R2 是正) + `phase3Integration.spec.tsx` EditRejection 文脈 +11 (グリッド内確定 kept / グリッド外 focusout discarded +
656
+ シンク即時再武装 + フォーカス非奪取 (`HTMLTextAreaElement.prototype.select` の spy 未呼出で `select()` 退行を構成的に遮断) /
657
+ Delete = `delete` / paste も cut-move も `{ source: "paste" }` (Ctrl+X 自体は無発火・`onOperationRejected` 沈黙) /
658
+ 無変更 focusout の無発火 (セッション開始の陽性コントロール前提つき) / 編集中の別セル押下の拒否は押下既定動作を止める
659
+ (`dispatchEvent` false) — ここまで R1 の 6 に、R2 是正の 3: 編集中の列ヘッダー押下の拒否も押下既定動作を止める (ヘッダー機械の
660
+ ゲート — E2E だけが覆っていた面の jsdom ピン) / mouse 右ボタン押下は全押下面 (セル領域 / 列ヘッダー / リサイズストリップ /
661
+ コーナー / fill ハンドル `[data-aqsp-autofill-handle]` / バー帯 `[data-aqsp-body]`) で既定動作だけ止め選択機械に入らない /
662
+ 右クリック (contextmenu) の成功経路はフォーカスをシンクへ回収する (グリッド外から / 編集中の確定成功 / 拒否)、そして R3 是正の 2:
663
+ ズーム死空間 (行ヘッダー帯下の余白) の主ボタン押下は既定動作を止めフォーカスを回収し、編集中は requestCommit ゲート (拒否 = kept) /
664
+ 中ボタン押下はリリースの既定動作 (Linux の PRIMARY 貼り付け) を 1 回限りで止める — グリッド外リリースも止め、待機中のシンク自身も
665
+ 止め、編集中のシンク自身だけ素通し (R4 是正で pointercancel の解除と待機シンクの陽性コントロールを追加)。右ボタン掃引は層ごとの
666
+ `preventDefault` 回数断定 (セル 1 / 列ヘッダー 2 / リサイズストリップ 2 / コーナー 1 / fill ハンドル 1 /
667
+ バー帯 2 / ズーム死空間 1) へ、cut-move は 8 件の単一トランザクション断定へ改訂 — 件数不変) + 同 R4 是正の chord 遷移 +3
668
+ (chord 遷移の pointermove の root capture 抑止 + chord 中ボタンの実 pointerup 抑止 / chord 中の主ボタン解放でセルドラッグ確定 /
669
+ 同 列ヘッダードラッグ確定) + `selectionIntegration.spec.tsx`
670
+ フォーカス不変条件 +2 (内部から root 自身に着地した迷子フォーカスはシンクへ戻す — Escape の脱出 (旗つき) だけが root に留まる /
671
+ 編集セッション中の迷子フォーカス回収は再武装しない — R3 是正) + 同 R4 是正 +2 (root 内の他要素からの root 着地は後方巡回の休止点 /
672
+ シンクの inputMode 待機中 none / 編集中 text — R5 是正で改題、R7 追補で撤去) + `v2bIntegration.spec.tsx` R4 是正 +2
673
+ (chord 中の主ボタン解放でリサイズ確定 / fill 確定) + `frozenColumns.spec.tsx` R6 是正 +1 (編集セッション中の凍結帯潜りでも
674
+ シンクの inputMode は text — R7 追補で招待基準へ改題) + `selectionIntegration.spec.tsx` R7 追補 +4 (R5 の inputMode ピン 1 件を撤去し describe
675
+ 「仮想キーボード招待 — シンクの inputmode (v0.7 UX 追補 R7)」5 件を新設 — 純増 4: キーボード / IME 経路の `inputmode` 変異 0 /
676
+ dblclick 招待の Enter・IME 跨ぎ維持 / セッション外の touch・ペン押下の撤回 (R8 是正) / グリッド外 focusout の撤回 /
677
+ composition 確定の内部 blur は非脱出)。
678
+ `selectionIntegration.spec.tsx` の右ボタン押下は `dispatchEvent` false を、`v2bIntegration.spec.tsx` の R1(B) dblclick は
679
+ `onEditRejected` 1 回 (押下ゲートのみ) を、同 fill 拒否は `{ source: "fill" }` を断定 — 件数不変) + E2E **173/173**
680
+ (19 ファイル — v0.6 の 142 に +31: `format.spec.ts` 14 新設 (EditRejection 文脈 4 — グリッド外フォーカスアウト = discarded で
681
+ Escape 非案内 + シンク掃除 + フォーカスはグリッド外のまま (グリッド外クリック先はページ `header` — サイドカードは初回ロードの
682
+ レイアウト確定前に座標がずれてグリッド内押下へ化ける競合があるため) / Tab = kept で Escape 取消 + フォーカス維持 /
683
+ 編集中の別セル・列ヘッダー押下 = kept でフォーカスはシンクに留まり Escape が効く (`data-demo-edit-rejected` = "1" → "2" の
684
+ 陽性コントロール込み — R2 是正) / 貼り付け = source "paste" — 込み) + selection 範囲外周枠 1 + editing 値自動揃え 1 +
685
+ R2 是正 3: `selection.spec.ts` 「選択内セル / 列ヘッダーの右クリック (置換も rearm も起きない経路) でもフォーカスはシンクに
686
+ 留まりキーボードが生きている」1 (既存の contextmenu テストもシンクフォーカス + ArrowDown 移動を断定) + `editing.spec.ts`
687
+ 「編集中の別セル右クリック: 確定成功でセッションが閉じ、フォーカスはシンクに残りキーボードが生きている」1 +
688
+ `a11y-demo.spec.ts` 「マウント時にページのフォーカスを奪わない (再武装はフォーカス中立 — WCAG 3.2.1 / 2.4.3)」1 +
689
+ R3 是正 4: `selection.spec.ts` 「ドラッグ中の chord 押下 (中ボタン — pointerdown を経ない pointermove 経路) でも焦点はシンクを離れず
690
+ キーボードが生きている」(R5 是正で改題)「行ヘッダー帯下の余白 (所有面の無い死空間) の押下でもフォーカスはシンクに留まりキーボードが
691
+ 生きている」(R4 是正で利用者帰結のピンへ改題)「シンク保持中の touch タップ (mouse 限定ゲートは素通し) でも焦点は root に迷子に
692
+ ならずキーボードが生きている」(`hasTouch` の入れ子 describe) 3 + `editing.spec.ts` 「中クリックはセルへ何も貼り付けない (Linux
693
+ Chromium の PRIMARY selection 貼り付けを中ボタンリリースの抑止で止める)」1 +
694
+ R4 是正 3: `selection.spec.ts` 「ドラッグ中の chord 押下で主ボタンを先に離してもドラッグは解放位置で確定する (中 / 右ボタンの chord —
695
+ 巻き戻さない)」「グリッド内の tabbable (スクロールバー矢印) からの Shift+Tab は root に休止し、もう一度 Shift+Tab でグリッドを出る
696
+ (後方巡回 — R4 是正)」2 + `editing.spec.ts` 「主ボタン保持中の chord 中ボタン (押下ゲートに見えない pointermove 経路) でも PRIMARY
697
+ 貼り付けはセルに届かない — 死空間 / 範囲選択 / 主ボタン先行解放」1 (`a11y-demo.spec.ts` のアクセシブルネームは `inputmode="none"`
698
+ も断定 — 件数不変) + R7 追補 5: `editing.spec.ts` 入れ子 describe「仮想キーボード招待 × IME (v0.7 UX 追補 R7 — 変換中に inputmode を
699
+ 変えない)」の「未編集から IME「あ」→ Enter: 変換中に inputmode は一度も変わらず、1 文字が確定する (利用者報告の回帰ゲート)」
700
+ 「F2 → IME「あ」→ Enter も同じく無変化で確定する」「ダブルクリックは招待 (text)、Enter の確定と続く未編集からの IME を跨いで
701
+ 維持し、グリッド外クリックで下ろす — 変換中の変化は 0」3 (CDP の実 composition 経路 + capture 段 compositionstart +
702
+ MutationObserver) +「変換中のグリッド外クリック (focusout 確定): blur が変換を先に確定し、招待の解除 (none) は compositionend の
703
+ 後に届く — 変換中の属性変化は 0」1 (R8 テスト品質レビュー — blur が変換を終えるブラウザのイベント順序に依る唯一の脱出脚を
704
+ 実ブラウザで固定) + 同 `hasTouch` の入れ子 `test.describe("touch")`「セッション中の touch タップは招待を下ろさない (OSK で
705
+ 入力中の利用者からキーボードを奪わない)、セッション外のタップで下ろす」1 (R8 是正)。R5 是正は改題 / 断定拡張のみで単体・E2E とも
706
+ 件数不変 (`selectionIntegration.spec.tsx` の inputMode ピン改題 +
707
+ `phase3Integration.spec.tsx` の chord 遷移ピン拡張 + `selection.spec.ts` の chord 押下テスト改題)。R6 是正は単体
708
+ `frozenColumns.spec.tsx` +1 (上記 +74 に計上) で E2E は不変 (`selectionIntegration.spec.tsx` の inputMode ピンは touch 文言を
709
+ 再改題、`phase3Integration.spec.tsx` の chord 遷移ピンは touch `pointercancel` 非解除へ拡張)。R7 追補は単体
710
+ `selectionIntegration.spec.tsx` +4 (上記 +74 に計上 — R5 の inputMode ピン撤去 + 招待 describe 5) と E2E `editing.spec.ts` +5
711
+ (上記 +31 に計上 — R8 是正の touch 1 と R8 テスト品質レビューの脱出脚ゲート 1 込み)、`frozenColumns.spec.tsx` の R6 ピンは招待基準へ改題で件数不変)、
712
+ tsc / biome / markdownlint 0。v0.7 中核 3 ファイル
713
+ (`cellFormat.ts` / `dataAdapter.ts` / `GridBody.tsx`) は全単体スイート下の v8 カバレッジで
714
+ statements / branches / functions / lines とも **100%**。E2E はブラウザ locale (`en-US`) / timezone
715
+ (`Asia/Tokyo`) を playwright.config で固定し `Intl` 出力を環境非依存化。表示形式変異 **CF1〜CF25**
716
+ (エンジン契約 6 条項 + 配線 4 面 — interned キャッシュ / signDisplay / 文字列入力 / 指数シフト / 純暦検証 /
717
+ 値による揃え / autofit 整形後計測 / セル単位キャレット / maxFD20 / 年 0 詰め / 同一区切り後方参照ほか) を
718
+ 単独適用 → red 実測 (各変異は fail + pass > 0 の実 ran 件数で確認・復元後に全数 green 再確認 —
719
+ 正準台帳は spec 07 §5.8)。
720
+
721
+ ## 0.6.0 (未リリース)
722
+
723
+ v0.6 は列凍結 (0.5.0) の**行軸への完全対称化**。本節の § は v0.6 実装プラン
724
+ (`docs/plans/2026.09.02 [AI] spreadsheet-v0.6-frozen-rows-plan.md`) を指す。
725
+
726
+ - **新機能 — 行凍結 (`frozenRows`)**: 先頭 N 行を上端に凍結するライブ prop (v0.6 計画 —
727
+ VirtualGrid `frozenLeadingRows` (virtualscroll 3.4.0 初出) の消費)。検証はエンジンの
728
+ fail-fast 契約 ([0, 128] 整数 RangeError・rowCount 超過は全行凍結の動的クランプ) を
729
+ そのまま採る (SSOT — 列と同文)。凍結行は縦スクロール位置に依らず常時可視。
730
+ - **座標パーティションの軸中立化 (v0.6 計画 §2)**: `src/model/frozenPartition.ts` の内部を
731
+ **軸中立**の純関数 (`frozenSizeOf` / `toViewportPos` / `viewportPosToIndex`) へ一般化し、
732
+ 列 (`frozenWidthOf` / `colToViewportX` / `viewportXToCol`) と行 (`frozenHeightOf` /
733
+ `rowToViewportY` / `viewportYToRow`) の薄いラッパーを重ねた。行の逆写像も H_F > 0 ガードで
734
+ 負 py を先頭凍結行へクランプ (R2 是正の行双子)。縦のヒットテスト 5 面 (選択エンジン /
735
+ ヘッダー / fill / オートスクロール hover / Shift+F10) と可視化 1 面へ配線。H_F の鮮度読みは
736
+ `handle.getFrozenSize().height` (§7-4)。
737
+ - **行ヘッダーの 2 帯化**: 静的凍結ラベル (`data-aqsp-frozen-row-header` — 木絶対 top 直接値) +
738
+ H_F クリップ (`data-aqsp-row-header-scroll`) 内の既存 track (`repaintHeaders` は translateY
739
+ (窓原点 − vy − H_F))。床クランプ max(rowStart, R) で凍結行はスクロール帯に再出現せず、
740
+ 境界の重複 unhide マーカーは凍結帯側の 1 個へ畳む (列側 R3 是正の行版)。R = 0 は従来 DOM 恒等。
741
+ - **overlay の 4 象限化 (v0.6 計画 §3-4)**: 各軸の凍結種 (主 / 凍結 / スクロール) をデカルト積し、
742
+ 必要な象限だけ描く。列凍結 × 行凍結の同時活性で最大 4 象限 (ff / sf / fs / ss + 単軸の主帯)。
743
+ 行のみ凍結は縦 2 象限 (`data-aqsp-overlay-quad="mf"` = クリップ [0, H_F)・`"ms"` = クリップ
744
+ [H_F, →)・y = 木 − vy − H_F)。境界跨ぎ矩形は各象限の mapY / clampHeight が切る。v0.5 の
745
+ data-attr フック (`data-aqsp-overlay-main` / `-frozen` / `-scroll`) は R = 0 の 3 象限へ温存
746
+ (E2E / 単体互換)。象限描画順は凍結ほど後置 (主 < スクロール < 凍結 — 境界での上位描画)。
747
+ - **可視化 / オートスクロール**: 凍結行の ensure-visible は縦 no-op (常時可視・横成分は続行)、
748
+ スクロール行の上可視限界は木座標 H_F + vy。上端オートスクロールゾーンは帯基準 (凍結アンカー ×
749
+ 帯内はホームテリトリー規則で上駆動なし — 列と対称)。
750
+ - **飛行中 R 変更の全ドラッグ取消**: `frozenColumns` / `frozenRows` いずれの飛行中変更も全
751
+ ドラッグジェスチャを復元なしで取り消す (座標系跨ぎの誤確定遮断 — 取消シームを両軸で共有)。
752
+ - **デモ — 行凍結トグル (`data-demo-freeze-row-toggle`)**: `/huge` の aside に**列凍結ボタンの
753
+ 上**へ配置 (0 ↔ 3 行)。深部縦ジャンプ (Ctrl+↓ = 最終行 999,999 へ 1 回) と組み合わせた行凍結の実機面。
754
+ - **新機能 — 命令 API `SpreadsheetHandle.scrollToCell`**: セルへの命令的 viewport ジャンプ
755
+ (`scrollToCell(cell, options?)` — エンジン `VirtualGridHandle.scrollToCell` の floor + アンカー
756
+ 張りジャンプへ委譲)。新 export 型 `ScrollToCellOptions` (`alignX` = start / end / center・`alignY` =
757
+ top / bottom / center・`offsetX` / `offsetY`)。選択は変えない純 viewport 移動で、どの尺度 (10^9 px)
758
+ でも深部到達性を保つ。両者ともパッケージルートから公開。
759
+ - **是正 — `/huge` の Ctrl+矢印が 1 回でシート端へ到達 (利用者実証)**: `/huge` の `findDataEdge` を
760
+ Excel 準拠の**均一データ面**セマンティクス (全セルに値 = 行 / 列全体が 1 連続ブロック → 1 回で
761
+ シート端) に是正した — Ctrl+→ は最右列 9,999,999、Ctrl+↓ は最終行 999,999 へ直行し、`findDataEdge`
762
+ を持たない `/` ルートと統一。旧実装は 2 段 (1 回目が中間点 500 万に停止) で 1 回では端に届かず、`/`
763
+ と不整合で利用者を混乱させた (パッケージの findDataEdge ディスパッチは不変 — 変更は demo アダプタの
764
+ 実装のみ)。深部内部ジャンプ (列 500 万、≈ 5 億 px、2^25 壁の先) は専用ボタン `data-demo-deep-jump`
765
+ (`scrollToCell` の参照実装) へ移設し、§10 の深部プローブ起点もこのボタンへ再配線した。
766
+ - **是正 — ダブルクリック編集開始時のカーソル点滅 (十字 → 矢印 → 十字) を根絶 (利用者実証・利用者選択)**:
767
+ カーソル方式を**非編集=既定矢印 (`default`) / 編集=キャレット (`text`)** へ差し替えた。旧 v0.3.2 は
768
+ 非編集=Excel 白十字 (`cell`) / 編集=`text` で、ダブルクリックでシンク (textarea) がカーソル直下に
769
+ 出現・フォーカスされる瞬間、カーソル値が `cell → text` へ変わるためブラウザが OS カーソルを
770
+ 再評価し十字とキャレットの間に既定の矢印を一瞬挟む点滅を招いていた (DOM の computed cursor には
771
+ 現れない OS 遷移 — rAF 記録で `default@cell → text@editing` を実測)。点滅の起点だった十字を撤去し
772
+ 矢印基調へ統一 (`.aqsp-row` / 待機シンクを `default`)、編集中だけ `text` を出して編集セル文字上の
773
+ キャレットは維持する。カーソル値変化は編集開始の 1 回のみ。方式は利用者が選択 (非編集=矢印 /
774
+ 編集=キャレット)。standalone-css ゲートの `.aqsp` 層判別子はセル cursor `cell` → `default` (未適用の
775
+ `auto` と別値) へ追随。
776
+ - **検証**: 単体 564/564 (28 ファイル — v0.5 の 545 に +19: frozenPartition 行双子 +6・frozenRows 統合 +7 新設・ensureCellVisible 凍結行 2 面 +2・useAutoScroller 上端帯 3 面 +3・Spreadsheet scrollToCell +1) + E2E
777
+ 142/142 (18 ファイル — frozen-rows 6 新設: ボタン DOM 順 / 深部静止整列 / 静的着地 / 縦 2 象限
778
+ クリップ / 凍結クリック vy 不動 / 実行時トグル恒等復帰) 全緑、tsc / biome / markdownlint 0。
779
+ 行専用の写像シーム (パーティション H_F ガード / 行ヘッダー床クランプ + unhide 畳み / overlay
780
+ 縦 clampHeight / 縦ヒットテスト / 縦 ensure-visible no-op / 飛行中 R 取消) を新ゲートで封印。
781
+ 共有の軸中立機構は列側 (FZ1〜FZ39) で認証済 — 行はその厳密な転置。
782
+ - **リリース義務**: 公開ペアは virtualscroll **3.4.0** (frozenLeadingRows + 横バー凍結帯食い込み
783
+ 修正の初出)。2 段 publish 振付 (engine 3.4.0 publish → 本 worktree へ `feat/develop` を merge して
784
+ ローカル virtualscroll = 3.4.0 を確認 → spreadsheet publish) は virtualscroll 現状仕様 §8 が正準。
785
+
786
+ ## 0.5.0 (未リリース)
787
+
788
+ - **新機能 — 列凍結 (`frozenColumns`)**: 先頭 N 列を左端に凍結するライブ prop (v0.5 計画 —
789
+ VirtualGrid `frozenLeadingCols` (virtualscroll 3.3.0 初出) の消費)。検証はエンジンの
790
+ fail-fast 契約 ([0, 128] 整数 RangeError・colCount 超過は全列凍結の動的クランプ) を
791
+ そのまま採り、二重検証は置かない (SSOT)。
792
+ - **座標パーティション (v0.5 計画 §2)**: 順写像 (凍結列は静的 / スクロール列は − hx) と
793
+ 逆写像 (px < W_F は静的凍結帯 / 以上は木 = px + hx) を `src/model/frozenPartition.ts` に
794
+ 純関数化し、ヒットテスト 6 面 (選択エンジン / ヘッダー / fill / オートスクロール hover /
795
+ Shift+F10 / overlay) を配線。W_F の鮮度読みは `handle.getFrozenSize()` (§7-4)。
796
+ - **列ヘッダーの 2 帯化**: 静的凍結ラベル (`data-aqsp-frozen-header` — 木絶対 left 直接値) +
797
+ W_F クリップ (`data-aqsp-col-header-scroll`) 内の既存 track (`repaintColHeaders` は
798
+ translateX(窓原点 − hx − W_F))。
799
+ - **overlay の 2 層化 (v0.5 計画 §3-5)**: F > 0 は常設ノード一式 (選択塗りプール / アクティブ
800
+ 枠 / ドラッグ矩形 / クリップマーキー / fill プレビュー / ガイドライン) を凍結層
801
+ (クリップ [0, W_F)) とスクロール層 (クリップ [W_F, →)・x = 木 − hx − W_F) へ複製し、
802
+ 境界跨ぎ矩形は各層のクリップが切る (標準の凍結ペイン技法)。fill ハンドル / シンクは
803
+ 順写像 + 凍結帯下の不可視ガード。ref コールバックは useMemo で identity 安定 (毎レンダー
804
+ 生成は detach/attach でプール二重化 — 実装中に実測・修正した欠陥クラス)。
805
+ - **可視化 / オートスクロール**: 凍結列の ensure-visible は横 no-op (常時可視)、スクロール列の
806
+ 左可視限界は木座標 W_F + hx。左端オートスクロールゾーンは帯基準 (凍結帯上の保持でも継続 —
807
+ Excel 同型)。
808
+ - **F = 0 の恒等**: 凍結 DOM / 属性 / 層を一切出力せず、全式が v0.4 形へ厳密に還元される
809
+ (overlay のみ主層コンテナ 1 枚の構造差分 — 全層一様の生命構造でトグル時のプール孤児化を遮断)。
810
+ - **敵対レビュー是正 (R1 / R2 — 4 レンズ × 検証ループの実証欠陥を全て根治)**: fill 隅アンカーの
811
+ パーティション化 + 内側 px の帯下限クランプ (小数 hx の隅すきま) / 逆写像の W_F > 0 ガード
812
+ (負 px の F = 0 恒等) / overlay 主層コンテナの生命構造 + 層コンテナ形状の器ヒール / 飛行中
813
+ F 変更の全ドラッグ取消 (座標系跨ぎの誤確定遮断) / オートスクロールゾーンの右端優先 (狭帯
814
+ 逆走防止) / drag-mode 同値抑止のノード単位化 / E2E ポートの fail-fast 化 (AQSP_E2E_PORT +
815
+ reuseExistingServer: false — 並行 worktree の同一パッケージ罠)。
816
+ - **編集 (§6.7.1 追補 — 利用者要望)**: 無変更 commit の no-op 化 — F2 で開いて変えずに閉じる
817
+ フォーカスアウトは applyEdits へ進まずセッション終了のみ (Sheets 同型)。読み取り専用ホストの
818
+ 拒否ループ (毎回のトースト + セッション生存) を根絶。変更ありの拒否維持は従来どおり。
819
+ - **デモ増強 (v0.5 ギャップ閉塞)**: `/edit` のシート切替 (`data-demo-sheet-toggle`)・`/tune` の
820
+ 空グリッド段取り (`data-demo-empty`) と 1,200 行化・`/huge` のコンテキストメニュー
821
+ (`data-demo-context-menu` + リードアウト / 閉じる)・`/utils` のクリップボード検分
822
+ (`data-demo-clip-inspect` / `data-demo-clip-html`)・`/` のマウント計測可視化
823
+ (`data-demo-mount-ms`)・`?css=standalone` の CSS 成果物切替 (webServer コマンド前段に
824
+ `build:css` を追加) — demo 全機能到達の E2E 段取り面。
825
+ - **凍結 UX 是正 (R5 — 利用者実証 3 件)**: (1) 凍結セル / 凍結ヘッダーのクリックで可動帯が勝手に
826
+ 左スクロールする欠陥を根治 — スクローラーの武装を 3 機械とも距離ゲート化 (押下点から累計
827
+ 4px 以上の実移動のみ武装 — 押下起動もジッタ 1 発も不可) + 押下時自己修復 (宙吊りジェスチャ
828
+ 掃除) と凍結ホームテリトリー規則 (アンカー凍結 (W_F > 0) × ポインタ帯内は左駆動なし。帯外
829
+ 戻りとスクロール帯アンカーの引き寄せは Excel 同型で維持)。(2) /huge の Ctrl+矢印を 2 段端へ (中間点 500 万 →
830
+ グリッド端 — 旧固定端は最右 9,999,999 に構造的到達不能だった)。(3) 横バーの凍結帯食い込みは
831
+ エンジン側修正 (次リリース) で対応。
832
+ - **検証**: 単体 545/545 (27 ファイル — frozenPartition 純関数 6 + 凍結統合 26 + 変異封鎖ピン 6 +
833
+ 編集 no-op 4 新設) + E2E
834
+ 136/136 (17 ファイル — frozen-columns 7 新設: 深部静止 / パーティション着地 / 2 層クリップ /
835
+ 凍結 Shift+F10 / 実行時トグル恒等復帰 / 凍結下ドラッグ鮮度 / 凍結クリック hx 不動、
836
+ virtualgrid-deep 10 = Ctrl+→ 2 段端込み) 全緑、tsc / biome / markdownlint 0。
837
+ v0.5 ギャップ閉塞 (demo 全機能到達の監査起点 — E2E +18 ゲート: selection 2 / clipboard 2 /
838
+ editing 1 / render 1 / tune 6 / utils 1 / v3-features 2 / 凍結 Shift+F10 1 + standalone-css 2
839
+ 新設) 込み。凍結機構の
840
+ 変異 39 種 (FZ1〜FZ39 — R1〜R7 敵対レビュー是正の封印 27 種込み、FZ24 は機械別 ×3・FZ29 は 2 変種 red 実測) を検証: 36 種 red 実測、FZ5 / FZ14 / FZ39 の 3 件は ⚠️ 等価防御 (分冊 07 の 5.6 節)。
841
+ - **demo dev サーバの起動死対策**: workspace リンクの `@aiquants/virtualscroll` を
842
+ `optimizeDeps.exclude` — `.vite/deps` の古い事前バンドルが dist 再ビルドを検知せず
843
+ 「does not provide an export named 'VirtualGrid'」で起動死する罠 (実測) を構造的に遮断。
844
+ dist はオンデマンド変換の直配信になり、再ビルドは再読込だけで届く。
845
+ - **リリース義務**: 公開ペアは virtualscroll **3.3.0** (frozenLeadingCols 初出) — 2 段 publish
846
+ 振付 (3.2.0 → 3.3.0) と本パッケージのローカル同期は virtualscroll 現状仕様 §8 が正準。
847
+
848
+ ## 0.4.0 (未リリース)
849
+
850
+ - **移行 — 横軸を `@aiquants/virtualscroll` の `VirtualGrid` へ全面移管** (VirtualGrid 設計計画 §7 の
851
+ クリーンカットオーバー)。v0.3 の自前列スタック (`useColumnWindow` / `HScrollBar` / CSS 変数
852
+ `--aqsp-hx` / 軸原点絶対 left の `.aqsp-cells`) を削除し、量子化列アンカー + 残差 transform を
853
+ 持つ `VirtualGrid` (virtualscroll 3.2.0) が両軸を所有する。これにより列方向の到達性が
854
+ ブラウザのレイアウト座標壁 (実測 ≈ 2^25 px) から解放され、**1,000 万列 (総幅 10^9 px) の深部
855
+ ジャンプでセル gBCR ±1px** を E2E で断定 (`tests/e2e/virtualgrid-deep.spec.ts` — デモ `/huge`)。
856
+ - **鮮度序列の両軸統一 (§7-4)**: 位置 / 寸法の読み手は全機械 (選択・ヘッダー・fill・キーボード・
857
+ オートスクロール・オーバーレイ) がグリッドハンドル (`getScrollPosition()` / `getViewportSize()` /
858
+ `scrollBy` 同期返り値) を参照する。viewStore から hx / bodyWidth を撤去し (vy は React 葉向け
859
+ rAF ミラーとして存続)、列窓 `colStart` / `colEnd` を追加 (ColumnHeader の再レンダー源 — 行側と
860
+ 完全対称)。退役面の残存参照は機械ゲート `src/hxCensus.spec.ts` が恒久検出する。
861
+ - **列ヘッダーの窓原点相対化 + commit ゲート (§7-5)**: ラベルを `offsetOf(col) − windowOriginX` で
862
+ 配置し、トラックは `repaintColHeaders` の命令的 translateX (RowHeader §6.8-2 と同型の二重駆動)。
863
+ 深部でもヘッダーとセルの整列 ±1px を E2E で断定。
864
+ - **バー帯ゲートの統合 (§6.8-3)**: スクロールバー 2 本がグリッド内在化したため、GridBody の
865
+ capture pointerdown 1 箇所でセル領域外の全押下 (縦バー / 横バー / 余白) にフォーカス回収 +
866
+ 編集中 requestCommit ゲートを適用する (v0.3 の HScrollBar 個別ゲートの後継)。
867
+ - **破壊的 — `initialColumnWidth` prop を撤去 (§7-8)**: 列毎の初期幅は命令的 `resizeColumn` を
868
+ マウント効果で適用する方式へ (デモ `/tune` が参照配線)。既定幅は `defaultColumnWidth` →
869
+ `VirtualGrid` の `behaviorOptions.defaultColWidth` (Fenwick baseValue — サンプリング省略) に直結。
870
+ - **セル / 行 DOM の所有移転 (§7-3)**: セル要素は VirtualGrid 所有となり、属性面 (role="gridcell" /
871
+ aria 座標 / `data-aqsp-cell` / `data-row` / `data-col`) は `getCellProps`、行面 (role="row" /
872
+ `data-aqsp-cells` イベントゲート) は `getRowProps` で装飾する。`CellView` は内容専用の memo 葉へ
873
+ 縮退 (配置 div 削除 — 中間ノード 1 段削減)。
874
+ - **検証**: 単体 / 統合 503 (census 4 + commit ゲート同期性ピン 4 含む) + E2E 111 (go/no-go 9 + §12 恒久プローブ 5 + sync 追補 2 を新設) 全緑、
875
+ `tsc` / `biome` 0。深部ジャンプ初回描画 < 150ms (3 回最小値)、横サムテレポートは 2 vsync 以内に
876
+ 着地窓を具現化。
877
+ - **リリース義務 — 公開は依存順 + ローカル同期必須 (設計計画 §11)**: `VirtualGrid` は
878
+ `@aiquants/virtualscroll` 3.2.0 初出。`workspace:^` は **publish を実行する worktree のローカル
879
+ `packages/virtualscroll/package.json` の version** から展開される (レジストリ状態は無関係) ため、
880
+ 順序だけでは不十分で 2 段が必要 — (1) virtualscroll worktree で 3.2.0 を publish (version bump
881
+ commit を `feat/develop` へ統合)、(2) **本 worktree へ `feat/develop` を merge してローカル
882
+ virtualscroll の version = 3.2.0 を確認してから** spreadsheet を publish。どちらを欠いても peer が
883
+ `^3.1.1` (VirtualGrid を持たない版で成立する床) に固定される。順守すれば成果物の peer は
884
+ `^3.2.0` になる。
885
+
886
+ ## 0.3.3 (未リリース)
887
+
888
+ - **修正 — ズームショートカットのレイアウト非依存化**: keydown ガードの Shift 一括拒否のため、
889
+ `=` を Shift+`-` でしか打てない JIS 配列 (および `0` を Shift+`à` でしか打てない AZERTY 配列)
890
+ ではメイン行のズームイン / リセットが到達不能だった。4 キー (`=` / `+` / `-` / `0`) とも
891
+ 生成文字 (event.key) の一致だけで請求する方式へ — JIS の Shift+`-` (key "=") と US の
892
+ Shift+`=` (key "+") はズームイン、不一致の US Shift+`-` ("\_") / Shift+`0` (")") は素通し。
893
+ **AltGraph 修飾状態つきの ctrl+alt は請求しない多層防御** — evergreen ブラウザは AltGr と
894
+ Ctrl/Alt を相互排他にするため実 AltGr 印字 (ラトビア語の AltGr+F = "=" 等) は修飾なしで届き
895
+ 本ガード非依存。守るのはレガシー AltGr エミュレーション形状 (旧エンジン / 合成イベント /
896
+ 非準拠組込ホスト) のみで挙動コストはゼロ。
897
+ 帰結として非 AltGr の束縛は配列依存 (独配列の Shift+0 = "=" はズームイン) — 配列非依存の
898
+ 和音が必要なホストは event.code で先に請求する (README Keymap notes)。
899
+ - **修正 — ctrl+alt 印字形状の遅延開始 (バレン和音の破壊経路根絶)**: 文字を生まない実ショート
900
+ カット和音 (Ctrl+Alt+X、US の Ctrl+Alt+Shift+`-` = "\_" 等) が keydown 即 startEdit の旧実装では
901
+ **空の置換セッションを開き、次クリックの確定で "" がコミットされてセル内容が破壊された**
902
+ (敵対レビュー R1(I) blocking — jsdom / 実 Chromium 両実測)。ctrl+alt 印字形状 (レガシー
903
+ AltGr エミュレーションとリテラル和音 — evergreen の実 AltGr 印字は修飾なしで届き即開始経路)
904
+ は実文字の到着 (input) までセッションを開かない遅延開始へ (IME の compositionstart 遅延開始と
905
+ 同型。保留は次 keydown / keyup / blur で失効する一発フラグ)。修飾なしの平印字は従来通り
906
+ 即開始。この遅延開始により capture 請求の [M151] は観測面を失い等価変異化 (⚠️ 降格 —
907
+ capture 配置は多層防御として維持)。
908
+ - **ライセンス — MIT から PolyForm Noncommercial 1.0.0 + 商用別売のデュアルへ**: 著作権者
909
+ AI Quants Trading, LLC.。非商用利用は `LICENSE.md` (PolyForm NC 1.0.0 逐語 + Required Notice)
910
+ の下で無償、商用利用は別途契約。旧 MIT の `LICENSE` ファイルは削除 (矛盾する二重付与の遮断)。
911
+ `package.json` の変更は `license` フィールドと `files` の同梱ライセンス名のみ — publish 経路
912
+ (publish:patch / minor / major) は維持し公開の可能性を保持する (公開時もデュアル条件で提供。
913
+ 旧節の publish 振付は公開実施時の到達版で再決定 — spec 00 の 2 節)。
914
+ - **品質**: 単体 508 / 508・E2E 95 / 95・変異 [M170] (Shift 一括拒否復帰) / [M172] (遅延開始の
915
+ 即開始退行 — jsdom red 1 + E2E red 2) / [M173] (保留スコープ撤去 — red 2) / [M174] (Shift 時
916
+ 方向反転 + "-" 枝 Shift 拒否の 2 変異) / [M175] (次 keydown 失効撤去) / [M176] (AltGraph 判別
917
+ 撤去 — レガシー形状面で red、現実形状では等価変異のため ⚠️ 併記) / [M177] (blur 失効撤去) /
918
+ [M178] (meta 印字除外撤去 — Cmd 和音の同型破壊) / [M179] (Ctrl+Alt 連言の選言退行 —
919
+ ブラウザ Ctrl+= の窃取) / [M180] (Alt+ホイール修飾連言の 3 拒否項撤去) red 実測。当初の `0` 非 Shift カーブアウト ([M171]) は R1(I)
920
+ blocking (AZERTY リセット到達不能 + 素通し面の空セッション破壊) で設計ごと撤回。
921
+
922
+ ## 0.3.2 (未リリース)
923
+
924
+ - **追加 — copy / cut のマーチングアンツ**: 静的破線だった cut マーキーを 4 辺グラデーションの
925
+ アニメーション破線 (8px 周期 × 2px 厚・0.4s 時計回り) へ置換し、**copy にも同一のアンツを装填**
926
+ (Excel / Google Sheets 準拠 — ノードは `data-aqsp-clip-marquee` へ統一、`data-aqsp-clip-kind`
927
+ で判別)。copy のアンツは**ペースト後も存続** (連続ペースト可)、Escape / 編集開始 / 他の成功
928
+ commit / undo-redo / シート切替 / dataVersion で消灯。判別共用体 (copy は指紋 matrix を
929
+ 持たない) + paste の kind ガードで「copy → paste が move 化して元セルが消える」事故を型と実行時の両層で
930
+ 遮断。アンツ本体は ::after が**選択枠の外側 3px** に描く (選択塗り / アクティブリングと重なら
931
+ ず常時可視 — ノード矩形は writer の論理レンジ矩形 SSOT のまま)。外側帯が overlay の
932
+ overflow: hidden に切られる縁 (行 0 / 列 0 の原点やビューポート端) は writer の**漸進端別
933
+ クランプ** (-min(3, 余白)px — 連続で閾値跳ねなし) が境界上へ畳む。ダッシュは**青 / 白の二色**
934
+ (Excel 同型) — 透明コマだと青リング / 青塗りの上で実線に化けて知覚不能になるため、白コマが
935
+ どの下地でも対比を作る。A1 コピーでも全辺が知覚可能。`prefers-reduced-motion` では静止破線、forced-colors は同じ端クランプ ::after 上の
936
+ Highlight 破線 border。
937
+ - **修正 — 非編集時のキャレットカーソル**: 文字入りセルの上で I ビームになっていたのを
938
+ `.aqsp-cells { cursor: cell }` (Excel の白十字) へ。キャレットカーソルは編集中の可視シンク
939
+ (`cursor: text`) のみ。
940
+ - **品質**: 単体 505 / 505・E2E 93 / 93・変異 [M160]〜[M169] red 実測 (copy 装填 / paste 温存 /
941
+ kind ガード = 統一前設計の再現変異 / 外側オフセット / 端別クランプ / ラッチ解放 / 二色)。
942
+
943
+ ## 0.3.1 (未リリース)
944
+
945
+ - **修正 — ウィンドウ縮小時の右端スクロールバー遅れ**: 幅リサイズだけ viewStore の rAF 量子化を
946
+ 経由し、RO コールバック内から積む rAF が必ず次フレームになるため、横バー幅と列ウィンドウが
947
+ リサイズ 1 ステップごとに 1 フレーム以上遅れて右端が欠けて見えた (高さ経路は CSS + 同フレーム
948
+ setState で対称欠如)。`setBodyWidth` のみ同期即時通知へ ([M158] — vy / hx の熱経路量子化は
949
+ 不変、等値ガードが再入を自己終端)。
950
+ - **修正 — 同症状の支配因: グリッドトラックの min-content 帰還ループ**: 素の `1fr` の暗黙最小
951
+ (min-content) により、横バーのインライン幅 (旧 bodyWidth) が列 2 トラックの下限となって
952
+ 「計測 → −12px → 再計測」の**収縮スパイラル** (12px/フレーム × 約 100 フレーム ≈ 2 秒) を作って
953
+ いた — CDP 実機プローブで特定。`minmax(0, 1fr)` (両軸) のトラック下限 0 化で**1 フレーム収束**
954
+ へ ([M159]、実レイアウト E2E ゲート 2 面)。上の同期通知修正は各ステップを同フレーム化する
955
+ 必要条件で、本修正がステップ数そのものを 1 にする支配因子。
956
+ - **依存 — `@aiquants/virtualscroll` 3.1.1 (タッチ / ペンパンの ÷scale)**: 開発中に登記して
957
+ いた既知逸脱「ズーム ≠ 1 のタッチパンが指速の z 倍」は virtualscroll 3.1.1 のパン / 慣性
958
+ 補正で**解消** — 逸脱表 (README / spec 00) からも撤去済み。
959
+ - **品質**: 単体 494 / 494・E2E 88 / 88・変異 [M158] red 4 / [M159] red 1 実測。
960
+
961
+ ## 0.3.0 (未リリース)
962
+
963
+ v0.3 機能トリオ (要望改善)。本節の § は v0.2 実装プランを指す。publish は 0.2.0 → 0.3.0 の
964
+ `pnpm run publish:minor` 2 段 (§12 の振付に 1 段追加)。
965
+
966
+ - **追加 — 可視編集エディタ**: 編集セッション中のシンクを可視化 (白背景 + アクティブ色の枠 +
967
+ 実キャレット + 行高連動の縦位置)。従来は常時 opacity 0 で入力本文もキャレットも見えなかった。
968
+ active セルが画面外へスクロールした間は可視化を降ろす (誤位置描画の遮断 — セッションと IME は
969
+ 生存し、スクロールバックで再表示)。非編集時の不可視は不変。
970
+ - **挙動変更 — fill の斜め対応**: 帯導出を支配軸方式 (0.2.0 の |dx| ≥ |dy| + 4px デッドバンド)
971
+ から**ボックス方式** (ボックス = ソース ∪ hover のバウンディング矩形) へ置換。斜めドラッグは
972
+ L 字領域へ 2D 剰余 anchoring でタイルし、単軸ドラッグは従来どおり (セル量子化がジッタを
973
+ 吸収)。エッジスクロールは武装時に両軸起動 (軸選択・反転なし)。Excel は斜めフィル非対応 —
974
+ 超過機能として逸脱表に記載。
975
+ - **追加 — パッケージズーム**: `zoom` (制御) / `defaultZoom` / `onZoomChange`。視覚は
976
+ transform スケール・論理は論理 px のまま (全ヒットテスト / ドラッグ差分は ÷z 変換、両
977
+ スクロールバーは祖先スケール補正内蔵)。ジェスチャは Alt+ホイールと Ctrl+Alt+= / − / 0 —
978
+ Ctrl+ホイールはブラウザ所有のため意図的に非束縛。ドラッグ中のズーム要求は無視 (押下時
979
+ gBCR キャッシュの歪み防止)。clamp [0.25, 4]。ズームキーは capture 段で完全請求し、シンクの
980
+ AltGr 印字規則との衝突 (空の置換セッション → 次クリックで "" コミット) を遮断。刻みは進行
981
+ 方向の最寄り 0.1 デテント着地 (0.25 → 0.3)、deltaY=0 の Alt+ホイール (チルト / 横パン) は
982
+ ブラウザへ素通し、連打は要求済み値から刻む (段飛ばし・同値二重通知なし)。
983
+ - **品質**: 単体 489 / 489・E2E 86 / 86 (Playwright、本番ビルド)・変異 [M139]〜[M157] red
984
+ 検証・敵対レビュー 9 ラウンド (Phase 5 台帳 Phase E 節)。
985
+
986
+ ## 0.2.0 (未リリース)
987
+
988
+ ヘッダーリサイズ / unhide UI・フィルハンドル・非表示トラック・複数レンジ操作・キーボード拡張を
989
+ 柱とするリリース。本節の § 番号は v0.2 実装プラン
990
+ (`docs/plans/2026.08.24 [AI] spreadsheet-v0.2-implementation-plan.md`) を指す
991
+ (0.1.0 節の § は v0.1 プランのまま)。
992
+
993
+ - **修正 — 実ブラウザで dblclick autofit / 復元が不到達だった問題**: リサイズドラッグの
994
+ pointer capture により up のターゲットが帯へ合成され、dblclick のターゲット (down / up の
995
+ 共通祖先) がストリップに届かなかった (Pointer Events / UI Events 仕様帰結 — デモ E2E で
996
+ 実証)。`event.target` がストリップ外のときは座標の実要素で解決し直す 2 段解決へ修正 —
997
+ ピッカーは帯の rootNode (shadow DOM ホストでは `ShadowRoot.elementFromPoint`、
998
+ 非 shadow は `band.ownerDocument` — document 版はホスト要素を返して恒久不発になるため)。
999
+ `elementFromPoint` 非実装環境 (jsdom) は能力ガードで命中なし扱い (throw させない)。
1000
+ - **追加 — `RenderCellContext` 型の公開**: `renderCell` prop の引数型を名指しできるように
1001
+ パッケージルートから export (型のみの非破壊追加)。
1002
+ - **破壊的変更 — 名前つき union へのカットオーバー**: `SpreadsheetOperation`
1003
+ (`"copy" | "cut" | "delete" | "paste" | "fill" | "resize"`) と `SpreadsheetDirection`
1004
+ (`"up" | "down" | "left" | "right"`) をパッケージルートから公開し、`onOperationRejected` の
1005
+ 第 1 引数を旧インライン union (4 値) から `SpreadsheetOperation` (6 値) へ切替。コールバック
1006
+ 引数は反変位置のため、旧 union を明示注釈していた consumer は型エラーになる — 注釈を
1007
+ 名前つき型へ差し替えること (実行時は 3 引数のまま・既存 4 値の発火条件は不変)
1008
+ - **挙動変更**:
1009
+ - ドラッグ中の右ボタンリリースは確定しなくなった — pointerup の確定は主ボタン
1010
+ (button === 0) のみ (v0.1 は非主ボタンの up でも確定していた)。Excel 同等化
1011
+ - idle の Shift+Space は行帯化へ (v0.1 は印字キー扱いでスペース 1 文字の replace 編集を
1012
+ 開始していた)
1013
+ - フィルドラッグは押下から 4px の武装閾値つき — 閾値未満のジッタ / ハンドル単クリックは
1014
+ 無書込。ソース内向きに解決した hover は帯 null = 無書込 (内向きドラッグのソースクリアは
1015
+ §10 の宣言逸脱 — v0.4 予定)
1016
+ - `onSelectionChange` に明文のドラッグ中発火点が 1 つ — ヘッダー減算予約が実 press へ変換
1017
+ される初動 move で遅延 press 置換が発火する (発火総数は v0.1 と同一 = 変換 1 + up 1)
1018
+ - **ヘッダーリサイズ / unhide UI (§7)**: 両ヘッダー帯のリサイズストリップ (境界内側 6px。
1019
+ 非表示ラン境界は 3px リサイズ + 3px unhide マーカーに分割、先頭非表示ランは最初の可視
1020
+ トラック先頭端の 3px マーカー)。ドラッグ中は rAF 合流の直接 DOM ガイドラインのみ描画し、
1021
+ セル実サイズは up 確定でだけ変わる (同値は skip)。全スパン選択への確定は選択トラック全体へ
1022
+ ファンアウト。帯 dblclick = 列 autofit (可視ウィンドウの canvas measureText 個別計測 —
1023
+ フォントはヘッダー帯 computed style のロングハンド合成、空計測は no-op) / 行は既定行高復元。
1024
+ `enableResizeUI` (既定 true) で一括 opt-out — false は unhide マーカーも消える (非表示
1025
+ トラックの復元手段は命令 API のみになる)
1026
+ - **フィルハンドル (§8)**: アクティブレンジ隅のハンドルをドラッグする Excel 型の連続フィル。
1027
+ 支配軸 1 軸固定 + 4px デッドバンドの軸フリップ、武装後の端ゾーン保持オートスクロール
1028
+ (フリップで stop / start 切替 — **0.3.0 でボックス方式へ置換**、上節参照)。確定は最終 rAF flush 済みプレビュー帯 (up 位置の再ヒット
1029
+ テストなし) を展開前見積もり → `expandFill` → `applyEdits` 1 トランザクションで書き込み、
1030
+ 成功時のみ選択をソース + 帯の合併矩形へ更新 (anchor / active 不動)。編集中・他機械ドラッグ中
1031
+ はハンドル非表示。`enableFillHandle` (既定 true) で opt-out (読み取り専用 adapter 向け)
1032
+ - **非表示トラック (§5)**: サイズ 0 トラック = 非表示の一級意味論化。描画は Fenwick ジャンプの
1033
+ 可視列挙 + サイズ 0 行 skip で非表示ラン幅に非依存、キーボードは可視 index ステップ (§5.2) と
1034
+ Ctrl+Arrow の可視着地再スキャン (§5.3 — 前方 → 後方 → 現位置)。復元は unhide マーカー /
1035
+ 命令 resize
1036
+ - **複数レンジの減算と巡回 (§3)**: 選択済みセルへの Ctrl+ドラッグで減算 (press 非書込 → up で
1037
+ 確定、最大 4 帯の互いに素分解 + 再 anchor 規則)。全スパン選択済みヘッダー index への
1038
+ Ctrl+press は減算予約 (up = 全スパン減算、移動で通常の Ctrl 追加ドラッグへ変換)。ドラッグ
1039
+ 矩形は減算中 `data-aqsp-drag-mode="subtract"` で減算スタイル。Tab / Enter の巡回は複数レンジ
1040
+ 横断へ拡張 (重なりセルは skip・全周一致は identity)、active クランプは「いずれかのレンジ内」
1041
+ へ緩和 (縮小 extend で全レンジ外に置き去りの場合のみ末尾レンジへ clamp)
1042
+ - **キーボード拡張 (§4 / §9)**: Ctrl+Space / Shift+Space = 全レンジの列帯化 / 行帯化。
1043
+ PageUp / PageDown は Fenwick 幾何の正確なページ着地 (indexAt(offsetOf ± viewportH)、
1044
+ Shift 系は伸長隅基点 — v0.1 の一様割り近似を置換)。Ctrl+Arrow は adapter が `findDataEdge`
1045
+ を宣言していればデータ端ジャンプ
1046
+ - **`findDataEdge` 能力 (§2)**: `SpreadsheetDataAdapter` の任意メンバー。Ctrl+Arrow の
1047
+ Excel 型データ端ジャンプ先をデータを知る consumer 側が O(占有セル) で計算する。未宣言なら
1048
+ v0.1 のグリッド端ジャンプ維持 (能力宣言 — 値フォールバックではない)。戻り値はディスパッチ前
1049
+ に完全 fail-fast 検証 (非整数 / 軸域外 / reset 経路の直交ドリフトは RangeError)
1050
+ - **caret シーディング (ED-1)**: dblclick 開始の編集はクリック位置の文字境界へ caret を置く
1051
+ (caretPositionFromPoint → caretRangeFromPoint の能力検出、判定不能時は v0.1 の末尾 caret)。
1052
+ `renderCell` 指定時は無効 (単一ゲート)
1053
+ - **上限と拒否チャネル (ADR-12 / §13 設計返却)**: リサイズの行ファンアウトは
1054
+ `maxOperationCells / 10`・autofit の計測セル数は `maxOperationCells / 4` でゲート (実測に
1055
+ よる §13 予算行の設計返却 — 素の上限では §9 予算を超過。経緯は変異検証台帳)。行の非表示化は
1056
+ 軸総数 1,000 本の bounds ゲート (超過は `onOperationRejected("resize", 総数, "bounds")` で
1057
+ 一括拒否)。fill は帯 + 必要ソース読みの和を展開前見積もりで "limit" ゲート
1058
+ - **常設オーバーレイノード**: `[data-aqsp-resize-guide]` / `[data-aqsp-autofill-preview]` /
1059
+ `[data-aqsp-autofill-handle]` を追加。全 5 機械 (セル選択 / 列ヘッダー / 行ヘッダー /
1060
+ ヘッダーリサイズ / fill) が press / cleanup の repaint エッジ規律を共有 (§8.2)
1061
+ - **品質**: 敵対的レビュー 9 ラウンド (V2-A R1〜R4 / V2-B R1(B)〜R5(B) — 双方の最終ラウンドで
1062
+ 5 レンズ全員 astonished)、変異検証台帳 1 冊追加
1063
+ (`docs/reports/2026.08.25 [AI] spreadsheet-phase5-mutation-log.md` — 安定 ID [M71]〜[M134]。
1064
+ [M120] の bump 面は測定等価の反証記録として残置)、単体 / 統合 464・E2E 54・
1065
+ tsc / biome / markdownlint 全 0
1066
+
1067
+ ## 0.1.0 (2026-08-21 — feat/develop 統合済み・未 publish)
1068
+
1069
+ 初回リリース分。v0.1.0 としての registry publish は行われておらず、v0.2 プラン §12 の
1070
+ 版番号運用に従い 0.2.0 の publish 時に中間 bump (`pnpm version 0.1.0 --no-git-tag-version`)
1071
+ を経て `publish:minor` で 0.2.0 が最初の公開版になる。
1072
+
1073
+ `@aiquants/virtualscroll` 上に構築した React 用 Excel 級スプレッドシートグリッド。
1074
+ 100 万行 × 16,384 列のデモを同梱し、全予算をテストで機械検証しています。
1075
+
1076
+ - **仮想化グリッド**: 両軸仮想化。DOM は可視セル + オーバースキャンのみ (選択サイズ・データ量に
1077
+ 非依存)。列は CSS 変数 `--aqsp-hx` 駆動でウィンドウ内横スクロールの再レンダーはゼロ
1078
+ - **選択エンジン**: ドラッグ矩形選択・Shift 拡張・Ctrl 追加選択・行/列ヘッダー選択・コーナー
1079
+ 全選択・Tab / Enter のレンジ内巡回・端ゾーンオートスクロール・Escape ドラッグ中断
1080
+ - **キーマップ**: §6.6.1 契約表の完全実装 (修飾は厳密一致・表に無い組合せは素通し)。
1081
+ WCAG 2.1.2 の Escape 脱出、Shift+F10 / ContextMenu のキーボード起点メニューチャネル
1082
+ - **クリップボード**: Excel 実挙動互換 TSV (タブ / 改行を含むセルのみ quote — literal `"hi"` は
1083
+ 無傷往復)、`text/plain` + `text/html` (`mso-data-placement:same-cell`) の二重ペイロード、
1084
+ 整数倍レンジへのタイリング貼り付け、cut-move (cut 時行列照合・マーキー・8 破棄条件)、
1085
+ 全操作の展開前上限見積もり (`maxOperationCells` 既定 100,000) と型付き拒否コールバック
1086
+ - **編集**: F2 / ダブルクリック / 印字キー開始の 2 モード編集セッション、Alt+Enter 複数行、
1087
+ IME 完全対応 (Safari / Chrome 両順序列)、グリッド外 focusout 確定、adapter throw による
1088
+ 拒否と `onEditRejected`
1089
+ - **データ境界**: 4 メンバーのアダプタ契約 (`rowCount` / `colCount` / `getCellText` /
1090
+ `applyEdits` = 1 呼び出し 1 トランザクション)。履歴は consumer 所有 — Ctrl+Z / Y は中継のみ
1091
+ (参照実装をデモ `/edit` に同梱)。`dataVersion` で外部書込を公開
1092
+ - **アクセシビリティ**: シンク集中型フォーカスモデル + デバウンス済みライブリージョン読み上げ
1093
+ (**opt-in** — 組込文字列なし。`formatCellAnnouncement` / `formatSelectionAnnouncement` を
1094
+ 指定した時のみ読み上げ領域が描画される)
1095
+ - **命令ハンドル**: `resizeRow` / `resizeColumn` (整数 px)
1096
+ - **公開ユーティリティ**: `parseTsv` / `serializeTsv` / `estimatePasteCellCount` / `expandPaste` /
1097
+ `enumerateRangeCells` / `makeRange` / `rangeContains` / `rangeSize` / `columnIndexToName` /
1098
+ `columnNameToIndex`
1099
+ - **品質**: 4 フェーズ × 敵対的レビュー計 25 ラウンド以上 (各フェーズ 5 レンズ全員 astonished
1100
+ 到達)、変異検証台帳 4 冊 — Phase 2 ✅ 59 / Phase 3 ✅ 67 / Phase 4 ✅ 3 (安定 ID [M1]〜[M70] は
1101
+ Phase 3 以降の連番・全 kill。Phase 1 台帳は等価変異の ⚠ 記録を含む正直な初期形)、
1102
+ 単体 / 統合 288・E2E 45
1103
+ (実クリップボード権限・プロダクションビルド実測 §9 予算含む) 全緑