@aiquants/spreadsheet 0.12.0 → 0.13.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 +60 -19
- package/dist/index.cjs +13 -13
- package/dist/index.d.cts +113 -119
- package/dist/index.d.ts +113 -119
- package/dist/index.js +3457 -3328
- package/package.json +3 -3
- package/src/Spreadsheet.tsx +68 -13
- package/src/grid/GridBody.tsx +30 -1
- package/src/index.ts +1 -1
- package/src/model/dataValidation.ts +84 -67
- package/src/model/labels.ts +313 -101
- package/src/model/validationMessages.ts +203 -0
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.13.0 (2026-09-24)
|
|
6
|
+
|
|
7
|
+
v0.13 は UI クロームの多言語化の列車 — 英語 (`"en"`) を既定、日本語 (`"ja"`) を第 2 言語とし、ラベル・組込み
|
|
8
|
+
検証メッセージ・グリッドのスクロールバーを 1 本の `locale` プロップで切り替える。ピア `@aiquants/virtualscroll`
|
|
9
|
+
3.7.0 のラベルカタログ (`VIRTUAL_SCROLL_LABEL_CATALOGS` / `resolveVirtualScrollLabels`) を単一真実源として再利用し、
|
|
10
|
+
エンジン由来の文字列は再記述しない。
|
|
11
|
+
|
|
12
|
+
- **新機能 — `locale` プロップ (UI クローム言語)**: `SpreadsheetProps.locale?: SpreadsheetLocale` (`"en"` | `"ja"`、
|
|
13
|
+
既定 `"en"` — 型 `SpreadsheetLocale` はエンジンの `VirtualScrollLocale` そのもの)。ラベルカタログ、組込み検証
|
|
14
|
+
メッセージ、グリッドの縦 / 横スクロールバー (矢印名とスクロール行 0 の空状態「No items」/「項目がありません」) を
|
|
15
|
+
選ぶ。非対応値 (`"fr"` / `"ja-JP"` / `"EN"` / `""` / `null`) は render 中 `RangeError` (0 行グリッドでも) —
|
|
16
|
+
言語ネゴシエーションはしない (`navigator.language` の写像はホスト側、対応一覧はピアの `VIRTUAL_SCROLL_LOCALES`)。
|
|
17
|
+
値書式の `formats.locale` (BCP-47) とは別軸。`lang` 属性は付けない。`locale` 省略と明示 `"en"` の DOM は
|
|
18
|
+
byte 同一 (v0.12 と同じ英語)。ライブ切替はグリッドを再マウントせず、選択・編集セッション・スクロール位置を
|
|
19
|
+
保ったまま文言だけ差し替える — 既知の制約: 開いたままの message ポップオーバーは提示時の言語のまま
|
|
20
|
+
(再提示で追随)。
|
|
21
|
+
- **新機能 — ロケール別ラベルカタログ**: `SPREADSHEET_LABEL_CATALOGS` (`en` / `ja`、外側と各カタログを凍結) と
|
|
22
|
+
`resolveLabels(locale, overrides)` を公開。`SpreadsheetLabels` は 28 キー = エンジン 7 キー (`scrollUp` /
|
|
23
|
+
`scrollDown` / `scrollLeft` / `scrollRight` / `scrollToTop` / `scrollToBottom` / `noItems` — 値は
|
|
24
|
+
`VIRTUAL_SCROLL_LABEL_CATALOGS` を再利用) + 自前 21 キー。`labels` で渡したエンジンキーもグリッドの
|
|
25
|
+
スクロールバーへ届く。GridBody へ渡すエンジン部分集合は 7 文字列の値で安定化し、インラインの
|
|
26
|
+
`labels={{…}}` でも本体の memo 境界を貫通しない。`en` は v0.12 までの英語既定と byte 同一。
|
|
27
|
+
- **新機能 — 組込み検証メッセージのロケール化**: `errorMessage` の無い拒否文は型 × op ごとの全文テンプレート
|
|
28
|
+
(日本語は境界が先に来る語順のため断片翻訳でなく文ごと) を `locale` から選ぶ — 例 `"Value must be a number
|
|
29
|
+
greater than 5."` / `"5 より大きい数値を入力してください。"`。`onValidationRejected` の `message` と message
|
|
30
|
+
ポップオーバー (題「Data validation」/「データの入力規則」) に効く。`errorMessage` (空文字含む) は従来どおり
|
|
31
|
+
言語に依らず優先。カタログは内部 (barrel 非公開)。
|
|
32
|
+
- **破壊的変更 (0.x 枠 — クリーンカットオーバー、互換別名なし)**:
|
|
33
|
+
- `DEFAULT_LABELS` を削除。移行: `DEFAULT_LABELS` → `SPREADSHEET_LABEL_CATALOGS.en`。
|
|
34
|
+
- 読む箇所の無かった 4 キー `validationRejected` / `indicatorNote` / `indicatorComment` / `indicatorInvalid`
|
|
35
|
+
を削除 (セル指標は属性だけの視覚フックでアクセシブルテキストを持たない)。移行: `labels` から当該キーを外す。
|
|
36
|
+
- `labels` の未知キー (上の退役キーを含む) と空白 / 非文字列の値は render 中 `RangeError` (従来は未知キーを
|
|
37
|
+
黙って無視、空文字を受理)。
|
|
38
|
+
- `validateCellText(rule, text, evaluate?)` → `validateCellText(rule, text, locale, evaluate?)` — 第 3 引数
|
|
39
|
+
`locale` は必須で判定より先に検証 (`undefined` / 非対応値は `RangeError` — 純関数に既定値を隠さない)。
|
|
40
|
+
移行: `validateCellText(rule, text, evaluate)` → `validateCellText(rule, text, "en", evaluate)`。
|
|
41
|
+
- ピア `@aiquants/virtualscroll` は ^3.7.0 が必要 (ラベルカタログ・解決器と `locale` / `labels` プロップ)。
|
|
42
|
+
- **デモの多言語化**: `?lang=en|ja` が言語の単一真実源 (既定 en — 不正値は `RangeError` で空白ページ)。ナビ末尾の
|
|
43
|
+
切替 `[data-demo-locale-select]` は再読込なしで URL・`<html lang>`・タブ表題・全ルートのクロームを差し替え、
|
|
44
|
+
ナビのリンクは `?lang` を運ぶ。全 `<Spreadsheet>` へ `locale` を渡す。セルのサンプルデータ (シード /
|
|
45
|
+
フィクスチャ / 読み取り専用ストア) は言語不変で日本語のまま (E2E の検査対象そのもの)。ページ別カタログは
|
|
46
|
+
型付き・凍結で、カタログ census (`catalogIssues` — キー過不足 / 空白 / ja = en / 和文スペース規則) を持つ。
|
|
47
|
+
- **E2E**: 既定経路は英語クローム — 既存 14 spec のクローム文言 assertion を en カタログ値へ置換 (件数不変)。
|
|
48
|
+
新設 `tests/e2e/i18n.spec.ts` 8 が `?lang=ja` 経路 (ポップオーバー / 読み上げ / 拒否トースト / スクロールバー
|
|
49
|
+
矢印) とライブ切替 (再読込・再マウントなしの保持) と不正 `?lang` の fail-fast、ja-JP ブラウザでも既定は英語
|
|
50
|
+
(ブラウザ言語からの言語ネゴシエーションなし) を実ブラウザで固定 (24 ファイル 271 テスト)。
|
|
51
|
+
- **検証 — 走行台帳 (census の錨は本未リリース節)**: 単体 / 統合 **92 ファイル 4,083 テスト** 全緑 —
|
|
52
|
+
0.12.0 出荷時の 82 ファイル 3,851 に v0.13 進行分 +232 (新規 `validationMessages` 11 + `localePassthrough` 9 +
|
|
53
|
+
デモ側 8 ファイル 153 (`locale` 18 / `common` 21 / `corePages` 24 / `gridPages` 28 / `toolPages` 15 /
|
|
54
|
+
`stylesPage` 17 / `stylesPanels` 24 / `readOnlyRejection` 6)、既存増分 `labels` +27 / `Spreadsheet` +8 /
|
|
55
|
+
`dataValidation` +7 / `GridBody` +3 / `popovers` +2 / popover 部品 +9 / `editRejection` +2、0.12.0 節の
|
|
56
|
+
出荷時凍結ピンで `hxCensus` +1)。公開面は `index.ts` の値 40 + 型 98 = 138 export (`DEFAULT_LABELS` 退役、
|
|
57
|
+
`SPREADSHEET_LABEL_CATALOGS` / `resolveLabels` / 型 `SpreadsheetLocale` 追加)。
|
|
58
|
+
|
|
5
59
|
## 0.12.0 (2026-09-22)
|
|
6
60
|
|
|
7
61
|
v0.12 はセルの見た目をホストへ渡す 2 つの継ぎ目 — `getCellAttributes` (ホスト `data-*` 属性の素通し) と
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ deep-reach proof route.
|
|
|
17
17
|
|
|
18
18
|
- 🕳️ **Hidden tracks are first-class**: zero-size rows/columns render nothing, arrows and Shift-arrows jump a hidden run in one keystroke, Home/End land on visible tracks, and rendering stays fast across 100,000-column hidden runs
|
|
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
|
-
- ♿ **Screen-reader announcements (opt-in)**: provide the formatter props and cell/selection changes are announced via a debounced live region — the
|
|
20
|
+
- ♿ **Screen-reader announcements (opt-in)**: provide the formatter props and cell/selection changes are announced via a debounced live region — the announcement wording is always yours (the built-in `locale` catalogs cover UI chrome only)
|
|
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
|
|
@@ -39,8 +39,9 @@ deep-reach proof route.
|
|
|
39
39
|
- ⌨️ **Rebindable Sheets-style commands** (ADR-17): 70 `SpreadsheetCommand` literals (bold / italic / underline / strikethrough / alignment / number formats / borders / links / notes / comments / fill down / right / selection / date / time / hide / unhide / paste special, plus unbound extension
|
|
40
40
|
commands for colours, self-calc and highlight toggles) driven by ONE built-in table (`BUILTIN_ROWS`), a canonical chord grammar (`Ctrl+Alt+Shift+KEY`, letters by the produced character or the `code` letter, everything else by `event.code`), prop-time validation with `RangeError`s, and an
|
|
41
41
|
imperative `executeCommand` / `applyFormat` / `setBorders` / `setNote` / `addComment` / `setHyperlink` / `setValidation` handle group
|
|
42
|
-
- 💬 **Popover layer and highlight bands** (ADR-15): note editor + hover note tip, comment thread, hyperlink editor, validation list picker and validation message on two resident anchors positioned by the overlay (zero DOM for a v0.7 adapter with default props, `renderPopover` to replace the defaults, a
|
|
42
|
+
- 💬 **Popover layer and highlight bands** (ADR-15): note editor + hover note tip, comment thread, hyperlink editor, validation list picker and validation message on two resident anchors positioned by the overlay (zero DOM for a v0.7 adapter with default props, `renderPopover` to replace the defaults, a `labels` bag folding the former `sinkAriaLabel` / `rootAriaLabel` props — see "Localization"), and Excel-style active row / column / intersection highlight bands (transform-only per scroll frame, `--aqsp-band-*` colours, controlled / uncontrolled `highlight` state)
|
|
43
43
|
- 🧭 **Formula reference pointing**: with `getCellFormula` declared, typing `=SUM(` and clicking, dragging or arrowing over cells inserts / replaces the reference token at the caret (`A1`, `A1:B2`, `A:A`, `1:1`), F4 cycles `$` anchors, F2 toggles the arm — the editing session, selection and sink position stay put
|
|
44
|
+
- 🌐 **English / Japanese UI chrome** (v0.13): one `locale` prop (`"en"` default, `"ja"`) switches the accessible names, popover strings, built-in validation messages and the grid scrollbars' arrow names / empty state — live, without remounting the grid; unsupported values throw a `RangeError` (no language negotiation — see "Localization")
|
|
44
45
|
- 🔧 **TypeScript**: complete type definitions, ESM + CJS artifacts
|
|
45
46
|
|
|
46
47
|
## Installation
|
|
@@ -49,7 +50,7 @@ deep-reach proof route.
|
|
|
49
50
|
pnpm add @aiquants/spreadsheet @aiquants/virtualscroll
|
|
50
51
|
```
|
|
51
52
|
|
|
52
|
-
`react` / `react-dom` (`^19.2`) and `@aiquants/virtualscroll` are peer dependencies.
|
|
53
|
+
`react` / `react-dom` (`^19.2`) and `@aiquants/virtualscroll` (`^3.7.0` — its label catalogs back the `locale` prop) are peer dependencies.
|
|
53
54
|
|
|
54
55
|
> **Browser floor**: the built-in cell display formats (v0.7) require `Intl.NumberFormat` v3
|
|
55
56
|
> (string mathematical-value input + `signDisplay: "negative"`) — **Chrome/Edge 106+, Firefox 116+,
|
|
@@ -253,8 +254,8 @@ undo utilities.
|
|
|
253
254
|
Idle fills, the fill handle, paste and self-calc are not validated — it is a commit-time gate, as in Excel.
|
|
254
255
|
|
|
255
256
|
An unchanged commit (opening the editor and leaving without changing the text) never reaches `applyEdits`, so a plain focus-out on a
|
|
256
|
-
read-only sheet produces no rejection at all. The demo's `demo/src/components/editRejection.ts` (`describeEditRejection`) is a
|
|
257
|
-
reference for wording a notice from this context.
|
|
257
|
+
read-only sheet produces no rejection at all. The demo's `demo/src/components/editRejection.ts` (`describeEditRejection(rejection, routeLabel, locale)`) is a
|
|
258
|
+
reference for wording a notice from this context in English or Japanese.
|
|
258
259
|
|
|
259
260
|
## Operation limits (`maxOperationCells`, default 100,000)
|
|
260
261
|
|
|
@@ -449,16 +450,18 @@ The grid renders cells as a **non-focusable virtualized surface** with a hidden
|
|
|
449
450
|
keyboard input — this is what makes IME composition, clipboard events, and 16M-cell virtualization coexist. The trade-offs:
|
|
450
451
|
|
|
451
452
|
- Screen readers do not walk cells as a native table/grid. Instead, cell and selection changes can be **announced** through a
|
|
452
|
-
debounced `aria-live="polite"` region — **strictly opt-in**: the package ships no built-in strings, and the live region is
|
|
453
|
+
debounced `aria-live="polite"` region — **strictly opt-in**: the package ships no built-in announcement strings (the `locale` catalogs cover UI chrome only), and the live region is
|
|
453
454
|
rendered only when you provide `formatCellAnnouncement` and/or `formatSelectionAnnouncement`. Omit both and the grid is
|
|
454
455
|
silent to assistive technology beyond its labels. The cell-announcement context carries both `text` (the raw canonical
|
|
455
456
|
`getCellText`) and `displayText` (the formatted string a sighted user sees — identical to `text` on unformatted cells),
|
|
456
457
|
so a formatter can announce `"87.0%"` rather than `"0.87"`.
|
|
457
|
-
- `labels.sinkAriaLabel` labels the input surface (its
|
|
458
|
-
labels the grid container (
|
|
459
|
-
`rootAriaLabel` props were folded into it
|
|
460
|
-
|
|
461
|
-
|
|
458
|
+
- `labels.sinkAriaLabel` labels the input surface (its catalog value explains the Escape hand-off — in English by default, in
|
|
459
|
+
Japanese under `locale="ja"`); `labels.rootAriaLabel` labels the grid container (`"Spreadsheet"` / `"スプレッドシート"`). Both
|
|
460
|
+
live in the `labels` prop (v0.8 — the former `sinkAriaLabel` / `rootAriaLabel` props were folded into it; together with the 19
|
|
461
|
+
popover strings they are the 21 package-owned keys, and the 7 engine keys name the grid scrollbars' arrows, edge pills and
|
|
462
|
+
empty state). The `locale` prop picks the catalog (`SPREADSHEET_LABEL_CATALOGS[locale]`), and an omitted or `undefined` key
|
|
463
|
+
keeps that catalog's value. Switching `locale` localizes both names together; if you override them, **override both**, and a
|
|
464
|
+
replacement `sinkAriaLabel` **must keep the Escape-exit notice** — the word `Escape` must appear, otherwise the grid throws a `RangeError` while rendering: Escape moves
|
|
462
465
|
focus from the sink to the root, and without the notice (or with an unlabeled landing point) screen-reader users are stranded
|
|
463
466
|
mid-grid. Popovers are keyboard-complete (`role="dialog"` editors with an autofocused field, `role="tooltip"` note tip wired to
|
|
464
467
|
the sink's `aria-describedby` while its cell is active — a while-invariant, re-evaluated as arrow keys move the active cell in
|
|
@@ -686,6 +689,37 @@ Keymap notes:
|
|
|
686
689
|
never change while pointing; the corner, resize strips, fill handle, scrollbar bands and zoom dead space are consumed without
|
|
687
690
|
inserting anything. A grid-exit focus-out commits the partial formula as typed.
|
|
688
691
|
|
|
692
|
+
## Localization
|
|
693
|
+
|
|
694
|
+
The grid's own UI chrome ships in English (`"en"`, the default) and Japanese (`"ja"`). One prop selects it:
|
|
695
|
+
|
|
696
|
+
```tsx
|
|
697
|
+
<Spreadsheet adapter={adapter} locale="ja" />
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
| Surface | `en` | `ja` |
|
|
701
|
+
| --- | --- | --- |
|
|
702
|
+
| Root / sink accessible names (`rootAriaLabel` / `sinkAriaLabel`) | `Spreadsheet` / `Spreadsheet grid. Press Escape to move focus out of the grid.` | `スプレッドシート` / `スプレッドシートのグリッド。Escape キーでグリッドの外へフォーカスを移動します。` |
|
|
703
|
+
| Popovers (note / comment / link / list / message, e.g. `noteTitle`, `commentCount(2)`, `messageTitle`, `popoverClose`) | `Note`, `2 comments`, `Data validation`, `Close` | `メモ`, `2 件のコメント`, `データの入力規則`, `閉じる` |
|
|
704
|
+
| Grid scrollbars (engine keys `scrollUp` … `noItems`) | `Scroll up`, `Scroll left`, `No items` | `上へスクロール`, `左へスクロール`, `項目がありません` |
|
|
705
|
+
| Built-in validation messages (rules without `errorMessage`) | `Value must be a number greater than 5.` | `5 より大きい数値を入力してください。` |
|
|
706
|
+
|
|
707
|
+
- **Catalogs**: `SPREADSHEET_LABEL_CATALOGS.en` / `.ja` are frozen and exported; the 7 scrollbar keys are the values of
|
|
708
|
+
`VIRTUAL_SCROLL_LABEL_CATALOGS` from `@aiquants/virtualscroll`, reused rather than restated. `labels` overrides any subset on top
|
|
709
|
+
of the active catalog (`resolveLabels(locale, overrides)` is the same fold, exported for hosts that render their own chrome).
|
|
710
|
+
- **No language negotiation**: `locale` accepts exactly the supported values — `VIRTUAL_SCROLL_LOCALES` from the peer
|
|
711
|
+
`@aiquants/virtualscroll` (not re-exported here). `"ja-JP"`, `"EN"`, `""`, `null` or an unknown language throw a `RangeError`
|
|
712
|
+
during render. Map `navigator.language` (or your router / user setting) yourself, e.g.
|
|
713
|
+
`const locale = navigator.language.toLowerCase().startsWith("ja") ? "ja" : "en"`.
|
|
714
|
+
- **Two different locales**: `locale` is the UI chrome language; `formats.locale` is the BCP-47 value-formatting locale used by
|
|
715
|
+
`formatCurrency` and HTML paste recovery. Cell values keep formatting per their `getCellFormat` regardless of `locale`.
|
|
716
|
+
- **No `lang` attribute is stamped**: the host owns the document's and ancestors' language (`<html lang>`).
|
|
717
|
+
- **Live switching**: changing `locale` re-words the chrome in place — selection, the edit session, scroll position and the grid
|
|
718
|
+
DOM survive. An already open validation message popover keeps the language it was presented in until it is shown again.
|
|
719
|
+
- **Your own strings stay yours**: announcement formatters (`formatCellAnnouncement` / `formatSelectionAnnouncement` /
|
|
720
|
+
`formatCommandAnnouncement` / `formatReadOnlyAnnouncement`), rule `errorMessage` / `inputMessage`, and cell data are never
|
|
721
|
+
translated by the package.
|
|
722
|
+
|
|
689
723
|
## Props reference
|
|
690
724
|
|
|
691
725
|
| Prop | Type (abridged) | Notes |
|
|
@@ -710,17 +744,18 @@ Keymap notes:
|
|
|
710
744
|
| `onUndoRequest` / `onRedoRequest` | `() => void` | `Ctrl+Z` / `Ctrl+Y` / `Ctrl+Shift+Z` relays. |
|
|
711
745
|
| `formatCellAnnouncement` / `formatSelectionAnnouncement` | formatter | Opt-in screen-reader announcements (see above). |
|
|
712
746
|
| `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. |
|
|
713
|
-
| `labels` | `SpreadsheetLabelOverrides` (any subset of the
|
|
747
|
+
| `labels` | `SpreadsheetLabelOverrides` (any subset of the 28 `SpreadsheetLabels` keys) | v0.8 — replaces the `sinkAriaLabel` / `rootAriaLabel` props (folded in) and names every UI chrome string: the 7 engine keys shared with `@aiquants/virtualscroll` (`scrollUp`, `scrollDown`, `scrollLeft`, `scrollRight`, `scrollToTop`, `scrollToBottom`, `noItems` — they reach the grid scrollbars) and the 21 package keys (`sinkAriaLabel`, `rootAriaLabel`, `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`, `popoverClose`). Omitted / `undefined` keys keep the value of the `locale` catalog (`SPREADSHEET_LABEL_CATALOGS[locale]`); an unknown key (including the removed `validationRejected` / `indicatorNote` / `indicatorComment` / `indicatorInvalid`) or a blank / non-string value throws a `RangeError` during render; `sinkAriaLabel` must mention `Escape` (`RangeError` during render otherwise). |
|
|
748
|
+
| `locale` | `SpreadsheetLocale` (`"en" \| "ja"`) | v0.13 — the UI chrome language, default `"en"`: selects the label catalog, the built-in validation messages (`onValidationRejected`'s `message` and the message popover) and the grid scrollbars' arrow names / empty state. Any other value (`"fr"`, `"ja-JP"`, `"EN"`, `""`, `null`) throws a `RangeError` during render, even on an empty grid — there is no language negotiation. Unrelated to `formats.locale`. See "Localization". |
|
|
714
749
|
| `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. |
|
|
715
750
|
| `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). |
|
|
716
|
-
| `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. |
|
|
751
|
+
| `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. `formats.locale` is a BCP-47 value-formatting locale (`"ja-JP"`, …) and is independent of the UI-chrome `locale` prop — changing one never changes the other. |
|
|
717
752
|
| `highlight` / `defaultHighlight` / `onHighlightChange` | `HighlightState` = `{ row: boolean; column: boolean; intersection: boolean }` / same / `(h) => void` | v0.8 — active row / column / intersection highlight bands, controlled (`highlight` + `onHighlightChange`) or uncontrolled (`defaultHighlight`), the same two-phase contract as `zoom`. Default all off ⇒ zero band nodes (v0.7 DOM identity). Toggled by the `toggle*Highlight` commands. When exactly ONE family (row XOR column) is on and `intersection` is off, the cross node lights up tinted with that family's colour so the band runs unbroken through the selection (sheets-enhancer parity — with both families on the hole is kept). A whole-row / whole-column selection (row / column header press, Shift+Space / Ctrl+Space) draws no row / column bands — only the intersection — so the cross-axis band never tints the rest of the sheet. |
|
|
718
753
|
| `highlightColors` | `HighlightColors` = `{ row; column; intersection; opacity }` | v0.8 — identity-stable band palette written to `--aqsp-band-row` / `-column` / `-intersection` / `-opacity` on the overlay in a layout effect; each colour must parse (`normalizeColor`), `opacity` must be a finite number in [0, 1] (`RangeError` otherwise). Undeclared ⇒ nothing is written while every band is off and `DEFAULT_HIGHLIGHT_COLORS` (`#0084FF` / `#42FF4F` / `#E1FF00` / 0.2, exported) is written once a band turns on. |
|
|
719
754
|
| `popovers` | `PopoverOverrides` = `{ [kind]?: boolean }` (`note` / `note-tip` / `comment` / `link` / `list` / `message`) | v0.8 — `false` disables a kind: its commands pass through and `handle.openPopover(kind)` throws a `RangeError`. The popover layer mounts only when at least one kind is enabled AND its capability is declared. |
|
|
720
755
|
| `renderPopover` | `(ctx: PopoverContext) => ReactNode` | v0.8 — replaces the default popover components; `ctx.kind` discriminates the six contexts (`NotePopoverContext` … `MessagePopoverContext`, exported) and the defaults (`NotePopover` / `NoteTip` / `CommentPopover` / `LinkPopover` / `ListPopover` / `MessagePopover`) are exported so a renderer can replace one kind and delegate the rest. |
|
|
721
756
|
| `onClipChange` | `(clip: ClipInfo \| null) => void` | v0.8 — armed clip notice: `{ kind: "copy" \| "cut", range, foreign }` on arm, `null` on discard, `foreign: true` for a cut kept across a sheet switch. Idempotent (no call when nothing changes). |
|
|
722
757
|
| `onHyperlinkOpen` | `(url: string, cell: CellCoord) => void` | v0.8 — navigation seam for `Alt+Enter`, the link popover's Open button and Ctrl / Meta+click on a link cell; `url` is always a normalised allow-listed href (`normalizeHyperlink` output — an adapter URL outside `http:` / `https:` / `mailto:` / `tel:` never reaches this seam, so `window.open(url)` is safe) (the click is consumed with the selection frozen; Ctrl+Shift stays range-extend); undeclared ⇒ no navigation, no Open button and no pointer gesture. |
|
|
723
|
-
| `onValidationRejected` | `(ctx: { cell; rule; text; message }) => void` | v0.8 — a `strict` validation rule refused a changed commit (the adapter was not called; the message popover shows `message`). |
|
|
758
|
+
| `onValidationRejected` | `(ctx: { cell; rule; text; message }) => void` | v0.8 — a `strict` validation rule refused a changed commit (the adapter was not called; the message popover shows `message`). `message` is the rule's `errorMessage` when set (even `""`), otherwise the built-in message in the active `locale`. |
|
|
724
759
|
| `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. |
|
|
725
760
|
| `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). |
|
|
726
761
|
| `now` | `() => number` | v0.8 — clock seam of `insertDate` / `insertTime` (declared default `Date.now`). |
|
|
@@ -791,7 +826,7 @@ importers with the exact same Excel-compatible semantics the grid uses. `enumera
|
|
|
791
826
|
`onOperationRejected` op), and `EditRejection` (the `onEditRejected` context — see "Editing, Undo, and rejection") are exported for
|
|
792
827
|
consumer-side annotations.
|
|
793
828
|
|
|
794
|
-
v0.8 adds the rich-cell engines as public pure functions (all React-free; `src/index.ts` exports
|
|
829
|
+
v0.8 adds the rich-cell engines as public pure functions (all React-free; `src/index.ts` exports 40 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):
|
|
795
830
|
|
|
796
831
|
- **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`),
|
|
797
832
|
`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).
|
|
@@ -799,10 +834,11 @@ v0.8 adds the rich-cell engines as public pure functions (all React-free; `src/i
|
|
|
799
834
|
before every allocation, so the reader never holds more than `maxCells` cells — and rows are pulled lazily through the sibling links, so the stop also ends the row walk), `{ outcome: "none" }` when there is no table; `maxCells` must be a positive integer or the call throws a `RangeError` before any `DOMParser` is built; its parameter types `HtmlTableKind` / `NumberFormatDecodeOptions`).
|
|
800
835
|
- **Formats** — `canonicalizeFormat` / `mergeFormat` / `formatEquals`, `BORDER_DEFAULT_COLOR`, and the types `CellFormat` / `CellFormatPatch` / `CellBorder` / `CellBorders` / `CellBorderStyle` / `CellVerticalAlign` (plus the v0.7 `CellAlign` / `CellValueFormat` / `formatCellValue`).
|
|
801
836
|
- **Formulas** — `shiftFormula(formula, dRow, dCol)` (relative references only, `#REF!` below zero), `toR1C1(formula, origin)` / `fromR1C1(r1c1, origin)` (the clip's origin-relative formula form, Google Sheets spelling `R[-1]C[0]`).
|
|
802
|
-
- **Annotations and validation** — `normalizeHyperlink(text)` (`http:` / `https:` / `mailto:` / `tel:` only, WHATWG-serialised href or `null`), `validateCellText(rule, text, evaluate?)`
|
|
837
|
+
- **Annotations and validation** — `normalizeHyperlink(text)` (`http:` / `https:` / `mailto:` / `tel:` only, WHATWG-serialised href or `null`), `validateCellText(rule, text, locale, evaluate?)` (`locale` is required — `"en"` or `"ja"`, anything else including `undefined` throws a `RangeError` before the verdict; the built-in message follows `locale`, while `errorMessage` — even `""` — always wins) with the types `DataValidationRule` (its base `ValidationRuleBase`, the seven members `ListValidationRule` / `NumberValidationRule` /
|
|
838
|
+
`TextLengthValidationRule` / `DateValidationRule` / `TextValidationRule` / `CheckboxValidationRule` / `CustomValidationRule`,
|
|
803
839
|
the op vocabularies `ValidationCompareOp` / `ValidationTextOp` and the evaluator `ValidationEvaluate`) / `ValidationVerdict` / `CellComment` / `CellCommentInput` / `CellPatch` / `CellEdit` / `EditSource` / `EditContext` / `PlainEditContext` / `PasteEditContext`.
|
|
804
840
|
- **Conditional formats** — the types `ConditionalFormatRule` / `ConditionalFormatRuleInput` / `ConditionalFormatStyle` / `CfCondition` (its six members `CellValueCondition` / `TextCondition` / `EmptyCondition` / `NotEmptyCondition` / `ColorScaleCondition` / `CustomFormulaCondition` and the `text` op vocabulary `CfTextOp`) / `CompareOp` / `ScalePoint` / `RangeExtrema` / `FormulaValue`.
|
|
805
|
-
- **Commands and labels** — `DEFAULT_SHORTCUTS`, `SPREADSHEET_RESERVED_SHORTCUTS`, `parseChord`, `DEFAULT_CUSTOM_COLORS`, `
|
|
841
|
+
- **Commands and labels** — `DEFAULT_SHORTCUTS`, `SPREADSHEET_RESERVED_SHORTCUTS`, `parseChord`, `DEFAULT_CUSTOM_COLORS`, `SPREADSHEET_LABEL_CATALOGS` (the frozen `en` / `ja` label catalogs), `resolveLabels(locale, overrides)` (the single label fold — the same validation the `labels` prop gets), `resolveLabel(labels, key, n?)`, and the types `SpreadsheetCommand` / `ShortcutMap` / `ShortcutOverrides` / `ParsedChord` / `SpreadsheetLabels` / `SpreadsheetLabelOverrides` / `SpreadsheetLocale` / `BorderPreset`.
|
|
806
842
|
- **Paste Special** — the types `PasteMode` / `PasteOperation` / `PasteOptions` and the `handle.paste` outcome `PasteOutcome` (`"applied" | "empty" | "rejected"`).
|
|
807
843
|
- **Popovers and bands** — the six default components `NotePopover` / `NoteTip` / `CommentPopover` / `LinkPopover` / `ListPopover` / `MessagePopover` (with their `*Props`), the types `PopoverKind` / `PopoverOverrides` / `PopoverContext` and its six members (their `anchorRect` is a `PopoverRect`), `DEFAULT_HIGHLIGHT_COLORS` and the types `HighlightState` / `HighlightColors`.
|
|
808
844
|
|
|
@@ -894,7 +930,12 @@ The `/utils` demo route exercises the v0.1 toolkit live; the `/styles` route dri
|
|
|
894
930
|
## Demo (feature showcase, doubles as the E2E fixture)
|
|
895
931
|
|
|
896
932
|
Run `pnpm run demo:dev` from a source checkout (the `demo/` app is not in the npm tarball) and open
|
|
897
|
-
`http://localhost:5179`. Ten routes, each an in-header-nav page AND an E2E fixture (`tests/e2e/`)
|
|
933
|
+
`http://localhost:5179`. Ten routes, each an in-header-nav page AND an E2E fixture (`tests/e2e/`). The demo UI is English by
|
|
934
|
+
default; `?lang=ja` (or the language switcher `[data-demo-locale-select]` at the end of the nav) switches every route's chrome and
|
|
935
|
+
passes `locale` to every `<Spreadsheet>` without a reload — the URL keeps `?lang`, nav links carry it, `<html lang>` and the tab
|
|
936
|
+
title follow, and an unsupported `?lang` value fails fast with a `RangeError` (blank page). Sample cell data (seeds, fixtures, the
|
|
937
|
+
read-only store) is locale-invariant and stays Japanese in both languages — it is the fixture under test. `tests/e2e/i18n.spec.ts`
|
|
938
|
+
covers the `?lang=ja` path and the live switch; every other E2E spec runs the English default:
|
|
898
939
|
|
|
899
940
|
| Route | What you experience |
|
|
900
941
|
| --- | --- |
|
|
@@ -905,7 +946,7 @@ Run `pnpm run demo:dev` from a source checkout (the `demo/` app is not in the np
|
|
|
905
946
|
| `/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 |
|
|
906
947
|
| `/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) |
|
|
907
948
|
| `/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 |
|
|
908
|
-
| `/a11y` |
|
|
949
|
+
| `/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 |
|
|
909
950
|
| `/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 |
|
|
910
951
|
| `/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) |
|
|
911
952
|
|