@aiquants/spreadsheet 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/README.md +160 -13
- package/dist/index.cjs +13 -13
- package/dist/index.d.cts +156 -7
- package/dist/index.d.ts +156 -7
- package/dist/index.js +4508 -4057
- package/dist/styles/spreadsheet.css +1 -1
- package/dist/styles/spreadsheet.standalone.css +1 -1
- package/package.json +5 -5
- package/src/Spreadsheet.tsx +350 -34
- package/src/clipboard/tiling.ts +106 -16
- package/src/clipboard/useClipboard.ts +120 -7
- package/src/commands/useCommands.ts +226 -42
- package/src/editor/useCellEditor.ts +62 -13
- package/src/grid/GridBody.tsx +45 -3
- 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/index.ts +5 -1
- package/src/keyboard/ensureCellVisible.ts +40 -17
- package/src/keyboard/useGridKeyboard.ts +34 -7
- package/src/model/dataAdapter.ts +121 -2
- package/src/model/frozenPartition.ts +183 -59
- package/src/model/labels.ts +8 -2
- package/src/overlay/SelectionOverlay.tsx +599 -116
- package/src/overlay/useFillHandle.ts +100 -13
- package/src/popover/CommentPopover.tsx +14 -2
- package/src/popover/LinkPopover.tsx +15 -3
- package/src/popover/NotePopover.tsx +24 -6
- package/src/popover/usePopovers.ts +128 -18
- package/src/selection/useAutoScroller.ts +58 -15
- package/src/selection/useSelectionEngine.ts +27 -10
- package/src/styles/spreadsheet.css +32 -0
package/src/model/dataAdapter.ts
CHANGED
|
@@ -10,8 +10,12 @@
|
|
|
10
10
|
* PAINTED format = base ⊕ conditional formats), lazyCellText (the once-only text thunk = the
|
|
11
11
|
* eighth read of a cell), resolveEditText (the editor seed — formula source or canonical text,
|
|
12
12
|
* fail-fast on a formula that does not start with "=") and aspectCapabilities (the write gate's
|
|
13
|
-
* single reader of the declared aspects).
|
|
14
|
-
*
|
|
13
|
+
* single reader of the declared aspects). v0.10 adds the 11th optional member `isCellReadOnly`
|
|
14
|
+
* (a NON-aspect capability), the refusal context `ReadOnlyRejection` and the read-only helper
|
|
15
|
+
* quartet (readOnlyDeclared / isReadOnlyCell / rangesContainReadOnly / rectContainsReadOnly) —
|
|
16
|
+
* the ONLY production module where the member name appears (layer-2 census, ADR-27). Every
|
|
17
|
+
* capability is a DECLARATION: absence is a code branch that reproduces the previous behaviour
|
|
18
|
+
* exactly, never a value fallback.
|
|
15
19
|
* グリッドと consumer の唯一のデータ境界 (v0.8 — ADR-13 リッチセル / ADR-16 書込語彙): 8 つの
|
|
16
20
|
* 任意リッチセル能力読みを持つアダプタ型、グリッドが書くセル単位パッチ (CellPatch / CellEdit —
|
|
17
21
|
* 未指定キーは「触らない」、`null` は「消す」)、全 applyEdits が受け取る判別可能な書込文脈
|
|
@@ -158,6 +162,41 @@ void plainContextNeverPaste
|
|
|
158
162
|
// 混入いずれも即赤)
|
|
159
163
|
export type EditRejection = { source: "edit"; editSession: "kept" | "discarded" } | { source: "paste" | "fill" | "delete" | "format" | "calc" | "annotate" }
|
|
160
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Context of a READ-ONLY refusal (v0.10, delivered by onReadOnlyRejected): fired once per
|
|
167
|
+
* refused gesture — the adapter was never called, so this is neither an {@link EditRejection}
|
|
168
|
+
* (applyEdits never threw) nor an operation limit. `kind: "edit"` is a refused single-cell
|
|
169
|
+
* text-write entry or commit and carries the cell; `kind: "operation"` is a refused whole
|
|
170
|
+
* operation with its producer's own overlap-naive `cellCount` arithmetic (the same numbers the
|
|
171
|
+
* limit channel reports — never a dedup count).
|
|
172
|
+
* 読み取り専用拒否の文脈 (v0.10 — onReadOnlyRejected が届ける): 拒否されたジェスチャ毎に 1 回
|
|
173
|
+
* 発火。adapter は呼ばれていないため {@link EditRejection} (applyEdits の throw) でも操作上限
|
|
174
|
+
* でもない。`kind: "edit"` はセル単位テキスト書込の入口 / 確定の拒否でセル座標を運び、
|
|
175
|
+
* `kind: "operation"` は操作全体の拒否で、`cellCount` はプロデューサ自身の overlap-naive 算術
|
|
176
|
+
* (上限チャネルと同じ数 — dedup 数ではない)。
|
|
177
|
+
*/
|
|
178
|
+
// 明示リテラル union で宣言する (api-extractor は Exclude<SpreadsheetOperation, ...> 等の lib 汎用型を
|
|
179
|
+
// d.ts ロールアップできない — EditRejection と同じ制約)。op の 7 リテラル (copy / cut / resize を除いた
|
|
180
|
+
// 集合 + "cut") と editSession の 3 リテラルは dataAdapter.spec.ts の型パリティガードが双方向に機械封印する
|
|
181
|
+
export type ReadOnlyRejection =
|
|
182
|
+
| {
|
|
183
|
+
readonly kind: "edit"
|
|
184
|
+
readonly cell: CellCoord
|
|
185
|
+
/**
|
|
186
|
+
* Fate of the edit session THIS refusal adjudicated: "none" = no session commit was
|
|
187
|
+
* adjudicated (no session existed at the refusing gate, or the gate does not commit
|
|
188
|
+
* sessions — an untouched session may still exist, e.g. the G15 list chord during a
|
|
189
|
+
* flipped session); "kept" / "discarded" = G16's caller-declared fate.
|
|
190
|
+
* この拒否が裁いた編集セッションの帰結。none = セッション確定を裁いていない。
|
|
191
|
+
*/
|
|
192
|
+
readonly editSession: "none" | "kept" | "discarded"
|
|
193
|
+
}
|
|
194
|
+
| {
|
|
195
|
+
readonly kind: "operation"
|
|
196
|
+
readonly op: "cut" | "delete" | "paste" | "fill" | "format" | "calc" | "annotate"
|
|
197
|
+
readonly cellCount: number
|
|
198
|
+
}
|
|
199
|
+
|
|
161
200
|
/**
|
|
162
201
|
* The single data boundary between the grid and the consumer. Reads via getCellText (renderCell
|
|
163
202
|
* — a SpreadsheetProps prop, not an adapter member — customizes only the presentation of that
|
|
@@ -344,6 +383,21 @@ export type SpreadsheetDataAdapter = {
|
|
|
344
383
|
* PasteEditContext.conditionalFormats で届く)。未宣言 = base 書式をそのまま塗る。
|
|
345
384
|
*/
|
|
346
385
|
getConditionalFormats?: () => readonly ConditionalFormatRule[]
|
|
386
|
+
/**
|
|
387
|
+
* Read-only declaration for one cell (v0.10 read-only cells). `true` = the cell can never be
|
|
388
|
+
* written by this grid: edit-mode entry is refused, an already-open session's commit re-checks
|
|
389
|
+
* this predicate live, and every write operation whose target set contains the cell is rejected
|
|
390
|
+
* WHOLE before the adapter is called. Consulted live per gesture (never memoised); publish
|
|
391
|
+
* changes via `dataVersion`. The grid only ever calls this with in-grid coordinates. Host cost
|
|
392
|
+
* budget: O(1) and allocation-free — the same budget as `getCellText` (it also runs once per
|
|
393
|
+
* painted read-only-capable cell). Absence = every cell editable = the previous version's
|
|
394
|
+
* behaviour byte-identically.
|
|
395
|
+
* 読み取り専用宣言 (v0.10)。true のセルは編集モードに入れず、開いているセッションの確定も
|
|
396
|
+
* ライブ再検査され、対象に含む書込操作は adapter 到達前に全体拒否。ジェスチャ毎に生読み、
|
|
397
|
+
* 変更は dataVersion で公開。座標は常にグリッド内。ホスト側コストは O(1)・割当なし
|
|
398
|
+
* (getCellText と同格、描画パスでも呼ばれるため)。未宣言 = 全セル編集可 = 従来挙動とバイト同一。
|
|
399
|
+
*/
|
|
400
|
+
isCellReadOnly?: (row: number, col: number) => boolean
|
|
347
401
|
/**
|
|
348
402
|
* OPTIONAL capability (v0.8): the numeric minimum / maximum over a range, for colorScale
|
|
349
403
|
* rules whose end points omit `value`; null = the range holds no numeric value. Memoised by
|
|
@@ -476,3 +530,68 @@ export const aspectCapabilities = (adapter: SpreadsheetDataAdapter): AspectCapab
|
|
|
476
530
|
cf: adapter.getConditionalFormats !== undefined,
|
|
477
531
|
formula: adapter.getCellFormula !== undefined,
|
|
478
532
|
})
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Whether the read-only capability is declared (v0.10 ADR-27). Used only where
|
|
536
|
+
* declaration-presence itself is the semantic — the announcement payload key and the popover
|
|
537
|
+
* context key: a declared host gets an explicit `false` for editable cells, an undeclared host
|
|
538
|
+
* gets no key at all (byte-identical payloads).
|
|
539
|
+
* 読み取り専用能力が宣言されているか (v0.10 ADR-27)。宣言の有無そのものが意味になる場所
|
|
540
|
+
* (announcement payload キー / ポップオーバー context キー) 専用 — 宣言ホストは編集可セルに
|
|
541
|
+
* 明示 `false`、未宣言ホストはキー自体なし (payload はバイト同一)。
|
|
542
|
+
*/
|
|
543
|
+
export const readOnlyDeclared = (adapter: SpreadsheetDataAdapter): boolean => adapter.isCellReadOnly !== undefined
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Live read-only answer for one cell: `false` in O(1) without calling anything when the
|
|
547
|
+
* capability is absent (the byte-identity lever — every gate and stamp goes through this
|
|
548
|
+
* helper, never the member; v0.10 ADR-27 / ADR-31). A throw from the host predicate is a host
|
|
549
|
+
* bug and propagates (no try/catch — Strict No-Fallback).
|
|
550
|
+
* セル 1 つの読み取り専用ライブ判定: 能力未宣言なら何も呼ばず O(1) で `false` (バイト恒等の
|
|
551
|
+
* 支点 — 全ゲート / スタンプはメンバー直接読みでなく必ず本ヘルパを通る。v0.10 ADR-27 /
|
|
552
|
+
* ADR-31)。ホスト述語の throw はホストのバグとして素通し (try/catch しない — Strict No-Fallback)。
|
|
553
|
+
*/
|
|
554
|
+
export const isReadOnlyCell = (adapter: SpreadsheetDataAdapter, row: number, col: number): boolean => adapter.isCellReadOnly?.(row, col) === true
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Whether a grid-clamped rect contains at least one read-only cell: rectangular walk with
|
|
558
|
+
* short-circuit on first hit; `false` in O(1) when the capability is absent. For the
|
|
559
|
+
* paste / cut / cut-move rect walls (v0.10 ADR-27).
|
|
560
|
+
* グリッド内 clamp 済み矩形に読み取り専用セルが 1 つでも含まれるか: 最初のヒットで短絡する
|
|
561
|
+
* 矩形 walk。能力未宣言は O(1) で `false`。paste / cut / cut-move の矩形壁用 (v0.10 ADR-27)。
|
|
562
|
+
*/
|
|
563
|
+
export const rectContainsReadOnly = (adapter: SpreadsheetDataAdapter, rect: CellRange): boolean => {
|
|
564
|
+
// 能力分岐: 未宣言は述語 0 呼びの O(1) false (v0.9 挙動とバイト同一)
|
|
565
|
+
const predicate = adapter.isCellReadOnly
|
|
566
|
+
if (predicate === undefined) {
|
|
567
|
+
return false
|
|
568
|
+
}
|
|
569
|
+
// 矩形 walk — 最初のヒットで短絡 (割当なし。上限ゲートが先に走るため walk は有界)
|
|
570
|
+
for (let row = rect.startRow; row <= rect.endRow; row += 1) {
|
|
571
|
+
for (let col = rect.startCol; col <= rect.endCol; col += 1) {
|
|
572
|
+
if (predicate(row, col)) {
|
|
573
|
+
return true
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return false
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Whether a range set contains at least one read-only cell: plain nested-loop enumeration with
|
|
582
|
+
* short-circuit on first hit and NO dedup (re-testing an overlapped cell of a pure predicate is
|
|
583
|
+
* harmless; a dedup Set would add O(cells) allocation inside a gate that must be
|
|
584
|
+
* allocation-free); `false` in O(1) when the capability is absent (v0.10 ADR-27).
|
|
585
|
+
* レンジ集合に読み取り専用セルが 1 つでも含まれるか: 素朴 2 重ループ列挙 + 最初のヒットで短絡、
|
|
586
|
+
* dedup なし (純述語の重複再検査は無害。dedup Set は割当ゼロであるべきゲートに O(セル数) の
|
|
587
|
+
* 割当を持ち込む)。能力未宣言は O(1) で `false` (v0.10 ADR-27)。
|
|
588
|
+
*/
|
|
589
|
+
export const rangesContainReadOnly = (adapter: SpreadsheetDataAdapter, ranges: readonly CellRange[]): boolean => {
|
|
590
|
+
// 矩形 walk は rectContainsReadOnly と単一実装を共有 (SSOT — 能力分岐も同関数の冒頭 1 箇所)
|
|
591
|
+
for (const range of ranges) {
|
|
592
|
+
if (rectContainsReadOnly(adapter, range)) {
|
|
593
|
+
return true
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return false
|
|
597
|
+
}
|
|
@@ -1,25 +1,82 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Frozen-axis coordinate partition (v0.
|
|
3
|
-
*
|
|
4
|
-
* tree
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* @fileoverview Frozen-axis coordinate partition, three-way (v0.9 plan §2.1; leading columns =
|
|
3
|
+
* v0.5 plan §2, leading rows = v0.6 plan §2). The engine paints leading frozen cells at their
|
|
4
|
+
* raw tree coordinate, trailing frozen cells anchored to the viewport far edge (extent), and
|
|
5
|
+
* scroll-band cells at tree − scroll — so the viewport ↔ index mapping splits three ways with
|
|
6
|
+
* the branch precedence LEADING > TRAILING > MIDDLE (ADR-19-4: at overlap a pixel resolves to
|
|
7
|
+
* the band that paints there). Pure, AXIS-NEUTRAL internals (frozenSizeOf / trailingSizeOf /
|
|
8
|
+
* toViewportPos / viewportPosToIndex) with column- and row-named thin wrappers taking one
|
|
9
|
+
* REQUIRED per-axis context object (AxisForwardCtx / AxisInverseCtx — no optionals, no
|
|
10
|
+
* defaults; clean cut-over from the v0.8 positional form).
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Registered rules (v0.9 plan §2.1 — binding on every call site):
|
|
13
|
+
* 1. Ctx objects are built FRESH per judgment from `handle.getFrozenSize()` trailing fields +
|
|
14
|
+
* `handle.getViewportSize()`, INLINE at each judgment site — a shared cross-site ctx-builder
|
|
15
|
+
* helper is prohibited (the §5.3 census scanner enforces this mechanically).
|
|
16
|
+
* 2. The canonical source of `AxisInverseCtx.trailingSize` is the TREE field
|
|
17
|
+
* (`getFrozenSize().trailingWidth` / `.trailingHeight`) — never the vis field. The two are
|
|
18
|
+
* classification-equivalent in the inverse, which is exactly why the choice is NAMED here:
|
|
19
|
+
* the §5.3 scanner pins the source field text-level.
|
|
20
|
+
* 3. Extent-0 degenerate — explicit, not emergent: the trailing branch of BOTH maps carries
|
|
21
|
+
* `extent > 0`. Pre-measure / hidden-host judgments (`getViewportSize()` = {0, 0}) leave the
|
|
22
|
+
* trailing branches inert, so the inverse resolves `p ≥ F_px` scroll-relative and the
|
|
23
|
+
* forward maps trailing indices scroll-relative (never to the negative far-edge anchor
|
|
24
|
+
* 0 − tail suffix) — byte-matching v0.8. This is a degenerate-state definition in the style
|
|
25
|
+
* of the `frozenSize > 0` guard, NOT a fallback; the leading branch never reads extent and
|
|
26
|
+
* is untouched.
|
|
27
|
+
* 4. Behaviour pins: `p ≥ extent` with trailingSize > 0 clamps into the LAST trailing track
|
|
28
|
+
* (Excel nearest-visible mirror of the negative-p leading clamp); at trailingSize = 0 every
|
|
29
|
+
* p keeps the v0.4 / v0.5 scroll-relative form (T = 0 identity twin of the F_px = 0 guard);
|
|
30
|
+
* the boundary pixel `p = extent − trailingSize` classifies TRAILING (the `>=`, mirroring
|
|
31
|
+
* the leading p = W_F scroll-side pin — FZ21 class); round-trip domain exclusion: scroll
|
|
32
|
+
* tracks whose viewport px ≥ extent − T_px are outside the partition's domain (mirror of
|
|
33
|
+
* the `p < W_F` exclusion).
|
|
34
|
+
*
|
|
35
|
+
* 凍結軸の 3 分岐座標パーティション (v0.9 計画 §2.1。先頭列 = v0.5 計画 §2、先頭行 = v0.6
|
|
36
|
+
* 計画 §2)。エンジンは先頭凍結セルを木座標の生値、末尾凍結セルを viewport 遠端 (extent)
|
|
37
|
+
* アンカー、スクロール帯セルを 木座標 − scroll で描くため、viewport ↔ index の写像は
|
|
38
|
+
* 先頭 > 末尾 > 中央 の分岐優先順で 3 分割される (ADR-19-4: overlap 画素はそこに描画する帯へ
|
|
39
|
+
* 解決)。純関数の**軸中立**内部に、必須 ctx オブジェクト (AxisForwardCtx / AxisInverseCtx —
|
|
40
|
+
* optional / default なし、v0.8 位置引数形からのクリーンカットオーバー) を取る列 / 行の
|
|
41
|
+
* 名前付き薄ラッパーを重ねる構成。登記規則 4 点: (1) ctx は判断毎にハンドル読みからその場
|
|
42
|
+
* インライン構築 (跨サイト共有ビルダ禁止 — §5.3 スキャナが機械強制)、(2) 逆写像 trailingSize
|
|
43
|
+
* の正準は TREE フィールド (`trailingWidth` / `trailingHeight` — vis 代替は分類等価ゆえ命名で
|
|
44
|
+
* ピン)、(3) extent-0 縮退は明示の縮退状態定義 (フォールバックではない — 順逆両写像の末尾分岐
|
|
45
|
+
* のみ不活性で順写像の末尾 index も scroll 相対、leading は extent を読まない)、(4) 挙動ピン
|
|
46
|
+
* (p ≥ extent は最終末尾トラックへクランプ /
|
|
47
|
+
* T = 0 恒等 / 境界画素は末尾分類 / 往復のドメイン除外)。
|
|
15
48
|
*/
|
|
16
49
|
|
|
17
50
|
import type { AxisModel } from "./axisModel.ts"
|
|
18
51
|
|
|
19
52
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
53
|
+
* REQUIRED forward-map context — count-named fields carry the unit (tracks, not px), keeping
|
|
54
|
+
* the type structurally disjoint from AxisInverseCtx (a wrong-object pass is a tsc error; a
|
|
55
|
+
* value transposition inside a correctly-typed literal is caught by the §5.2 mutation set +
|
|
56
|
+
* asymmetric-fixture discipline, not by the type system — stated honestly, v0.9 plan §1.3).
|
|
57
|
+
* `extent` is the viewport size on this axis from `handle.getViewportSize()` at judgment time.
|
|
58
|
+
* 必須の順写像 ctx — 件数系フィールド名が単位 (トラック数) を運び、AxisInverseCtx と構造的に
|
|
59
|
+
* 不交 (オブジェクト取り違えは tsc エラー。正しく型付いたリテラル内の値転置は §5.2 の変異集合 +
|
|
60
|
+
* 非対称フィクスチャ規律が守る — 型システムではない、v0.9 計画 §1.3)。`extent` は判断時
|
|
61
|
+
* `handle.getViewportSize()` のこの軸の viewport 寸法。
|
|
62
|
+
*/
|
|
63
|
+
export type AxisForwardCtx = { scroll: number; frozenCount: number; trailingCount: number; extent: number }
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* REQUIRED inverse-map context — size-named fields carry the unit (logical px), structurally
|
|
67
|
+
* disjoint from AxisForwardCtx. `trailingSize` is canonically the TREE field
|
|
68
|
+
* (`getFrozenSize().trailingWidth` / `.trailingHeight` — registered rule 2 above); `extent` is
|
|
69
|
+
* the judgment-time viewport size on this axis.
|
|
70
|
+
* 必須の逆写像 ctx — 寸法系フィールド名が単位 (論理 px) を運び、AxisForwardCtx と構造的に不交。
|
|
71
|
+
* `trailingSize` の正準は TREE フィールド (`getFrozenSize().trailingWidth` / `.trailingHeight` —
|
|
72
|
+
* 上記登記規則 2)。`extent` は判断時のこの軸の viewport 寸法。
|
|
73
|
+
*/
|
|
74
|
+
export type AxisInverseCtx = { scroll: number; frozenSize: number; trailingSize: number; extent: number }
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Axis-neutral tree-prefix size of the leading frozen band (0 when frozen <= 0 or empty axis;
|
|
78
|
+
* totalSize when frozen covers every track). Both frozenWidthOf and frozenHeightOf delegate here.
|
|
79
|
+
* 軸中立の先頭凍結帯木接頭辞サイズ (frozen ≤ 0 / 空軸は 0、全トラック凍結は totalSize)。
|
|
23
80
|
* frozenWidthOf / frozenHeightOf の共通実体。
|
|
24
81
|
*/
|
|
25
82
|
const frozenSizeOf = (axis: AxisModel, frozen: number): number => {
|
|
@@ -30,71 +87,138 @@ const frozenSizeOf = (axis: AxisModel, frozen: number): number => {
|
|
|
30
87
|
}
|
|
31
88
|
|
|
32
89
|
/**
|
|
33
|
-
* Axis-neutral
|
|
34
|
-
*
|
|
35
|
-
*
|
|
90
|
+
* Axis-neutral tree-suffix size of the trailing frozen band (0 when trailing <= 0 or empty
|
|
91
|
+
* axis; totalSize when trailing covers every track; else the tail prefix sum — hidden tracks
|
|
92
|
+
* contribute 0 px through the prefix-sum arithmetic). Both trailingWidthOf and
|
|
93
|
+
* trailingHeightOf delegate here (v0.9 plan §2.1).
|
|
94
|
+
* 軸中立の末尾凍結帯木接尾辞サイズ (trailing ≤ 0 / 空軸は 0、全トラック凍結は totalSize、
|
|
95
|
+
* それ以外は末尾接頭和 — 非表示トラックは接頭和算術を通じて 0 寄与)。trailingWidthOf /
|
|
96
|
+
* trailingHeightOf の共通実体 (v0.9 計画 §2.1)。
|
|
36
97
|
*/
|
|
37
|
-
const
|
|
98
|
+
const trailingSizeOf = (axis: AxisModel, trailing: number): number => {
|
|
99
|
+
if (trailing <= 0 || axis.count === 0) {
|
|
100
|
+
return 0
|
|
101
|
+
}
|
|
102
|
+
// 末尾接尾和 = totalSize − offsetOf(count − trailing) (末尾 T トラックの Σ — 導出: §2.1)
|
|
103
|
+
return trailing >= axis.count ? axis.totalSize() : axis.totalSize() - axis.offsetOf(axis.count - trailing)
|
|
104
|
+
}
|
|
38
105
|
|
|
39
106
|
/**
|
|
40
|
-
* Axis-neutral
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
107
|
+
* Axis-neutral three-way forward map: index to viewport position (v0.9 plan §2.1). Leading
|
|
108
|
+
* frozen tracks are static at the raw tree offset; trailing frozen tracks are static anchored
|
|
109
|
+
* to the far edge (extent − tail suffix — scroll-independent); scroll tracks subtract scroll.
|
|
110
|
+
* Branch precedence LEADING > TRAILING > MIDDLE. trailingCount = 0 short-circuits to the exact
|
|
111
|
+
* v0.8 two-branch form (T = 0 identity as a capability branch); the trailing branch also
|
|
112
|
+
* carries `extent > 0` (registered rule 3 — pre-measure {0, 0} keeps trailing indices
|
|
113
|
+
* scroll-relative, byte-identical to v0.8, instead of the negative far-edge anchor).
|
|
114
|
+
* 軸中立の 3 分岐順写像: index → viewport 位置 (v0.9 計画 §2.1)。先頭凍結トラックは木座標の
|
|
115
|
+
* 生値で静的、末尾凍結トラックは遠端アンカー (extent − 末尾接尾和 — scroll 非依存) で静的、
|
|
116
|
+
* スクロールトラックは scroll を引く。分岐優先順は 先頭 > 末尾 > 中央。trailingCount = 0 は
|
|
117
|
+
* v0.8 の 2 分岐形へそのまま短絡 (T = 0 恒等 = 能力分岐)。末尾分岐は `extent > 0` も持つ
|
|
118
|
+
* (登記規則 3 — 未測定 {0, 0} では末尾 index も scroll 相対で v0.8 と byte 一致、負の
|
|
119
|
+
* 遠端アンカーへは写らない)。
|
|
48
120
|
*/
|
|
49
|
-
const
|
|
121
|
+
const toViewportPos = (axis: AxisModel, index: number, { scroll, frozenCount, trailingCount, extent }: AxisForwardCtx): number =>
|
|
122
|
+
index < frozenCount
|
|
123
|
+
? // 先頭: 木座標の生値 (scroll 非依存の静的位置)
|
|
124
|
+
axis.offsetOf(index)
|
|
125
|
+
: trailingCount > 0 && extent > 0 && index >= axis.count - trailingCount
|
|
126
|
+
? // 末尾: extent − 末尾接尾和 (totalSize − offsetOf(index) = index 以降の Σ)。遠端アンカーで静的 (ADR-19-4)
|
|
127
|
+
extent - (axis.totalSize() - axis.offsetOf(index))
|
|
128
|
+
: // 中央: 木座標 − scroll (v0.8 恒等形)
|
|
129
|
+
axis.offsetOf(index) - scroll
|
|
50
130
|
|
|
51
131
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
132
|
+
* Axis-neutral three-way inverse map: viewport position to index (v0.9 plan §2.1). Precedence
|
|
133
|
+
* LEADING > TRAILING > MIDDLE (registered rule: at overlap the pixel resolves to the band that
|
|
134
|
+
* paints there). The leading branch exists only when frozenSize > 0 (negative p clamps into the
|
|
135
|
+
* leading track — Excel nearest visible); the trailing branch only when trailingSize > 0 AND
|
|
136
|
+
* extent > 0 (registered rule 3 — pre-measure {0, 0} keeps v0.8 conduct byte-identical; without
|
|
137
|
+
* the term every p ≥ −T_px would claim trailing); the boundary pixel p = extent − trailingSize
|
|
138
|
+
* classifies TRAILING (`>=` — FZ21 mirror), and p ≥ extent clamps into the LAST trailing track
|
|
139
|
+
* through indexAt's tree clamp (tree = totalSize + (p − extent) ≥ totalSize). Everything else
|
|
140
|
+
* continues scroll-relative at tree = p + scroll.
|
|
141
|
+
* 軸中立の 3 分岐逆写像: viewport 位置 → index (v0.9 計画 §2.1)。優先順は 先頭 > 末尾 > 中央
|
|
142
|
+
* (overlap 画素はそこに描画する帯へ解決 — 登記規則)。先頭分岐は frozenSize > 0 のときのみ
|
|
143
|
+
* (負 p は先頭トラックへクランプ — Excel 最寄り可視)、末尾分岐は trailingSize > 0 かつ
|
|
144
|
+
* extent > 0 のときのみ (登記規則 3 — 未測定 {0, 0} では v0.8 挙動と byte 一致。この項が
|
|
145
|
+
* なければ全 p ≥ −T_px が末尾を主張する)。境界画素 p = extent − trailingSize は末尾に分類
|
|
146
|
+
* (`>=` — FZ21 ミラー)、p ≥ extent は 木 = totalSize + (p − extent) ≥ totalSize となり
|
|
147
|
+
* indexAt の木クランプで最終末尾トラックへ着地。残余は 木 = p + scroll のスクロール相対。
|
|
148
|
+
*/
|
|
149
|
+
const viewportPosToIndex = (axis: AxisModel, p: number, { scroll, frozenSize, trailingSize, extent }: AxisInverseCtx): number =>
|
|
150
|
+
frozenSize > 0 && p < frozenSize
|
|
151
|
+
? // 先頭が最優先: 木座標の生値 (負 p は indexAt の下限クランプで先頭トラックへ)
|
|
152
|
+
axis.indexAt(p)
|
|
153
|
+
: trailingSize > 0 && extent > 0 && p >= extent - trailingSize
|
|
154
|
+
? // 末尾が第二 (ADR-19-4): 木 = totalSize − (extent − p) (遠端からの残距離を木末尾から引く)
|
|
155
|
+
axis.indexAt(axis.totalSize() - (extent - p))
|
|
156
|
+
: // 中央が最後: 木 = p + scroll (v0.8 恒等形)
|
|
157
|
+
axis.indexAt(p + scroll)
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Tree-prefix width of the leading frozen band on this axis (0 when F <= 0; totalSize when F
|
|
161
|
+
* covers every column). The render-surface twin of the engine's getFrozenSize().width — the
|
|
162
|
+
* pair-call discipline keeps the two equal.
|
|
163
|
+
* この軸の先頭凍結帯の木接頭辞幅 (F ≤ 0 は 0、全列凍結は totalSize)。エンジン
|
|
164
|
+
* getFrozenSize().width の render 面双子 — 対呼び出し規律が両者を一致させる。
|
|
57
165
|
*/
|
|
58
166
|
export const frozenWidthOf = (axis: AxisModel, frozenCols: number): number => frozenSizeOf(axis, frozenCols)
|
|
59
167
|
|
|
60
168
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
169
|
+
* Leading frozen-band tree-prefix HEIGHT H_F (row twin of frozenWidthOf — v0.6 plan §2).
|
|
170
|
+
* 先頭凍結帯の木接頭辞高 H_F (frozenWidthOf の行双子 — v0.6 計画 §2)。
|
|
63
171
|
*/
|
|
64
172
|
export const frozenHeightOf = (axis: AxisModel, frozenRows: number): number => frozenSizeOf(axis, frozenRows)
|
|
65
173
|
|
|
66
174
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
175
|
+
* Tree-suffix WIDTH W_T of the trailing frozen band (v0.9 plan §1.3). The render-surface twin
|
|
176
|
+
* of the engine's getFrozenSize().trailingWidth — feed it the clamped effective count
|
|
177
|
+
* (min(max(0, T), count − effectiveLeading) — leading wins, byte-matching the engine clamp).
|
|
178
|
+
* 末尾凍結帯の木接尾辞幅 W_T (v0.9 計画 §1.3)。エンジン getFrozenSize().trailingWidth の
|
|
179
|
+
* render 面双子 — クランプ済み実効件数 (min(max(0, T), count − effectiveLeading) —
|
|
180
|
+
* leading 勝ち、エンジンクランプと byte 一致) を渡す。
|
|
181
|
+
*/
|
|
182
|
+
export const trailingWidthOf = (axis: AxisModel, trailingCols: number): number => trailingSizeOf(axis, trailingCols)
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Trailing frozen-band tree-suffix HEIGHT H_T (row twin of trailingWidthOf — v0.9 plan §1.3).
|
|
186
|
+
* 末尾凍結帯の木接尾辞高 H_T (trailingWidthOf の行双子 — v0.9 計画 §1.3)。
|
|
187
|
+
*/
|
|
188
|
+
export const trailingHeightOf = (axis: AxisModel, trailingRows: number): number => trailingSizeOf(axis, trailingRows)
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Forward map: column index to viewport x (the left edge), three-way (v0.9 plan §2.1). Leading
|
|
192
|
+
* frozen columns static; trailing frozen columns anchored to ctx.extent; scroll columns
|
|
193
|
+
* subtract ctx.scroll. Takes the REQUIRED AxisForwardCtx built fresh at the judgment site
|
|
194
|
+
* (the extent > 0 term keeps pre-measure conduct byte-identical to v0.8 — registered rule 3).
|
|
195
|
+
* 順写像: 列 index → viewport x (左端) の 3 分岐 (v0.9 計画 §2.1)。先頭凍結列は静的、末尾
|
|
196
|
+
* 凍結列は ctx.extent アンカー、スクロール列は ctx.scroll を引く。判断サイトでその場構築した
|
|
197
|
+
* 必須 AxisForwardCtx を取る (extent > 0 項が未測定挙動を v0.8 と byte 一致に保つ — 登記
|
|
198
|
+
* 規則 3)。
|
|
70
199
|
*/
|
|
71
|
-
export const colToViewportX = (axis: AxisModel, col: number,
|
|
200
|
+
export const colToViewportX = (axis: AxisModel, col: number, ctx: AxisForwardCtx): number => toViewportPos(axis, col, ctx)
|
|
72
201
|
|
|
73
202
|
/**
|
|
74
|
-
* Forward map: row index to viewport y (the top edge)
|
|
75
|
-
* 順写像: 行 index → viewport y (上端)
|
|
203
|
+
* Forward map: row index to viewport y (the top edge) — row twin of colToViewportX (v0.9 plan §2.1).
|
|
204
|
+
* 順写像: 行 index → viewport y (上端) — colToViewportX の行双子 (v0.9 計画 §2.1)。
|
|
76
205
|
*/
|
|
77
|
-
export const rowToViewportY = (axis: AxisModel, row: number,
|
|
206
|
+
export const rowToViewportY = (axis: AxisModel, row: number, ctx: AxisForwardCtx): number => toViewportPos(axis, row, ctx)
|
|
78
207
|
|
|
79
208
|
/**
|
|
80
|
-
* Inverse map: viewport x to column index
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* スクロール帯 (木座標 = px + hx)。W_F = 0 は負の px (左ドラッグでセル領域外へ出た
|
|
89
|
-
* ポインタ) も含め全域が v0.4 の hx 相対形 (R2 是正: 無ガードの `px < 0` は負 px を
|
|
90
|
-
* indexAt(px) = 列 0 へ吸い込み、F = 0 恒等を破る)。
|
|
209
|
+
* Inverse map: viewport x to column index, three-way with precedence LEADING > TRAILING >
|
|
210
|
+
* MIDDLE (v0.9 plan §2.1). Takes the REQUIRED AxisInverseCtx built fresh at the judgment site
|
|
211
|
+
* (trailingSize from the TREE field — registered rule 2; extent from the judgment-time
|
|
212
|
+
* getViewportSize() read — the extent > 0 term keeps pre-measure conduct byte-identical to v0.8).
|
|
213
|
+
* 逆写像: viewport x → 列 index の 3 分岐 (優先順 先頭 > 末尾 > 中央 — v0.9 計画 §2.1)。判断
|
|
214
|
+
* サイトでその場構築した必須 AxisInverseCtx を取る (trailingSize は TREE フィールド — 登記
|
|
215
|
+
* 規則 2。extent は判断時 getViewportSize() 読み — extent > 0 項が未測定挙動を v0.8 と byte
|
|
216
|
+
* 一致に保つ)。
|
|
91
217
|
*/
|
|
92
|
-
export const viewportXToCol = (axis: AxisModel, px: number,
|
|
218
|
+
export const viewportXToCol = (axis: AxisModel, px: number, ctx: AxisInverseCtx): number => viewportPosToIndex(axis, px, ctx)
|
|
93
219
|
|
|
94
220
|
/**
|
|
95
|
-
* Inverse map: viewport y to row index
|
|
96
|
-
*
|
|
97
|
-
* 逆写像: viewport y → 行 index (viewportXToCol の行双子 — v0.6 計画 §2)。H_F > 0 ガードつき、
|
|
98
|
-
* H_F = 0 は負 py も含め v0.4 の vy 相対形。
|
|
221
|
+
* Inverse map: viewport y to row index — row twin of viewportXToCol (v0.9 plan §2.1).
|
|
222
|
+
* 逆写像: viewport y → 行 index — viewportXToCol の行双子 (v0.9 計画 §2.1)。
|
|
99
223
|
*/
|
|
100
|
-
export const viewportYToRow = (axis: AxisModel, py: number,
|
|
224
|
+
export const viewportYToRow = (axis: AxisModel, py: number, ctx: AxisInverseCtx): number => viewportPosToIndex(axis, py, ctx)
|
package/src/model/labels.ts
CHANGED
|
@@ -45,6 +45,8 @@ export type SpreadsheetLabels = {
|
|
|
45
45
|
noteEscapeHint: string
|
|
46
46
|
/** Note popover hint in the rejected state (Ctrl+Enter retries, Escape closes without saving). / 拒否状態のノートヒント。 */
|
|
47
47
|
noteRejectedHint: string
|
|
48
|
+
/** Note popover hint in the v0.10 read-only viewer mode (Escape closes without saving — the commit affordance is disabled, so the hint must not promise a save). / 読み取り専用ビューアモード (v0.10) のノートヒント (Escape は保存せず閉じる — 確定操作は disabled のため保存を約束しない)。 */
|
|
49
|
+
noteViewerHint: string
|
|
48
50
|
/** Comment popover heading. / コメントポップオーバーの見出し。 */
|
|
49
51
|
commentTitle: string
|
|
50
52
|
/** Comment reply field label. / 返信入力欄のラベル。 */
|
|
@@ -103,6 +105,7 @@ export const LABEL_KEYS = [
|
|
|
103
105
|
"noteSave",
|
|
104
106
|
"noteEscapeHint",
|
|
105
107
|
"noteRejectedHint",
|
|
108
|
+
"noteViewerHint",
|
|
106
109
|
"commentTitle",
|
|
107
110
|
"commentReply",
|
|
108
111
|
"commentPost",
|
|
@@ -129,10 +132,12 @@ void labelKeysCoverEveryKey
|
|
|
129
132
|
|
|
130
133
|
/**
|
|
131
134
|
* 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"
|
|
135
|
+
* v0.7 prop defaults (the fold is DOM-identical); `noteEscapeHint` = "Escape saves and closes"
|
|
136
|
+
* and its v0.10 viewer counterpart `noteViewerHint` = "Escape closes without saving";
|
|
133
137
|
* `commentCount` and `listMore` pluralise ("1 comment" / "n comments", "1 more value" / "n more values"). Frozen.
|
|
134
138
|
* 全キーの英語既定。`sinkAriaLabel` / `rootAriaLabel` は v0.7 の prop 既定と byte 同一 (畳込みは
|
|
135
|
-
* DOM 恒等)、`noteEscapeHint` = "Escape saves and closes"
|
|
139
|
+
* DOM 恒等)、`noteEscapeHint` = "Escape saves and closes" とその v0.10 ビューア対応
|
|
140
|
+
* `noteViewerHint` = "Escape closes without saving"、`commentCount` / `listMore` は単複を区別。凍結済み。
|
|
136
141
|
*/
|
|
137
142
|
export const DEFAULT_LABELS: SpreadsheetLabels = Object.freeze({
|
|
138
143
|
sinkAriaLabel: "Spreadsheet grid. Press Escape to move focus out of the grid.",
|
|
@@ -141,6 +146,7 @@ export const DEFAULT_LABELS: SpreadsheetLabels = Object.freeze({
|
|
|
141
146
|
noteSave: "Save",
|
|
142
147
|
noteEscapeHint: "Escape saves and closes",
|
|
143
148
|
noteRejectedHint: "The note was rejected. Press Ctrl+Enter to retry or Escape to close without saving",
|
|
149
|
+
noteViewerHint: "Escape closes without saving",
|
|
144
150
|
commentTitle: "Comments",
|
|
145
151
|
commentReply: "Reply",
|
|
146
152
|
commentPost: "Post",
|