@aiquants/spreadsheet 0.8.0 → 0.9.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 +50 -0
- package/README.md +86 -8
- package/dist/index.cjs +14 -14
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +4159 -3886
- package/dist/styles/spreadsheet.css +1 -1
- package/dist/styles/spreadsheet.standalone.css +1 -1
- package/package.json +3 -3
- package/src/Spreadsheet.tsx +198 -22
- package/src/grid/GridBody.tsx +16 -0
- package/src/header/ColumnHeader.tsx +84 -12
- package/src/header/RowHeader.tsx +79 -11
- package/src/header/resizeStrips.ts +86 -1
- package/src/header/useHeaderResize.ts +88 -20
- package/src/header/useHeaderSelection.ts +28 -9
- package/src/keyboard/ensureCellVisible.ts +40 -17
- package/src/keyboard/useGridKeyboard.ts +10 -4
- package/src/model/frozenPartition.ts +183 -59
- package/src/overlay/SelectionOverlay.tsx +599 -116
- package/src/overlay/useFillHandle.ts +65 -11
- package/src/popover/usePopovers.ts +39 -7
- package/src/selection/useAutoScroller.ts +58 -15
- package/src/selection/useSelectionEngine.ts +27 -10
- package/src/styles/spreadsheet.css +32 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@aiquants/spreadsheet` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.9.0 (2026-09-18)
|
|
6
|
+
|
|
7
|
+
v0.9 は末尾凍結 (trailing freeze) と強い凍結分割線 (freeze divider) — 統治文書は
|
|
8
|
+
[v0.9 実装計画](<./docs/plans/2026.09.11 [AI] spreadsheet-v0.9-trailing-freeze-divider-plan.md>)。エンジンは
|
|
9
|
+
`@aiquants/virtualscroll` 3.5.0 (`frozenTrailingCols` / `frozenTrailingRows` + `getFrozenSize()` 10 フィールド)。
|
|
10
|
+
|
|
11
|
+
- **新機能 — 末尾凍結 (ADR-18 / ADR-19)**: `SpreadsheetProps.frozenTrailingColumns` / `frozenTrailingRows`
|
|
12
|
+
(既定 0、LIVE、エンジン検証の素通し — 第二検証層なし)。座標分割は先頭 / 末尾 / 中央の 3 分岐
|
|
13
|
+
(`frozenPartition.ts` の必須 ctx オブジェクト `AxisForwardCtx` / `AxisInverseCtx` へクリーンカットオーバー —
|
|
14
|
+
位置引数形は削除)、overlay は {m, f, s, t}² 象限へ一般化 (旧互換属性は mm / fm / sm に据置き)、ヘッダーは
|
|
15
|
+
3 帯 (末尾クリップは登記 calc() 形)。末尾帯のリサイズ / 非表示復元は近端ストリップ系
|
|
16
|
+
(`buildTrailingResizeStrips` — ADR-26: 反転デルタ / クランプ済み動く辺ガイド / `data-aqsp-resize-anchor`)。
|
|
17
|
+
- **新機能 — 凍結分割線 (ADR-20 / ADR-24)**: 全凍結境界に consumer 所有の強い分割線
|
|
18
|
+
(`[data-aqsp-freeze-divider]`、既定 2px `#9ca3af`、CSS 変数 `--aqsp-divider-color` / `--aqsp-divider-width` のみ —
|
|
19
|
+
prop なし)。存在 = 実効件数、可視 = 帯 px の等値抑制 display heal、幾何 = 象限 / ヘッダークリップ所有。
|
|
20
|
+
バーは不活性 (`pointer-events: none`) でリサイズ operand は帯側隣接ストリップ。`--aqsp-divider-width: 0px` は
|
|
21
|
+
逃げ道 (fill 断片の継ぎ目が再露出、forced-colors も無描画 — 登記済み)。
|
|
22
|
+
- **挙動変更 — autofit の測定集合 (ADR-21、ユーザー承認済み)**: 列 autofit の測定行集合が可視窓のみから
|
|
23
|
+
**先頭凍結帯 ∪ 可視窓 ∪ 末尾凍結帯** (各 `sizeOf(row) > 0` でフィルタ) へ広がる — 末尾帯 (合計行 = 最幅セルの典型)
|
|
24
|
+
の包含が本命の欠陥修正で、先頭帯の包含は既存 `frozenRows > 0` 利用者へのマイナーリリース内の挙動変更
|
|
25
|
+
(v0.9 計画 §0.2 保留 2 の確定)。`measuredCells` 予算ゲートは帯行を数える。R = T = 0 の恒等は不変。
|
|
26
|
+
- **逸脱の要約 (完全列挙は README「Deviations from Excel」)**: 分割線は不活性 — バー上のドラッグはヘッダー選択を
|
|
27
|
+
開始し、リサイズストリップは線の帯側にすぐ隣接 (Sheets の凍結ドラッグハンドルではない)。分割線は論理 px で
|
|
28
|
+
ズームと共に縮尺 (z = 0.25 で 0.5 視覚 px)。leading + trailing の extent が viewport を超える overlap では遮蔽された
|
|
29
|
+
末尾セルはスクロールで可視化できず (件数クランプは leading 勝ち)、中央帯全崩壊では分割線ゼロ + f / t 突合継ぎ目が
|
|
30
|
+
再露出。凍結対応 Ctrl+Home / Ctrl+End は範囲外 (ネイティブ素通しのまま — 記録済み将来機能)。末尾ヘッダーストリップは
|
|
31
|
+
pinned-right イディオム (遠側トラック制御・ドラッグした辺が動く辺・dblclick autofit も遠側)。末尾行は位置的 (同一
|
|
32
|
+
データの末尾 N 行) — ソート実装 consumer は合計行を自前免除する。凍結帯シャドウは無し (導出可能性は
|
|
33
|
+
`getFrozenSize()` の trailing フィールドで担保)。
|
|
34
|
+
- **DOM 属性 census 追補 (分冊 05 §10 末尾追加)**: `data-aqsp-freeze-divider` / `data-aqsp-col-header-trailing` /
|
|
35
|
+
`data-aqsp-row-header-trailing` / `data-aqsp-trailing-header` / `data-aqsp-trailing-row-header` /
|
|
36
|
+
`data-aqsp-resize-anchor` の 6 種を追加 — リテラル census は 79 種 (dataset 直書きの同値抑止署名キー 4 種
|
|
37
|
+
`data-aqsp-anchor-sig` / `data-aqsp-clip-kind-last` / `data-aqsp-outset-sig` / `data-aqsp-drag-mode-last` は
|
|
38
|
+
grep に掛からず、DOM に現れる属性名の総数は 83)。
|
|
39
|
+
- T = 0 (両軸・未宣言含む) は v0.8 と DOM byte 恒等 (能力分岐 — `trailingFreeze.spec.tsx` の outerHTML 封印)。
|
|
40
|
+
- **検証 — 走行台帳 (census の錨は本未リリース節)**: 単体 / 統合 **74 ファイル 3,631 テスト** 全緑 — 0.8.0 出荷時の
|
|
41
|
+
72 ファイル 3,456 に v0.9 進行分 +175 (step 5e (2026-09-16) の行別再導出 — 新規 `trailingFreeze` 統合 spec
|
|
42
|
+
(`src/trailingFreeze.spec.tsx`) 34 + リリースパス docs census (`src/releaseCensus.spec.ts`) 10 + 既存 17 ファイル +131
|
|
43
|
+
(うち +2 は v0.8 出荷時登記の過少是正: `ListPopover` 実測 12 / `richCells` 実測 46)。3,456 + 175 = 3,631、ファイルは
|
|
44
|
+
72 → 74) 込み。E2E は **21 ファイル 236 テスト** 全緑 (2026-09-16 — 新設 `tests/e2e/trailing-freeze.spec.ts` 18 +
|
|
45
|
+
`styles.spec.ts` の UNGATED 書込カウンタ / バブルブラケット +1。帯件数ゲートは計画 §5.8 の選択分岐列 = 単一 45 /
|
|
46
|
+
16 レンジ 450)。ADR-22 step 5c 認証は横レッグ発火 (per-event p90 > 2.0 ms @ pre-derate 720 ノード) — 選択分岐
|
|
47
|
+
(`bandRangeCapFor` 9 象限 cap 10 = 450 帯ノード) の認証記録 (縦 in-callback rAF p90 1.35 ms / 横 per-event p90 1.80 ms /
|
|
48
|
+
events-per-frame 分布 {"1": 60} / HeadlessChrome 148.0.7778.96) は分冊 08 §9。step 5e 登記パス再実測 (デレート済み
|
|
49
|
+
9 象限アンビエント、2 solo run 最小値): 自前 copy 26.5 ms / 自前 Ctrl+V 29.2 ms / メニュー paste 61.2 ms / 外来 Sheets
|
|
50
|
+
99,900 セル 1,001.9 ms。TF 変異台帳は全 24 行 ✅ red 実測 (分冊 07 §5.10 の判定列 — 生存者 0、TF-5 の初回生存は
|
|
51
|
+
§5.3 (c) スキャナ強化で kill)。
|
|
52
|
+
出荷済み 0.8.0 節は出荷時リリースノートに凍結 (`hxCensus.spec.ts` の本文 SHA-256 ピン) — 台帳が動いたとき更新する
|
|
53
|
+
のは本節と分冊 07 §2 台帳だけで、released ノートは遡及更新しない。
|
|
54
|
+
|
|
5
55
|
## 0.8.0 (2026-09-14)
|
|
6
56
|
|
|
7
57
|
v0.8 はリッチセル — 宣言式のセル書式 / 注釈 / 検証 / 条件付き書式、Excel / Google Sheets クリップボード相互運用、
|
package/README.md
CHANGED
|
@@ -19,6 +19,9 @@ deep-reach proof route.
|
|
|
19
19
|
- 🛡️ **Never silently freezes**: every bulk operation (copy / cut / delete / paste / fill / resize fan-out / autofit) is size-estimated **before** any expansion and rejected past its `maxOperationCells`-derived ceiling with a typed callback
|
|
20
20
|
- ♿ **Screen-reader announcements (opt-in)**: provide the formatter props and cell/selection changes are announced via a debounced live region — the package ships no built-in strings
|
|
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
|
+
- 📌 **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
|
+
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"
|
|
22
25
|
- 🎨 **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
|
|
23
26
|
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
|
|
24
27
|
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)`
|
|
@@ -281,8 +284,11 @@ When the pressed track belongs to a **full row/column selection** (header select
|
|
|
281
284
|
every selected track — hidden tracks included, which makes fan-out an implicit unhide (Excel-compatible). Fan-outs and
|
|
282
285
|
hides are gated (see the limits table above; row hides are additionally capped at 1,000 hidden rows per axis).
|
|
283
286
|
|
|
284
|
-
**Double-click autofit** (columns): each selected column is measured individually over the cells of the **
|
|
285
|
-
|
|
287
|
+
**Double-click autofit** (columns): each selected column is measured individually over the cells of the **leading frozen
|
|
288
|
+
band ∪ current visible window ∪ trailing frozen band** (not all data; each set filtered to visible rows — v0.9 ADR-21:
|
|
289
|
+
a pinned totals row is typically the widest cell and used to go unmeasured, and the leading band joins the set as a
|
|
290
|
+
deliberate behaviour change for existing `frozenRows > 0` users) and set to the widest text + padding; columns whose
|
|
291
|
+
measured cells are all empty are left untouched. The measured-cell budget gate counts band rows.
|
|
286
292
|
The measured text is the **formatted display string** (what the screen actually shows — v0.7): a currency column autofits
|
|
287
293
|
to `"¥203,428"`, not to the raw `"203428"`, which used to left-clip the most significant digits of right-aligned cells.
|
|
288
294
|
Row autofit restores the default row height (cells render single-line clipped). Autofit is disabled entirely when
|
|
@@ -323,6 +329,51 @@ transaction. On success the selection becomes the box (anchor and active stay pu
|
|
|
323
329
|
`enableFillHandle={false}` removes the handle and inerts the machine — recommended for read-only adapters, where every fill
|
|
324
330
|
attempt would otherwise end in `onEditRejected`.
|
|
325
331
|
|
|
332
|
+
## Trailing freeze and the freeze divider (v0.9)
|
|
333
|
+
|
|
334
|
+
`frozenTrailingColumns` / `frozenTrailingRows` pin the **last** N tracks of the same data as static right / bottom bands
|
|
335
|
+
(count-based, Glide-style `freezeTrailingRows` asymmetry — deliberately **not** an extra-rows API, which would break the
|
|
336
|
+
flat address space). Trailing cells are ordinary cells: selection, fill, clipboard, editing and popovers cross the
|
|
337
|
+
boundary with zero special-casing (fragments are clipped per quadrant); a trailing cell edits in place like any other.
|
|
338
|
+
Trailing rows/columns are **positional** — always the last N of the adapter's data — so a consumer that implements
|
|
339
|
+
sorting must exempt its totals rows itself (see deviations). Effective counts clamp with **leading wins**
|
|
340
|
+
(`min(N, count − effectiveLeading)`), and when the leading + trailing pixel extent exceeds the viewport (overlap), the
|
|
341
|
+
occluded trailing cells cannot be scrolled into view — registered consequence, not an error state.
|
|
342
|
+
|
|
343
|
+
**Freeze divider.** Every freeze boundary — leading and trailing, both axes, body and headers — draws a strong
|
|
344
|
+
Excel / Sheets-style divider line over the **scroll band's** edge pixels (content scrolls in under the line; frozen band
|
|
345
|
+
cells are never occluded). The package owns the nodes (`[data-aqsp-freeze-divider]`, `pointer-events: none`) and reads
|
|
346
|
+
two CSS custom properties — the only consumer surface (there is no prop):
|
|
347
|
+
|
|
348
|
+
| Variable | Default | Meaning |
|
|
349
|
+
| :--- | :--- | :--- |
|
|
350
|
+
| `--aqsp-divider-color` | `#9ca3af` (gray-400 — one step darker than every gridline) | Divider colour |
|
|
351
|
+
| `--aqsp-divider-width` | `2px` (Sheets strength) | Divider thickness |
|
|
352
|
+
|
|
353
|
+
```css
|
|
354
|
+
/* consumer stylesheet — any scope that contains the grid root */
|
|
355
|
+
.my-sheet {
|
|
356
|
+
--aqsp-divider-color: #475569;
|
|
357
|
+
--aqsp-divider-width: 3px;
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
- Setting `--aqsp-divider-width: 0px` is the escape hatch: the per-quadrant fill-fragment seams at each freeze boundary
|
|
362
|
+
re-appear (they are normally absorbed by the divider), and the forced-colors rule paints nothing either — intended.
|
|
363
|
+
- The divider scales in **logical px** with the package zoom (at z = 0.25 a 2px divider renders 0.5 visual px —
|
|
364
|
+
registered deviation).
|
|
365
|
+
- The divider is **inert**: a drag on the bar starts a header selection; the resize strip sits immediately adjacent on
|
|
366
|
+
the band side of the line (freeze-by-drag stays out of scope). Under forced colors the divider paints `CanvasText`.
|
|
367
|
+
- Under full overlap (middle band collapsed) no divider renders at all — there are no scroll pixels to overlay — and the
|
|
368
|
+
leading / trailing fill fragments butt directly, re-exposing that seam (registered artefact).
|
|
369
|
+
|
|
370
|
+
**Trailing resize idiom (pinned-right).** Trailing header strips sit at each trailing track's left / top boundary and
|
|
371
|
+
resize the track on their **far side** (column: right, row: below); the dragged edge is the moving edge, so the pending
|
|
372
|
+
size is `initialSize − delta` (dragging left / up grows the track). Double-click autofits the same far-side track. At
|
|
373
|
+
T = 1 every grow gesture pins the guideline at the band edge while the size follows the pointer. Hidden trailing runs
|
|
374
|
+
restore from the **band-side** near-edge unhide markers (the scroll-side markers pointing into the band are deduplicated
|
|
375
|
+
away while the band is visible; with every trailing track hidden the scroll-side marker remains the only restore path).
|
|
376
|
+
|
|
326
377
|
## Accessibility model and trade-offs
|
|
327
378
|
|
|
328
379
|
The grid renders cells as a **non-focusable virtualized surface** with a hidden 1px textarea ("sink") owning focus and
|
|
@@ -575,6 +626,8 @@ Keymap notes:
|
|
|
575
626
|
| `defaultRowHeight` / `defaultColumnWidth` | `number` | Integer px (defaults 24 / 100). Per-column widths are applied through the imperative `resizeColumn` (the v0.3 `initialColumnWidth` prop was retired in v0.4). |
|
|
576
627
|
| `frozenColumns` | `number` | Freezes the first N columns at the left edge (v0.5; default 0, LIVE). Engine-validated (integer in [0, 128] — `RangeError` otherwise; values beyond `colCount` freeze every column). Frozen columns stay visible at any horizontal scroll; `scrollToCell` / keyboard ensure-visible treat them as horizontal no-ops. |
|
|
577
628
|
| `frozenRows` | `number` | Freezes the first N rows at the top edge (v0.6; default 0, LIVE — the row-axis twin of `frozenColumns`). Engine-validated (integer in [0, 128] — `RangeError` otherwise; values beyond `rowCount` freeze every row). Frozen rows stay visible at any vertical scroll; keyboard ensure-visible treats them as vertical no-ops. |
|
|
629
|
+
| `frozenTrailingColumns` | `number` | Freezes the **last** N columns at the right edge (v0.9; default 0, LIVE). Engine-validated pass-through, no second validation layer (integer in [0, 128] — `RangeError` otherwise); the effective count is clamped so the leading freeze wins the count space (`min(N, colCount − effectiveFrozenColumns)`). Trailing columns stay visible at any horizontal scroll — **except** when the leading + trailing extent exceeds the viewport: occluded trailing cells cannot be scrolled into view and keyboard focus draws no visible frame there (registered overlap consequence). `scrollToCell` / keyboard ensure-visible treat trailing targets as horizontal no-ops. An in-flight change of any of the four freeze props cancels every drag without restore. |
|
|
630
|
+
| `frozenTrailingRows` | `number` | Freezes the **last** N rows at the bottom edge (v0.9; default 0, LIVE — the row-axis twin of `frozenTrailingColumns`; a pinned totals row is the canonical use). Same engine validation and leading-wins clamp; trailing rows stay visible at any vertical scroll (same overlap exception); keyboard ensure-visible treats them as vertical no-ops. |
|
|
578
631
|
| `renderCell` | `(ctx: RenderCellContext) => ReactNode` — `RenderCellContext = { row; col; text }` (exported) | Custom cell rendering. **Must be identity-stable** (`useCallback`) — an inline arrow defeats the cell memo and re-renders every visible cell each pass. |
|
|
579
632
|
| `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". |
|
|
580
633
|
| `enableResizeUI` | `boolean` | Default `true`. `false` removes resize strips **and unhide markers** — hidden tracks then have no in-package restore path (imperative API only). |
|
|
@@ -708,7 +761,7 @@ The `/utils` demo route exercises the v0.1 toolkit live; the `/styles` route dri
|
|
|
708
761
|
| Fill-handle double-click | No-op | Fills down to adjacent data | Needs composition with `findDataEdge` (roadmap) |
|
|
709
762
|
| Copy / TSV of formatted cells | Carries the **raw** canonical `getCellText` | Copies the **display** string into `text/plain` | Raw preserves the numeric round-trip (paste back, spreadsheets, importers); the display string is a lossy projection |
|
|
710
763
|
| Too-narrow formatted cells | Content clips (right-aligned cells clip on the **left**, Sheets-style); autofit now measures the formatted display, preventing the common case | Renders `####` for numeric cells that don't fit | A `####` substitute needs per-cell width probing on every paint; clipping keeps rendering O(visible) |
|
|
711
|
-
| Autofit scope |
|
|
764
|
+
| Autofit scope | Leading band ∪ visible window ∪ trailing band measurement (columns — v0.9 ADR-21; the band inclusion is a deliberate behaviour change for existing `frozenRows > 0` users) / default-height restore (rows) | Scans all data / wrap-measures row heights | Scanning 1M rows would freeze; band rows (totals) are typically the widest cells; cells render single-line clipped |
|
|
712
765
|
| Autofit / dblclick caret with `renderCell` | Disabled (no-op / caret at end) | — | Custom cell DOM cannot be measured or offset-mapped |
|
|
713
766
|
| Same-value resize release | No-op | Equalizes all selected columns to the dragged width | A no-move release should change nothing |
|
|
714
767
|
| Hiding everything | The last visible track of an axis clamps to 1px | Allows hiding all columns/rows | No select-all + Unhide menu exists here — an all-hidden grid would be unrecoverable |
|
|
@@ -748,6 +801,16 @@ The `/utils` demo route exercises the v0.1 toolkit live; the `/styles` route dri
|
|
|
748
801
|
| Validation list picker | Case-insensitive **prefix** filter; mounts at most 500 options per render — a larger match set appends one `listMore` row naming the hidden count | Sheets filters by substring and virtualises the list | A pasted foreign rule may carry an arbitrarily large values array; mounting it whole would freeze the tab, and the prefix filter reaches everything beyond the cap |
|
|
749
802
|
| Self-calc | Writes canonical decimals (exponent-form inputs keep exponent form), flips `TRUE` / `FALSE` case-insensitively, leaves custom checkbox labels alone, treats an `=`-prefixed text as text unless `getCellFormula` is declared, and wraps compound formulas (`=(A1)/100+(B1)/100` × 100 ⇒ `=((A1)/100+(B1)/100)*100`) instead of peeling | The reference extension peels greedily | The greedy regex produces the broken `=A1)/100+(B1` |
|
|
750
803
|
| Excel / Word HTML without a table | `mso-` paragraphs (Word / Outlook) paste as text, not as an unreadable Excel clip | — | No table reader can read them; ADR-14 forbids trying another reader |
|
|
804
|
+
| Freeze divider interactivity | The divider is inert (`pointer-events: none`): a drag on the bar starts a header selection; the resize strip sits immediately adjacent on the band side of the line | In Sheets this exact visual is the freeze drag handle | Excel behaviour (static thin line) in Sheets clothing — freeze-by-drag would be its own freeze-UI feature, out of scope |
|
|
805
|
+
| Freeze divider under zoom | Logical px — scales with the package zoom (z = 0.25 renders a 2px divider at 0.5 visual px) | Excel / Sheets dividers are UI chrome, unaffected by sheet zoom | One coordinate space for all chrome; a zoom-compensating width var is a recorded future path, not added for this alone |
|
|
806
|
+
| `--aqsp-divider-width: 0px` | Escape hatch: per-quadrant fill-fragment seams re-appear at every freeze boundary, and forced-colors paints nothing | — | The divider is what absorbs those seams; removing it re-exposes the pre-v0.9 artefact — intended |
|
|
807
|
+
| Occluded trailing cells (overlap) | When leading + trailing extent exceeds the viewport, occluded trailing cells cannot be scrolled into view and keyboard focus draws no visible frame; the count clamp gives leading the count space | Excel refuses to freeze past the viewport | Count-based trailing freeze is LIVE and viewport-independent; the degenerate state is registered, not guarded |
|
|
808
|
+
| Overlap divider collapse | With the middle band fully collapsed no divider renders, and the leading / trailing fill fragments butt directly (seam re-exposed) | — | Divider pixels live only on scroll-band pixels (structural placement rule); a collapsed middle band has none |
|
|
809
|
+
| Ctrl+Home / Ctrl+End with freezes | Native pass-through (unchanged) | Land on the first / last unfrozen cell | Freeze-aware Ctrl+Home/End is a NEW feature — recorded future work, not part of the trailing-freeze release |
|
|
810
|
+
| Trailing resize idiom | Near-edge one-sided strips; each strip controls its far-side track; the dragged edge is the moving edge (pinned-right); double-click autofits the same far-side track; T = 1 grow gestures pin the guide at the band edge; hidden trailing runs restore from band-side near-edge markers | Excel has no trailing freeze | Mirrors the shipped leading idiom under the pinned-right constraint |
|
|
811
|
+
| Trailing autofit boundary side | An internal trailing boundary autofits the track **after** it (far side); the visually identical leading-side boundary autofits the track **before** it | — | Consequence of pinned-right control — the strip and its controlled track sit on opposite sides of the two idioms |
|
|
812
|
+
| Trailing rows under consumer sorting | Trailing rows are positional (the last N of the same data) — a consumer implementing sorting must exempt its totals rows itself | — | The package has no sort/filter feature (adapter-owned pull model); the footer-permutation trap is a consumer contract |
|
|
813
|
+
| Frozen-band shadow | None — the divider is the only boundary emphasis | Sheets adds a drop shadow while scrolled | Derivability is kept (`getFrozenSize()` trailing fields); a shadow is a recorded future path |
|
|
751
814
|
|
|
752
815
|
## Demo (feature showcase, doubles as the E2E fixture)
|
|
753
816
|
|
|
@@ -760,8 +823,8 @@ Run `pnpm run demo:dev` from a source checkout (the `demo/` app is not in the np
|
|
|
760
823
|
| `/edit` | Editing, the reference inverse-patch Undo/Redo store, `findDataEdge` Ctrl+Arrow jumps, value-based auto-alignment via a bare `getCellFormat: () => undefined` declaration (typed numbers right-align like Excel), the imperative resize handle, and a Sheet A/B toggle (adapter-identity swap: selection resets to A1, the pending edit and an armed copy are discarded, an armed cut is kept as a foreign clip for a cross-sheet move) |
|
|
761
824
|
| `/render` | `renderCell` formatting (currency / delta / meter / badge) over canonical strings, the selection readout (`onSelectionChange`), a custom context menu on both origins (right-click and Shift+F10), `onCopyMultiRange` |
|
|
762
825
|
| `/format` | The built-in display formats (v0.7): a typed sheet whose columns declare `getCellFormat` (number with grouping / currency ¥ / percent / scientific / date / explicit-center text) with **no** `renderCell` — value-aware auto-alignment, raw-value editing and copy, formatted-display autofit, an **interactive format toolbar** (apply a value format and alignment to the selected cell/range independently, restore the column default — published through the documented `dataVersion` bump), plus the standard read-only wiring (`enableFillHandle={false}` and an `onEditRejected` readout whose wording is derived from the `EditRejection` context — a kept session says Escape cancels, a discarded one says the value is already gone — so Enter is never a silent dead key and a focus-out never gets bad advice) |
|
|
763
|
-
| `/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 |
|
|
764
|
-
| `/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 |
|
|
826
|
+
| `/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 |
|
|
827
|
+
| `/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) |
|
|
765
828
|
| `/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 |
|
|
766
829
|
| `/a11y` | Custom Japanese `formatCellAnnouncement` / `formatSelectionAnnouncement` mirrored into visible readouts, overridden accessible names (Escape advisement kept), the WCAG 2.1.2 escape route |
|
|
767
830
|
| `/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) |
|
|
@@ -796,11 +859,26 @@ seam never meets: ≈ 18 ms cold median on the B5 host, 3.7 / 13.1 ms on a 12-co
|
|
|
796
859
|
cells against a 10,000-item list paint their invalid marks with zero formula evaluations and one text read per cell, < 3 ms solo
|
|
797
860
|
(≈ 1.4 ms measured; the in-suite gate is 3 ms × 5 slack, again in an `it` of its own); `getRangeExtrema` is called exactly once per
|
|
798
861
|
value-less colour-scale range per render version (0 on a selection change); band repaints write only `transform` per scroll frame
|
|
799
|
-
(Chromium: per-repaint p90 ≤ 4 ms with 16 ranges and both axes frozen —
|
|
862
|
+
(Chromium: per-repaint p90 ≤ 4 ms with 16 ranges and both axes frozen — certified since v0.9 on the shipped nine-quadrant
|
|
863
|
+
`/styles` ambient (leading 1×1 plus trailing 1×1): the step-5c certification fired the pre-declared ADR-22 derate (trigger:
|
|
864
|
+
vertical p90 > 3.2 ms or horizontal per-event p90 > 2.0 ms; the horizontal leg exceeded 2.0 ms at the pre-derate 720 nodes —
|
|
865
|
+
the retired v0.8 four-quadrant registration was 1.30 ms solo), so `bandRangeCapFor` caps bands at 10 ranges beyond four
|
|
866
|
+
quadrants and the current gate column = 450 band nodes (10 × 5 × 9; at ≤ 4 quadrants the flat cap 16 is unchanged);
|
|
867
|
+
`styles.spec.ts` also adds the horizontal per-event wheel-bracket p90 ≤ 2 ms/event plus two zero-write invariants
|
|
868
|
+
(freeze-divider style writes = 0, fill non-transform writes = 0); since the 2026-09-16 review loop the plan §5.9 step-3
|
|
869
|
+
first-stage retreat is ADOPTED on top of the retained derate: `transform` itself is same-value cached on band segments and
|
|
870
|
+
fill slots, so a pure-horizontal wheel window writes only the moving x-kind-s third of the band clones (5,400 writes per
|
|
871
|
+
60-event window, deterministic — mechanically pinned by the window bound "band transform writes ≤ events × armed bands / 2");
|
|
872
|
+
the current certification record — horizontal per-event bracket p90 1.00 / 1.30 / 1.20 ms and vertical in-callback rAF p90
|
|
873
|
+
1.30 / 2.50 / 1.20 ms over three solo runs at 450 band nodes under session load 5.7-11.5 / 8 cores (the pre-retreat 1.80 ms
|
|
874
|
+
quietest-window figure had zero effective headroom under load), distribution {"1": 60}, host — is registered in spec 08 §9
|
|
875
|
+
and mirrored in spec 00 §6); the
|
|
800
876
|
Chromium gates on the 300 × 26 `/styles` demo (7,800 cells — a 100,000-cell own range is dimensionally unreachable there, so
|
|
801
877
|
the "100k" rows are certified whole-sheet, with the foreign table generated in-page) add an own copy of the whole sheet < 150 ms
|
|
802
|
-
(
|
|
803
|
-
|
|
878
|
+
(26.5 ms measured solo on the derated nine-quadrant ambient — 2026-09-16 step-5e registry pass, minimum of two solo runs
|
|
879
|
+
on the shared 8-core certification host; the retired v0.8 four-quadrant figures were 30.4 / 27.1 / 54.7 / 1,133.9 ms;
|
|
880
|
+
`html.length ≤ 160 × cells`), an own `Ctrl+V` of it < 120 ms (29.2 ms), a menu-path paste < 350 ms
|
|
881
|
+
(61.2 ms), a foreign 999 × 100 = 99,900-cell Sheets table < 1.5 s (1,001.9 ms), the 25,000-distinct-string autofit < 300 ms
|
|
804
882
|
(`tests/e2e/v2b.spec.ts`), and a `ctx.font` write-count gate on the same demo (one assignment per distinct painted font,
|
|
805
883
|
conditional-format italics included). The solo inventory above and the operation-limits table's row-axis ceilings are
|
|
806
884
|
themselves pinned by `src/hxCensus.spec.ts` against the spec files that read the variable and against `ROW_FANOUT_DIVISOR`.
|