@aiquants/spreadsheet 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1103 -0
- package/LICENSE.md +155 -0
- package/README.md +817 -0
- package/dist/index.cjs +21 -0
- package/dist/index.d.cts +1960 -0
- package/dist/index.d.ts +1960 -0
- package/dist/index.js +7711 -0
- package/dist/styles/spreadsheet.css +2 -0
- package/dist/styles/spreadsheet.standalone.css +3 -0
- package/package.json +94 -0
- package/src/Spreadsheet.tsx +2313 -0
- package/src/clipboard/clipPayload.ts +1242 -0
- package/src/clipboard/fixtures/excel.fixture.ts +301 -0
- package/src/clipboard/fixtures/html.fixture.ts +90 -0
- package/src/clipboard/fixtures/sheets.fixture.ts +83 -0
- package/src/clipboard/htmlInterop.ts +1694 -0
- package/src/clipboard/numberFormatCodec.ts +870 -0
- package/src/clipboard/pasteSpecial.ts +608 -0
- package/src/clipboard/tiling.ts +370 -0
- package/src/clipboard/tsv.ts +186 -0
- package/src/clipboard/useClipboard.ts +1154 -0
- package/src/commands/selfCalc.ts +279 -0
- package/src/commands/useCommands.ts +946 -0
- package/src/editor/useCellEditor.ts +708 -0
- package/src/grid/CellView.tsx +51 -0
- package/src/grid/GridBody.tsx +489 -0
- package/src/header/ColumnHeader.tsx +167 -0
- package/src/header/RowHeader.tsx +144 -0
- package/src/header/columnName.ts +31 -0
- package/src/header/resizeGates.ts +146 -0
- package/src/header/resizeStrips.ts +79 -0
- package/src/header/useHeaderResize.ts +598 -0
- package/src/header/useHeaderSelection.ts +454 -0
- package/src/index.ts +122 -0
- package/src/keyboard/compositionGuard.ts +53 -0
- package/src/keyboard/ensureCellVisible.ts +77 -0
- package/src/keyboard/shortcuts.ts +718 -0
- package/src/keyboard/useGridKeyboard.ts +940 -0
- package/src/model/axisModel.ts +186 -0
- package/src/model/cellAnnotations.ts +92 -0
- package/src/model/cellCoord.ts +114 -0
- package/src/model/cellFormat.ts +562 -0
- package/src/model/cellPaint.ts +537 -0
- package/src/model/conditionalFormat.ts +805 -0
- package/src/model/dataAdapter.ts +478 -0
- package/src/model/dataValidation.ts +542 -0
- package/src/model/formulaRefs.ts +984 -0
- package/src/model/frozenPartition.ts +100 -0
- package/src/model/labels.ts +228 -0
- package/src/model/viewStore.ts +124 -0
- package/src/overlay/SelectionOverlay.tsx +1425 -0
- package/src/overlay/useFillHandle.ts +432 -0
- package/src/popover/CommentPopover.tsx +109 -0
- package/src/popover/LinkPopover.tsx +108 -0
- package/src/popover/ListPopover.tsx +161 -0
- package/src/popover/MessagePopover.tsx +36 -0
- package/src/popover/NotePopover.tsx +106 -0
- package/src/popover/NoteTip.tsx +26 -0
- package/src/popover/PopoverLayer.tsx +210 -0
- package/src/popover/placement.ts +138 -0
- package/src/popover/usePopovers.ts +642 -0
- package/src/selection/selectionState.ts +239 -0
- package/src/selection/useAutoScroller.ts +226 -0
- package/src/selection/usePointDrag.ts +273 -0
- package/src/selection/useSelectionEngine.ts +687 -0
- package/src/styles/components.entry.css +10 -0
- package/src/styles/spreadsheet.css +1029 -0
- package/src/styles/standalone.entry.css +9 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Frozen-axis coordinate partition (v0.5 plan §2 = columns; v0.6 plan §2 = rows).
|
|
3
|
+
* The engine paints frozen leading cells at their raw tree coordinate and scroll-band cells at
|
|
4
|
+
* tree − scroll, so a viewport ↔ index mapping splits at the frozen prefix size F_px: the
|
|
5
|
+
* forward map is `i < F ? offsetOf(i) : offsetOf(i) − scroll`, the inverse map (guarded on
|
|
6
|
+
* F_px > 0) is `F_px > 0 && p < F_px ? indexAt(p) : indexAt(p + scroll)`. Pure, AXIS-NEUTRAL
|
|
7
|
+
* internals (frozenSizeOf / toViewportPos / viewportPosToIndex) with column- and row-named
|
|
8
|
+
* thin wrappers — the freshness discipline (F/scroll from the handle) stays at the call sites.
|
|
9
|
+
*
|
|
10
|
+
* 凍結軸の座標パーティション (v0.5 計画 §2 = 列、v0.6 計画 §2 = 行)。エンジンは先頭凍結セルを
|
|
11
|
+
* 木座標の生値・スクロール帯セルを 木座標 − scroll で描くため、viewport ↔ index の写像は凍結
|
|
12
|
+
* 接頭辞幅 F_px で分割される。純関数の**軸中立**内部 (frozenSizeOf / toViewportPos /
|
|
13
|
+
* viewportPosToIndex) に列 / 行の名前付き薄ラッパーを重ねる — 鮮度規律 (F / scroll はハンドル
|
|
14
|
+
* 読み) は呼び出しサイトの責務。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { AxisModel } from "./axisModel.ts"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Axis-neutral tree-prefix size of the frozen band (0 when frozen <= 0 or empty axis; totalSize
|
|
21
|
+
* when frozen covers every track). Both frozenWidthOf and frozenHeightOf delegate here.
|
|
22
|
+
* 軸中立の凍結帯木接頭辞サイズ (frozen ≤ 0 / 空軸は 0、全トラック凍結は totalSize)。
|
|
23
|
+
* frozenWidthOf / frozenHeightOf の共通実体。
|
|
24
|
+
*/
|
|
25
|
+
const frozenSizeOf = (axis: AxisModel, frozen: number): number => {
|
|
26
|
+
if (frozen <= 0 || axis.count === 0) {
|
|
27
|
+
return 0
|
|
28
|
+
}
|
|
29
|
+
return frozen >= axis.count ? axis.totalSize() : axis.offsetOf(frozen)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Axis-neutral forward map: index to viewport position. Frozen leading tracks are static; scroll
|
|
34
|
+
* tracks subtract the scroll offset.
|
|
35
|
+
* 軸中立の順写像: index → viewport 位置。先頭凍結トラックは静的、スクロールトラックは scroll を引く。
|
|
36
|
+
*/
|
|
37
|
+
const toViewportPos = (axis: AxisModel, index: number, scroll: number, frozen: number): number => (index < frozen ? axis.offsetOf(index) : axis.offsetOf(index) - scroll)
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Axis-neutral inverse map: viewport position to index. The frozen branch exists ONLY when
|
|
41
|
+
* F_px > 0 — below F_px it resolves at the raw tree coordinate (negative p clamps into the
|
|
42
|
+
* leading frozen track, Excel-like nearest visible); at/after F_px the scroll band continues at
|
|
43
|
+
* tree = p + scroll. At F_px = 0 every p (incl. negative) keeps the v0.4 scroll-relative form
|
|
44
|
+
* (guard prevents an unguarded p < 0 from sinking into indexAt(p) = track 0).
|
|
45
|
+
* 軸中立の逆写像: viewport 位置 → index。凍結分岐は F_px > 0 のときのみ — F_px 未満は木座標の
|
|
46
|
+
* 生値 (負 p は先頭凍結トラックへクランプ)、以上はスクロール帯 (木 = p + scroll)。F_px = 0 は
|
|
47
|
+
* 負 p も含め全域 v0.4 形 (無ガードの p < 0 → indexAt(p) = トラック 0 の吸込を防ぐ)。
|
|
48
|
+
*/
|
|
49
|
+
const viewportPosToIndex = (axis: AxisModel, p: number, scroll: number, frozenSize: number): number => (frozenSize > 0 && p < frozenSize ? axis.indexAt(p) : axis.indexAt(p + scroll))
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Tree-prefix width of the frozen band on this axis (0 when F <= 0; totalSize when F covers
|
|
53
|
+
* every column). The render-surface twin of the engine's getFrozenSize().width — the pair-call
|
|
54
|
+
* discipline keeps the two equal.
|
|
55
|
+
* この軸の凍結帯の木接頭辞幅 (F ≤ 0 は 0、全列凍結は totalSize)。エンジン getFrozenSize().width
|
|
56
|
+
* の render 面双子 — 対呼び出し規律が両者を一致させる。
|
|
57
|
+
*/
|
|
58
|
+
export const frozenWidthOf = (axis: AxisModel, frozenCols: number): number => frozenSizeOf(axis, frozenCols)
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Frozen-band tree-prefix HEIGHT H_F (row twin of frozenWidthOf — v0.6 plan §2).
|
|
62
|
+
* 凍結帯の木接頭辞高 H_F (frozenWidthOf の行双子 — v0.6 計画 §2)。
|
|
63
|
+
*/
|
|
64
|
+
export const frozenHeightOf = (axis: AxisModel, frozenRows: number): number => frozenSizeOf(axis, frozenRows)
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Forward map: column index to viewport x (the left edge). Frozen columns are static; scroll
|
|
68
|
+
* columns subtract hx (v0.5 plan §2).
|
|
69
|
+
* 順写像: 列 index → viewport x (左端)。凍結列は静的、スクロール列は hx を引く。
|
|
70
|
+
*/
|
|
71
|
+
export const colToViewportX = (axis: AxisModel, col: number, hx: number, frozenCols: number): number => toViewportPos(axis, col, hx, frozenCols)
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Forward map: row index to viewport y (the top edge). Frozen rows static; scroll rows subtract vy (v0.6 plan §2).
|
|
75
|
+
* 順写像: 行 index → viewport y (上端)。凍結行は静的、スクロール行は vy を引く (v0.6 計画 §2)。
|
|
76
|
+
*/
|
|
77
|
+
export const rowToViewportY = (axis: AxisModel, row: number, vy: number, frozenRows: number): number => toViewportPos(axis, row, vy, frozenRows)
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Inverse map: viewport x to column index. The frozen branch exists ONLY when W_F > 0: below
|
|
81
|
+
* W_F the frozen band resolves at the raw tree coordinate (negative px clamps into the leading
|
|
82
|
+
* frozen column — Excel-like nearest visible); at or right of W_F the scroll band continues at
|
|
83
|
+
* tree = px + hx. At W_F = 0 every px — including negative (pointer left of the cells area
|
|
84
|
+
* during a leftward drag) — keeps the v0.4 hx-relative form (R2 fix: an unguarded `px < 0`
|
|
85
|
+
* would swallow negative px into indexAt(px) → column 0, breaking the F = 0 identity).
|
|
86
|
+
* 逆写像: viewport x → 列 index。凍結分岐は W_F > 0 のときのみ存在: W_F 未満は凍結帯
|
|
87
|
+
* (木座標の生値 — 負の px は先頭凍結列へクランプ。Excel 同型の最寄り可視)、W_F 以上は
|
|
88
|
+
* スクロール帯 (木座標 = px + hx)。W_F = 0 は負の px (左ドラッグでセル領域外へ出た
|
|
89
|
+
* ポインタ) も含め全域が v0.4 の hx 相対形 (R2 是正: 無ガードの `px < 0` は負 px を
|
|
90
|
+
* indexAt(px) = 列 0 へ吸い込み、F = 0 恒等を破る)。
|
|
91
|
+
*/
|
|
92
|
+
export const viewportXToCol = (axis: AxisModel, px: number, hx: number, frozenWidth: number): number => viewportPosToIndex(axis, px, hx, frozenWidth)
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Inverse map: viewport y to row index (row twin of viewportXToCol — v0.6 plan §2). Guarded on
|
|
96
|
+
* H_F > 0; H_F = 0 keeps the v0.4 vy-relative form for all py including negative.
|
|
97
|
+
* 逆写像: viewport y → 行 index (viewportXToCol の行双子 — v0.6 計画 §2)。H_F > 0 ガードつき、
|
|
98
|
+
* H_F = 0 は負 py も含め v0.4 の vy 相対形。
|
|
99
|
+
*/
|
|
100
|
+
export const viewportYToRow = (axis: AxisModel, py: number, vy: number, frozenHeight: number): number => viewportPosToIndex(axis, py, vy, frozenHeight)
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview UI label vocabulary (v0.8 §1.6 / §4.1.17) — the ONE label bag of the package.
|
|
3
|
+
* The v0.7 `sinkAriaLabel` / `rootAriaLabel` props are folded into it (clean cut-over: no alias
|
|
4
|
+
* props remain), joined by the popover / indicator strings of the rich-cell UI. Pure, React-free.
|
|
5
|
+
*
|
|
6
|
+
* Contract (each clause is pinned by labels.spec.ts):
|
|
7
|
+
* - `SpreadsheetLabels` is the closed key set; `DEFAULT_LABELS` supplies an English default for
|
|
8
|
+
* every key. Count-aware keys are typed `string | ((n: number) => string)`.
|
|
9
|
+
* - `resolveLabels(overrides)` is the single fold point: a key takes its override when the
|
|
10
|
+
* override is not `undefined` and the default otherwise (exactly the destructuring-default
|
|
11
|
+
* semantics the two folded props had in v0.7), then `assertLabels` runs on the result.
|
|
12
|
+
* `undefined` overrides resolve to `DEFAULT_LABELS` by identity.
|
|
13
|
+
* - `assertLabels`: `sinkAriaLabel` MUST be a string mentioning "Escape" (WCAG 2.1.2 escape-method
|
|
14
|
+
* advisement, spec 00 §10-4) — otherwise a RangeError, thrown at mount by the root.
|
|
15
|
+
* - `resolveLabel(labels, key, n)` is the single read point: a function-valued label receives `n`
|
|
16
|
+
* (omitting `n` for a function value is a RangeError — never a silent 0), a string is returned
|
|
17
|
+
* as-is (`n` ignored, so a consumer may override a count key with a plain string).
|
|
18
|
+
*
|
|
19
|
+
* UI ラベル語彙 (v0.8 §1.6 / §4.1.17) — パッケージ唯一のラベル集合。v0.7 の `sinkAriaLabel` /
|
|
20
|
+
* `rootAriaLabel` prop を畳み込み (クリーンカットオーバー: 別名 prop は残さない)、リッチセル UI の
|
|
21
|
+
* ポップオーバー / インジケータ文言を加える。純関数・React 非依存。
|
|
22
|
+
* 契約: `SpreadsheetLabels` は閉じたキー集合、`DEFAULT_LABELS` は全キーの英語既定、件数キーは
|
|
23
|
+
* `string | ((n: number) => string)`。`resolveLabels` が唯一の畳込み点 (undefined でない override を
|
|
24
|
+
* 採用、それ以外は既定 — 畳み込んだ 2 prop が v0.7 で持っていた分割代入既定と同じ意味論。
|
|
25
|
+
* override 未指定は `DEFAULT_LABELS` を identity で返却) で、続けて `assertLabels`
|
|
26
|
+
* (`sinkAriaLabel` は "Escape" を含む文字列であること — spec 00 §10-4、違反はマウント時 RangeError)。
|
|
27
|
+
* `resolveLabel` が唯一の読み出し点 (関数値は n 必須 — 省略は RangeError、文字列はそのまま)。
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The closed label vocabulary. Every key has a default in DEFAULT_LABELS; consumers override any
|
|
32
|
+
* subset through the `labels` prop (SpreadsheetLabelOverrides).
|
|
33
|
+
* 閉じたラベル語彙。全キーが DEFAULT_LABELS に既定を持ち、消費者は `labels` prop で任意の部分集合を上書き。
|
|
34
|
+
*/
|
|
35
|
+
export type SpreadsheetLabels = {
|
|
36
|
+
/** Accessible name of the sink (grid input receptor); MUST mention "Escape" (spec 00 §10-4). / シンクのアクセシブルネーム ("Escape" 必須)。 */
|
|
37
|
+
sinkAriaLabel: string
|
|
38
|
+
/** Accessible name of the root role="group" landing point. / root 着地点のアクセシブルネーム。 */
|
|
39
|
+
rootAriaLabel: string
|
|
40
|
+
/** Note popover heading. / ノートポップオーバーの見出し。 */
|
|
41
|
+
noteTitle: string
|
|
42
|
+
/** Note popover save button. / ノートの保存ボタン。 */
|
|
43
|
+
noteSave: string
|
|
44
|
+
/** Note popover hint in the normal state (Escape saves and closes). / 通常状態のノートヒント。 */
|
|
45
|
+
noteEscapeHint: string
|
|
46
|
+
/** Note popover hint in the rejected state (Ctrl+Enter retries, Escape closes without saving). / 拒否状態のノートヒント。 */
|
|
47
|
+
noteRejectedHint: string
|
|
48
|
+
/** Comment popover heading. / コメントポップオーバーの見出し。 */
|
|
49
|
+
commentTitle: string
|
|
50
|
+
/** Comment reply field label. / 返信入力欄のラベル。 */
|
|
51
|
+
commentReply: string
|
|
52
|
+
/** Comment post button. / コメント投稿ボタン。 */
|
|
53
|
+
commentPost: string
|
|
54
|
+
/** Comment count text; a function receives the thread length. / コメント件数 (関数はスレッド長を受け取る)。 */
|
|
55
|
+
commentCount: string | ((n: number) => string)
|
|
56
|
+
/** Link popover heading. / リンクポップオーバーの見出し。 */
|
|
57
|
+
linkTitle: string
|
|
58
|
+
/** Link popover display-text field label. / リンク表示文字列の入力欄ラベル。 */
|
|
59
|
+
linkTextField: string
|
|
60
|
+
/** Link popover URL field label. / リンク URL の入力欄ラベル。 */
|
|
61
|
+
linkUrlField: string
|
|
62
|
+
/** Link popover open button (only when onHyperlinkOpen is declared). / リンクを開くボタン。 */
|
|
63
|
+
linkOpen: string
|
|
64
|
+
/** Link popover save button. / リンクの保存ボタン。 */
|
|
65
|
+
linkSave: string
|
|
66
|
+
/** Validation-list popover heading. / 検証リストポップオーバーの見出し。 */
|
|
67
|
+
listTitle: string
|
|
68
|
+
/** Validation-list popover text when no option matches. / 候補なしの表示。 */
|
|
69
|
+
listEmpty: string
|
|
70
|
+
/** Validation-list popover overflow row (options beyond the render cap); a function receives the hidden-option count. / 検証リストの上限超過行 (描画上限を超えた候補) — 関数は非表示件数を受け取る。 */
|
|
71
|
+
listMore: string | ((n: number) => string)
|
|
72
|
+
/** Message popover heading. / メッセージポップオーバーの見出し。 */
|
|
73
|
+
messageTitle: string
|
|
74
|
+
/** Message shown when a strict validation rule rejects the typed value. / 厳格検証で拒否されたときの文言。 */
|
|
75
|
+
validationRejected: string
|
|
76
|
+
/** Close button of every popover. / ポップオーバー共通の閉じるボタン。 */
|
|
77
|
+
popoverClose: string
|
|
78
|
+
/** Accessible label of the note indicator on a cell. / ノートインジケータのラベル。 */
|
|
79
|
+
indicatorNote: string
|
|
80
|
+
/** Accessible label of the comment indicator on a cell. / コメントインジケータのラベル。 */
|
|
81
|
+
indicatorComment: string
|
|
82
|
+
/** Accessible label of the invalid-value indicator on a cell. / 無効値インジケータのラベル。 */
|
|
83
|
+
indicatorInvalid: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The `labels` prop shape: any subset of SpreadsheetLabels (an omitted or `undefined` key keeps
|
|
88
|
+
* the default). Keys are closed to SpreadsheetLabels.
|
|
89
|
+
* `labels` prop の形: SpreadsheetLabels の任意の部分集合 (省略 / undefined は既定のまま)。キーは
|
|
90
|
+
* SpreadsheetLabels に閉じる。
|
|
91
|
+
*/
|
|
92
|
+
export type SpreadsheetLabelOverrides = { readonly [K in keyof SpreadsheetLabels]?: SpreadsheetLabels[K] }
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Every label key, in declaration order — the iteration source of the fold and the census list
|
|
96
|
+
* for docs / tests. Compile-time proof below that it covers EVERY SpreadsheetLabels key.
|
|
97
|
+
* 全ラベルキー (宣言順) — 畳込みの走査元と docs / テストの census 表。直下で全キー網羅を型水準で証明。
|
|
98
|
+
*/
|
|
99
|
+
export const LABEL_KEYS = [
|
|
100
|
+
"sinkAriaLabel",
|
|
101
|
+
"rootAriaLabel",
|
|
102
|
+
"noteTitle",
|
|
103
|
+
"noteSave",
|
|
104
|
+
"noteEscapeHint",
|
|
105
|
+
"noteRejectedHint",
|
|
106
|
+
"commentTitle",
|
|
107
|
+
"commentReply",
|
|
108
|
+
"commentPost",
|
|
109
|
+
"commentCount",
|
|
110
|
+
"linkTitle",
|
|
111
|
+
"linkTextField",
|
|
112
|
+
"linkUrlField",
|
|
113
|
+
"linkOpen",
|
|
114
|
+
"linkSave",
|
|
115
|
+
"listTitle",
|
|
116
|
+
"listEmpty",
|
|
117
|
+
"listMore",
|
|
118
|
+
"messageTitle",
|
|
119
|
+
"validationRejected",
|
|
120
|
+
"popoverClose",
|
|
121
|
+
"indicatorNote",
|
|
122
|
+
"indicatorComment",
|
|
123
|
+
"indicatorInvalid",
|
|
124
|
+
] as const satisfies readonly (keyof SpreadsheetLabels)[]
|
|
125
|
+
|
|
126
|
+
/** Compile-time proof that LABEL_KEYS lists EVERY SpreadsheetLabels key (a key added to the type but not to the list would never be folded). / LABEL_KEYS が SpreadsheetLabels の全キーを網羅することの型水準の証明 (型に増えたキーが表に無いと畳込みから漏れる)。 */
|
|
127
|
+
const labelKeysCoverEveryKey: [Exclude<keyof SpreadsheetLabels, (typeof LABEL_KEYS)[number]>] extends [never] ? true : never = true
|
|
128
|
+
void labelKeysCoverEveryKey
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* English defaults for every key. `sinkAriaLabel` / `rootAriaLabel` are byte-identical to the
|
|
132
|
+
* v0.7 prop defaults (the fold is DOM-identical); `noteEscapeHint` = "Escape saves and closes";
|
|
133
|
+
* `commentCount` and `listMore` pluralise ("1 comment" / "n comments", "1 more value" / "n more values"). Frozen.
|
|
134
|
+
* 全キーの英語既定。`sinkAriaLabel` / `rootAriaLabel` は v0.7 の prop 既定と byte 同一 (畳込みは
|
|
135
|
+
* DOM 恒等)、`noteEscapeHint` = "Escape saves and closes"、`commentCount` / `listMore` は単複を区別。凍結済み。
|
|
136
|
+
*/
|
|
137
|
+
export const DEFAULT_LABELS: SpreadsheetLabels = Object.freeze({
|
|
138
|
+
sinkAriaLabel: "Spreadsheet grid. Press Escape to move focus out of the grid.",
|
|
139
|
+
rootAriaLabel: "Spreadsheet",
|
|
140
|
+
noteTitle: "Note",
|
|
141
|
+
noteSave: "Save",
|
|
142
|
+
noteEscapeHint: "Escape saves and closes",
|
|
143
|
+
noteRejectedHint: "The note was rejected. Press Ctrl+Enter to retry or Escape to close without saving",
|
|
144
|
+
commentTitle: "Comments",
|
|
145
|
+
commentReply: "Reply",
|
|
146
|
+
commentPost: "Post",
|
|
147
|
+
commentCount: (n: number): string => (n === 1 ? "1 comment" : `${n} comments`),
|
|
148
|
+
linkTitle: "Link",
|
|
149
|
+
linkTextField: "Text",
|
|
150
|
+
linkUrlField: "URL",
|
|
151
|
+
linkOpen: "Open",
|
|
152
|
+
linkSave: "Save",
|
|
153
|
+
listTitle: "Choose a value",
|
|
154
|
+
listEmpty: "No matching values",
|
|
155
|
+
listMore: (n: number): string => (n === 1 ? "1 more value — keep typing to narrow" : `${n} more values — keep typing to narrow`),
|
|
156
|
+
messageTitle: "Data validation",
|
|
157
|
+
validationRejected: "The value does not satisfy this cell's validation rule",
|
|
158
|
+
popoverClose: "Close",
|
|
159
|
+
indicatorNote: "Has a note",
|
|
160
|
+
indicatorComment: "Has comments",
|
|
161
|
+
indicatorInvalid: "Invalid value",
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Mount-time check of a resolved label bag: `sinkAriaLabel` must be a string that mentions
|
|
166
|
+
* "Escape" (case-sensitive — the WCAG 2.1.2 escape-method advisement of spec 00 §10-4). Anything
|
|
167
|
+
* else is a consumer configuration error ⇒ RangeError (fail-fast, never silently repaired).
|
|
168
|
+
* 解決済みラベル集合のマウント時検証: `sinkAriaLabel` は "Escape" を含む文字列であること
|
|
169
|
+
* (大文字小文字を区別 — spec 00 §10-4 の WCAG 2.1.2 脱出告知)。それ以外は消費者の設定誤りとして
|
|
170
|
+
* RangeError (fail-fast、黙って修復しない)。
|
|
171
|
+
*/
|
|
172
|
+
export const assertLabels = (labels: SpreadsheetLabels): void => {
|
|
173
|
+
const sink = labels.sinkAriaLabel
|
|
174
|
+
// 文字列以外 (JS 呼び手) と Escape 告知の欠落は同じ設定誤り
|
|
175
|
+
if (typeof sink !== "string" || !sink.includes("Escape")) {
|
|
176
|
+
throw new RangeError(`[spreadsheet] labels.sinkAriaLabel must be a string mentioning "Escape" (WCAG 2.1.2 escape advisement, spec 00 §10-4); got ${JSON.stringify(sink)}`)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Writes one resolved label; generic so the union-typed key of the fold loop type-checks against the per-key value type. / 解決済みラベルを 1 件書く (畳込みループの union キーをキー毎の値型と突き合わせるための総称関数)。 */
|
|
181
|
+
const putLabel = <K extends keyof SpreadsheetLabels>(target: SpreadsheetLabels, key: K, value: SpreadsheetLabels[K]): void => {
|
|
182
|
+
target[key] = value
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The single fold point of the `labels` prop: every key takes the override when it is not
|
|
187
|
+
* `undefined`, the default otherwise (destructuring-default semantics of the folded v0.7 props);
|
|
188
|
+
* the result is validated by assertLabels and frozen. `undefined` overrides return DEFAULT_LABELS
|
|
189
|
+
* by identity. Inputs are never mutated.
|
|
190
|
+
* `labels` prop の唯一の畳込み点: 各キーは override が undefined でなければ override、さもなくば
|
|
191
|
+
* 既定 (畳み込んだ v0.7 prop の分割代入既定の意味論)。結果は assertLabels で検証し凍結。override
|
|
192
|
+
* 未指定は DEFAULT_LABELS を identity で返す。入力は変更しない。
|
|
193
|
+
*/
|
|
194
|
+
export const resolveLabels = (overrides: SpreadsheetLabelOverrides | undefined): SpreadsheetLabels => {
|
|
195
|
+
// override なしは既定そのもの (identity 安定 — 消費者の useMemo 再計算でも同一 object)
|
|
196
|
+
if (overrides === undefined) {
|
|
197
|
+
return DEFAULT_LABELS
|
|
198
|
+
}
|
|
199
|
+
const resolved: SpreadsheetLabels = { ...DEFAULT_LABELS }
|
|
200
|
+
// 明示 undefined は「未指定」= 既定のまま (分割代入既定と同じ)
|
|
201
|
+
for (const key of LABEL_KEYS) {
|
|
202
|
+
const value = overrides[key]
|
|
203
|
+
if (value !== undefined) {
|
|
204
|
+
putLabel(resolved, key, value)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
assertLabels(resolved)
|
|
208
|
+
return Object.freeze(resolved)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The single read point: resolves one label to its display string. A function-valued label is
|
|
213
|
+
* called with `n` (a missing `n` is a caller bug ⇒ RangeError — no silent 0); a string label is
|
|
214
|
+
* returned as-is and ignores `n`.
|
|
215
|
+
* 唯一の読み出し点: ラベル 1 件を表示文字列へ解決。関数値は n で呼ぶ (n 省略は呼び手バグとして
|
|
216
|
+
* RangeError — 黙って 0 にしない)、文字列値はそのまま (n は無視)。
|
|
217
|
+
*/
|
|
218
|
+
export const resolveLabel = (labels: SpreadsheetLabels, key: keyof SpreadsheetLabels, n?: number): string => {
|
|
219
|
+
const value = labels[key]
|
|
220
|
+
// 関数値だけが件数を要する
|
|
221
|
+
if (typeof value === "function") {
|
|
222
|
+
if (n === undefined) {
|
|
223
|
+
throw new RangeError(`[spreadsheet] labels.${key} is count-aware; resolveLabel needs n`)
|
|
224
|
+
}
|
|
225
|
+
return value(n)
|
|
226
|
+
}
|
|
227
|
+
return value
|
|
228
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tiny external store for the scroll view state. Scroll positions change every
|
|
3
|
+
* frame, so they must NOT live in React state of the root — and since v0.4 they do not live
|
|
4
|
+
* here either: the VirtualGrid handle is the SINGLE truth for both axes (`getScrollPosition()`
|
|
5
|
+
* / `getViewportSize()` — synchronous ref reads), and this store carries only the values React
|
|
6
|
+
* leaves need for re-render loops: the row/column RENDERING ranges (RowHeader / ColumnHeader
|
|
7
|
+
* placement) and the geometry/edit versions (re-slice levers). vy remains here solely as the
|
|
8
|
+
* rAF-notified mirror the row-side leaves subscribe to; there is deliberately NO hx and NO
|
|
9
|
+
* bodyWidth field (the v0.3 sync-hx SSOT is retired — §7-4 of the VirtualGrid design plan:
|
|
10
|
+
* freshness reads belong to the handle, and a store copy would be a second source of truth).
|
|
11
|
+
*
|
|
12
|
+
* スクロール視点の極小外部ストア。毎フレーム変わる値を root の state に置けないのは従来どおり
|
|
13
|
+
* だが、v0.4 からは**本ストアにも置かない**: 両軸の位置は VirtualGrid ハンドル
|
|
14
|
+
* (`getScrollPosition()` / `getViewportSize()` — 同期 ref 読み) が単一の真実で、ここは React の
|
|
15
|
+
* 葉が再レンダーに必要とする値だけを運ぶ — 行 / 列の描画レンジ (RowHeader / ColumnHeader の
|
|
16
|
+
* 配置) と幾何 / 編集 version (再スライスのレバー)。vy は行側の葉が購読する rAF 通知写像として
|
|
17
|
+
* のみ残る。hx / bodyWidth フィールドは**意図的に存在しない** (v0.3 の同期 hx SSOT は退役 —
|
|
18
|
+
* 設計計画 §7-4: 鮮度読みはハンドルへ、ストア複製は第 2 の真実になる)。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Snapshot of the scroll view state. / スクロール視点状態のスナップショット。 */
|
|
22
|
+
export type ViewState = {
|
|
23
|
+
/** rAF-notified mirror of the vertical position (leaves only; freshness reads use the handle). / 縦位置の rAF 通知写像 (葉専用 — 鮮度読みはハンドル)。 */
|
|
24
|
+
vy: number
|
|
25
|
+
rowStart: number
|
|
26
|
+
rowEnd: number
|
|
27
|
+
/** Rendering column range from VirtualGrid's onRangeChange (overscan-inclusive; ColumnHeader source — the §7-5 re-render loop). / VirtualGrid onRangeChange 由来の描画列レンジ (ColumnHeader の再レンダー源 — §7-5)。 */
|
|
28
|
+
colStart: number
|
|
29
|
+
colEnd: number
|
|
30
|
+
colVersion: number
|
|
31
|
+
rowGeomVersion: number
|
|
32
|
+
editVersion: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The store contract. / ストア契約。 */
|
|
36
|
+
export type ViewStore = {
|
|
37
|
+
get: () => ViewState
|
|
38
|
+
setVy: (vy: number) => void
|
|
39
|
+
/** Rendering row range from onRangeChange (overscan-inclusive; RowHeader source). / onRangeChange 由来の描画行レンジ。 */
|
|
40
|
+
setRowRange: (start: number, end: number) => void
|
|
41
|
+
/** Rendering column range from onRangeChange (overscan-inclusive; ColumnHeader source — row-symmetric §7-5). / onRangeChange 由来の描画列レンジ (行側 setRowRange の完全対称)。 */
|
|
42
|
+
setColRange: (start: number, end: number) => void
|
|
43
|
+
/** Bumped by resizeColumn to re-render the column header (cells re-slice via the grid's own width epoch). / 列幅変更 (resizeColumn) の列ヘッダー再レンダー通知 (セル再スライスはグリッド側の幅エポックが担う)。 */
|
|
44
|
+
bumpColVersion: () => void
|
|
45
|
+
/** Bumped by resizeRow to re-place row labels. / 行ジオメトリ変更のヘッダー再配置通知。 */
|
|
46
|
+
bumpRowGeometry: () => void
|
|
47
|
+
/** Bumped after each successful applyEdits (internal half of renderVersion). / applyEdits 成功の再描画通知。 */
|
|
48
|
+
bumpEdit: () => void
|
|
49
|
+
/**
|
|
50
|
+
* The ONLY subscription form: all changes coalesce inside one frame into a single rAF
|
|
51
|
+
* notification. getSnapshot always reads the synchronous latest value, so delivery timing
|
|
52
|
+
* never harms correctness. A separate synchronous subscribe deliberately does not exist
|
|
53
|
+
* (the v0.3 bodyWidth sync-notify exception retired with the field — width now lives in
|
|
54
|
+
* the VirtualGrid viewport and its ResizeObserver drives React directly).
|
|
55
|
+
* 唯一の購読形式。全変更はフレーム内 rAF 量子化で 1 通知に合流する。getSnapshot は常に同期
|
|
56
|
+
* 最新値を読むため配送タイミングは正しさを害さない。同期購読は意図的に存在しない (v0.3 の
|
|
57
|
+
* bodyWidth 同期通知例外はフィールドごと退役 — 幅は VirtualGrid ビューポートが所有)。
|
|
58
|
+
*/
|
|
59
|
+
subscribeCoalesced: (listener: () => void) => () => void
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Vertical scrollbar width in px (SSOT — §6.9-3; also the grid's horizontal bar height). / 縦バー幅 px (SSOT — 横バー高も同値)。 */
|
|
63
|
+
export const SCROLLBAR_WIDTH = 12
|
|
64
|
+
|
|
65
|
+
/** Creates a view store instance. / viewStore インスタンスを構築。 */
|
|
66
|
+
export const createViewStore = (): ViewStore => {
|
|
67
|
+
let state: ViewState = { vy: 0, rowStart: 0, rowEnd: 0, colStart: 0, colEnd: 0, colVersion: 0, rowGeomVersion: 0, editVersion: 0 }
|
|
68
|
+
const listeners = new Set<() => void>()
|
|
69
|
+
let rafId: number | null = null
|
|
70
|
+
const flush = () => {
|
|
71
|
+
// 共有 rAF 1 本で全リスナーへ通知。bail はリスナー側 (useSyncExternalStore の
|
|
72
|
+
// スナップショット比較) が担うため、ここでは無差別に呼んでよい
|
|
73
|
+
rafId = null
|
|
74
|
+
for (const listener of [...listeners]) {
|
|
75
|
+
listener()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const schedule = () => {
|
|
79
|
+
// 購読者ゼロなら予約しない (unmount 後の set が rAF をリークしない — 単体テスト (c))
|
|
80
|
+
if (listeners.size === 0) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
// フレーム内の何度目の変更でも予約は 1 本 — これが量子化の本体
|
|
84
|
+
if (rafId === null) {
|
|
85
|
+
rafId = requestAnimationFrame(flush)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const set = (partial: Partial<ViewState>) => {
|
|
89
|
+
state = { ...state, ...partial }
|
|
90
|
+
schedule()
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
get: () => state,
|
|
94
|
+
setVy: (vy) => {
|
|
95
|
+
if (vy !== state.vy) {
|
|
96
|
+
set({ vy })
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
setRowRange: (rowStart, rowEnd) => {
|
|
100
|
+
if (rowStart !== state.rowStart || rowEnd !== state.rowEnd) {
|
|
101
|
+
set({ rowStart, rowEnd })
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
setColRange: (colStart, colEnd) => {
|
|
105
|
+
if (colStart !== state.colStart || colEnd !== state.colEnd) {
|
|
106
|
+
set({ colStart, colEnd })
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
bumpColVersion: () => set({ colVersion: state.colVersion + 1 }),
|
|
110
|
+
bumpRowGeometry: () => set({ rowGeomVersion: state.rowGeomVersion + 1 }),
|
|
111
|
+
bumpEdit: () => set({ editVersion: state.editVersion + 1 }),
|
|
112
|
+
subscribeCoalesced: (listener) => {
|
|
113
|
+
listeners.add(listener)
|
|
114
|
+
return () => {
|
|
115
|
+
listeners.delete(listener)
|
|
116
|
+
if (listeners.size === 0 && rafId !== null) {
|
|
117
|
+
// 全購読解除で保留 rAF を回収 (unmount 後の宙吊り通知を残さない)
|
|
118
|
+
cancelAnimationFrame(rafId)
|
|
119
|
+
rafId = null
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
}
|