@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
@@ -0,0 +1,478 @@
1
+ /**
2
+ * @fileoverview The single data boundary between the grid and the consumer (v0.8 — ADR-13
3
+ * rich cells / ADR-16 write vocabulary): the adapter type with its eight optional rich-cell
4
+ * capability reads, the per-cell PATCH the grid writes (CellPatch / CellEdit — an absent key is
5
+ * untouched, `null` clears), the discriminated write context every applyEdits receives
6
+ * (EditContext — one transaction id per gesture; the paste arm carries `sameSheet` and the
7
+ * pasted conditional-format inputs), the write-rejection context (EditRejection — what
8
+ * onEditRejected reports when applyEdits throws) and the four adapter-level pure helpers every
9
+ * consumer of the boundary shares: resolveCellAlign (the ONE alignment judgment, over the
10
+ * PAINTED format = base ⊕ conditional formats), lazyCellText (the once-only text thunk = the
11
+ * eighth read of a cell), resolveEditText (the editor seed — formula source or canonical text,
12
+ * fail-fast on a formula that does not start with "=") and aspectCapabilities (the write gate's
13
+ * single reader of the declared aspects). Every capability is a DECLARATION: absence is a code
14
+ * branch that reproduces the v0.7 behaviour exactly, never a value fallback.
15
+ * グリッドと consumer の唯一のデータ境界 (v0.8 — ADR-13 リッチセル / ADR-16 書込語彙): 8 つの
16
+ * 任意リッチセル能力読みを持つアダプタ型、グリッドが書くセル単位パッチ (CellPatch / CellEdit —
17
+ * 未指定キーは「触らない」、`null` は「消す」)、全 applyEdits が受け取る判別可能な書込文脈
18
+ * (EditContext — ジェスチャ毎に 1 トランザクション id、paste 枝は `sameSheet` と貼り付けた条件付き
19
+ * 書式入力を運ぶ)、書込拒否文脈 (EditRejection — applyEdits の throw を onEditRejected が報告する
20
+ * 形)、境界の全消費者が共有するアダプタ水準の純関数 4 つ: resolveCellAlign (唯一の揃え判定 —
21
+ * **塗り書式** = base ⊕ 条件付き書式に対して)、lazyCellText (遅延 1 回読みの text thunk = セル
22
+ * 8 読み目)、resolveEditText (エディタ初期文字列 — 数式ソースか正準文字列、"=" 非始まりの数式は
23
+ * fail-fast)、aspectCapabilities (書込ゲートの宣言済み側面の単一読み)。全能力は**宣言**であり、
24
+ * 未宣言は v0.7 の振る舞いを厳密に再現するコード分岐 — 値フォールバックではない。
25
+ */
26
+
27
+ import type { CellComment, CellCommentInput } from "./cellAnnotations.ts"
28
+ import type { CellCoord, CellRange, SpreadsheetDirection } from "./cellCoord.ts"
29
+ import { type CellAlign, type CellFormat, resolveAlign } from "./cellFormat.ts"
30
+ import type { ConditionalFormatRule, ConditionalFormatRuleInput } from "./conditionalFormat.ts"
31
+ import type { DataValidationRule } from "./dataValidation.ts"
32
+
33
+ /**
34
+ * One per-cell PATCH (ADR-16): an ABSENT key leaves that aspect untouched, `null` CLEARS it, a
35
+ * value replaces it whole. `text` is the canonical string (a cleared cell is `text: ""` — text
36
+ * has no `null`); `format` is the complete CANONICAL CellFormat of the cell after the gesture
37
+ * (the command runner merges its patch with the base format BEFORE the adapter sees it — a
38
+ * CellFormatPatch never crosses this boundary; `null` removes every format key); `note` is the
39
+ * note text; `comments` carries the WHOLE thread: an input WITH `id` is an existing comment the
40
+ * consumer keeps verbatim, an input WITHOUT `id` is a new comment the consumer stamps with
41
+ * id / author / createdAt (the package never mints an id; `author` / `createdAt` travel only when a
42
+ * paste replays a foreign thread), `null` clears the thread; `hyperlink` is the normalized URL
43
+ * (normalizeHyperlink — one of http / https / mailto / tel); `validation` is the rule object.
44
+ * セル 1 件のパッチ (ADR-16): **未指定**キーはその側面を触らず、`null` は消去、値は丸ごと置換。
45
+ * `text` は正準文字列 (消去は `text: ""` — text に `null` は無い)。`format` はジェスチャ後のセルの
46
+ * **完全な正準** CellFormat (コマンド実行器はアダプタに渡す前に base 書式とパッチを合成する —
47
+ * CellFormatPatch はこの境界を跨がない。`null` は全書式キーの除去)。`note` はノート本文。
48
+ * `comments` は**スレッド全体**: `id` つき入力は consumer が verbatim に保持する既存コメント、`id`
49
+ * なし入力は consumer が id / author / createdAt を刻印する新規コメント (パッケージは採番しない。
50
+ * `author` / `createdAt` は paste が外部スレッドを再生するときだけ運ぶ)、`null` はスレッド消去。
51
+ * `hyperlink` は正規化済み URL (normalizeHyperlink — http / https / mailto / tel のいずれか)、
52
+ * `validation` は規則 object。
53
+ */
54
+ export type CellPatch = { text?: string; format?: CellFormat | null; note?: string | null; comments?: readonly CellCommentInput[] | null; hyperlink?: string | null; validation?: DataValidationRule | null }
55
+
56
+ /**
57
+ * One cell write: a coordinate plus a {@link CellPatch}. A v0.7 text edit `{ row, col, text }`
58
+ * is still a CellEdit; a format-only edit omits `text` entirely.
59
+ * セル書込 1 件: 座標 + {@link CellPatch}。v0.7 のテキスト編集 `{ row, col, text }` もそのまま
60
+ * CellEdit であり、書式だけの編集は `text` を持たない。
61
+ */
62
+ export type CellEdit = { row: number; col: number } & CellPatch
63
+
64
+ /**
65
+ * Every write path of the grid (the union of the two EditContext arms' `source`; equal to
66
+ * `EditRejection["source"]`, sealed in dataAdapter.spec.ts): the editor commit ("edit" — also
67
+ * the idle insertDate / insertTime write), the ADR-12 write gestures ("paste" / "fill" /
68
+ * "delete") and the v0.8 command families ("format" = format commands / handle writes, "calc"
69
+ * = multiply / divide / sign-reversal self-calculation, "annotate" = note / comment / hyperlink /
70
+ * validation writes). A cut-move is the Ctrl+V gesture and is "paste" (Ctrl+X only serializes).
71
+ * グリッドの全書込経路 (EditContext 2 枝の `source` の和 = `EditRejection["source"]`、
72
+ * dataAdapter.spec.ts が封印): エディタ確定 ("edit" — 待機中の insertDate / insertTime 書込も)、
73
+ * ADR-12 の書込ジェスチャ ("paste" / "fill" / "delete")、v0.8 のコマンド族 ("format" = 書式
74
+ * コマンド / handle 書込、"calc" = 乗除 / 符号反転の自己計算、"annotate" = ノート / コメント /
75
+ * リンク / 検証の書込)。cut-move は Ctrl+V のジェスチャで "paste" (Ctrl+X は直列化のみ)。
76
+ */
77
+ export type EditSource = "edit" | "paste" | "fill" | "delete" | "format" | "calc" | "annotate"
78
+
79
+ /**
80
+ * Write context of every non-paste applyEdits: `transaction` is the id the grid allocated for
81
+ * this gesture (strictly increasing per grid instance, one id per applyEdits call — the
82
+ * consumer's undo groups by it) and `source` names the write path. Spelled as an explicit
83
+ * literal union (api-extractor cannot roll up `Exclude<EditSource, "paste">`); its agreement
84
+ * with EditSource is sealed by the type-parity guard in dataAdapter.spec.ts.
85
+ * paste 以外の全 applyEdits の書込文脈: `transaction` はグリッドがこのジェスチャに採番した id
86
+ * (グリッド実体毎に厳密増加、applyEdits 呼び出し 1 回につき 1 id — consumer の Undo はこれで
87
+ * 束ねる)、`source` は書込経路。明示リテラル union で綴る (api-extractor は
88
+ * `Exclude<EditSource, "paste">` を d.ts ロールアップできない)。EditSource との一致は
89
+ * dataAdapter.spec.ts の型パリティガードが封印する。
90
+ */
91
+ export type PlainEditContext = { transaction: number; source: "edit" | "fill" | "delete" | "format" | "calc" | "annotate" }
92
+
93
+ /**
94
+ * Write context of a paste (incl. a cut-move, which is the Ctrl+V gesture): `sameSheet` is
95
+ * true when the clip was copied from THIS adapter and false for a cross-sheet paste — and for
96
+ * the source-side clear of a cross-sheet cut, which the grid delivers to the SOURCE adapter with
97
+ * the same transaction id and `conditionalFormats: []` (a cut never moves or removes conditional
98
+ * formats from the source sheet — deviation row). `conditionalFormats` are the rules pasted with
99
+ * the cells, translated to the target ranges (`[]` when none; a CF-only paste has `edits: []`
100
+ * and a non-empty list); the consumer mints each new rule's id — `sourceRuleId` is the id on the
101
+ * source sheet.
102
+ * 貼り付け (Ctrl+V のジェスチャである cut-move を含む) の書込文脈: `sameSheet` はクリップが
103
+ * **この**アダプタからコピーされたとき true、跨ぎ貼り付けは false — 跨ぎ cut の元シート側の消去
104
+ * も false で、グリッドは**元**アダプタへ同じ transaction id と `conditionalFormats: []` で届ける
105
+ * (cut は元シートの条件付き書式を移動も削除もしない — 逸脱行)。`conditionalFormats` はセルと共に
106
+ * 貼り付けた規則を対象レンジへ平行移動したもの (規則なしは `[]`。CF だけの貼り付けは `edits: []`
107
+ * + 非空リスト)。新規則の id は consumer が採番 — `sourceRuleId` は元シートでの id。
108
+ */
109
+ export type PasteEditContext = { transaction: number; source: "paste"; sameSheet: boolean; conditionalFormats: readonly ConditionalFormatRuleInput[] }
110
+
111
+ /**
112
+ * The write context of applyEdits — a discriminated union on `source`: exactly the paste arm
113
+ * carries `sameSheet` / `conditionalFormats`. An EMPTY transaction (no edits and no conditional
114
+ * formats) never reaches the adapter.
115
+ * applyEdits の書込文脈 — `source` で判別する union: paste 枝だけが `sameSheet` /
116
+ * `conditionalFormats` を持つ。**空**トランザクション (edits も条件付き書式も無し) はアダプタへ
117
+ * 届かない。
118
+ */
119
+ export type EditContext = PlainEditContext | PasteEditContext
120
+
121
+ /** Compile-time seal: the plain arm never carries "paste" (the discriminant stays unambiguous, so `context.source === "paste"` narrows to the paste arm alone). / コンパイル時封印: plain 枝は "paste" を持たない (判別子が曖昧にならず `context.source === "paste"` は paste 枝だけに絞れる)。 */
122
+ const plainContextNeverPaste: [Extract<PlainEditContext["source"], "paste">] extends [never] ? true : never = true
123
+ void plainContextNeverPaste
124
+
125
+ /**
126
+ * Context of a rejected applyEdits (delivered with the thrown error by onEditRejected). The
127
+ * write path that was refused — the editor commit ("edit") or one of the SpreadsheetOperation
128
+ * WRITE vocabulary ("paste" / "fill" / "delete" / "format" / "calc" / "annotate", the same
129
+ * names onOperationRejected uses for the same gestures: a cut-move is the Ctrl+V gesture and is
130
+ * "paste" in both callbacks, because Ctrl+X only serializes — "cut" and "copy" never write,
131
+ * "resize" is geometry) — and, for an in-cell editing commit, the FATE of the session, so a
132
+ * consumer can tell the user the truth: "kept" (an in-grid commit: Enter / Tab / click — the
133
+ * session stays open with the typed value and focus, Escape cancels it, or the value can be
134
+ * fixed) versus "discarded" (a grid-exit focusout: the grid discards the session as this
135
+ * rejection completes — once the callback returns the typed value is dropped and the sink
136
+ * re-armed WITHOUT moving focus: it stays on the element the user chose, or on the hidden input
137
+ * after a window blur (alt-tab — a null relatedTarget takes the same path) — advising "press
138
+ * Escape" here would be wrong, and an idle Escape would hand focus out of the grid per WCAG
139
+ * 2.1.2). Non-editing writes carry no session. A discriminated union: impossible combinations
140
+ * do not type-check.
141
+ * applyEdits 拒否の文脈 (onEditRejected が throw されたエラーと共に届ける)。拒否された書込経路
142
+ * (エディタ確定の "edit"、または onOperationRejected が同じジェスチャに使うのと同じ
143
+ * SpreadsheetOperation の**書込**語彙 "paste" / "fill" / "delete" / "format" / "calc" /
144
+ * "annotate" — cut-move は Ctrl+V のジェスチャで両コールバックとも "paste"。Ctrl+X は直列化のみで
145
+ * "cut" と "copy" は書かず、"resize" は幾何) と、セル内編集の確定なら**セッションの行方** —
146
+ * "kept" (グリッド内確定: Enter / Tab / クリック — 入力値とフォーカスごとセッション維持、Escape で
147
+ * 取消か修正可能) と "discarded" (グリッド外 focusout: この拒否の完了と同時にグリッドが
148
+ * セッションを破棄する — コールバックが返った時点で入力値は消え、シンクはフォーカスを動かさずに
149
+ * 再武装される: フォーカスはユーザーが選んだ要素、またはウィンドウ blur (alt-tab — relatedTarget
150
+ * null も同じ経路) なら隠し入力のまま。ここで「Escape を押せ」と案内するのは誤りで、待機中の
151
+ * Escape は WCAG 2.1.2 のフォーカス脱出になる) を消費者が区別できる。非編集の書込はセッションを
152
+ * 持たない。判別可能 union — あり得ない組合せは型が拒む。
153
+ */
154
+ // 明示リテラル union で宣言する (api-extractor は Extract<SpreadsheetOperation, ...> 等の lib 汎用型を
155
+ // d.ts ロールアップできず build が落ちる — ScrollToCellOptions と同じ制約)。非 edit の source 集合が
156
+ // SpreadsheetOperation の書込部分集合 (copy / cut / resize を除く) および EditSource と**集合として
157
+ // 一致**することは dataAdapter.spec.ts の型パリティガードが tsc で機械封印する (欠落・外来・非書込の
158
+ // 混入いずれも即赤)
159
+ export type EditRejection = { source: "edit"; editSession: "kept" | "discarded" } | { source: "paste" | "fill" | "delete" | "format" | "calc" | "annotate" }
160
+
161
+ /**
162
+ * The single data boundary between the grid and the consumer. Reads via getCellText (renderCell
163
+ * — a SpreadsheetProps prop, not an adapter member — customizes only the presentation of that
164
+ * text) plus the optional capability reads, writes via applyEdits as ONE transaction per
165
+ * gesture (editor commit / Delete / paste / cut-clear / fill / format / calc / annotate). This
166
+ * seam is where the consumer implements undo (inverse patches grouped by `transaction`). Every
167
+ * optional member is a CAPABILITY DECLARATION ("what cannot be read cannot be written"): the
168
+ * grid enables a feature only when the read it depends on is declared, and an undeclared
169
+ * capability reproduces the v0.7 behaviour exactly (never a value fallback). Declaring
170
+ * getConditionalFormats without getCellFormat is a RangeError at mount (a painted format is
171
+ * base ⊕ CF and there is no base without getCellFormat).
172
+ * グリッドと consumer の唯一のデータ境界。読みは getCellText (renderCell は SpreadsheetProps の
173
+ * prop であってアダプタメンバーではない — その文字列の表現のみを差し替える) と任意の能力読み、
174
+ * 書きは applyEdits のジェスチャ毎 1 トランザクション (編集確定 / Delete / 貼り付け / 切り取り
175
+ * 清算 / フィル / 書式 / 自己計算 / 注釈)。consumer の Undo 実装点 (`transaction` で束ねる逆
176
+ * パッチ)。任意メンバーは全て**能力宣言** (「読めないものは書けない」): グリッドは依存する読みが
177
+ * 宣言されたときだけ機能を有効化し、未宣言能力は v0.7 の振る舞いを厳密に再現する (値フォール
178
+ * バックではない)。getCellFormat なしの getConditionalFormats 宣言はマウント時 RangeError
179
+ * (塗り書式は base ⊕ CF で、getCellFormat なしに base は無い)。
180
+ */
181
+ export type SpreadsheetDataAdapter = {
182
+ rowCount: number
183
+ colCount: number
184
+ /** Canonical string used for TSV copy and the editor initial value. / コピーとエディタ初期値の正準文字列。 */
185
+ getCellText: (row: number, col: number) => string
186
+ /**
187
+ * Applies ONE transaction of per-cell patches. String-to-value interpretation is the
188
+ * consumer's job. Rejection = throw (grid state is preserved and onEditRejected fires with
189
+ * the {@link EditRejection} context). `context` carries the transaction id and the write path
190
+ * ({@link EditContext}); a paste additionally says whether the clip came from this adapter and
191
+ * which conditional formats travel with it. An empty transaction never arrives.
192
+ * セル単位パッチの 1 トランザクションを適用する。文字列→値の解釈は consumer の責務。拒否は
193
+ * throw (グリッド状態は保たれ、onEditRejected が {@link EditRejection} 文脈つきで通知される)。
194
+ * `context` はトランザクション id と書込経路 ({@link EditContext}) を運び、貼り付けはさらに
195
+ * クリップがこのアダプタ由来かと同伴する条件付き書式を伝える。空トランザクションは届かない。
196
+ * ❗ 契約: edits は**配列順に適用し、同一セルへの重複はキー単位で後勝ち (last-wins)** — 後の edit が
197
+ * 持つキーは前の edit の同じキーを上書きし、後の edit が持たないキーは前の edit の値のまま残る。
198
+ * cut の移動 (clear → 貼り付けの重ね積み) がこの順序意味論に依存する。逆パッチで Undo を実装する
199
+ * consumer は「最初に現れた edit の適用前値」を inverse として記録すること (参照実装は demo/edit)。
200
+ */
201
+ applyEdits: (edits: readonly CellEdit[], context: EditContext) => void
202
+ /**
203
+ * OPTIONAL capability: computes the Excel Ctrl+Arrow jump target from `from` in
204
+ * `direction`. Absence = the grid keeps its v0.1 grid-edge jump (a capability declaration,
205
+ * NOT a value fallback). The consumer implements Excel semantics (non-empty cell with a
206
+ * non-empty neighbor: the end of the contiguous non-empty run; otherwise: the next
207
+ * non-empty cell in the direction; none: the grid edge) because only the store can jump
208
+ * data-aware in O(occupied) instead of O(cells). The grid validates the return fully
209
+ * fail-fast BEFORE dispatch (non-integer / out-of-axis / orthogonal drift on the reset
210
+ * path all throw RangeError, propagated uncaught — reference implementation: demo/edit).
211
+ * 任意能力: Excel の Ctrl+Arrow ジャンプ先を計算する。未宣言 = v0.1 のグリッド端ジャンプ
212
+ * 維持 (能力宣言であり値フォールバックではない)。Excel 意味論 (非空 + 隣接非空 → 連続
213
+ * 非空の末端 / それ以外 → 方向の次の非空セル / 無ければグリッド端) は consumer 責務 —
214
+ * データを知るストアだけが O(占有セル) で跳べる。戻り値はディスパッチ前に完全 fail-fast
215
+ * 検証 (非整数 / 軸域外 / reset 経路の直交ドリフトは RangeError を未捕捉で伝播)。
216
+ */
217
+ findDataEdge?: (from: CellCoord, direction: SpreadsheetDirection) => CellCoord
218
+ /**
219
+ * OPTIONAL capability (v0.7, widened in v0.8): the cell's BASE display format — value
220
+ * format (undefined = raw text), horizontal alignment (undefined = auto BY THE ACTUAL VALUE:
221
+ * text that parses under the cell's numeric/date format — or as a strict decimal with no
222
+ * format — resolves right, labels resolve left), vertical alignment, wrap, colours, font
223
+ * traits, font family / size and per-side borders. Absence = the v0.1 plain-text,
224
+ * left-aligned rendering with no format command, no format paste aspect and no handle
225
+ * format write (a capability declaration, NOT a value fallback). The grid paints the
226
+ * PAINTED format (this base ⊕ conditional formats); the value format applies to the DISPLAY
227
+ * string only — editing keeps the raw getCellText (Excel semantics) and copy/TSV carry the raw
228
+ * value too (a DELIBERATE deviation from Excel, which copies the display — raw preserves the
229
+ * numeric round-trip). Autofit measures the FORMATTED display, and screen-reader
230
+ * announcements receive both the raw text and the display string. `renderCell` (a prop)
231
+ * takes precedence over the built-in VALUE format (raw text flows to the custom renderer),
232
+ * but the resolved ALIGNMENT and paint are cell properties that still apply under
233
+ * renderCell. Every format is fail-soft: a non-numeric value in a numeric format, an
234
+ * invalid date, an out-of-range Intl option or an unparseable colour renders / paints
235
+ * nothing for that key. Return a shared/interned CellFormat per column (or per cell object)
236
+ * to avoid per-cell allocation and to keep the paint intern stable; because return values
237
+ * (like getCellText) are only re-read on a re-slice, publish a live format change by bumping
238
+ * dataVersion (or swapping the adapter).
239
+ * 任意能力 (v0.7、v0.8 で拡張): セルの **base** 表示形式 — 値書式 (undefined = 素の文字列)・
240
+ * 水平揃え (undefined = **実際の値**による自動: 数値 / 日付書式の下で実際に解釈できる文字列と、
241
+ * 書式なしの厳密 10 進だけ右、ラベルは左)・垂直揃え・折返し・色・字体・フォント族 / サイズ・
242
+ * 辺毎の罫線。未宣言 = v0.1 の素テキスト左寄せ描画で、書式コマンドも書式の貼り付け側面も
243
+ * handle の書式書込も無い (能力宣言であり値フォールバックではない)。グリッドが塗るのは
244
+ * **塗り書式** (この base ⊕ 条件付き書式)。値書式は表示文字列にのみ適用し、編集は素の
245
+ * getCellText (Excel 意味論)、コピー / TSV も素の値を運ぶ (Excel は表示を写すため**意図的な
246
+ * 逸脱** — 素値は数値の往復を保つ)。autofit は**整形後の表示**を計測し、読み上げには素の
247
+ * text と表示文字列の両方を渡す。`renderCell` (prop) は組み込み「値書式」に優先する (素の
248
+ * text がカスタムレンダラーへ) が、解決済み「揃え」と塗りはセル固有属性として renderCell 下
249
+ * でも効く。全書式は fail-soft — 数値書式の非数値・無効な日付・範囲外 Intl オプション・解釈
250
+ * 不能な色はそのキーだけ何も描かない。列毎 (またはセル object 毎) に共有 CellFormat を返すと
251
+ * セル毎の確保を避け塗りの intern も安定する。戻り値は (getCellText 同様) 再スライス時のみ
252
+ * 再読込されるため、書式変更の反映は dataVersion 増分 (またはアダプタ差替) で行う。
253
+ */
254
+ getCellFormat?: (row: number, col: number) => CellFormat | undefined
255
+ /**
256
+ * OPTIONAL capability (v0.8): the cell's formula SOURCE — a string that MUST start with
257
+ * "=" (anything else is a RangeError raised by resolveEditText before dispatch, the
258
+ * findDataEdge precedent); undefined = the cell holds a plain value. Declared, the editor
259
+ * seeds with the formula instead of getCellText, copy serializes formulas as R1C1 so a
260
+ * paste re-anchors them, the self-calculation commands wrap formula cells, and the
261
+ * formula-reference pointing gestures (click / Arrow / drag while typing after "=") exist.
262
+ * Absence = none of that: "=" is ordinary text and Arrow keys commit exactly as v0.7.
263
+ * 任意能力 (v0.8): セルの数式**ソース** — 必ず "=" で始まる文字列 (それ以外は resolveEditText が
264
+ * ディスパッチ前に RangeError — findDataEdge の前例)。undefined = 素の値のセル。宣言時、エディタ
265
+ * は getCellText でなく数式で初期化し、コピーは数式を R1C1 で直列化して貼り付けが再アンカー
266
+ * し、自己計算コマンドは数式セルをラップし、数式参照ポインティング ("=" 後のクリック / Arrow /
267
+ * ドラッグ) が存在する。未宣言 = いずれも無し: "=" は普通の文字で、Arrow は v0.7 どおり確定。
268
+ */
269
+ getCellFormula?: (row: number, col: number) => string | undefined
270
+ /**
271
+ * OPTIONAL capability (v0.8): the cell's note text; undefined = no note. Declared, cells
272
+ * with a note carry the `data-aqsp-note` marker, the hover note tip and the note popover
273
+ * (Shift+F2) exist, and the note aspect of copy / paste / fill travels. Absence = no marker,
274
+ * no popover, the chord passes through, the aspect is stripped before tiling.
275
+ * 任意能力 (v0.8): セルのノート本文。undefined = ノート無し。宣言時、ノートつきセルは
276
+ * `data-aqsp-note` マーカーを持ち、ホバーの note tip と note ポップオーバー (Shift+F2) が存在し、
277
+ * コピー / 貼り付け / フィルの note 側面が運ばれる。未宣言 = マーカーもポップオーバーも無く、
278
+ * チョードは素通し、側面は tiling 前に除去。
279
+ */
280
+ getCellNote?: (row: number, col: number) => string | undefined
281
+ /**
282
+ * OPTIONAL capability (v0.8): the cell's comment thread on the READ side — every field
283
+ * stamped by the consumer (id / author / createdAt / text); undefined or an empty array = no
284
+ * thread. Declared, threaded cells carry `data-aqsp-comment`, the comment popover
285
+ * (Ctrl+Alt+M) exists and a reply writes the whole thread back through CellPatch.comments
286
+ * (existing ids kept, the new input id-less). Absence = no marker / popover, the chord passes
287
+ * through, the aspect is stripped before tiling (fill never copies comments).
288
+ * 任意能力 (v0.8): セルのコメントスレッド (**読み側** — id / author / createdAt / text は全て
289
+ * consumer が刻印済み)。undefined または空配列 = スレッド無し。宣言時、スレッドつきセルは
290
+ * `data-aqsp-comment` を持ち、コメントポップオーバー (Ctrl+Alt+M) が存在し、返信は
291
+ * CellPatch.comments でスレッド全体を書き戻す (既存 id 保持、新規入力は id なし)。未宣言 =
292
+ * マーカー / ポップオーバー無し、チョード素通し、側面は tiling 前に除去 (フィルはコメントを写さない)。
293
+ */
294
+ getCellComments?: (row: number, col: number) => readonly CellComment[] | undefined
295
+ /**
296
+ * OPTIONAL capability (v0.8): the cell's hyperlink URL; undefined = none. Every read is the
297
+ * trust boundary (ADR-10): the raw URL goes through `normalizeAdapterHyperlink`, so a URL
298
+ * outside the allowlist (http / https / mailto / tel) is treated as link-less at every seam —
299
+ * never navigated, never exported into a clip flavour, never re-committed through
300
+ * `CellPatch.hyperlink`. Declared, linked cells carry `data-aqsp-link`, Alt+Enter opens the
301
+ * link (onHyperlinkOpen or the link popover), Ctrl / Meta+click opens it too when
302
+ * onHyperlinkOpen is declared (spec 08 §11 D-53 — the selection stays frozen), Ctrl+K edits
303
+ * it, Delete clears the link together with the text (`hyperlink: null`), and the link aspect
304
+ * of copy / paste / fill travels. Absence = plain cells, the chords pass through, Delete
305
+ * writes text only.
306
+ * 任意能力 (v0.8): セルのハイパーリンク URL。undefined = 無し。全読み口が信頼境界 (ADR-10):
307
+ * 生 URL は `normalizeAdapterHyperlink` を通り、許可リスト外 (http / https / mailto / tel 以外)
308
+ * の URL は全シームでリンク無し扱い — 航行せず、クリップフレーバーへ書き出さず、
309
+ * `CellPatch.hyperlink` へ再確定しない。宣言時、リンクつきセルは `data-aqsp-link` を持ち、
310
+ * Alt+Enter がリンクを開き (onHyperlinkOpen または link ポップオーバー)、onHyperlinkOpen 宣言時は
311
+ * Ctrl / Meta+クリックでも開き (分冊 08 §11 D-53 — 選択は凍結)、Ctrl+K が編集し、Delete は
312
+ * テキストと共にリンクを消し (`hyperlink: null`)、コピー / 貼り付け / フィルの link 側面が
313
+ * 運ばれる。未宣言 = 素のセル、チョード素通し、Delete はテキストだけ書く。
314
+ */
315
+ getCellHyperlink?: (row: number, col: number) => string | undefined
316
+ /**
317
+ * OPTIONAL capability (v0.8): the cell's data-validation rule; undefined = none. Declared,
318
+ * an editor commit is validated (strict rules reject and fire onValidationRejected,
319
+ * non-strict rules only notify), visibly invalid cells carry `data-aqsp-invalid` (custom
320
+ * formulas are never evaluated at paint), list rules get the dropdown (Alt+ArrowDown) and
321
+ * the validation aspect of copy / paste / fill travels. Return a STABLE rule object per cell
322
+ * (identity) so the list-value intern hits. Absence = every commit passes as v0.7.
323
+ * 任意能力 (v0.8): セルのデータ検証規則。undefined = 無し。宣言時、エディタ確定は検証され
324
+ * (strict は拒否して onValidationRejected、非 strict は通知のみ)、目に見えて無効なセルは
325
+ * `data-aqsp-invalid` を持ち (custom 数式は描画時に評価しない)、list 規則はドロップダウン
326
+ * (Alt+ArrowDown) を持ち、コピー / 貼り付け / フィルの validation 側面が運ばれる。セル毎に
327
+ * **安定した**規則 object (identity) を返すと list 値の intern が当たる。未宣言 = 全確定が
328
+ * v0.7 どおり通る。
329
+ */
330
+ getCellValidation?: (row: number, col: number) => DataValidationRule | undefined
331
+ /**
332
+ * OPTIONAL capability (v0.8): the sheet-level conditional-format rules in PRIORITY order
333
+ * (array order = evaluation order; an earlier true rule wins per property, `stopIfTrue` ends
334
+ * the walk). Requires getCellFormat (declared alone ⇒ RangeError at mount). Return a stable
335
+ * array identity — the grid rebuilds its bucket index per (rules identity, renderVersion), so
336
+ * publish a rule change by bumping dataVersion. Declared, the CF aspect of copy / paste
337
+ * travels (pasted rules arrive as PasteEditContext.conditionalFormats). Absence = base
338
+ * formats paint as-is.
339
+ * 任意能力 (v0.8): シート水準の条件付き書式規則を**優先順** (配列順 = 評価順。先の真規則が
340
+ * プロパティ毎に勝ち、`stopIfTrue` で打ち切り) で返す。getCellFormat を要する (単独宣言は
341
+ * マウント時 RangeError)。配列 identity を安定させること — グリッドは (規則 identity,
342
+ * renderVersion) 毎にバケット索引を再構築するため、規則変更は dataVersion 増分で公開する。
343
+ * 宣言時、コピー / 貼り付けの CF 側面が運ばれる (貼り付けた規則は
344
+ * PasteEditContext.conditionalFormats で届く)。未宣言 = base 書式をそのまま塗る。
345
+ */
346
+ getConditionalFormats?: () => readonly ConditionalFormatRule[]
347
+ /**
348
+ * OPTIONAL capability (v0.8): the numeric minimum / maximum over a range, for colorScale
349
+ * rules whose end points omit `value`; null = the range holds no numeric value. Memoised by
350
+ * the grid per (rule id, renderVersion). Absence = such rules are skipped whole (rules with
351
+ * explicit values still paint).
352
+ * 任意能力 (v0.8): レンジ内の数値の最小 / 最大 — `value` 省略の端点を持つ colorScale 規則用。
353
+ * null = 数値が無い。グリッドが (規則 id, renderVersion) 毎に memo する。未宣言 = そのような
354
+ * 規則は丸ごとスキップ (値明示の規則は塗る)。
355
+ */
356
+ getRangeExtrema?: (range: CellRange) => { min: number; max: number } | null
357
+ /**
358
+ * OPTIONAL capability (v0.8): evaluates a formula string (passed VERBATIM, "=" included) in
359
+ * the context of `cell`; truth for a customFormula rule / custom validation = `true` or a
360
+ * finite non-zero number. Memoised by the grid per (rule id, row, col, renderVersion) for
361
+ * conditional formats; custom validation calls it at commit only (never at paint). Absence =
362
+ * customFormula rules are skipped and custom validation accepts.
363
+ * 任意能力 (v0.8): 数式文字列 ("=" を含め**そのまま**渡す) を `cell` の文脈で評価する。
364
+ * customFormula 規則 / custom 検証の真 = `true` または有限非 0 数。条件付き書式ではグリッドが
365
+ * (規則 id, row, col, renderVersion) 毎に memo し、custom 検証は確定時だけ呼ぶ (描画時は呼ばない)。
366
+ * 未宣言 = customFormula 規則はスキップ、custom 検証は通す。
367
+ */
368
+ evaluateFormula?: (formula: string, cell: CellCoord) => string | number | boolean | null
369
+ }
370
+
371
+ /**
372
+ * The ONE alignment judgment every alignment consumer shares (cell props, the in-cell editor's
373
+ * text-align, and any future surface), over the PAINTED format (base ⊕ conditional formats)
374
+ * rather than the adapter: an explicit `align` wins outright; otherwise the alignment is auto BY
375
+ * THE ACTUAL VALUE, so the text is read through `readText` — the eighth read of a cell, shared
376
+ * with CF conditions and the invalid marker via {@link lazyCellText} — only then. The
377
+ * getCellFormat capability branch (undeclared = the v0.1 identity: no alignment attribute at
378
+ * all, which is NOT the same as "left") lives at the caller: a painted format has no adapter to
379
+ * inspect, so the caller skips this judgment entirely when the capability is absent.
380
+ * 揃えの全消費者 (セル属性・セル内エディタの text-align・将来の面) が共有する唯一の揃え判定 —
381
+ * アダプタでなく**塗り書式** (base ⊕ 条件付き書式) に対して: 明示 `align` は無条件に勝ち、
382
+ * それ以外は**実際の値**による自動判定なので、そのときだけ `readText` で text を読む (セル
383
+ * 8 読み目 — {@link lazyCellText} で CF 条件 / 無効マーカーと共有)。getCellFormat の能力分岐
384
+ * (未宣言 = v0.1 恒等: 揃え属性そのものを出さない — 「左」とは別物) は呼び手側にある: 塗り書式
385
+ * には調べるべきアダプタが無いので、未宣言なら呼び手がこの判定自体を省く。
386
+ */
387
+ export const resolveCellAlign = (painted: CellFormat | undefined, readText: () => string): CellAlign => {
388
+ // 塗り書式なし = 揃えも値書式も未指定 → 実際の値で自動判定 (8 読み目)
389
+ if (painted === undefined) {
390
+ return resolveAlign(undefined, undefined, readText())
391
+ }
392
+ // 明示 align があれば text 読み取り不要 (自動判定だけが実際の値を必要とする)
393
+ return resolveAlign(painted.align, painted.value, painted.align === undefined ? readText() : "")
394
+ }
395
+
396
+ /**
397
+ * Lazy once-only text thunk — the single construction point of a cell's eighth read, shared by
398
+ * alignment (align unset), conditional-format conditions and the visible-invalid check so the
399
+ * text is read at most ONCE per cell per slice. Nothing is read at construction; the first call
400
+ * reads getCellText and every later call returns that same string (an empty "" is a valid,
401
+ * cached value — the thunk never re-reads).
402
+ * 遅延 1 回読みの text thunk — セル 8 読み目の単一構築点。揃え (align 未設定)・条件付き書式の
403
+ * 条件・無効表示判定が共有し、text はスライス毎セル毎に最大 **1 回**しか読まれない。構築時には
404
+ * 読まず、初回呼び出しで getCellText を読み、以後は同じ文字列を返す (空文字 "" も正当なキャッシュ
405
+ * 値 — 再読はしない)。
406
+ */
407
+ export const lazyCellText = (adapter: SpreadsheetDataAdapter, row: number, col: number): (() => string) => {
408
+ // 未読の番兵は null (空文字 "" は正当な値なので番兵に使わない)
409
+ let text: string | null = null
410
+ return () => {
411
+ if (text === null) {
412
+ text = adapter.getCellText(row, col)
413
+ }
414
+ return text
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Editor seed text: the formula source when the getCellFormula capability is declared and the
420
+ * cell holds one (it must start with "=" — anything else, including a non-string, is a
421
+ * RangeError before dispatch, the findDataEdge precedent), else the canonical text. A capability
422
+ * branch, not a value fallback: without the capability getCellFormula is never consulted, and
423
+ * with a formula getCellText is not read.
424
+ * エディタ初期文字列: getCellFormula 能力が宣言され、セルに数式があればその数式ソース ("=" 始まり
425
+ * 必須 — それ以外は非文字列を含めディスパッチ前に RangeError、findDataEdge の前例)、それ以外は
426
+ * 正準文字列。能力分岐であって値フォールバックではない: 未宣言なら getCellFormula には触れず、
427
+ * 数式があれば getCellText は読まない。
428
+ */
429
+ export const resolveEditText = (adapter: SpreadsheetDataAdapter, row: number, col: number): { text: string; isFormula: boolean } => {
430
+ // 能力分岐: getCellFormula 未宣言なら「数式」という概念自体が無い (v0.7 恒等)
431
+ if (adapter.getCellFormula !== undefined) {
432
+ const formula = adapter.getCellFormula(row, col)
433
+ if (formula !== undefined) {
434
+ // "=" 始まり以外は consumer のバグ — findDataEdge と同じくディスパッチ前に fail-fast
435
+ if (typeof formula !== "string" || !formula.startsWith("=")) {
436
+ throw new RangeError(`[spreadsheet] getCellFormula(${row}, ${col}) must return a string starting with "=", received ${JSON.stringify(formula)}`)
437
+ }
438
+ return { text: formula, isFormula: true }
439
+ }
440
+ }
441
+ return { text: adapter.getCellText(row, col), isFormula: false }
442
+ }
443
+
444
+ /**
445
+ * The declared aspect capabilities of an adapter — one boolean per rich-cell aspect, derived
446
+ * purely from which optional reads are declared ("what cannot be read cannot be written"):
447
+ * `format` = getCellFormat, `note` = getCellNote, `comments` = getCellComments, `hyperlink` =
448
+ * getCellHyperlink, `validation` = getCellValidation, `cf` = getConditionalFormats, `formula` =
449
+ * getCellFormula. findDataEdge is navigation, not an aspect.
450
+ * アダプタの宣言済み側面能力 — リッチセル側面毎に 1 真偽値、どの任意読みが宣言されているかだけ
451
+ * から導く (「読めないものは書けない」): `format` = getCellFormat、`note` = getCellNote、
452
+ * `comments` = getCellComments、`hyperlink` = getCellHyperlink、`validation` = getCellValidation、
453
+ * `cf` = getConditionalFormats、`formula` = getCellFormula。findDataEdge は航行であり側面ではない。
454
+ */
455
+ export type AspectCapabilities = { format: boolean; note: boolean; comments: boolean; hyperlink: boolean; validation: boolean; cf: boolean; formula: boolean }
456
+
457
+ /**
458
+ * Declared aspect capabilities (the write gate's single reader): the paste path strips every
459
+ * undeclared aspect before tiling, and commands and popovers are available only for declared
460
+ * aspects (their default chords pass through otherwise). A live read of the adapter (no memo)
461
+ * so the judgment is always the adapter's current declaration. The paint path (getCellProps)
462
+ * makes the same live judgment through direct member reads and never builds this snapshot —
463
+ * a per-cell allocation has no place on the paint hot path.
464
+ * 宣言済み側面能力 (書込ゲートの単一読み): 貼り付け経路は tiling 前に未宣言側面を除去し、
465
+ * コマンド / ポップオーバーは宣言済み側面でだけ利用可能 (それ以外は既定チョードが素通し)。
466
+ * アダプタの生読み (memo なし) なので判定は常にアダプタの現在の宣言。塗り経路 (getCellProps) は
467
+ * 同じ生判定をメンバー直接読みで行いこのスナップショットを組まない — セル毎割当は塗りの
468
+ * hot path に置かない。
469
+ */
470
+ export const aspectCapabilities = (adapter: SpreadsheetDataAdapter): AspectCapabilities => ({
471
+ format: adapter.getCellFormat !== undefined,
472
+ note: adapter.getCellNote !== undefined,
473
+ comments: adapter.getCellComments !== undefined,
474
+ hyperlink: adapter.getCellHyperlink !== undefined,
475
+ validation: adapter.getCellValidation !== undefined,
476
+ cf: adapter.getConditionalFormats !== undefined,
477
+ formula: adapter.getCellFormula !== undefined,
478
+ })