@aiquants/spreadsheet 0.13.1 → 0.15.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 +38 -0
- package/README.md +53 -7
- package/dist/index.cjs +8 -15
- package/dist/index.d.cts +12 -11
- package/dist/index.d.ts +12 -11
- package/dist/index.js +1176 -1255
- package/dist/styles/spreadsheet.standalone.css +1 -1
- package/dist/tsv-B6TDExlk.js +91 -0
- package/dist/tsv-BHXbWK5W.cjs +8 -0
- package/dist/tsv.cjs +1 -0
- package/dist/tsv.d.cts +13 -0
- package/dist/tsv.d.ts +13 -0
- package/dist/tsv.js +5 -0
- package/dist/view.cjs +1 -0
- package/dist/view.d.cts +49 -0
- package/dist/view.d.ts +49 -0
- package/dist/view.js +14 -0
- package/package.json +31 -7
- package/src/Spreadsheet.tsx +20 -50
- package/src/grid/GridBody.tsx +7 -6
- package/src/header/ColumnHeader.tsx +10 -6
- package/src/header/RowHeader.tsx +4 -3
- package/src/model/labels.ts +23 -22
- package/src/model/viewStore.ts +1 -4
- package/src/overlay/SelectionOverlay.tsx +3 -3
- package/src/popover/placement.ts +4 -4
- package/src/styles/spreadsheet.css +1 -1
- package/src/tsv.ts +13 -0
- package/src/view.ts +62 -0
package/src/grid/GridBody.tsx
CHANGED
|
@@ -69,7 +69,8 @@ import { type CfIndex, type CfMemo, createCfMemo, resolvePaintedFormat } from ".
|
|
|
69
69
|
import { isReadOnlyCell, lazyCellText, resolveCellAlign, resolveCellAttributes, type SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
70
70
|
import { isVisiblyInvalid } from "../model/dataValidation.ts"
|
|
71
71
|
import type { SpreadsheetLocale } from "../model/labels.ts"
|
|
72
|
-
import {
|
|
72
|
+
import type { ViewStore } from "../model/viewStore.ts"
|
|
73
|
+
import { SPREADSHEET_SCROLLBAR_SIZE_PX } from "../view.ts"
|
|
73
74
|
import { CellView, type RenderCellContext } from "./CellView.tsx"
|
|
74
75
|
|
|
75
76
|
/** Inline style of a cell (React camelCase keys). / セルのインライン style (React の camelCase キー)。 */
|
|
@@ -354,12 +355,12 @@ type GridBodyProps = {
|
|
|
354
355
|
*/
|
|
355
356
|
locale: SpreadsheetLocale
|
|
356
357
|
/**
|
|
357
|
-
* The resolved engine chrome strings (
|
|
358
|
+
* The resolved engine chrome strings (every VirtualScrollLabels key, e.g. scroll arrows,
|
|
358
359
|
* scroll-to-edge pills, "No items"), forwarded to VirtualGrid's `labels`. MUST be value-stable:
|
|
359
|
-
* its identity may change only when one of the
|
|
360
|
+
* its identity may change only when one of the strings changes, otherwise every shell render
|
|
360
361
|
* pierces the memo boundary.
|
|
361
|
-
* 解決済みエンジンクローム文言 (VirtualScrollLabels
|
|
362
|
-
* "No items")。VirtualGrid の `labels` へ素通し。値安定であること (
|
|
362
|
+
* 解決済みエンジンクローム文言 (VirtualScrollLabels の全キー: スクロール矢印・端スクロールピル・
|
|
363
|
+
* "No items" など)。VirtualGrid の `labels` へ素通し。値安定であること (文字列のどれかが変わった時だけ
|
|
363
364
|
* identity が変わる — さもなくば shell 再レンダー毎に memo 境界を貫通)。
|
|
364
365
|
*/
|
|
365
366
|
scrollLabels: VirtualScrollLabels
|
|
@@ -430,7 +431,7 @@ const GridBodyInner = ({
|
|
|
430
431
|
// (マウスドラッグは選択エンジンの所有)
|
|
431
432
|
const behaviorOptions = useMemo(() => ({ enableKeyboardNavigation: false, pointerDragInputs: ["pen", "touch"] as const, resetOnGetItemHeightChange: true, resetOnGetColWidthChange: true, defaultColWidth: defaultColumnWidth }), [defaultColumnWidth])
|
|
432
433
|
// タップサークル素通し (v0.10 §2.1): 未指定は undefined キー同乗 = エンジン宣言の不在信号
|
|
433
|
-
const scrollBarOptions = useMemo(() => ({ width:
|
|
434
|
+
const scrollBarOptions = useMemo(() => ({ width: SPREADSHEET_SCROLLBAR_SIZE_PX, tapScrollCircleOptions }), [tapScrollCircleOptions])
|
|
434
435
|
// 表示値書式の掛け金 (getCellProps が書き、同じセルの children が直後に消費 — 番兵は届かない座標)
|
|
435
436
|
const displayLatchRef = useRef<DisplayLatch>({ row: -1, col: -1, value: undefined })
|
|
436
437
|
/**
|
|
@@ -40,7 +40,8 @@ import { type ReactNode, type RefObject, useCallback, useLayoutEffect, useSyncEx
|
|
|
40
40
|
import { type AxisModel, visibleIndicesInRange } from "../model/axisModel.ts"
|
|
41
41
|
import type { CellRange } from "../model/cellCoord.ts"
|
|
42
42
|
import { frozenWidthOf, trailingWidthOf } from "../model/frozenPartition.ts"
|
|
43
|
-
import {
|
|
43
|
+
import type { ViewStore } from "../model/viewStore.ts"
|
|
44
|
+
import { SPREADSHEET_SCROLLBAR_SIZE_PX } from "../view.ts"
|
|
44
45
|
import { columnIndexToName } from "./columnName.ts"
|
|
45
46
|
import { buildResizeStrips, buildTrailingResizeStrips } from "./resizeStrips.ts"
|
|
46
47
|
|
|
@@ -195,10 +196,13 @@ export const ColumnHeader = ({ viewStore, colAxis, frozenCols, frozenTrailingCol
|
|
|
195
196
|
),
|
|
196
197
|
)}
|
|
197
198
|
{/* T_eff > 0 の inline right = 登記 calc() 形 #4 (ADR-19 例外レジスタ — 等価:
|
|
198
|
-
|
|
199
|
-
|
|
199
|
+
SPREADSHEET_SCROLLBAR_SIZE_PX + W_T_vis の CSS 衣装。W_T_vis = min(W_T, max(0, 100% −
|
|
200
|
+
SPREADSHEET_SCROLLBAR_SIZE_PX − W_F)) — 100% の再解決がブラウザ自身の heal チャネル。
|
|
200
201
|
軸モデル値のみ補間しハンドルは読まない。T = 0 は inline right 不在 = v0.8 恒等) */}
|
|
201
|
-
<div
|
|
202
|
+
<div
|
|
203
|
+
className="aqsp-col-header-scroll"
|
|
204
|
+
data-aqsp-col-header-scroll=""
|
|
205
|
+
style={{ left: frozenWidth, right: effectiveTrailingCols > 0 ? `calc(${SPREADSHEET_SCROLLBAR_SIZE_PX}px + min(${trailingWidth}px, max(0px, 100% - ${SPREADSHEET_SCROLLBAR_SIZE_PX + frozenWidth}px)))` : undefined }}>
|
|
202
206
|
{track}
|
|
203
207
|
{/* ヘッダー分割線セグメント (§2.4 — 存在 = 実効件数駆動の render 双子、可視 =
|
|
204
208
|
repaintColHeaders の等値抑制 display heal、幾何 = クリップ任せの静的値。
|
|
@@ -210,10 +214,10 @@ export const ColumnHeader = ({ viewStore, colAxis, frozenCols, frozenTrailingCol
|
|
|
210
214
|
{effectiveTrailingCols > 0 ? (
|
|
211
215
|
/* 末尾クリップ (§4.12): inline left = 登記 calc() 形 #3 (ADR-19 例外レジスタ —
|
|
212
216
|
等価: クリップ left = extent − W_T_vis の CSS 衣装。max() は leading 被覆
|
|
213
|
-
画素を隠すだけでアンカーを動かさない)。right =
|
|
217
|
+
画素を隠すだけでアンカーを動かさない)。right = SPREADSHEET_SCROLLBAR_SIZE_PX (帯右端の
|
|
214
218
|
縦バー帯ぶん)。クリップ箱の描画特性は scroll クリップと同一のため同クラスを
|
|
215
219
|
再利用 (フックは data-aqsp-col-header-trailing) */
|
|
216
|
-
<div className="aqsp-col-header-scroll" data-aqsp-col-header-trailing="" style={{ left: `max(${frozenWidth}px, calc(100% - ${trailingWidth +
|
|
220
|
+
<div className="aqsp-col-header-scroll" data-aqsp-col-header-trailing="" style={{ left: `max(${frozenWidth}px, calc(100% - ${trailingWidth + SPREADSHEET_SCROLLBAR_SIZE_PX}px))`, right: SPREADSHEET_SCROLLBAR_SIZE_PX }}>
|
|
217
221
|
{/* 右詰めインナー (width = W_T の木寸法箱): overlap ではクリップが左縁を隠し、
|
|
218
222
|
右アンカーが帯の遠端整列を保つ (エンジン末尾クリップの右詰めインナー同文) */}
|
|
219
223
|
<div style={{ position: "absolute", top: 0, height: "100%", right: 0, width: trailingWidth }}>
|
package/src/header/RowHeader.tsx
CHANGED
|
@@ -26,7 +26,8 @@ import { type ReactNode, type RefObject, useCallback, useLayoutEffect, useSyncEx
|
|
|
26
26
|
import { type AxisModel, visibleIndicesInRange } from "../model/axisModel.ts"
|
|
27
27
|
import type { CellRange } from "../model/cellCoord.ts"
|
|
28
28
|
import { frozenHeightOf, trailingHeightOf } from "../model/frozenPartition.ts"
|
|
29
|
-
import {
|
|
29
|
+
import type { ViewStore } from "../model/viewStore.ts"
|
|
30
|
+
import { SPREADSHEET_SCROLLBAR_SIZE_PX } from "../view.ts"
|
|
30
31
|
import { buildResizeStrips, buildTrailingResizeStrips } from "./resizeStrips.ts"
|
|
31
32
|
|
|
32
33
|
type RowHeaderProps = {
|
|
@@ -145,9 +146,9 @@ export const RowHeader = ({ viewStore, rowAxis, handleRef, bandRef, selectionRan
|
|
|
145
146
|
)
|
|
146
147
|
|
|
147
148
|
// margin-bottom はグリッド内在の横バー帯ぶんの切り上げ — CSS は TS 定数を読めないため
|
|
148
|
-
// インライン指定 (
|
|
149
|
+
// インライン指定 (SPREADSHEET_SCROLLBAR_SIZE_PX の単一 SSOT — SelectionOverlay の right / bottom と同方式)
|
|
149
150
|
return (
|
|
150
|
-
<div ref={mergedBandRef} className="aqsp-row-header" aria-hidden="true" data-aqsp-row-header="" style={{ marginBottom:
|
|
151
|
+
<div ref={mergedBandRef} className="aqsp-row-header" aria-hidden="true" data-aqsp-row-header="" style={{ marginBottom: SPREADSHEET_SCROLLBAR_SIZE_PX }}>
|
|
151
152
|
{/* track transform は repaintHeaders (命令パス) の専有物 — React は書かない (§6.8-2 (b))。
|
|
152
153
|
v0.6: R > 0 はスクロール track を静的クリップ (top = H_F) で包み、凍結ラベルを帯直下の
|
|
153
154
|
静的 subtree に置く (ColumnHeader の 2 帯を転置)。v0.9: T > 0 は末尾静的サブツリー
|
package/src/model/labels.ts
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* @fileoverview UI label vocabulary (v0.8 §1.6 / §4.1.17) — the ONE label bag of the package, now
|
|
3
3
|
* per UI chrome locale ("en" default, "ja" second). The v0.7 `sinkAriaLabel` / `rootAriaLabel`
|
|
4
4
|
* props are folded into it (clean cut-over: no alias props remain), joined by the popover strings
|
|
5
|
-
* of the rich-cell UI and by the
|
|
6
|
-
*
|
|
7
|
-
* `VIRTUAL_SCROLL_LABEL_CATALOGS` and never re-typed here. Pure, React-free.
|
|
5
|
+
* of the rich-cell UI and by the engine chrome strings of `@aiquants/virtualscroll` (every key of
|
|
6
|
+
* `VIRTUAL_SCROLL_LABEL_KEYS`, e.g. scroll arrows, scroll-to-edge pills, "No items"), which are
|
|
7
|
+
* REUSED from `VIRTUAL_SCROLL_LABEL_CATALOGS` and never re-typed here. Pure, React-free.
|
|
8
8
|
*
|
|
9
9
|
* Contract (each clause is pinned by labels.spec.ts):
|
|
10
10
|
* - `SpreadsheetLocale` is the engine's locale set (`VirtualScrollLocale`); it is the UI chrome
|
|
11
11
|
* language and a different axis from `formats.locale` (BCP-47 value formatting).
|
|
12
|
-
* - `SpreadsheetLabels` is the closed
|
|
12
|
+
* - `SpreadsheetLabels` is the closed key set (every engine key + the 21 own keys);
|
|
13
13
|
* `SPREADSHEET_LABEL_CATALOGS` supplies a frozen catalog per locale. Count-aware keys are typed
|
|
14
14
|
* `string | ((n: number) => string)`.
|
|
15
15
|
* - `resolveLabels(locale, overrides)` is the single fold point: the locale is resolved by
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* - `assertLabels`: every non-count own key is a non-blank string, a count key is a non-blank
|
|
22
22
|
* string or a function, and `sinkAriaLabel` MUST mention "Escape" (WCAG 2.1.2 escape-method
|
|
23
23
|
* advisement, spec 00 §10-4) — otherwise a RangeError, thrown at mount by the root.
|
|
24
|
-
* - `pickEngineLabels(labels)` hands
|
|
24
|
+
* - `pickEngineLabels(labels)` hands every engine key (`VIRTUAL_SCROLL_LABEL_KEYS`) to the embedded VirtualGrid.
|
|
25
25
|
* - `resolveLabel(labels, key, n)` is the single read point: a function-valued label receives `n`
|
|
26
26
|
* (omitting `n` for a function value is a RangeError — never a silent 0), a string is returned
|
|
27
27
|
* as-is (`n` ignored, so a consumer may override a count key with a plain string).
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
* UI ラベル語彙 (v0.8 §1.6 / §4.1.17) — パッケージ唯一のラベル集合で、UI クロームロケール別
|
|
30
30
|
* ("en" が既定、"ja" が第 2 ロケール)。v0.7 の `sinkAriaLabel` / `rootAriaLabel` prop を畳み込み
|
|
31
31
|
* (クリーンカットオーバー: 別名 prop は残さない)、リッチセル UI のポップオーバー文言と
|
|
32
|
-
* `@aiquants/virtualscroll`
|
|
33
|
-
* "No items") を加える。エンジン文言は `VIRTUAL_SCROLL_LABEL_CATALOGS`
|
|
34
|
-
*
|
|
32
|
+
* `@aiquants/virtualscroll` のエンジンクローム文言 (`VIRTUAL_SCROLL_LABEL_KEYS` の全キー — スクロール
|
|
33
|
+
* 矢印、端スクロールピル、"No items" など) を加える。エンジン文言は `VIRTUAL_SCROLL_LABEL_CATALOGS` を
|
|
34
|
+
* 再利用し、ここでは再記述しない。純関数・React 非依存。
|
|
35
35
|
* 契約: `SpreadsheetLocale` はエンジンのロケール集合 (UI クローム言語で、値書式の `formats.locale`
|
|
36
|
-
* とは別軸)。`SpreadsheetLabels`
|
|
36
|
+
* とは別軸)。`SpreadsheetLabels` は閉じたキー集合 (エンジンキー全件 + 固有 21)、`SPREADSHEET_LABEL_CATALOGS`
|
|
37
37
|
* はロケール別の凍結カタログ、件数キーは `string | ((n: number) => string)`。`resolveLabels` が唯一の
|
|
38
38
|
* 畳込み点 (ロケールは `resolveVirtualScrollLocale` で解決 — undefined は "en"、未知は RangeError。
|
|
39
39
|
* override 未指定はカタログを identity で返却。それ以外は override を素の object (プロトタイプが
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
* `resolveVirtualScrollLabels` で解決、固有キーは undefined でない override を採用し、`assertLabels`
|
|
42
42
|
* 検証の後に凍結)。`assertLabels` は固有キーの非空白文字列性 (件数キーは関数も可) と
|
|
43
43
|
* `sinkAriaLabel` の "Escape" 告知 (spec 00 §10-4) を検証し、違反はマウント時 RangeError。
|
|
44
|
-
* `pickEngineLabels` は埋込み VirtualGrid
|
|
44
|
+
* `pickEngineLabels` は埋込み VirtualGrid へ渡すエンジンキー全件の抽出。`resolveLabel` が唯一の
|
|
45
45
|
* 読み出し点 (関数値は n 必須 — 省略は RangeError、文字列はそのまま)。
|
|
46
46
|
*/
|
|
47
47
|
|
|
@@ -59,10 +59,11 @@ import { resolveVirtualScrollLabels, resolveVirtualScrollLocale, VIRTUAL_SCROLL_
|
|
|
59
59
|
export type SpreadsheetLocale = VirtualScrollLocale
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* The closed label vocabulary:
|
|
63
|
-
* own keys of the spreadsheet. Every key has a value in
|
|
62
|
+
* The closed label vocabulary: every engine chrome key of `@aiquants/virtualscroll`
|
|
63
|
+
* (VIRTUAL_SCROLL_LABEL_KEYS) plus the 21 own keys of the spreadsheet. Every key has a value in
|
|
64
|
+
* each SPREADSHEET_LABEL_CATALOGS entry;
|
|
64
65
|
* consumers override any subset through the `labels` prop (SpreadsheetLabelOverrides).
|
|
65
|
-
* 閉じたラベル語彙: `@aiquants/virtualscroll`
|
|
66
|
+
* 閉じたラベル語彙: `@aiquants/virtualscroll` のエンジンクロームキー全件 (VIRTUAL_SCROLL_LABEL_KEYS) とスプレッドシート固有の 21 キー。
|
|
66
67
|
* 全キーが SPREADSHEET_LABEL_CATALOGS の各ロケールに値を持ち、消費者は `labels` prop で任意の部分集合を上書き。
|
|
67
68
|
*/
|
|
68
69
|
export type SpreadsheetLabels = VirtualScrollLabels & {
|
|
@@ -148,11 +149,11 @@ const OWN_LABEL_KEYS = [
|
|
|
148
149
|
] as const satisfies readonly (keyof SpreadsheetLabels)[]
|
|
149
150
|
|
|
150
151
|
/**
|
|
151
|
-
* Every label key:
|
|
152
|
+
* Every label key: every engine key (VIRTUAL_SCROLL_LABEL_KEYS, in engine order) followed by the
|
|
152
153
|
* 21 own keys — the closed key set that overrides are validated against, and the census list for
|
|
153
154
|
* docs / tests. Compile-time proofs below that it covers EVERY SpreadsheetLabels key and that the
|
|
154
155
|
* own keys never shadow an engine key.
|
|
155
|
-
* 全ラベルキー:
|
|
156
|
+
* 全ラベルキー: エンジンキー全件 (VIRTUAL_SCROLL_LABEL_KEYS、エンジン順) に固有 21 キーを続けたもの —
|
|
156
157
|
* override 検証の閉じたキー集合で docs / テストの census 表。直下で全キー網羅と、固有キーが
|
|
157
158
|
* エンジンキーを覆い隠さないことを型水準で証明。
|
|
158
159
|
*/
|
|
@@ -268,13 +269,13 @@ export const describeLabelValue = (value: unknown): string => {
|
|
|
268
269
|
const isCountLabelKey = (key: (typeof OWN_LABEL_KEYS)[number]): key is "commentCount" | "listMore" => key === "commentCount" || key === "listMore"
|
|
269
270
|
|
|
270
271
|
/**
|
|
271
|
-
* Mount-time check of a resolved label bag's own keys (the
|
|
272
|
+
* Mount-time check of a resolved label bag's own keys (the engine keys are validated by
|
|
272
273
|
* `resolveVirtualScrollLabels`): `sinkAriaLabel` must be a string that mentions "Escape"
|
|
273
274
|
* (case-sensitive — the WCAG 2.1.2 escape-method advisement of spec 00 §10-4); every other
|
|
274
275
|
* non-count own key must be a string with non-whitespace content; `commentCount` / `listMore` must
|
|
275
276
|
* be such a string or a function. Anything else is a consumer configuration error ⇒ RangeError
|
|
276
277
|
* naming the key (fail-fast, never silently repaired).
|
|
277
|
-
* 解決済みラベル集合の固有キーのマウント時検証 (
|
|
278
|
+
* 解決済みラベル集合の固有キーのマウント時検証 (エンジンキーは `resolveVirtualScrollLabels` が検証):
|
|
278
279
|
* `sinkAriaLabel` は "Escape" を含む文字列 (大文字小文字を区別 — spec 00 §10-4 の WCAG 2.1.2 脱出告知)、
|
|
279
280
|
* その他の件数以外の固有キーは空白以外を含む文字列、`commentCount` / `listMore` はその文字列か関数で
|
|
280
281
|
* あること。それ以外は消費者の設定誤りとしてキー名入りの RangeError (fail-fast、黙って修復しない)。
|
|
@@ -403,13 +404,13 @@ export const resolveLabels = (locale: SpreadsheetLocale | undefined, overrides:
|
|
|
403
404
|
}
|
|
404
405
|
|
|
405
406
|
/**
|
|
406
|
-
* Extracts
|
|
407
|
-
* object — the `labels` handed to the embedded VirtualGrid.
|
|
408
|
-
*
|
|
409
|
-
* 埋込み VirtualGrid へ渡す `labels`。
|
|
407
|
+
* Extracts every engine chrome key (VIRTUAL_SCROLL_LABEL_KEYS) of a resolved label bag as a new
|
|
408
|
+
* frozen VirtualScrollLabels object — the `labels` handed to the embedded VirtualGrid.
|
|
409
|
+
* 解決済みラベル集合からエンジンクロームキー全件 (VIRTUAL_SCROLL_LABEL_KEYS) を新しい凍結
|
|
410
|
+
* VirtualScrollLabels object として抽出 — 埋込み VirtualGrid へ渡す `labels`。
|
|
410
411
|
*
|
|
411
412
|
* @param labels - The resolved label bag / 解決済みラベル集合
|
|
412
|
-
* @returns A new frozen object holding exactly the
|
|
413
|
+
* @returns A new frozen object holding exactly the engine keys / エンジンキーだけを持つ新しい凍結 object
|
|
413
414
|
*/
|
|
414
415
|
export const pickEngineLabels = (labels: SpreadsheetLabels): VirtualScrollLabels => {
|
|
415
416
|
const engine: { -readonly [K in keyof VirtualScrollLabels]?: VirtualScrollLabels[K] } = {}
|
package/src/model/viewStore.ts
CHANGED
|
@@ -40,7 +40,7 @@ export type ViewStore = {
|
|
|
40
40
|
setRowRange: (start: number, end: number) => void
|
|
41
41
|
/** Rendering column range from onRangeChange (overscan-inclusive; ColumnHeader source — row-symmetric §7-5). / onRangeChange 由来の描画列レンジ (行側 setRowRange の完全対称)。 */
|
|
42
42
|
setColRange: (start: number, end: number) => void
|
|
43
|
-
/** Bumped by resizeColumn to re-render the column header (cells re-slice
|
|
43
|
+
/** Bumped by resizeColumn to re-render the column header (cells re-slice when the grid's column-width tree revision changes). / 列幅変更 (resizeColumn) の列ヘッダー再レンダー通知 (セル再スライスはグリッド側の列幅木の版数が担う)。 */
|
|
44
44
|
bumpColVersion: () => void
|
|
45
45
|
/** Bumped by resizeRow to re-place row labels. / 行ジオメトリ変更のヘッダー再配置通知。 */
|
|
46
46
|
bumpRowGeometry: () => void
|
|
@@ -59,9 +59,6 @@ export type ViewStore = {
|
|
|
59
59
|
subscribeCoalesced: (listener: () => void) => () => void
|
|
60
60
|
}
|
|
61
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
62
|
/** Creates a view store instance. / viewStore インスタンスを構築。 */
|
|
66
63
|
export const createViewStore = (): ViewStore => {
|
|
67
64
|
let state: ViewState = { vy: 0, rowStart: 0, rowEnd: 0, colStart: 0, colEnd: 0, colVersion: 0, rowGeomVersion: 0, editVersion: 0 }
|
|
@@ -93,9 +93,9 @@ import { isWholeCols, isWholeRows } from "../model/cellAnnotations.ts"
|
|
|
93
93
|
import { type CellCoord, type CellRange, makeRange } from "../model/cellCoord.ts"
|
|
94
94
|
import { type EditPaint, IDLE_EDIT_PAINT } from "../model/cellPaint.ts"
|
|
95
95
|
import { type AxisForwardCtx, colToViewportX, frozenHeightOf, frozenWidthOf, rowToViewportY, trailingHeightOf, trailingWidthOf } from "../model/frozenPartition.ts"
|
|
96
|
-
import { SCROLLBAR_WIDTH } from "../model/viewStore.ts"
|
|
97
96
|
import type { PopoverAnchorState } from "../popover/usePopovers.ts"
|
|
98
97
|
import type { SelectionState } from "../selection/selectionState.ts"
|
|
98
|
+
import { SPREADSHEET_SCROLLBAR_SIZE_PX } from "../view.ts"
|
|
99
99
|
|
|
100
100
|
/** Viewport-space rect (private — distinct from the logical-space LogicalRect). / ビューポート矩形 (非公開)。 */
|
|
101
101
|
type ViewportRect = { top: number; left: number; width: number; height: number }
|
|
@@ -1862,10 +1862,10 @@ export const SelectionOverlay = ({
|
|
|
1862
1862
|
const legacyAttr = (key: string): Record<string, string> => (key === "mm" ? { "data-aqsp-overlay-main": "" } : key === "fm" ? { "data-aqsp-overlay-frozen": "" } : key === "sm" ? { "data-aqsp-overlay-scroll": "" } : {})
|
|
1863
1863
|
|
|
1864
1864
|
return (
|
|
1865
|
-
// 右 / 下オフセットは JSX インライン (CSS は TS 定数を読めない — SSOT は
|
|
1865
|
+
// 右 / 下オフセットは JSX インライン (CSS は TS 定数を読めない — SSOT は SPREADSHEET_SCROLLBAR_SIZE_PX
|
|
1866
1866
|
// 1 本 §6.7.2-1)。下は v0.4 で追加: 横バー帯がグリッド内在化し、オーバーレイはセル
|
|
1867
1867
|
// ビューポート (バー帯を除く) だけを覆う
|
|
1868
|
-
<div ref={overlayRef} className="aqsp-overlay" data-aqsp-overlay="" style={{ right:
|
|
1868
|
+
<div ref={overlayRef} className="aqsp-overlay" data-aqsp-overlay="" style={{ right: SPREADSHEET_SCROLLBAR_SIZE_PX, bottom: SPREADSHEET_SCROLLBAR_SIZE_PX }}>
|
|
1869
1869
|
{quadrants.map(({ xk, yk, key, style }) => {
|
|
1870
1870
|
const refs = quadRefs[key]
|
|
1871
1871
|
return (
|
package/src/popover/placement.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { type CellCoord, coordEquals } from "../model/cellCoord.ts"
|
|
24
|
-
import {
|
|
24
|
+
import { SPREADSHEET_SCROLLBAR_SIZE_PX } from "../view.ts"
|
|
25
25
|
|
|
26
26
|
/** A rectangle in the popover layer's coordinate space (logical viewport px). / ポップオーバー層座標系 (論理 viewport px) の矩形。 */
|
|
27
27
|
export type PopoverRect = { top: number; left: number; width: number; height: number }
|
|
@@ -50,7 +50,7 @@ export const NOTE_TIP_LEAVE_GRACE_MS = 150
|
|
|
50
50
|
* 使用可能幅 = body − スクロールバー帯。body だけで決まるため、層はフレームの高さを読む**前**に
|
|
51
51
|
* これを書く (狭いフレームは行数が増える — 別の幅で測ると高さを過小評価する)。
|
|
52
52
|
*/
|
|
53
|
-
export const popoverWidth = (bodyRect: PopoverBodySize): number => Math.max(0, Math.min(POPOVER_MAX_WIDTH, bodyRect.width -
|
|
53
|
+
export const popoverWidth = (bodyRect: PopoverBodySize): number => Math.max(0, Math.min(POPOVER_MAX_WIDTH, bodyRect.width - SPREADSHEET_SCROLLBAR_SIZE_PX - POPOVER_MARGIN))
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* Places a popover of `measuredHeight` (measured at `popoverWidth(bodyRect)`) next to
|
|
@@ -73,8 +73,8 @@ export const popoverWidth = (bodyRect: PopoverBodySize): number => Math.max(0, M
|
|
|
73
73
|
*/
|
|
74
74
|
export const placePopover = (anchorRect: PopoverRect, measuredHeight: number, bodyRect: PopoverBodySize): PopoverRect => {
|
|
75
75
|
// 使用可能領域 = body − スクロールバー帯 (両軸)
|
|
76
|
-
const usableWidth = bodyRect.width -
|
|
77
|
-
const usableHeight = bodyRect.height -
|
|
76
|
+
const usableWidth = bodyRect.width - SPREADSHEET_SCROLLBAR_SIZE_PX
|
|
77
|
+
const usableHeight = bodyRect.height - SPREADSHEET_SCROLLBAR_SIZE_PX
|
|
78
78
|
// 幅は body から (層が計測前に書いた値と同じ源)、高さは実測をマージン込みの使用可能高で上限 (負にしない)
|
|
79
79
|
const width = popoverWidth(bodyRect)
|
|
80
80
|
const height = Math.max(0, Math.min(measuredHeight, usableHeight - POPOVER_MARGIN))
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
grid-row: 2;
|
|
120
120
|
position: relative;
|
|
121
121
|
overflow: hidden;
|
|
122
|
-
/* 下端の横バー帯ぶんの margin-bottom は RowHeader.tsx が
|
|
122
|
+
/* 下端の横バー帯ぶんの margin-bottom は RowHeader.tsx が SPREADSHEET_SCROLLBAR_SIZE_PX (src/view.ts) からインライン
|
|
123
123
|
指定する (TS 定数の単一 SSOT — SelectionOverlay の right / bottom と同方式) */
|
|
124
124
|
border-right: 1px solid #d1d5db;
|
|
125
125
|
background: #f3f4f6;
|
package/src/tsv.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The TSV codec the grid copies and pastes with (Excel's real behaviour: one trailing line terminator
|
|
3
|
+
* dropped, empty text read as one empty cell, ragged rows padded with "", only fields holding a tab or a line break
|
|
4
|
+
* quoted), React-free, so a host reads or writes a grid's text before the grid itself is loaded — a
|
|
5
|
+
* server-rendered placeholder table, a lazily imported grid. Published as `@aiquants/spreadsheet/tsv`, which loads
|
|
6
|
+
* none of the grid; the barrel exports the same two functions.
|
|
7
|
+
*
|
|
8
|
+
* グリッドがコピーと貼り付けに使う TSV の符号化 (Excel の実際の振る舞い: 末尾の改行 1 つを除く・空の文字は空のセル 1 つ・
|
|
9
|
+
* 不揃いの行は "" で埋める・引用符で囲むのはタブか改行を含む値だけ) を React を読まずに渡すモジュール。ホストがグリッドを
|
|
10
|
+
* 読み込む前に (サーバーで描く代わりの表、遅延で読み込むグリッド) グリッドの文字を読み書きできる。グリッドを読み込まない
|
|
11
|
+
* `@aiquants/spreadsheet/tsv` として公開し、barrel も同じ 2 つの関数を公開する。
|
|
12
|
+
*/
|
|
13
|
+
export { parseTsv, serializeTsv } from "./clipboard/tsv.ts"
|
package/src/view.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The grid's fixed view geometry and its zoom rules, React-free, so a host can size a box for the grid and
|
|
3
|
+
* drive the zoom from its own controls with exactly the numbers the grid itself uses. Published as
|
|
4
|
+
* `@aiquants/spreadsheet/view`, which loads none of the grid.
|
|
5
|
+
*
|
|
6
|
+
* グリッドの固定の寸法と拡大率の規則 (React を読まない)。ホストがグリッドの箱の大きさを決め、自前の操作部品から拡大率を
|
|
7
|
+
* 動かすときに、グリッド自身と同じ数を使えるようにするモジュール。グリッドを読み込まない `@aiquants/spreadsheet/view` として
|
|
8
|
+
* 公開する。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** The height of the column-header band in CSS px (`--aqsp-col-header-height` in the stylesheet). 列見出しの帯の高さ (CSS px。スタイルシートの `--aqsp-col-header-height`)。 */
|
|
12
|
+
export const SPREADSHEET_COLUMN_HEADER_HEIGHT_PX = 24
|
|
13
|
+
|
|
14
|
+
/** The width of the row-header band in CSS px (`--aqsp-row-header-width` in the stylesheet). 行見出しの帯の幅 (CSS px。スタイルシートの `--aqsp-row-header-width`)。 */
|
|
15
|
+
export const SPREADSHEET_ROW_HEADER_WIDTH_PX = 64
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The thickness of each scroll bar the grid always reserves, in CSS px: the vertical bar's width and the horizontal
|
|
19
|
+
* bar's height, both reserved whether or not the grid scrolls.
|
|
20
|
+
* グリッドが常に取っておくスクロールバーの太さ (CSS px)。縦のバーの幅と横のバーの高さで、スクロールしなくても両方を取る。
|
|
21
|
+
*/
|
|
22
|
+
export const SPREADSHEET_SCROLLBAR_SIZE_PX = 12
|
|
23
|
+
|
|
24
|
+
/** The smallest package zoom. パッケージの拡大率の下限。 */
|
|
25
|
+
export const SPREADSHEET_ZOOM_MIN = 0.25
|
|
26
|
+
|
|
27
|
+
/** The largest package zoom. パッケージの拡大率の上限。 */
|
|
28
|
+
export const SPREADSHEET_ZOOM_MAX = 4
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Clamps a zoom into [SPREADSHEET_ZOOM_MIN, SPREADSHEET_ZOOM_MAX] without quantizing it, so a host may set any value
|
|
32
|
+
* such as 1.25; the gestures quantize their own arithmetic before they request a zoom. A non-finite value (NaN or
|
|
33
|
+
* ±Infinity) fails fast: Math.min and Math.max would pass NaN through every division by the zoom and defeat the grid's
|
|
34
|
+
* same-value bail.
|
|
35
|
+
* 拡大率を [下限, 上限] へ切る処理。量子化しないため、ホストは 1.25 などどの値でも置ける (操作は求める前に自分の計算で刻む)。
|
|
36
|
+
* 有限でない値 (NaN・±Infinity) はすぐに投げる (Math.min と Math.max は NaN を素通しし、拡大率で割る所をすべて汚し、同じ値なら
|
|
37
|
+
* 何もしないグリッドの判定も効かなくなるため)。
|
|
38
|
+
*
|
|
39
|
+
* @param value - The requested zoom. 求める拡大率。
|
|
40
|
+
* @returns The clamped zoom. 切った拡大率。
|
|
41
|
+
* @throws {RangeError} When the value is not finite. 有限でない場合。
|
|
42
|
+
*/
|
|
43
|
+
export const clampSpreadsheetZoom = (value: number): number => {
|
|
44
|
+
if (!Number.isFinite(value)) {
|
|
45
|
+
throw new RangeError(`[spreadsheet] zoom must be a finite number (got ${value})`)
|
|
46
|
+
}
|
|
47
|
+
return Math.min(SPREADSHEET_ZOOM_MAX, Math.max(SPREADSHEET_ZOOM_MIN, value))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The 0.1-grid step of the zoom gestures (Alt+Wheel and Ctrl+Alt+= / -), unclamped and free of float drift. From an
|
|
52
|
+
* off-grid value (the 0.25 bound or a host's 1.25) the step lands on the nearest detent in the travel direction (0.25
|
|
53
|
+
* goes to 0.3 up and 0.2 down); rounding would skip a detent (0.25 to 0.4). Clamp the result with clampSpreadsheetZoom.
|
|
54
|
+
* 拡大縮小の操作 (Alt+ホイールと Ctrl+Alt+= / -) の 0.1 刻み (範囲で切らず、浮動小数のずれを持ち込まない)。刻みの外の値
|
|
55
|
+
* (下限の 0.25 やホストの 1.25) からは、進む向きの最寄りの刻みへ着く (0.25 から上は 0.3、下は 0.2)。丸めると刻みを飛ばす
|
|
56
|
+
* (0.25 から 0.4) ため使わない。結果は clampSpreadsheetZoom で切る。
|
|
57
|
+
*
|
|
58
|
+
* @param value - The current zoom. 今の拡大率。
|
|
59
|
+
* @param direction - 1 to zoom in, -1 to zoom out. 拡大は 1、縮小は -1。
|
|
60
|
+
* @returns The next detent in that direction. その向きの次の刻み。
|
|
61
|
+
*/
|
|
62
|
+
export const stepSpreadsheetZoom = (value: number, direction: 1 | -1): number => (direction > 0 ? (Math.floor(value * 10 + 1e-6) + 1) / 10 : (Math.ceil(value * 10 - 1e-6) - 1) / 10)
|