@aiquants/spreadsheet 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README.md +74 -5
- package/dist/index.cjs +14 -14
- package/dist/index.d.cts +129 -7
- package/dist/index.d.ts +129 -7
- package/dist/index.js +4318 -4140
- package/dist/styles/spreadsheet.standalone.css +1 -1
- package/package.json +5 -5
- package/src/Spreadsheet.tsx +152 -12
- package/src/clipboard/tiling.ts +106 -16
- package/src/clipboard/useClipboard.ts +120 -7
- package/src/commands/useCommands.ts +226 -42
- package/src/editor/useCellEditor.ts +62 -13
- package/src/grid/GridBody.tsx +29 -3
- package/src/index.ts +5 -1
- package/src/keyboard/useGridKeyboard.ts +24 -3
- package/src/model/dataAdapter.ts +121 -2
- package/src/model/labels.ts +8 -2
- package/src/overlay/useFillHandle.ts +35 -2
- package/src/popover/CommentPopover.tsx +14 -2
- package/src/popover/LinkPopover.tsx +15 -3
- package/src/popover/NotePopover.tsx +24 -6
- package/src/popover/usePopovers.ts +89 -11
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@aiquants/spreadsheet` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.10.0 (2026-09-22)
|
|
6
|
+
|
|
7
|
+
v0.10 は読み取り専用セル (isCellReadOnly) — 統治文書は
|
|
8
|
+
[v0.10 実装計画](<./docs/plans/2026.09.18 [AI] spreadsheet-v0.10-readonly-cells-plan.md>) (以下「v0.10 計画 §N」)。
|
|
9
|
+
同乗スコープはタップサークル素通し (H2) と貼り付けフィードバック (onPasteResolved)。
|
|
10
|
+
|
|
11
|
+
- **新機能 — 読み取り専用セル (ADR-27..31)**: `SpreadsheetDataAdapter.isCellReadOnly?: (row, col) => boolean`
|
|
12
|
+
(11 番目の optional・非 aspect 能力)。true のセルは編集モードに入れず (全 6 入口 + OSK 招待前置 G2)、
|
|
13
|
+
開いているセッションの確定はライブ再検査され (G16 — keep / focusout discard の帰結つき)、対象に読み取り
|
|
14
|
+
専用セルを含む書込操作 (削除 / 貼り付け / cut 二重壁 / フィル (対角 L 分解) / 書式 / 計算 / 注釈) は
|
|
15
|
+
adapter 到達前に**操作全体**が拒否される (G1..G16 — per-cell skip なし、Excel 同等)。拒否は新設の単一
|
|
16
|
+
チャネル `onReadOnlyRejected(rejection: ReadOnlyRejection)` (edit 変種は必須 `editSession: "none" | "kept" |
|
|
17
|
+
"discarded"`、operation 変種は明示 7 リテラル op + overlap-naive `cellCount`) と
|
|
18
|
+
`formatReadOnlyAnnouncement` (live region 経由・デバウンス継承) で通知 — `onEditRejected` /
|
|
19
|
+
`onOperationRejected` へは流用しない (チャネル純度)。塗りセルは `aria-readonly="true"` +
|
|
20
|
+
`data-aqsp-readonly=""` を帯同 (パッケージ CSS なし — ADR-31)、announce payload / popover context は宣言
|
|
21
|
+
ゲートキー `readOnly` / `cellReadOnly` を得る (既定 note / comment / link コンポーネントは確定操作を
|
|
22
|
+
disabled 化 = 閲覧者モード、注釈チョードは内容有で viewer 開通・内容無で消費拒否 — G15 両腕)。能力未宣言
|
|
23
|
+
= 従来挙動とバイト恒等 (能力分岐 — フォールバックではない)。デモは新設 `/readonly` ルート。
|
|
24
|
+
- **新機能 — `tapScrollCircleOptions` プロップ新設 (位置・有効 / 無効の調整口)**: エンジン
|
|
25
|
+
`scrollBarOptions.tapScrollCircleOptions` へのエンジン検証素通し (3 ファイル鎖 — 検証 SSOT はエンジンの
|
|
26
|
+
`resolveTapScrollCircleOptions`、未指定 = エンジン既定)。barrel は `ScrollBarTapCircleOptions` を型のみ
|
|
27
|
+
再 export (v0.10 計画 §2)。
|
|
28
|
+
- **新機能 — `onPasteResolved(outcome, channel)` (貼り付け結果の要約)**: menu チャネルは `paste(options)` の
|
|
29
|
+
resolve 値の 1:1 鏡 (読み拒否・書込時再ゲート込み)、event チャネルは貼り付けパイプライン到達ゲスチャ毎
|
|
30
|
+
1 回 (画像のみクリップボードの `"empty"` 込み)。ドラッグ凍結消費と `clipboardData` null の到達前 2 分岐は
|
|
31
|
+
意図的無言、空グリッドはリスナ不装着 (構造的到達不能)。原因特定系 callback (`onClipboardUnavailable` /
|
|
32
|
+
`onOperationRejected` / `onReadOnlyRejected`) が先、outcome は後追いの要約 (v0.10 計画 §3)。
|
|
33
|
+
- **DOM 属性 census 追補 (分冊 05 §10 末尾追加)**: `data-aqsp-readonly` の 1 種を追加 — `data-aqsp-*` 属性は 79 → 80
|
|
34
|
+
(新規 1 — 分冊 05 §10 / 分冊 06 §4)。リテラル census は 80 種 (dataset 直書きの同値抑止署名キー 4 種は
|
|
35
|
+
grep に掛からず、DOM に現れる属性名の総数は 84)。
|
|
36
|
+
- **消費面 — 統合 2 軸タップサークル (上流エンジン v3.6.0)**: グリッドのタップサークルが 1 つに統合 —
|
|
37
|
+
縦横バー別の 2 サークルは消滅し、グリッド所有の 1 サークル (`[data-aqvs-grid-tap-circle]`) が 1 本の
|
|
38
|
+
ドラッグベクトルで両軸を連続スクロールする。位置はコーナーアンカーのエンジン ADR-30 の 3 アーム配置則
|
|
39
|
+
(root 寸・scrollbar 幅・size・オフセットのみに依存 — 帯幅非依存で凍結トグル不変、W_F = 0 の
|
|
40
|
+
クリップ危険クラスは消滅)。ブラウザ実証は新設 `tests/e2e/tap-circle.spec.ts` (12 テスト — エンジン 3.6 設計 §6.3:
|
|
41
|
+
1 サークル / 3 アーム則 ±1 px + 小ビューポートのアーム Q / 対角・純軸ドラッグ / キャンセル 3 系 /
|
|
42
|
+
末尾帯フロートとディスク外クリック到達 (大 W_F 先頭凍結込み) / ルートスワップ再マウント)。
|
|
43
|
+
- **公開面 — 型のみ再 export (ADR-33 縮小形)**: `TapScrollCircleRenderProps` を `@aiquants/virtualscroll`
|
|
44
|
+
から型のみ再 export — consumer がエンジンを直接 import せずに `renderVisual` を型付ける options
|
|
45
|
+
型付け面 (値 `tapScrollCircleSampleVisual` は意図的に非再 export)。v0.10 の追加 (`ReadOnlyRejection` +
|
|
46
|
+
`ScrollBarTapCircleOptions` の型 2) と併せ、公開面は `index.ts` の値 39 + 型 97 = 136 export
|
|
47
|
+
(`src/index.spec.ts` の census がコンパイル証明つきでピン)。
|
|
48
|
+
- **検証 — 走行台帳 (census の錨は本未リリース節)**: 単体 / 統合 **78 ファイル 3,806 テスト** 全緑 —
|
|
49
|
+
0.9.0 出荷時の 74 ファイル 3,631 に v0.10 進行分 +175 (新規 4 ファイル 52 = `readOnly` 28 +
|
|
50
|
+
`readOnlyCensus` 8 (2026-09-22 の側 (iv) raise 漏斗 census 追補 +1 込み — 計画 §6.4 の raise 単一性の機械封印) +
|
|
51
|
+
`tapCirclePassthrough` 6 + `readonlyStore` 10 (`/readonly` デモストアの実書込 — 宣言済み全側面の編集可対照、計画 §6.13)、既存 16 ファイル +116 —
|
|
52
|
+
2026-09-22 の登記過少是正 (`labels` 実測 22 / `NotePopover` 実測 13 — 計画 §4.1 の per-file 台帳規律) と分冊 07 §2 台帳 literal census 追補 (`hxCensus` +1 — 登記過少をソース錨で red にする常設ゲート) と
|
|
53
|
+
収束ラウンドの環境特性登記ピン (`htmlInterop` +1 — jsdom `DOMParser` の入れ子深さ二次 parse 壁時計は ADR-12 の保護の外、分冊 03 §3.3 / §7.1-5 の登記の機械ピン) 込み、是正 +7 は v0.9 step 5e 登記の
|
|
54
|
+
過少: `SelectionOverlay` 実測 61 (旧登記 54)。3,631 + 175 = 3,806、ファイルは 74 → 78) 込み。
|
|
55
|
+
E2E は **23 ファイル 263 テスト** 全緑 (2026-09-18 — 読み取り専用セルの新設 `tests/e2e/readonly.spec.ts` 14 +
|
|
56
|
+
タップサークルの新設 `tests/e2e/tap-circle.spec.ts` 12 + trailing-freeze 過少登記の是正 +1 で、0.9.0 出荷時の
|
|
57
|
+
21 ファイル 236 から 23 ファイルへ)。出荷済み 0.9.0 節は出荷時リリースノートに凍結 (`hxCensus.spec.ts` の本文 SHA-256 ピン — 0.8.0 凍結の鏡)。
|
|
58
|
+
|
|
5
59
|
## 0.9.0 (2026-09-18)
|
|
6
60
|
|
|
7
61
|
v0.9 は末尾凍結 (trailing freeze) と強い凍結分割線 (freeze divider) — 統治文書は
|
package/README.md
CHANGED
|
@@ -22,6 +22,10 @@ deep-reach proof route.
|
|
|
22
22
|
- 📌 **Trailing freeze & strong freeze dividers** (v0.9): `frozenTrailingColumns` / `frozenTrailingRows` pin the **last** N columns / rows (a fixed totals row is the canonical use) as static right / bottom bands over the **same data** (count-based, Glide-style — no extra-row API), with three-band headers,
|
|
23
23
|
a {main, frozen, scroll, trailing}² clipped selection overlay, three-way partitioned hit-testing / ensure-visible, near-edge resize strips with pinned-right semantics, and band-side unhide markers; **every** freeze boundary (leading and trailing, both axes) draws a strong Excel / Sheets-style
|
|
24
24
|
divider over the scroll band's edge pixels — 2px `#9ca3af` by default, consumer-tunable through the CSS variables `--aqsp-divider-color` / `--aqsp-divider-width` (no prop), `pointer-events: none` — see "Trailing freeze and the freeze divider"
|
|
25
|
+
- 🔒 **Read-only cells as a capability** (v0.10 — ADR-27..31): one optional adapter predicate `isCellReadOnly(row, col)` locks cells — no edit session can open on them (all six entries, the touch on-screen-keyboard invitation included), an open session's commit is live re-checked,
|
|
26
|
+
and every write whose target set touches a read-only cell (Delete / paste / cut both walls / fill with diagonal L-legs decomposed / format / self-calc / annotations) refuses the **whole operation** before the adapter is reached (Excel-style, no per-cell skip); painted cells carry
|
|
27
|
+
`aria-readonly="true"` + `data-aqsp-readonly` (style it yourself — no package CSS), refusals speak through the single sealed `onReadOnlyRejected` (+ opt-in `formatReadOnlyAnnouncement`), and copying or viewing notes / comments / links stays legal (content-aware chords,
|
|
28
|
+
viewer-mode popovers with the commit affordance disabled); undeclared ⇒ byte-identical v0.9 behavior
|
|
25
29
|
- 🎨 **Rich cells as capability declarations** (v0.8 — ADR-13): eight optional adapter reads (`getCellFormat` / `getCellFormula` / `getCellNote` / `getCellComments` / `getCellHyperlink` / `getCellValidation` / `getConditionalFormats` / `getRangeExtrema` + `evaluateFormula`) unlock the full cell
|
|
26
30
|
format model (value format, alignment, vertical alignment, wrap, background / font colour, bold / italic / underline / strikethrough, font family / size in pt, per-side borders with Excel-style ownership folding), note / comment / link indicators, paint-time validation marks and sheet-level
|
|
27
31
|
conditional formats (cell value / text / empty / colour scale / custom formula, 2-D bucket index, first-match-per-property precedence) — every undeclared aspect keeps the v0.7 DOM byte-identical, and writes go through a patch-typed `applyEdits(edits, context)`
|
|
@@ -110,6 +114,7 @@ The host element must have a resolvable height — the grid fills its parent.
|
|
|
110
114
|
| `getCellValidation(row, col)` | **Optional capability** (v0.8) — the cell's `DataValidationRule` (`list` / `number` / `textLength` / `date` / `text` / `checkbox` / `custom`, each with `strict?` / `inputMessage?` / `errorMessage?`) or `undefined`; return a **stable rule object per cell** (list Sets are interned by identity). A commit that changes the text is validated (`validateCellText`): a non-strict rule accepts the value and paints the `data-aqsp-invalid` mark, a `strict: true` rule refuses the commit (`onValidationRejected` + the message popover — the editing-column Ctrl+Enter `fillSelection` runs the same gate on every selected cell's own rule before its single `applyEdits`), a blank is always valid, a half-written rule (unparseable bound, missing `b`) is inert, and `custom` rules are evaluated only at commit through `evaluateFormula` (never at paint). Unlocks the list picker (`Alt+ArrowDown`), the input-message popover on active-cell change, `handle.setValidation` and the validation paste aspect. |
|
|
111
115
|
| `getConditionalFormats()` | **Optional capability** (v0.8) — the sheet's `ConditionalFormatRule[]` in priority order (`{ id, ranges, condition, style, stopIfTrue? }`; conditions `cellValue` / `text` / `empty` / `notEmpty` / `colorScale` / `customFormula`; style = background / color / bold / italic / underline / strikethrough). **Requires `getCellFormat`** (declaring it alone throws a `RangeError` at mount). Keep the array identity stable and publish changes through `dataVersion`; the grid rebuilds a lazy 2-D 256 × 256 bucket index per (rules identity, render version) and, inside each bucket, a lazily materialised per-column slice, so per-cell cost is O(rules intersecting the cell's column within its 256-row band) — for whole-column rules exactly the rules on that column, whatever the sheet width (a sheet of ≤ 256 columns has a single column band, so the bucket alone would be the whole rule set). Every true rule contributes, an earlier rule wins per property, `stopIfTrue` ends the walk. Pasted rules arrive as `context.conditionalFormats` (`ConditionalFormatRuleInput` with `sourceRuleId` — you mint the new id). |
|
|
112
116
|
| `getRangeExtrema(range)` / `evaluateFormula(formula, cell)` | **Optional capabilities** (v0.8) consumed by the rules above: `getRangeExtrema` returns `{ min, max }` (or `null` = no numeric value) for a `colorScale` point without `value` — called once per rule range per render version and memoised; `evaluateFormula` receives a `customFormula` verbatim (no reference shifting — you own relative semantics) and a `custom` validation formula at commit, truth = `true` or a finite non-zero number. Undeclared ⇒ the rules that need them are skipped (nothing is substituted). |
|
|
117
|
+
| `isCellReadOnly(row, col)` | **Optional capability** (v0.10 — the 11th optional member, a non-aspect capability). `true` marks the cell read-only: no edit session can open on it (all six entries, including the touch on-screen-keyboard invitation, refuse **before** any side effect), an already-open session's commit is live re-checked, and every write producer whose target set contains one read-only cell — Delete, paste, cut (both the Ctrl+X arming and the move-time source re-check), fill (diagonal L-legs decomposed exactly), format, self-calc, annotations — refuses the **whole operation** before the adapter is reached (Excel-style, never a per-cell skip). Painted read-only cells carry `aria-readonly="true"` + `data-aqsp-readonly` (no package CSS — style the attribute yourself); refusals fire the single sealed callback `onReadOnlyRejected` (+ opt-in `formatReadOnlyAnnouncement` for the live region) and never reuse `onEditRejected` / `onOperationRejected`. Reading stays legal: copying from read-only cells, filling **from** a read-only source into editable cells, and viewing existing notes / comments / links (content-aware chords open viewer-mode popovers with the commit affordance disabled). The predicate is a **live read** — no memoization; publish changes through `dataVersion` (or an adapter swap) like every other read. It is called with in-grid coordinates only, on gesture paths and for painted cells, so budget O(1) per call without allocating; a throw propagates uncaught (fail-fast, never swallowed). Undeclared ⇒ every cell is editable and the DOM / callbacks are byte-identical to v0.9 (a capability branch, not a fallback). Reference: the `/readonly` demo. |
|
|
113
118
|
|
|
114
119
|
- **Adapter identity is the sheet identity.** Replacing the adapter object means "sheet switch": selection resets to A1, any in-flight drag, editing session and open popover are terminated (value-discarding, exactly like Excel switching sheets — nothing is ever committed across a swap), an armed
|
|
115
120
|
**copy** is discarded, and an armed **cut** is kept as a *foreign* clip (`onClipChange({ kind: "cut", range, foreign: true })`, its marquee hidden) so the next `Ctrl+V` on the new sheet is a **cross-sheet move** — the target sheet is written first, then the source sheet is cleared under the same
|
|
@@ -148,8 +153,64 @@ gesture: `"edit"` (cell commit, list pick, `insertDate` / `insertTime` while idl
|
|
|
148
153
|
`sameSheet` = the armed payload came from this adapter, and `conditionalFormats` = the pasted rules translated per tile, `[]` when
|
|
149
154
|
none), `"fill"` (fill handle, `fillDown` / `fillRight` / `fillSelection`), `"delete"` (Delete / Backspace), `"format"` (format commands,
|
|
150
155
|
`applyFormat` / `setBorders`), `"calc"` (`multiplyBy100` … `signReversal`), `"annotate"` (note / comment / link / validation writes). The
|
|
151
|
-
`/styles` demo store (`demo/src/pages/styles/richStore.ts`) is the reference for a patch-applying store that mints comment ids and
|
|
152
|
-
folds a transaction's inverse patches into one Undo entry.
|
|
156
|
+
`/styles` demo store (`demo/src/pages/styles/richStore.ts` — source checkout only, not in the npm tarball) is the reference for a patch-applying store that mints comment ids and
|
|
157
|
+
folds a transaction's inverse patches into one Undo entry. A minimal inline version of that store (the shape most consumers start from):
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
// Map-backed store + inverse-patch undo grouped by EditContext.transaction (package owns no history — ADR-6).
|
|
161
|
+
type Inverse = { edits: CellEdit[] } // inverse patches of ONE transaction (first-seen pre-values)
|
|
162
|
+
const cells = new Map<string, string>() // "row:col" -> text (undefined = default)
|
|
163
|
+
const undoStack: Inverse[] = []
|
|
164
|
+
const redoStack: Inverse[] = []
|
|
165
|
+
let dataVersion = 0 // pass to <Spreadsheet dataVersion={dataVersion}> and bump per external write
|
|
166
|
+
|
|
167
|
+
const keyOf = (row: number, col: number) => `${row}:${col}`
|
|
168
|
+
const textOf = (row: number, col: number) => cells.get(keyOf(row, col)) ?? ""
|
|
169
|
+
|
|
170
|
+
const adapter: SpreadsheetDataAdapter = {
|
|
171
|
+
rowCount: 200,
|
|
172
|
+
colCount: 26,
|
|
173
|
+
getCellText: textOf,
|
|
174
|
+
applyEdits: (edits, context) => {
|
|
175
|
+
// one applyEdits call = one transaction = one undo entry; record the FIRST-seen pre-value per cell
|
|
176
|
+
// (edits apply in array order with last-wins per key — the inverse must restore the value before the
|
|
177
|
+
// first edit of the transaction, which is what makes a cut-move's clear+write fold correctly)
|
|
178
|
+
const inverse: Inverse = { edits: [] }
|
|
179
|
+
const seen = new Set<string>()
|
|
180
|
+
for (const edit of edits) {
|
|
181
|
+
const key = keyOf(edit.row, edit.col)
|
|
182
|
+
if (!seen.has(key)) {
|
|
183
|
+
seen.add(key)
|
|
184
|
+
inverse.edits.push({ row: edit.row, col: edit.col, text: textOf(edit.row, edit.col) })
|
|
185
|
+
}
|
|
186
|
+
if (edit.text !== undefined) {
|
|
187
|
+
cells.set(key, edit.text)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// context.transaction groups the two applyEdits calls of a cross-sheet cut-move into ONE undo entry
|
|
191
|
+
// on the consumer side (both calls carry the same id — see the facts below); a single-sheet gesture
|
|
192
|
+
// is always one call, so pushing per call and merging equal ids both work.
|
|
193
|
+
undoStack.push(inverse)
|
|
194
|
+
redoStack.length = 0
|
|
195
|
+
void context
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const undo = () => {
|
|
200
|
+
const entry = undoStack.pop()
|
|
201
|
+
if (entry === undefined) return
|
|
202
|
+
const redo: Inverse = { edits: entry.edits.map((e) => ({ row: e.row, col: e.col, text: textOf(e.row, e.col) })) }
|
|
203
|
+
for (const e of entry.edits) cells.set(keyOf(e.row, e.col), e.text ?? "")
|
|
204
|
+
redoStack.push(redo)
|
|
205
|
+
dataVersion += 1 // publish the external write
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Three facts already registered above interlock with this store: a **rejected** `applyEdits` call consumes its transaction id too
|
|
210
|
+
(ids name attempts, not successes); a **cross-sheet cut-move** is two `applyEdits` calls under ONE id (fold them into one undo
|
|
211
|
+
entry by the id); and **track sizes are not undoable** (`resizeTracks` geometry never reaches `applyEdits`, so a paste that also
|
|
212
|
+
resized columns undoes its cell writes only). The store helpers stay consumer-owned by design (ADR-6) — the package exports no
|
|
213
|
+
undo utilities.
|
|
153
214
|
|
|
154
215
|
- **In-grid commits** (Enter, Tab / Shift+Tab, PageUp / PageDown, clicking or right-clicking another cell, or a primary-button
|
|
155
216
|
press on a header / corner / scrollbar band / resize strip — a secondary-button press on those surfaces is a native-menu
|
|
@@ -641,7 +702,7 @@ Keymap notes:
|
|
|
641
702
|
| `onUndoRequest` / `onRedoRequest` | `() => void` | `Ctrl+Z` / `Ctrl+Y` / `Ctrl+Shift+Z` relays. |
|
|
642
703
|
| `formatCellAnnouncement` / `formatSelectionAnnouncement` | formatter | Opt-in screen-reader announcements (see above). |
|
|
643
704
|
| `formatCommandAnnouncement` | `(ctx: { command: SpreadsheetCommand; state: boolean \| null }) => string` | Opt-in (v0.8) live-region text after a successful format command; `state` is the toggle's post-state (`bold` / `italic` / `underline` / `strikethrough` / colour toggles), `null` for non-toggle commands. Also makes the live region render on its own. |
|
|
644
|
-
| `labels` | `SpreadsheetLabelOverrides` (any subset of the
|
|
705
|
+
| `labels` | `SpreadsheetLabelOverrides` (any subset of the 25 `SpreadsheetLabels` keys) | v0.8 — replaces the `sinkAriaLabel` / `rootAriaLabel` props (folded in) and names every popover / indicator string (`noteTitle`, `noteSave`, `noteEscapeHint`, `noteRejectedHint`, `noteViewerHint` (v0.10 — the note hint in the read-only viewer mode, which never promises a save), `commentTitle`, `commentReply`, `commentPost`, `commentCount` (string or `(n) => string`), `linkTitle`, `linkTextField`, `linkUrlField`, `linkOpen`, `linkSave`, `listTitle`, `listEmpty`, `listMore` (string or `(n) => string` — the validation-list picker mounts at most 500 options and announces the hidden-match count on this row), `messageTitle`, `validationRejected`, `popoverClose`, `indicatorNote`, `indicatorComment`, `indicatorInvalid`). Omitted / `undefined` keys keep the English `DEFAULT_LABELS`; `sinkAriaLabel` must mention `Escape` (`RangeError` during render otherwise). |
|
|
645
706
|
| `shortcuts` | `ShortcutOverrides` = `{ [command]?: readonly string[] \| null }` | v0.8 — rebinds command chords (array = replace the default list, `null` = unbind); validated during render with `RangeError`s (see "Command shortcuts"). Identity-stable — consulted through a ref. |
|
|
646
707
|
| `colors` | `{ fill?: readonly string[]; font?: readonly string[] }` | v0.8 — the five `#rrggbb` slots of `fillColorCustom1..5` / `fontColorCustom1..5` (exactly 5 six-digit hex strings per axis, `RangeError` otherwise); an omitted axis uses `DEFAULT_CUSTOM_COLORS` (`#FF0000` / `#00FF00` / `#0000FF` / `#FFFF00` / `#FF00FF`, exported). |
|
|
647
708
|
| `formats` | `{ currency: string; locale?: string }` | v0.8 — the host currency (ISO code) and rendering locale for `formatCurrency` and for recovering bare currency symbols (`$`, `¥`) in pasted Excel / Sheets HTML; undeclared ⇒ `formatCurrency` passes through and bare symbols are not recovered. |
|
|
@@ -655,6 +716,10 @@ Keymap notes:
|
|
|
655
716
|
| `onPasteSpecialRequest` | `(origin: { kind: "keyboard"; cellRect }) => void` | v0.8 — `Ctrl+Alt+V` asks the host for its Paste Special UI (the active cell's viewport rect in visual px, the `onCellContextMenu` keyboard shape); undeclared ⇒ `Ctrl+Alt+V` is a formats-only paste through the menu path. |
|
|
656
717
|
| `onClipboardUnavailable` | `(cause: "insecure-origin" \| "permission-denied" \| "empty" \| "unreadable") => void` | v0.8 — the menu path's refusal channel (`handle.paste`, `Ctrl+Alt+V` without a request handler, the `paste*` commands); whenever it fires the paste resolves `"rejected"`. `"unreadable"` also fires on the paste event when the detected source cannot be parsed (the package never slides to another reader). |
|
|
657
718
|
| `now` | `() => number` | v0.8 — clock seam of `insertDate` / `insertTime` (declared default `Date.now`). |
|
|
719
|
+
| `onPasteResolved` | `(outcome: PasteOutcome, channel: ClipChannel) => void` | v0.10 — paste-outcome summary. The menu channel mirrors `handle.paste(options)`’s resolve value 1:1 (read refusals and the write-time re-gate included); the event channel fires once per gesture that reaches the paste pipeline (incl. the image-only-clipboard `"empty"`). The drag-freeze consumption and a null `clipboardData` stay deliberately silent, and an empty grid attaches no paste listener at all (structurally unreachable). Cause-specific callbacks (`onClipboardUnavailable` / `onOperationRejected` / `onReadOnlyRejected`) fire FIRST — a `"rejected"` outcome can be the trailing summary of one of them. |
|
|
720
|
+
| `onReadOnlyRejected` | `(rejection: ReadOnlyRejection) => void` | v0.10 — the single read-only refusal channel: fired once per refused gesture, the adapter was never called (neither an `EditRejection` nor an operation limit). The edit variant carries the cell and the required `editSession: "none" \| "kept" \| "discarded"` fate; the operation variant carries the explicit 7-literal `op` (`"cut"` has two producers — the Ctrl+X arming refusal and the move-time source re-check) and the producer’s overlap-naive `cellCount`. Never fired when `isCellReadOnly` is undeclared. |
|
|
721
|
+
| `formatReadOnlyAnnouncement` | `(rejection: ReadOnlyRejection) => string` | v0.10 — opt-in live-region text for a read-only refusal (inherits the 200 ms announce debounce + rAF collapse, so autorepeat / IME refusal storms cannot spam the live region). Omit for silence (no built-in strings). |
|
|
722
|
+
| `tapScrollCircleOptions` | `ScrollBarTapCircleOptions` | v0.10 — position / enable-disable tuning of the touch tap-scroll circle; an engine-validated pass-through onto VirtualGrid’s `scrollBarOptions.tapScrollCircleOptions` (the engine’s `resolveTapScrollCircleOptions` is the validation SSOT — no second layer here). LIVE prop. |
|
|
658
723
|
| `testId` / `className` | `string` | Test hook / root class extension. |
|
|
659
724
|
|
|
660
725
|
## Imperative handle
|
|
@@ -715,7 +780,7 @@ importers with the exact same Excel-compatible semantics the grid uses. `enumera
|
|
|
715
780
|
`onOperationRejected` op), and `EditRejection` (the `onEditRejected` context — see "Editing, Undo, and rejection") are exported for
|
|
716
781
|
consumer-side annotations.
|
|
717
782
|
|
|
718
|
-
v0.8 adds the rich-cell engines as public pure functions (all React-free; `src/index.ts` exports 39 values and
|
|
783
|
+
v0.8 adds the rich-cell engines as public pure functions (all React-free; `src/index.ts` exports 39 values and 97 types in total — every named type an exported declaration reaches is exported too, so nothing has to be extracted with `Awaited<ReturnType<>>` / `Extract<>`; `src/index.spec.ts` pins it):
|
|
719
784
|
|
|
720
785
|
- **Clip payload** — `serializeClipPayload(payload)` / `parseClipPayload(json)` (the lossless wire format `{ v: 1, rows, cols, clipId, formats, validations, cells, colWidths?, rowHeights?, wholeRows, wholeCols, conditionalFormats }`; the parser is total and returns `null` on ANY malformed input, the serializer validates the same way and throws a `RangeError`),
|
|
721
786
|
`detectClipSource(flavours, channel)` (`"aqsp" | "sheets" | "excel" | "html" | "tsv" | "none"` — the ADR-14 detection over `{ types, getData }`), and the types `ClipPayload` / `ClipCell` / `ClipComment` / `ClipSource` / `ClipInfo` / `ClipFlavours` / `ClipChannel` (the `detectClipSource` parameter types).
|
|
@@ -741,6 +806,9 @@ The `/utils` demo route exercises the v0.1 toolkit live; the `/styles` route dri
|
|
|
741
806
|
| Editor while scrolled away | The visible editor disappears once the edited cell's top-left corner leaves the viewport (session and value survive; it re-shows on scroll-back) | The editor scrolls off with its cell, partially clipped | The sink is viewport-clamped for focus/IME survival — showing it clamped would paint an editor over the wrong cells |
|
|
742
807
|
| Zoom shortcuts | Alt+Wheel / Ctrl+Alt+`=` `+` `-` `0` (produced-character match, Shift-agnostic) | Ctrl+Wheel | The browser owns Ctrl+Wheel in a web page — a conflicting binding would fight page zoom |
|
|
743
808
|
| Multi-range copy | Tail range only + `onCopyMultiRange` notice | Aligned ranges concatenate | Ambiguous general case; explicit callback instead of silent difference |
|
|
809
|
+
| Multi-range paste | Pastes into the **tail** range of a multi-range selection only (no per-range fan-out; a dedicated `onPasteMultiRange` notice is deferred until demanded) | Repeats the clip into every selected range when shapes align | The tail range is the anchor the tiling geometry already targets; fanning out multiplies every gate (limit / bounds / read-only) per range for a gesture whose Excel semantics are themselves shape-conditional |
|
|
810
|
+
| Paste numeric normalization | Flavour-specific by contract: the HTML path canonicalises displayed numbers back to raw values only for cells whose incoming payload declares a numeric format (`numberDisplayToCanonical` — format-fidelity recovery); General-format and TSV text passes through verbatim | Excel re-parses displayed strings by locale heuristics on every paste | Re-parsing unformatted text guesses; the declared-format path is the only one where the display→canonical inversion is well-defined |
|
|
811
|
+
| Paste vs data validation | Paste bypasses `getCellValidation` rules BY DESIGN (strict rules gate commits, not pastes; an opt-in prop is deferred until demanded) | Same — Excel pastes over validated cells without running validation | Excel parity; validation is a commit-time gate (see "Editing, Undo, and rejection") |
|
|
744
812
|
| Cut paste clipping | Rejected entirely (`"bounds"`) | Rejects likewise | Clipping a move would silently destroy the clipped cells |
|
|
745
813
|
| Ctrl+Arrow without `findDataEdge` | Grid edge | Data edge | Data-end scan is a store capability — declare `findDataEdge` for Excel behavior |
|
|
746
814
|
| Ctrl+Arrow landing on a hidden cell | Rescans forward first, then backward, else stays | Can land on the hidden cell | Forward keeps data beyond a hidden edge reachable; the backward snap only fires when nothing is visible ahead |
|
|
@@ -815,7 +883,7 @@ The `/utils` demo route exercises the v0.1 toolkit live; the `/styles` route dri
|
|
|
815
883
|
## Demo (feature showcase, doubles as the E2E fixture)
|
|
816
884
|
|
|
817
885
|
Run `pnpm run demo:dev` from a source checkout (the `demo/` app is not in the npm tarball) and open
|
|
818
|
-
`http://localhost:5179`.
|
|
886
|
+
`http://localhost:5179`. Ten routes, each an in-header-nav page AND an E2E fixture (`tests/e2e/`):
|
|
819
887
|
|
|
820
888
|
| Route | What you experience |
|
|
821
889
|
| --- | --- |
|
|
@@ -827,6 +895,7 @@ Run `pnpm run demo:dev` from a source checkout (the `demo/` app is not in the np
|
|
|
827
895
|
| `/huge` | The v0.4 deep-reach proof: 1,000,000 × 10,000,000 on a pure functional adapter — Ctrl+→ (and End) reach the rightmost column 9,999,999 and Ctrl+↓ the last row 999,999 in one press (uniform-block Excel semantics, unified with the `/` route; ±1px cell placement), a "deep jump" button (`scrollToCell` to the interior column 5,000,000 ≈ 5 億 px, past the 2^25 wall), zoom ×4 holds ±1 logical px — plus runtime freeze toggles (0 ↔ 3 frozen rows above 0 ↔ 3 frozen columns), a context menu (right-click / Shift+F10) that opens at static frozen-cell coordinates even mid-deep-scroll, and the drag-freshness and performance-budget E2E gates — plus the v0.9 trailing-freeze controls consumed by `tests/e2e/trailing-freeze.spec.ts`: trailing freeze toggles for 0 ↔ 2 last rows / columns (`data-demo-trailing-row-toggle` / `data-demo-trailing-col-toggle` — deliberately not the leading toggles' 0 ↔ 3), a row-axis deep jump to the last scroll row `rowCount − T − 1` (`data-demo-deep-jump-row`, reading the live toggle), and an overlap toggle (`data-demo-overlap-toggle` — 16 / 16 frozen rows and 8 / 8 frozen columns exceed the default E2E viewport on both axes) |
|
|
828
896
|
| `/tune` | Sizing knobs (`defaultRowHeight` / `defaultColumnWidth` / banded per-column widths via the imperative `resizeColumn` mount effect), hide / unhide (drag a boundary to 0, marker drag / dblclick restore, single-column dblclick autofit), a deliberately low LIVE `maxOperationCells` staging every rejection class (limit, row fan-out ÷10, autofit ÷4, bounds via the multi-cell corner recipe), live `enableResizeUI` / `enableFillHandle` opt-outs, an empty-grid staging toggle (rowCount 0 adapter swap), a data-reset button, and the zoom card (v0.3) — the controlled-mode reference wiring (`zoom` + `onZoomChange`, 50–200% select plus every zoom gesture) and the fixture for the zoom E2E gates |
|
|
829
897
|
| `/a11y` | Custom Japanese `formatCellAnnouncement` / `formatSelectionAnnouncement` mirrored into visible readouts, overridden accessible names (Escape advisement kept), the WCAG 2.1.2 escape route |
|
|
898
|
+
| `/readonly` | The v0.10 read-only cells (`tests/e2e/readonly.spec.ts`): a 200 × 10 sheet whose adapter declares `isCellReadOnly` (one locked COLUMN plus scattered locked cells for mixed selections) alongside note / comment / link / validation reads — the whole-operation refusals live (edit entries, Delete, paste, cut, fill incl. the diagonal legs, annotation chords on empty cells), the content-aware viewers (the locked column carries a comment thread, a note and a hyperlink; their chords open viewer-mode popovers with the commit affordance disabled), a host-side tint on `[data-aqsp-readonly]` (the package ships no CSS), a refusal toast worded per `ReadOnlyRejection` (the three `editSession` fates told apart, the two-producer cut wording), and an `onPasteResolved` two-line log that sits beside the refusal toast so a read-only-refused paste shows BOTH callbacks |
|
|
830
899
|
| `/utils` | The exported pure functions live: TSV round-trip, paste-expansion rules, column-name converter (incl. its fail-fast `RangeError`), range algebra, and a clipboard inspector card (discloses the raw text/html payload, textContent-rendered) |
|
|
831
900
|
|
|
832
901
|
Appending `?css=standalone` to any route swaps the source CSS pair for the built single-file
|