@aiquants/spreadsheet 0.16.0 → 0.17.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 CHANGED
@@ -2,7 +2,90 @@
2
2
 
3
3
  All notable changes to `@aiquants/spreadsheet` are documented here.
4
4
 
5
- ## 0.16.0 (unreleased)
5
+ ## 0.17.0 (2026-10-27)
6
+
7
+ A host can now fit the grid's box to its content: hide the column letters and row numbers, read the content size, fit
8
+ every column over all its rows, and keep the active cell on screen when the grid is taller than its scroll container.
9
+
10
+ ### Added
11
+
12
+ - **`headings`** (prop, true by default) shows the column letters, the row numbers and the corner (Excel's "headings";
13
+ not `headerRows`). False draws none of the three and collapses their grid tracks to 0, so the cells start at the
14
+ grid's top-left corner. The gestures that live only on them go with them (header presses, the corner's select-all,
15
+ the resize strips and unhide markers), and their keyboard twins stay (Ctrl+Space, Shift+Space, Ctrl+A,
16
+ Alt+Shift+Arrow, Alt+Shift+=). The bands are `aria-hidden`, so assistive technology meets the same grid. The root
17
+ carries `data-aqsp-headings-hidden` while they are hidden, and switching the prop wires the header machines to the
18
+ new bands.
19
+ - **`onContentSizeChange(size)`** (prop) receives the size of the grid's content (`SpreadsheetContentSize`): the bands,
20
+ every track and the two scroll bars the grid always reserves, all scaled by the zoom (the scroll bars live inside the
21
+ zoom wrapper, so they scale too). It is called after the first layout and after every change of a track's size, the
22
+ row or column count, the default sizes, the zoom or `headings`, never with an unchanged size. A host sizes the grid's
23
+ box to its content with it.
24
+ - **`SpreadsheetHandle.autoFitColumns(columns)`** fits the listed columns to the widest text of all their rows (header
25
+ rows included), as Alt+Shift+= fits the active column over the rows in view: the same painted fonts, 9 px padding and
26
+ 10,000 px bound. It refuses through `onOperationRejected("resize", cells, "limit")` when the cells to measure exceed
27
+ `maxOperationCells`, keeps a hidden or empty column's width, and throws a `RangeError` for a column that is not a
28
+ whole number below the column count.
29
+
30
+ ### Changed
31
+
32
+ - **A keyboard move keeps the active cell on screen outside the grid too.** After the grid lands the cell in its own
33
+ viewport, the scroll containers outside the grid (a host's box, the page) scroll by the least amount that shows the
34
+ cell (block and inline "nearest", their `scroll-padding` honoured). A grid taller or wider than its scroll container
35
+ (a box that shows every row, a grid half outside the window) kept the active cell off-screen before, since the grid
36
+ saw it inside its own viewport (WCAG 2.4.11 / 2.4.12). The reveal goes through a hidden marker under the root
37
+ (`data-aqsp-reveal`), clipped to the root's box, so no container inside the grid moves.
38
+ - **Autofit measures with the cells' base font, read from the body pane**, instead of the column-letter band's. Both
39
+ inherit the root's font, so the widths do not change, and the keyboard fit and `autoFitColumns` now work without the
40
+ bands.
41
+
42
+ ### Demo
43
+
44
+ - **`/fit`** shows a host that fits the grid's box to `onContentSizeChange`: the grid shows every row and the page
45
+ scrolls instead, with the headings switch, the zoom, the all-rows fit and the content-size readout.
46
+
47
+ ### Verification
48
+
49
+ - **公開面**: 公開面は `index.ts` の値 44 + 型 99 = 143 export (型 `SpreadsheetContentSize` を追加)。
50
+ - **DOM 属性**: `data-aqsp-headings-hidden` (根) と `data-aqsp-reveal` (ページの追従の印) の 2 種を追加 — リテラル census は
51
+ 83 種 (dataset 直書きの同値抑止署名キー 4 種を加えた総数は 87。分冊 05 §10・分冊 00 §3.2)。
52
+ - **検証 — 走行台帳 (census の錨は本未リリース節)**: 単体 / 統合 **98 ファイル 4,231 テスト** 全緑 —
53
+ 0.16.1 出荷時の 97 ファイル 4,208 に v0.17 進行分 +23 (`hostFit` +20 新規、`toolPages` +2、0.16.1 節の出荷時凍結ピンで
54
+ `hxCensus` +1)。`tsc --noEmit` / biome 0。E2E は `host-fit` 10 テスト (新規) を加えた全 25 ファイル 281 テストを
55
+ 全数実行で確かめた。負荷の中で 1 回落ちた `styles` の 1 本 (書式のツールバーの直後に computed を 1 回だけ読む競合。単独では 3 回とも
56
+ 合格) は、書式と罫線の直後の 6 つの読みを `expect.poll` に直した。
57
+
58
+ ## 0.16.1 (2026-10-27)
59
+
60
+ ### Added
61
+
62
+ - **`iterateTsvPrefixRows(text)`** (`/tsv` and the barrel) reads a text that may be only the beginning of a TSV — a file
63
+ read partway, a stream not yet ended — and yields, unpadded, only the rows the rest cannot change: every row the text
64
+ ends with a line terminator, up to the first field that opens a quote the text does not close. Whether such a quote is
65
+ one quoted field or a literal split at its line breaks is the rest's to decide, so the rows of any beginning are the
66
+ first rows of the whole (`iterateTsvRows` reads the same quote of a whole text as a literal, which a reader of a cut
67
+ text would otherwise show as many rows). The one grammar walk serves both readings.
68
+
69
+ ### Changed
70
+
71
+ - A quoted field's content is read a run between quotes at a time instead of a character at a time: the same fields,
72
+ without a string grown once per character (a quoted cell of 15 MB with 3 million line breaks reads in about 1 ms
73
+ where it took over a second).
74
+
75
+ ### Documentation
76
+
77
+ - **The freeze divider's default reads `#858e9d` everywhere in the README.** Two places still named `#9ca3af`, the default
78
+ 0.16.0 replaced to reach 3:1 against the surface and the header band, and the non-text contrast paragraph listed the
79
+ values the defaults replaced; it now lists the current ones only.
80
+
81
+ ### Verification
82
+
83
+ - **公開面**: 公開面は `index.ts` の値 44 + 型 98 = 142 export (`iterateTsvPrefixRows` を追加)。
84
+ - **検証 — 走行台帳 (census の錨は本未リリース節)**: 単体 / 統合 **97 ファイル 4,208 テスト** 全緑 —
85
+ 0.16.0 出荷時の 97 ファイル 4,202 に v0.16.1 進行分 +6 (`tsv` の `iterateTsvPrefixRows` +5、0.16.0 節の出荷時凍結ピンで
86
+ `hxCensus` +1)。`tsc --noEmit` / biome 0。
87
+
88
+ ## 0.16.0 (2026-10-26)
6
89
 
7
90
  TSV の往復を文字列のどの矩形行列でも厳密にし、拡大率の操作を等比の段に揃え、`@aiquants/spreadsheet/view` にホストが最初の画面を
8
91
  グリッドと同じ数で描くための既定値を加える。
package/README.md CHANGED
@@ -21,7 +21,7 @@ deep-reach proof route.
21
21
  - 🧊 **Frozen columns & rows** (v0.5 / v0.6): `frozenColumns` pins the first N columns as a static left band and `frozenRows` pins the first N rows as a static top band — two-band headers, a Cartesian-product 4-quadrant clipped selection overlay, partition-aware hit-testing and ensure-visible on both axes, all riding the engine's anchor-free frozen bands
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
- 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"
24
+ divider over the scroll band's edge pixels — 2px `#858e9d` by default (3:1 against the surface and the header band), 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
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
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
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,
@@ -364,11 +364,12 @@ to `"¥203,428"`, not to the raw `"203428"`, which used to left-clip the most s
364
364
  Row autofit restores the default row height (cells render single-line clipped). Autofit is disabled entirely when
365
365
  `renderCell` is set — custom cell DOM cannot be measured.
366
366
 
367
- > **Autofit measures with the header's font as its base.** Text measurement composes its font from the **header band's**
368
- > computed style (style / weight / size / family); since v0.8 the cell's **painted** format (bold / italic from the base format
369
- > and width-relevant conditional formats, `fontSize` in pt × 4/3, `fontFamily`) overrides those parts per cell, bucketed so
370
- > `ctx.font` is assigned once per distinct font. If your CSS gives body cells a different base font than the headers, autofit
371
- > will mismeasure — keep cell and header base fonts identical (the shipped stylesheets do).
367
+ > **Autofit measures with the cells' base font.** Text measurement composes its font from the **body pane's** computed
368
+ > style (style / weight / size / family — the cells' parent, so the fit works whether `headings` shows the bands or not);
369
+ > the cell's **painted** format (bold / italic from the base format and width-relevant conditional formats, `fontSize` in
370
+ > pt × 4/3, `fontFamily`) overrides those parts per cell, bucketed so `ctx.font` is assigned once per distinct font. If your
371
+ > CSS gives the cells a different base font than their pane, autofit will mismeasure — set fonts on the root, which the
372
+ > pane, the cells and the header bands all inherit (the shipped stylesheets do).
372
373
 
373
374
  `enableResizeUI={false}` removes the strips **and the unhide markers** — a consumer with hidden tracks then has no
374
375
  in-package restore affordance (only the imperative API). The imperative `resizeRow` / `resizeColumn` calls bypass every UI
@@ -417,7 +418,7 @@ two CSS custom properties — the only consumer surface (there is no prop):
417
418
 
418
419
  | Variable | Default | Meaning |
419
420
  | :--- | :--- | :--- |
420
- | `--aqsp-divider-color` | `#9ca3af` (gray-400 — one step darker than every gridline) | Divider colour |
421
+ | `--aqsp-divider-color` | `#858e9d` (3:1 against the surface and the header band: the divider is a mark, not decoration) | Divider colour |
421
422
  | `--aqsp-divider-width` | `2px` (Sheets strength) | Divider thickness |
422
423
 
423
424
  ```css
@@ -604,11 +605,10 @@ name itself.
604
605
 
605
606
  **Non-text contrast (WCAG 1.4.11, 3:1).** The marks that carry meaning reach 3:1 against the default surface and the
606
607
  header-row surface: accent 5.17 / 4.7, the selection outline 3.97 (over the surface),
607
- the note marker 7.56 / 6.87, the comment marker `#eb6406` 3.3 / 3.0 (was `#f97316`, 2.80),
608
- danger 4.83 / 4.39, the reference and fill-preview green `#16a24a` 3.33 / 3.03 (was `#16a34a`, 3.30 / 2.99),
609
- the freeze divider `#858e9d` 3.31 / 3.0 (was `#9ca3af`, 2.54), the unhide markers at full opacity 4.7 on the
610
- header band (they were 70 % opaque, 2.86), and the popover input border `#8b95a4` 3.03 (new; inputs used the decorative
611
- `#d1d5db`, 1.47). Grid lines, band edges, popover frames and the note tip's frame are decorative (the tip is identified by its
608
+ the note marker 7.56 / 6.87, the comment marker `#eb6406` 3.3 / 3.0,
609
+ danger 4.83 / 4.39, the reference and fill-preview green `#16a24a` 3.33 / 3.03,
610
+ the freeze divider `#858e9d` 3.31 / 3.0, the unhide markers at full opacity 4.7 on the header band, and the popover
611
+ input border `#8b95a4` 3.03. Grid lines, band edges, popover frames and the note tip's frame are decorative (the tip is identified by its
612
612
  surface and shadow).
613
613
 
614
614
  **Contrast (WCAG 1.4.6, 7:1).** Every text / background pair of the light defaults is at least 7:1, measured with the WCAG 2
@@ -897,6 +897,8 @@ The grid's own UI chrome ships in English (`"en"`, the default) and Japanese (`"
897
897
  | `renderCell` | `(ctx: RenderCellContext) => ReactNode` — `RenderCellContext = { row; col; text; displayText }` (exported) | Custom cell rendering. **Must be identity-stable** (`useCallback`) — an inline arrow defeats the cell memo and re-renders every visible cell each pass. **Per-cell opt-out** (v0.12 — ADR-32): return `undefined` and that one cell falls back to the package default rendering, value-format engine included (only `undefined` opts out — `null`, `false` and any other ReactNode are custom output, i.e. an empty custom cell). `ctx.text` is the raw canonical string; `ctx.displayText` is the value-format display — the exact string the default rendering, announcements, the HTML clipboard and autofit share, so a custom cell that renders it can never drift from what gets copied. Autofit stays a whole-grid no-op while `renderCell` is set. |
898
898
  | `maxOperationCells` | `number` | Bulk-operation ceiling (default 100,000). Row fan-out resize gates at 1/10 of it and autofit at 1/4 — see "Operation limits". |
899
899
  | `enableResizeUI` | `boolean` | Default `true`. `false` removes resize strips **and unhide markers** — hidden tracks then have no in-package restore path (imperative API only). |
900
+ | `headings` | `boolean` | v0.17 — default `true`: shows the column letters, the row numbers and the corner (Excel's "headings"; not `headerRows`). `false` draws none of the three and collapses their grid tracks to 0, so the cells start at the grid's top-left corner; the gestures that live only on them go with them (header presses, the corner's select-all, the resize strips and unhide markers) and their keyboard twins stay (Ctrl+Space, Shift+Space, Ctrl+A, Alt+Shift+Arrow, Alt+Shift+=). The bands are `aria-hidden`, so assistive technology meets the same grid. The root carries `data-aqsp-headings-hidden` while they are hidden. LIVE prop. |
901
+ | `onContentSizeChange` | `(size: SpreadsheetContentSize) => void` | v0.17 — receives the size of the grid's content in CSS px: the bands (when shown), every column and row at its current size and the two scroll bars the grid always reserves, all scaled by the zoom (the scroll bars live inside the zoom wrapper, so they scale too). Called after the first layout and after every change of it — a track's size (a resize, an autofit, a hide or unhide, the imperative `resizeRow` / `resizeColumn`), the row or column count, the default sizes, the zoom or `headings` — never with an unchanged size. See *Fitting a box to the grid*. |
900
902
  | `zoom` / `defaultZoom` / `onZoomChange` | `number` / `number` / `(zoom: number) => void` | Package zoom (v0.3): visual transform scale, logic stays in logical px. Controlled (`zoom`) or uncontrolled (`defaultZoom`, default 1). Gestures — Alt+Wheel and Ctrl+Alt+`=` / `+` / `-` / `0` (reset; the four keys match the produced character, Shift-agnostic; AltGraph-state chords are never claimed — legacy-shape defense in depth, see Keymap notes) — request via `onZoomChange`; Ctrl+Wheel is deliberately unbound (the browser owns it). Clamped to [`SPREADSHEET_ZOOM_MIN`, `SPREADSHEET_ZOOM_MAX`] (0.25–4, `clampSpreadsheetZoom` of `@aiquants/spreadsheet/view`); a non-finite value (NaN / ±Infinity) throws a `RangeError` (fail-fast — no silent substitution). |
901
903
  | `enableFillHandle` | `boolean` | Default `true`. Fill writes through `applyEdits` — disable for read-only adapters. |
902
904
  | `onSelectionChange` | `(selection: SelectionState) => void` | Fires on confirmed changes only — never on mount, and never mid-drag with one documented exception: the header subtract reservation converts to a real press on the first movement that both really changes the pointer coordinates and lands on a different header index (in-index jitter and synthetic same-position moves keep the reservation), firing the deferred press replace mid-drag (total fire count matches v0.1). |
@@ -983,6 +985,39 @@ that fails to parse `"unreadable"` — each fires `onClipboardUnavailable` and r
983
985
  called; `"empty"` means nothing was written (an own payload whose identity was suppressed, every cell skipped, a mode without cell
984
986
  aspects such as `columnWidths` — sizes may still have been applied).
985
987
 
988
+ ## Fitting a box to the grid (v0.17)
989
+
990
+ A host that shows a whole table — a document, a report — sizes the grid's box to the grid's content instead of giving
991
+ the grid a fixed box:
992
+
993
+ ```tsx
994
+ const [size, setSize] = useState<SpreadsheetContentSize | null>(null)
995
+ const box = size === null ? undefined : { width: `min(${Math.ceil(size.width)}px, 100%)`, height: `${Math.ceil(size.height) + 1}px` }
996
+ // <div style={box}><Spreadsheet ref={ref} headings={false} onContentSizeChange={setSize} ... /></div>
997
+ ref.current?.autoFitColumns(Array.from({ length: colCount }, (_, col) => col))
998
+ ```
999
+
1000
+ - **`onContentSizeChange`** reports the size that shows every column and row without scrolling (see the props table).
1001
+ Round the height up and keep a pixel of slack: if a rounding leaves the rows a fraction taller than the box, the row
1002
+ list becomes scrollable and the grid takes vertical finger swipes for itself (`touch-action: none`), so a box taller
1003
+ than the window would trap a touch reader. A box that shows every row lets a vertical wheel and a vertical swipe over
1004
+ the grid scroll the page instead.
1005
+ - **`autoFitColumns(columns)`** fits the listed columns to the widest text of **all** their rows (header rows
1006
+ included) — Alt+Shift+= fits the active column over the rows in view — with the same painted fonts, 9 px padding and
1007
+ 10,000 px bound. A hidden or empty column keeps its width; more measured cells than `maxOperationCells` are refused
1008
+ through `onOperationRejected("resize", cells, "limit")` (false), and a column that is not a whole number below the
1009
+ column count is a `RangeError`. It returns false while `renderCell` is set (custom cell DOM cannot be measured) and
1010
+ works whatever `enableResizeUI` and `headings` say. Measure after `document.fonts.ready` when the page uses web fonts.
1011
+ - **`headings={false}`** drops the column letters, the row numbers and the corner (see the props table).
1012
+ - **Keyboard moves keep the active cell on screen.** A grid taller or wider than its scroll container sees the active
1013
+ cell inside its own viewport, so after landing the cell there the grid also scrolls the containers outside it (the
1014
+ host's box, the page) by the least amount that shows the cell — block and inline "nearest", honouring their
1015
+ `scroll-padding` (give a scroller with a sticky bar a matching `scroll-padding`). The reveal goes through a hidden
1016
+ marker under the root (`data-aqsp-reveal`), clipped to the root's box, so no container inside the grid moves; a
1017
+ pointer selection does not scroll (the pressed cell is on screen).
1018
+
1019
+ The demo's `/fit` route is the reference wiring.
1020
+
986
1021
  ## View geometry and zoom rules (`@aiquants/spreadsheet/view`)
987
1022
 
988
1023
  `@aiquants/spreadsheet/view` publishes the grid's fixed geometry, its defaults and its zoom ladder without loading the grid
@@ -1017,11 +1052,12 @@ const zoomIn = () => setZoom((zoom) => stepSpreadsheetZoom(zoom, 1))
1017
1052
 
1018
1053
  ## TSV codec (`@aiquants/spreadsheet/tsv`)
1019
1054
 
1020
- `@aiquants/spreadsheet/tsv` publishes `parseTsv`, `iterateTsvRows`, `serializeTsv`, `serializeClipboardTsv` and
1021
- `measureTsv` — the codec the grid copies and pastes with — without loading the grid (no React, no `@aiquants/virtualscroll`; about 1 KB). A host that lazily imports the grid can
1055
+ `@aiquants/spreadsheet/tsv` publishes `parseTsv`, `iterateTsvRows`, `iterateTsvPrefixRows`, `serializeTsv`,
1056
+ `serializeClipboardTsv` and `measureTsv` — the codec the grid copies and pastes with — without loading the grid (no React, no `@aiquants/virtualscroll`; about 1 KB). A host that lazily imports the grid can
1022
1057
  still read the grid's text during the first render, for example to server-render a placeholder table of the rows the grid
1023
- will show (the same markup on the client, so hydration matches). The barrel exports the same five functions (one
1024
- definition), so code that already loads the grid keeps importing them from `@aiquants/spreadsheet`.
1058
+ will show (the same markup on the client, so hydration matches), or preview a TSV file it reads as a stream. The barrel
1059
+ exports the same six functions (one definition), so code that already loads the grid keeps importing them from
1060
+ `@aiquants/spreadsheet`.
1025
1061
 
1026
1062
  The round trip is exact: `parseTsv(serializeTsv(m))` deep-equals `m` for every rectangular matrix of strings of at least
1027
1063
  1 × 1, whatever the cells hold (tabs, CR, LF, CRLF, quotes, a cell that starts with `"`). Store tables with `serializeTsv`.
@@ -1039,6 +1075,7 @@ other sources.
1039
1075
  | `parseTsv(text)` | A rectangular `string[][]`: one trailing line terminator dropped, empty text read as one empty cell (`[[""]]`), ragged rows padded with `""`; a field that starts with `"` is read as quoted only when the quote closes properly (the closing quote is followed by a tab, a line break or the end) and the unescaped content holds a tab, CR, LF or `"` — anything else is literal text, so Excel's raw output reads as Excel wrote it (`"hi"` stays `"hi"`). The one raw shape that reads unquoted is a value that looks exactly like a quoted field holding a doubled quote (`"a""b"` reads `a"b`) |
1040
1076
  | `serializeTsv(matrix)` | Rows joined with CRLF and a trailing CRLF; a field is quoted (its quotes doubled) when it holds a tab, CR or LF — the cells Excel quotes — or starts with `"`, the one other cell a reader could take for a quoted field; every other field, inner quotes included, is written verbatim |
1041
1077
  | `iterateTsvRows(text)` | A `Generator<string[]>` that yields the rows one at a time in exactly `parseTsv`'s grammar (same quoting rule, line terminators and final-terminator handling), **unpadded**; a consumer that stops early materialises nothing past the last row it took (preview a large file under a cell budget). `parseTsv` is these rows, right-padded |
1078
+ | `iterateTsvPrefixRows(text)` | The rows of a text that may be only the **beginning** of a TSV (a file read partway, a stream not yet ended), in the same grammar, unpadded, but only those the rest of the text cannot change: every row the text ends with a line terminator, up to the first field that opens a quote the text does not close (whether it is one quoted field or a literal one split at its line breaks is the rest's to decide; `iterateTsvRows` reads such a quote of a whole text as a literal). So the rows of any beginning of a TSV are the first rows of the whole, and a stream's reader can show them as final; empty text yields none |
1042
1079
  | `serializeClipboardTsv(matrix)` | The clipboard's `text/plain` (Excel's form): rows joined with CRLF and a trailing CRLF; a field is quoted (its quotes doubled) only when it holds a tab, CR or LF, so `"Important" note` is written as typed. Not an exact round trip for cells that start with `"`; the grid's own copy uses it, and a host's copy control should too |
1043
1080
  | `measureTsv(text)` | `{ rows, cols }` — the dimensions `parseTsv(text)` would build, from the same grammar walk, without building any row array (the paste limit check runs on it before anything is materialised) |
1044
1081
 
@@ -1057,7 +1094,7 @@ importers with the exact same Excel-compatible semantics the grid uses. `enumera
1057
1094
  `onOperationRejected` op), and `EditRejection` (the `onEditRejected` context — see "Editing, Undo, and rejection") are exported for
1058
1095
  consumer-side annotations.
1059
1096
 
1060
- v0.8 adds the rich-cell engines as public pure functions (all React-free; `src/index.ts` exports 43 values and 98 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):
1097
+ v0.8 adds the rich-cell engines as public pure functions (all React-free; `src/index.ts` exports 44 values and 99 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):
1061
1098
 
1062
1099
  - **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`),
1063
1100
  `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).
@@ -1177,6 +1214,7 @@ covers the `?lang=ja` path and the live switch; every other E2E spec runs the En
1177
1214
  | `/styles` | The v0.8 rich cells (`tests/e2e/styles.spec.ts`): a 300 × 26 sheet whose store declares all eight capabilities (a mini formula evaluator for `=A1+B1` / `SUM` / `AVERAGE` feeds `evaluateFormula` and the computed `getCellText`, `getCellFormula` returns the source), a patch-applying store with per-transaction inverse-patch Undo (a cross-sheet cut-move folds into one entry) and consumer-minted comment ids, a format toolbar driven by `handle.executeCommand` (every button keeps focus in the grid), colour pickers, border / number-format presets, link / note / comment / validation / conditional-format editors, a Paste Special menu opened by `onPasteSpecialRequest` (Excel accelerators — modes `A`ll / `F`ormulas / `V`alues / `T` formats / `C`omments and notes / `N` validation / `X` all except borders / `W`idths / `R` conditional formats, operations `O` none / `D` add / `S` subtract / `M` multiply / `I` divide, flags `B` skip blanks / `E` transpose / `J` inject formats → `handle.paste`), highlight-band settings, a shortcut rebinding table with reserved-chord warnings, a clip-info chip (`onClipChange`), a Sheet A / B toggle for cross-sheet paste and cut-move, and Excel / Sheets fixture buttons that write real interop HTML to the clipboard — and, since v0.9, the trailing freeze live by default: `frozenTrailingRows={1}` / `frozenTrailingColumns={1}` alongside the leading 1×1 (this nine-quadrant state is the ambient every `/styles` budget number is certified on) pin the sheet's grand-total row (the last row's `SUM` / `AVERAGE`, bold at the base font) to the bottom edge, with a `data-demo-trailing` checkbox to release it |
1178
1215
  | `/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) |
1179
1216
  | `/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 |
1217
+ | `/fit` | v0.17 — a host that fits the grid's box to `onContentSizeChange` (`tests/e2e/host-fit.spec.ts`): a read-only 60 × 6 table in the page's flow whose box shows every row so the page scrolls instead, with the headings switch (`data-demo-fit-headings`), the zoom (`data-demo-fit-zoom`), the all-rows fit through `autoFitColumns` (`data-demo-fit-autofit`) and the content-size readout (`data-demo-fit-size`); arrow keys keep the active cell in the window |
1180
1218
  | `/a11y` | Locale-aware custom `formatCellAnnouncement` / `formatSelectionAnnouncement` (English by default, Japanese under `?lang=ja`) mirrored into visible readouts, per-locale overridden accessible names (Escape advisement kept), the WCAG 2.1.2 escape route |
1181
1219
  | `/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 |
1182
1220
  | `/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) |