@aiquants/spreadsheet 0.9.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 +54 -0
- package/README.md +74 -5
- package/dist/index.cjs +14 -14
- package/dist/index.d.cts +129 -7
- package/dist/index.d.ts +129 -7
- package/dist/index.js +4318 -4140
- package/dist/styles/spreadsheet.standalone.css +1 -1
- package/package.json +5 -5
- package/src/Spreadsheet.tsx +152 -12
- 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 +29 -3
- package/src/index.ts +5 -1
- package/src/keyboard/useGridKeyboard.ts +24 -3
- package/src/model/dataAdapter.ts +121 -2
- package/src/model/labels.ts +8 -2
- package/src/overlay/useFillHandle.ts +35 -2
- 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 +89 -11
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
* F2 — plus the extension anchor of the pointed token (the corner a Shift+Arrow chain is
|
|
19
19
|
* pinned to — Excel / Sheets keyboard pointing), and `data-aqsp-pointing` is a progressive
|
|
20
20
|
* visual hint written after input / selectionchange and after every mutator here — never a
|
|
21
|
-
* decision input.
|
|
21
|
+
* decision input. v0.10 read-only cells (plan §6.3): G1 refuses edit-mode entry for all six
|
|
22
|
+
* entry paths at the single `startEdit` funnel (between the sink-null bail and `discardClip` —
|
|
23
|
+
* ants kept, no scroll jump, before seeding) and G16 live-re-checks the session cell at
|
|
24
|
+
* `finishEdit` commit time (after the unchanged short-circuit, before `validateCommit`);
|
|
25
|
+
* both fire the required `rejectReadOnly` funnel, and an undeclared capability is byte-identical.
|
|
22
26
|
*
|
|
23
27
|
* セル編集セッション状態機械 (§6.7.1)。idle / editing(edit) / editing(replace) のセッション
|
|
24
28
|
* 状態・composition 追跡・グリッド外 focusout commit・suppressFocusoutCommit を所有する。
|
|
@@ -34,13 +38,17 @@
|
|
|
34
38
|
* Excel の Enter モード)、F2 でトグル — と指しトークンの伸長アンカー (Shift+Arrow 連鎖が固定
|
|
35
39
|
* する角 — Excel / Sheets のキーボードポインティング)、`data-aqsp-pointing` は input /
|
|
36
40
|
* selectionchange 後と当フックの各変更操作後に書く漸進的な視覚ヒントであり判断根拠にはしない。
|
|
41
|
+
* v0.10 読み取り専用セル (計画 §6.3): G1 は全 6 入口の単一漏斗 `startEdit` で編集モード進入を
|
|
42
|
+
* 拒否し (sink-null bail と `discardClip` の間 — アンツ保持・スクロール不跳び・シード前)、G16 は
|
|
43
|
+
* `finishEdit` の確定時にセッションセルを生再検査する (無変更短絡の後・`validateCommit` の前)。
|
|
44
|
+
* どちらも必須 `rejectReadOnly` 漏斗へ発火し、能力未宣言は従来挙動とバイト同一。
|
|
37
45
|
*/
|
|
38
46
|
|
|
39
47
|
import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
40
48
|
import type { CellCoord, CellRange } from "../model/cellCoord.ts"
|
|
41
49
|
import type { CellAlign } from "../model/cellFormat.ts"
|
|
42
50
|
import { type EditPaint, IDLE_EDIT_PAINT } from "../model/cellPaint.ts"
|
|
43
|
-
import { type CellEdit, type EditRejection, resolveEditText, type SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
51
|
+
import { type CellEdit, type EditRejection, isReadOnlyCell, type ReadOnlyRejection, resolveEditText, type SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
44
52
|
import type { ValidationVerdict } from "../model/dataValidation.ts"
|
|
45
53
|
import { cycleAnchorAt, findReferenceAt, formatRangeReference, isPointingPrefix, type PointingTarget, type ReferenceAxis } from "../model/formulaRefs.ts"
|
|
46
54
|
|
|
@@ -100,6 +108,17 @@ type UseCellEditorParams = {
|
|
|
100
108
|
discardClip: () => void
|
|
101
109
|
/** Drag/edit mutual exclusion probe (§6.7.1 entry guard). / ドラッグと編集セッションの併存禁止 (冒頭ガード)。 */
|
|
102
110
|
isDragging: () => boolean
|
|
111
|
+
/**
|
|
112
|
+
* v0.10 read-only refusal funnel (required seam — the root always wires it; identity-stable,
|
|
113
|
+
* read through paramsRef): G1 (a refused entry, `editSession: "none"`) and G16 (a refused
|
|
114
|
+
* commit of a mid-session flipped cell, `editSession: "kept" | "discarded"` per the caller's
|
|
115
|
+
* declared fate) fire it once per attempt. Capability absent ⇒ never called (byte identity).
|
|
116
|
+
* v0.10 読み取り専用拒否漏斗 (必須 seam — root が常に渡す。identity 安定、paramsRef 読み):
|
|
117
|
+
* G1 (入口拒否 — `editSession: "none"`) と G16 (セッション途中で読み取り専用化されたセルの
|
|
118
|
+
* 確定拒否 — 呼び手宣言の帰結どおり `"kept" | "discarded"`) が試行毎 1 回発火する。能力
|
|
119
|
+
* 未宣言なら一度も呼ばれない (バイト恒等)。
|
|
120
|
+
*/
|
|
121
|
+
rejectReadOnly: (rejection: ReadOnlyRejection) => void
|
|
103
122
|
/**
|
|
104
123
|
* The cell's effective alignment for the editing textarea (Excel keeps the cell alignment
|
|
105
124
|
* while editing in place). null = the adapter has no getCellFormat capability — the sink's
|
|
@@ -285,8 +304,11 @@ export const useCellEditor = (params: UseCellEditorParams): CellEditor => {
|
|
|
285
304
|
}, [])
|
|
286
305
|
|
|
287
306
|
/**
|
|
288
|
-
* The single terminal transition (idempotent — no-op when idle). commit=true
|
|
289
|
-
*
|
|
307
|
+
* The single terminal transition (idempotent — no-op when idle). commit=true walks the
|
|
308
|
+
* pinned order: unchanged short-circuit → the v0.10 read-only live re-check (G16 — a
|
|
309
|
+
* mid-session flip refuses before `validateCommit`, firing `rejectReadOnly` with the
|
|
310
|
+
* caller-declared fate) → the validation gate → applyEdits; on rejection `onReject`
|
|
311
|
+
* decides the session's fate
|
|
290
312
|
* and is DECLARED up front so the wrapper can relay it to onEditRejected: "keep" (in-grid
|
|
291
313
|
* commits — the session stays open with the typed value) or "discard" (grid-exit focusout
|
|
292
314
|
* — the session closes, the text-align / paint retire and the sink is re-armed immediately
|
|
@@ -295,7 +317,9 @@ export const useCellEditor = (params: UseCellEditorParams): CellEditor => {
|
|
|
295
317
|
* applyEdits: the session just ends (Sheets-like — opening an editor and leaving without a
|
|
296
318
|
* change is not a write, so read-only hosts get a smooth focus-out instead of a rejection
|
|
297
319
|
* loop).
|
|
298
|
-
* 終端遷移の一本化 (冪等 — idle なら no-op)。commit=true
|
|
320
|
+
* 終端遷移の一本化 (冪等 — idle なら no-op)。commit=true は 無変更短絡 → v0.10 読み取り専用
|
|
321
|
+
* 生再検査 (G16 — セッション途中の読み取り専用化は `validateCommit` より前に拒否し、呼び手
|
|
322
|
+
* 宣言の帰結つきで `rejectReadOnly` へ発火) → 検証ゲート → applyEdits の順に
|
|
299
323
|
* 試み、拒否時のセッションの行方は `onReject` が**事前に宣言**する (ラッパーが onEditRejected
|
|
300
324
|
* へ中継できるように): "keep" (グリッド内確定 — 入力値ごと維持) / "discard" (グリッド外
|
|
301
325
|
* focusout — セッションを閉じ、text-align / 塗りを退役し、フォーカスに触れずシンクを即再武装)。
|
|
@@ -333,13 +357,28 @@ export const useCellEditor = (params: UseCellEditorParams): CellEditor => {
|
|
|
333
357
|
// R4 是正)。replace モードで「打って全消し → 確定」は原文と異なるため従来どおり書込 (セルクリア)
|
|
334
358
|
const canonical = resolveEditText(p.adapter, session.cell.row, session.cell.col).text.replace(/\r\n?/g, "\n")
|
|
335
359
|
const unchanged = text === canonical
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
360
|
+
// 無変更は G16 にも検証ゲートにも書込にも進まない (短絡が先頭 — 開いて閉じただけは書込で
|
|
361
|
+
// ない: 読み取り専用化されたセルの focus-out も無音で閉じ、拒否ループに化けない)。
|
|
362
|
+
// 順序契約 unchanged → G16 → validateCommit → commitEdits (v0.10 §6.3 / RC-G16v)
|
|
363
|
+
let committed: boolean
|
|
364
|
+
if (unchanged) {
|
|
365
|
+
committed = true
|
|
366
|
+
} else if (isReadOnlyCell(p.adapter, session.cell.row, session.cell.col)) {
|
|
367
|
+
// G16 (v0.10 §6.3 / 設計 ADR-29): セッションと読み取り専用状態は両方ライブ —
|
|
368
|
+
// dataVersion 増分はセッションを取り消さないため、確定時にここで生再検査する。
|
|
369
|
+
// validateCommit より前 (退路の無い確定が strict 拒否メッセージポップオーバーを
|
|
370
|
+
// 開かない — RC-G16v)。帰結は呼び手宣言の keep / discard がそのまま payload の
|
|
371
|
+
// editSession になり、後段の既存 keep / discard 機構へ委譲する (strict 検証拒否と同形)
|
|
372
|
+
p.rejectReadOnly({ kind: "edit", cell: session.cell, editSession: options.onReject === "keep" ? "kept" : "discarded" })
|
|
373
|
+
committed = false
|
|
374
|
+
} else {
|
|
375
|
+
// 拒否文脈は呼び手が事前宣言する (onEditRejected はラッパーが発火 — ここで重ねて発火しない):
|
|
376
|
+
// keep = グリッド内確定 (セッション維持) / discard = グリッド外 focusout (破棄) の 2 値だけ
|
|
377
|
+
const rejection: EditRejection = { source: "edit", editSession: options.onReject === "keep" ? "kept" : "discarded" }
|
|
378
|
+
// 検証ゲート (能力分岐) の拒否判定はアダプタへ届けず applyEdits 拒否と同じ keep / discard
|
|
379
|
+
// 経路へ。commitEdits は成功でトランザクション id、拒否で null (= 未確定)
|
|
380
|
+
committed = (p.validateCommit === undefined || p.validateCommit(session.cell, text).ok) && p.commitEdits([{ row: session.cell.row, col: session.cell.col, text }], rejection) !== null
|
|
381
|
+
}
|
|
343
382
|
if (!committed) {
|
|
344
383
|
if (options.onReject === "discard") {
|
|
345
384
|
// グリッド外 focusout の拒否 (§6.7.1): フォーカス無き宙吊りセッションを残さず、
|
|
@@ -380,9 +419,19 @@ export const useCellEditor = (params: UseCellEditorParams): CellEditor => {
|
|
|
380
419
|
if (sink === null) {
|
|
381
420
|
return
|
|
382
421
|
}
|
|
422
|
+
// G1 (v0.10 §6.3 / 設計 ADR-29): 全 6 入口の単一漏斗で読み取り専用を拒否する。座席は
|
|
423
|
+
// sink-null bail の直後・discardClip より前 — アンツ保持 (Excel 同様、拒否試行でコピー /
|
|
424
|
+
// カットのアンツを消さない)・スクロール不跳び・シード前の非破壊配置。既存 1 回の
|
|
425
|
+
// getActive 読みをゲート前へ巻上げ (§0.2 R3 — 追加読みゼロ、selection 読みは純粋で
|
|
426
|
+
// discardClip と独立)。E2/E3/E4 の打鍵 / 変換は武装シンクの自己修復が捨てる (isDragging
|
|
427
|
+
// 劣化と同じ検証済みの形)。発火は試行毎 1 回 (autorepeat = キーストローク毎)
|
|
428
|
+
const active = p.getActive()
|
|
429
|
+
if (isReadOnlyCell(p.adapter, active.row, active.col)) {
|
|
430
|
+
p.rejectReadOnly({ kind: "edit", cell: active, editSession: "none" })
|
|
431
|
+
return
|
|
432
|
+
}
|
|
383
433
|
// 編集開始でクリップを破棄 (§6.5.4-4 — copy アンツも Excel 同様ここで消灯)
|
|
384
434
|
p.discardClip()
|
|
385
|
-
const active = p.getActive()
|
|
386
435
|
// active はスクロールで画面外に出得る — 画面外セルの上にエディタ / IME 候補窓を開かない (§6.7.1)
|
|
387
436
|
p.ensureCellVisible(active)
|
|
388
437
|
if (mode === "edit") {
|
package/src/grid/GridBody.tsx
CHANGED
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
* engine-validated pass-throughs onto VirtualGrid's `frozenTrailingCols` / `frozenTrailingRows`
|
|
24
24
|
* — validation SSOT is the engine's fail-fast contract, no second layer here (§1.1).
|
|
25
25
|
*
|
|
26
|
+
* v0.10 read-only cells (plan §6.5, S1): `getCellProps` conditionally stamps painted read-only
|
|
27
|
+
* cells with `aria-readonly="true"` + `data-aqsp-readonly=""` through the `isReadOnlyCell`
|
|
28
|
+
* helper (ADR-31 — no package CSS; capability absent ⇒ O(1) false ⇒ keys absent ⇒ the DOM
|
|
29
|
+
* stays byte-identical).
|
|
30
|
+
*
|
|
26
31
|
* グリッド本体。VirtualGrid 結線 (両軸) とセルレンダラーを所有する。v0.4 から横軸は VirtualGrid
|
|
27
32
|
* の所有 (量子化列アンカー + 残差 transform + 横バー) — v0.3 の列スタック (useColumnWindow /
|
|
28
33
|
* --aqsp-hx CSS 変数 / 軸原点絶対 left / 自前 HScrollBar) は設計計画 §7-1 に従い削除。
|
|
@@ -41,16 +46,20 @@
|
|
|
41
46
|
* v0.9 末尾凍結 (計画 §4.15): `frozenTrailingColumns` / `frozenTrailingRows` は VirtualGrid の
|
|
42
47
|
* `frozenTrailingCols` / `frozenTrailingRows` へのエンジン検証素通し — 検証 SSOT はエンジンの
|
|
43
48
|
* fail-fast 契約で、ここに第二検証層は置かない (§1.1)。
|
|
49
|
+
*
|
|
50
|
+
* v0.10 読み取り専用セル (計画 §6.5、S1): `getCellProps` が塗られた読み取り専用セルへ
|
|
51
|
+
* `aria-readonly="true"` + `data-aqsp-readonly=""` を `isReadOnlyCell` ヘルパ経由で条件スタンプ
|
|
52
|
+
* する (ADR-31 — パッケージ CSS なし。能力未宣言 = O(1) false = キー不在 = DOM バイト同一)。
|
|
44
53
|
*/
|
|
45
54
|
|
|
46
|
-
import { VirtualGrid, type VirtualGridHandle, type VirtualGridRange } from "@aiquants/virtualscroll"
|
|
55
|
+
import { type ScrollBarTapCircleOptions, VirtualGrid, type VirtualGridHandle, type VirtualGridRange } from "@aiquants/virtualscroll"
|
|
47
56
|
import { type HTMLAttributes, memo, type ProfilerOnRenderCallback, type ReactNode, type RefObject, useCallback, useMemo, useRef, useSyncExternalStore } from "react"
|
|
48
57
|
import { type AxisModel, nextVisibleIndex } from "../model/axisModel.ts"
|
|
49
58
|
import { normalizeAdapterHyperlink } from "../model/cellAnnotations.ts"
|
|
50
59
|
import { alignToJustify, type CellAlign, type CellValueFormat, formatCellValue } from "../model/cellFormat.ts"
|
|
51
60
|
import { type CellPaint, resolveCellPaint } from "../model/cellPaint.ts"
|
|
52
61
|
import { type CfIndex, type CfMemo, createCfMemo, resolvePaintedFormat } from "../model/conditionalFormat.ts"
|
|
53
|
-
import { lazyCellText, resolveCellAlign, type SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
62
|
+
import { isReadOnlyCell, lazyCellText, resolveCellAlign, type SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
54
63
|
import { isVisiblyInvalid } from "../model/dataValidation.ts"
|
|
55
64
|
import { SCROLLBAR_WIDTH, type ViewStore } from "../model/viewStore.ts"
|
|
56
65
|
import { CellView, type RenderCellContext } from "./CellView.tsx"
|
|
@@ -299,6 +308,16 @@ type GridBodyProps = {
|
|
|
299
308
|
* 焦点回収 — §4.2.11)。未指定 ⇒ v0.7 のゲート。
|
|
300
309
|
*/
|
|
301
310
|
isPointing?: () => boolean
|
|
311
|
+
/**
|
|
312
|
+
* Tap-scroll circle tuning (v0.10 — the engine `scrollBarOptions.tapScrollCircleOptions`
|
|
313
|
+
* engine-validated pass-through; the engine's `resolveTapScrollCircleOptions` is the
|
|
314
|
+
* validation SSOT). Undefined rides the memo as an explicit key = the engine's declared
|
|
315
|
+
* absence signal (engine defaults), never a value fallback here.
|
|
316
|
+
* タップサークル調整 (v0.10 — エンジン `scrollBarOptions.tapScrollCircleOptions` の
|
|
317
|
+
* エンジン検証素通し。検証の SSOT はエンジンの `resolveTapScrollCircleOptions`)。undefined は
|
|
318
|
+
* キー同乗のままエンジン宣言の不在信号 (エンジン既定) — この層での値フォールバックなし。
|
|
319
|
+
*/
|
|
320
|
+
tapScrollCircleOptions?: ScrollBarTapCircleOptions
|
|
302
321
|
}
|
|
303
322
|
|
|
304
323
|
/**
|
|
@@ -328,6 +347,7 @@ const GridBodyInner = ({
|
|
|
328
347
|
cfIndexRef,
|
|
329
348
|
cfMemoRef,
|
|
330
349
|
isPointing,
|
|
350
|
+
tapScrollCircleOptions,
|
|
331
351
|
}: GridBodyProps): ReactNode => {
|
|
332
352
|
const internalBodyRef = useRef<HTMLDivElement | null>(null)
|
|
333
353
|
// 外部 ref (エンジン結線用) と内部 ref の合成
|
|
@@ -357,7 +377,8 @@ const GridBodyInner = ({
|
|
|
357
377
|
// (§7-8 — サンプリング省略の Fenwick baseValue)。pointerDragInputs はペン / タッチのみ
|
|
358
378
|
// (マウスドラッグは選択エンジンの所有)
|
|
359
379
|
const behaviorOptions = useMemo(() => ({ enableKeyboardNavigation: false, pointerDragInputs: ["pen", "touch"] as const, resetOnGetItemHeightChange: true, resetOnGetColWidthChange: true, defaultColWidth: defaultColumnWidth }), [defaultColumnWidth])
|
|
360
|
-
|
|
380
|
+
// タップサークル素通し (v0.10 §2.1): 未指定は undefined キー同乗 = エンジン宣言の不在信号
|
|
381
|
+
const scrollBarOptions = useMemo(() => ({ width: SCROLLBAR_WIDTH, tapScrollCircleOptions }), [tapScrollCircleOptions])
|
|
361
382
|
// 表示値書式の掛け金 (getCellProps が書き、同じセルの children が直後に消費 — 番兵は届かない座標)
|
|
362
383
|
const displayLatchRef = useRef<DisplayLatch>({ row: -1, col: -1, value: undefined })
|
|
363
384
|
/**
|
|
@@ -381,6 +402,11 @@ const GridBodyInner = ({
|
|
|
381
402
|
"data-row": row,
|
|
382
403
|
"data-col": col,
|
|
383
404
|
className: "aqsp-cell",
|
|
405
|
+
// S1 (v0.10 §6.5 / 設計 ADR-31): 読み取り専用スタンプ — 閲覧モード / a11y ツリー用の
|
|
406
|
+
// aria-readonly と data-* フック (パッケージ CSS なし)。ヘルパ経由が規範 (member 名の
|
|
407
|
+
// 単一居住 — layer-2 census)。未宣言 = O(1) false = キー不在 = v0.9 と DOM バイト同一、
|
|
408
|
+
// 宣言ホストは塗りセル毎 1 述語読み (ADR-31 の登記コスト)
|
|
409
|
+
...(isReadOnlyCell(adapter, row, col) ? { "aria-readonly": "true", "data-aqsp-readonly": "" } : {}),
|
|
384
410
|
}
|
|
385
411
|
const rich = resolveRichCellProps(adapter, rowAxis, colAxis, cf, row, col)
|
|
386
412
|
// 掛け金は毎セル更新 (v0.1 恒等の null でも座標を進める — children が消費するのは書式能力下だけ)
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
* (分冊 00 §5.2)。
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
+
// 統合タップサークル (エンジン v3.6.0 — ADR-33 縮小形): tapScrollCircleOptions prop (v0.10 H2) と
|
|
40
|
+
// renderVisual をエンジン直接依存なしで型付ける型のみ再 export。値面 (tapScrollCircleSampleVisual) は
|
|
41
|
+
// 意図的に非再 export — エンジン所有のまま
|
|
42
|
+
export type { ScrollBarTapCircleOptions, TapScrollCircleRenderProps } from "@aiquants/virtualscroll"
|
|
39
43
|
export { type ClipCell, type ClipChannel, type ClipComment, type ClipFlavours, type ClipPayload, type ClipSource, detectClipSource, parseClipPayload, serializeClipPayload } from "./clipboard/clipPayload.ts"
|
|
40
44
|
export { type HtmlTableKind, type HtmlTableResult, parseHtmlTable, serializeHtmlTable } from "./clipboard/htmlInterop.ts"
|
|
41
45
|
// parseHtmlTable の第 3 引数型 — 復号器のホスト宣言 (codec 本体は非公開のまま、引数型だけ名指しできるようにする)
|
|
@@ -87,7 +91,7 @@ export type {
|
|
|
87
91
|
ScalePoint,
|
|
88
92
|
TextCondition,
|
|
89
93
|
} from "./model/conditionalFormat.ts"
|
|
90
|
-
export type { CellEdit, CellPatch, EditContext, EditRejection, EditSource, PasteEditContext, PlainEditContext, SpreadsheetDataAdapter } from "./model/dataAdapter.ts"
|
|
94
|
+
export type { CellEdit, CellPatch, EditContext, EditRejection, EditSource, PasteEditContext, PlainEditContext, ReadOnlyRejection, SpreadsheetDataAdapter } from "./model/dataAdapter.ts"
|
|
91
95
|
// データ検証の語彙 — 規則 union / 判定に加え、共通オプション・7 つの腕・op 語彙・validateCellText の評価器型も名指しできる
|
|
92
96
|
export {
|
|
93
97
|
type CheckboxValidationRule,
|
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
* Latin letter, else the physical `Key*` code), so they fire on non-Latin layouts (RU / EL …)
|
|
30
30
|
* exactly like Excel / Sheets and like every command row; on Latin layouts the match is
|
|
31
31
|
* identical to the produced-character test. Every addition is a capability param — absent =
|
|
32
|
-
* the v0.7 keymap.
|
|
32
|
+
* the v0.7 keymap. v0.10 read-only cells (design §4.1, via the required `rejectReadOnly`
|
|
33
|
+
* funnel): G3 refuses the whole Delete / Backspace when the selection contains a read-only
|
|
34
|
+
* cell — after the limit gate, before enumeration; an undeclared capability is byte-identical.
|
|
33
35
|
*
|
|
34
36
|
* シンク keydown 上のキーマップ実装 (§6.6.1 の契約表 — idle 列。編集列は Phase 3 の編集
|
|
35
37
|
* セッションへ委譲)。v0.2 で findDataEdge ナビゲーション + ディスパッチ前 fail-fast 検証 (§2)・
|
|
@@ -54,7 +56,9 @@
|
|
|
54
56
|
* Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y / Ctrl+Shift+V) は表と同じ VK 規則 (`matchChord` — 生成文字がラテン
|
|
55
57
|
* 文字ならその文字、非ラテンは物理 `Key*` code) で照合し、非ラテン配列 (RU / EL …) でも Excel /
|
|
56
58
|
* Sheets やコマンド行と同じく成立する (ラテン配列では生成文字照合と同一)。追加は全て能力 param —
|
|
57
|
-
* 不在 = v0.7 キーマップ。
|
|
59
|
+
* 不在 = v0.7 キーマップ。v0.10 読み取り専用セル (設計 §4.1、必須 `rejectReadOnly` 漏斗経由):
|
|
60
|
+
* G3 は選択に読み取り専用セルを含む Delete / Backspace を全体拒否する — 限度ゲート後・列挙前。
|
|
61
|
+
* 能力未宣言はバイト恒等。
|
|
58
62
|
*/
|
|
59
63
|
|
|
60
64
|
import type { VirtualGridHandle } from "@aiquants/virtualscroll"
|
|
@@ -63,7 +67,7 @@ import type { PasteOptions } from "../clipboard/pasteSpecial.ts"
|
|
|
63
67
|
import type { PointingSeams } from "../editor/useCellEditor.ts"
|
|
64
68
|
import { type AxisModel, nextVisibleIndex } from "../model/axisModel.ts"
|
|
65
69
|
import { type CellCoord, type CellRange, countRangeCells, enumerateRangeCells, makeRange, type SpreadsheetDirection, type SpreadsheetOperation } from "../model/cellCoord.ts"
|
|
66
|
-
import { aspectCapabilities, type CellEdit, type EditRejection, type SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
70
|
+
import { aspectCapabilities, type CellEdit, type EditRejection, type ReadOnlyRejection, rangesContainReadOnly, type SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
67
71
|
import { colToViewportX, rowToViewportY } from "../model/frozenPartition.ts"
|
|
68
72
|
import { reduceActiveCycle, type SelectionState } from "../selection/selectionState.ts"
|
|
69
73
|
import { COMPOSITION_CLEAR_MS, isCompositionKeydown } from "./compositionGuard.ts"
|
|
@@ -155,6 +159,17 @@ type UseGridKeyboardParams = {
|
|
|
155
159
|
pendingPasteOptionsRef?: RefObject<PasteOptions | null>
|
|
156
160
|
/** v0.8 capability (§2.7): the keyboard-pointing reference-rect writer — paints the pointed cell / range on the overlay's resident drag-rect node with the fixed "point" mode (the same node the reference drag paints; null hides it). Undefined = no rect (the pointing rows rewrite the reference only). / v0.8 能力 (§2.7): キーボードポインティングの参照矩形 writer — 指し先セル / レンジを overlay の常設ドラッグ矩形ノードへ固定 "point" モードで描く (参照ドラッグと同一ノード。null は消灯)。undefined = 矩形なし (ポインティング行は参照書換のみ)。 */
|
|
157
161
|
onPointFrame?: (range: CellRange | null) => void
|
|
162
|
+
/**
|
|
163
|
+
* v0.10 read-only refusal funnel (required seam — the root always wires it; identity-stable,
|
|
164
|
+
* read through paramsRef): G3 (the Delete / Backspace whole-refusal when the selection
|
|
165
|
+
* contains a read-only cell, `{op:"delete"}`) fires it once per refused keystroke with the
|
|
166
|
+
* limit channel's own overlap-naive count. Capability absent ⇒ never called (byte identity).
|
|
167
|
+
* v0.10 読み取り専用拒否漏斗 (必須 seam — root が常に渡す。identity 安定、paramsRef 読み):
|
|
168
|
+
* G3 (選択に読み取り専用セルを含む Delete / Backspace の全体拒否 — `{op:"delete"}`) が拒否
|
|
169
|
+
* キーストローク毎 1 回、限度チャネルと同じ overlap-naive の数で発火する。能力未宣言なら
|
|
170
|
+
* 一度も呼ばれない (バイト恒等)。
|
|
171
|
+
*/
|
|
172
|
+
rejectReadOnly: (rejection: ReadOnlyRejection) => void
|
|
158
173
|
}
|
|
159
174
|
|
|
160
175
|
/**
|
|
@@ -691,6 +706,12 @@ export const useGridKeyboard = (params: UseGridKeyboardParams): void => {
|
|
|
691
706
|
p.onOperationRejected?.("delete", count, "limit")
|
|
692
707
|
return
|
|
693
708
|
}
|
|
709
|
+
// G3 (v0.10 設計 §4.1): 限度ゲート通過後・enumerateRangeCells 前の読み取り専用 walk —
|
|
710
|
+
// 混在選択は全体拒否 (消費形は限度拒否と同一、cellCount は既計算の overlap-naive 値を再利用)
|
|
711
|
+
if (p.adapter !== undefined && rangesContainReadOnly(p.adapter, state.ranges)) {
|
|
712
|
+
p.rejectReadOnly({ kind: "operation", op: "delete", cellCount: count })
|
|
713
|
+
return
|
|
714
|
+
}
|
|
694
715
|
const edits: CellEdit[] = []
|
|
695
716
|
// Delete はパッチ (v0.8 §4.2.7): text を消し、getCellHyperlink 宣言時だけ hyperlink も消す
|
|
696
717
|
// (Excel: 内容の削除はリンクも外す)。未宣言側面のキーは出さない — 能力分岐 (adapter 未配線 = text のみ)
|
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
|
+
}
|
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",
|
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
* plus every declared aspect (format / validation / hyperlink / note), comments never; a v0.7
|
|
16
16
|
* adapter yields exactly `{ row, col, text }`.
|
|
17
17
|
*
|
|
18
|
+
* v0.10 read-only wall (G7 — design ADR-28 rule 5): `confirmFill` walks the ≤2-rect disjoint
|
|
19
|
+
* decomposition of box ∖ source (`fillExtensionRects` — diagonal L legs included) after the limit
|
|
20
|
+
* gate and before `expandFill`; a hit refuses the WHOLE fill through the required
|
|
21
|
+
* `rejectReadOnly` funnel with `cellCount` = box area − source area (exact — deliberately not the
|
|
22
|
+
* limit gate's estimate) and the limit-refusal cleanup (no selection growth, no bumpEdit). Fill
|
|
23
|
+
* FROM a read-only source stays legal; capability undeclared = byte-identical behaviour.
|
|
24
|
+
*
|
|
18
25
|
* v0.9 trailing freeze (plan §2.1 / §4.5 — census #5 / #6): the hover hit test builds BOTH
|
|
19
26
|
* inverse ctx literals inline from one handle snapshot (a NEW judgment-time `getViewportSize()`
|
|
20
27
|
* read supplies `extent`; `trailingSize` cites the TREE fields); the press-time grab arithmetic
|
|
@@ -38,6 +45,12 @@
|
|
|
38
45
|
* 宣言済み側面 (format / validation / hyperlink / note) の全部、comments は決して運ばず、v0.7
|
|
39
46
|
* アダプタは `{ row, col, text }` ちょうど。
|
|
40
47
|
*
|
|
48
|
+
* v0.10 読み取り専用壁 (G7 — 設計 ADR-28 rule 5): `confirmFill` は限度ゲート後・`expandFill` 前に
|
|
49
|
+
* box ∖ source の ≤2 非交矩形分解 (`fillExtensionRects` — 斜めの L 字 2 レッグ込み) を walk し、
|
|
50
|
+
* hit は必須 `rejectReadOnly` 漏斗経由で fill 全体を拒否する (cellCount = box 面積 − source 面積 —
|
|
51
|
+
* 限度ゲートの見積りとは意図的に別の数)。後始末は限度拒否と同一 (選択不伸長・bumpEdit なし)。
|
|
52
|
+
* 読み取り専用ソースからの fill は合法のまま。能力未宣言は挙動バイト恒等。
|
|
53
|
+
*
|
|
41
54
|
* v0.9 末尾凍結 (計画 §2.1 / §4.5 — census #5 / #6): hover ヒットテストは 1 ハンドル快照から
|
|
42
55
|
* 両軸の逆写像 ctx をインライン構築 (新規の判断時 `getViewportSize()` 読みが `extent` を供給、
|
|
43
56
|
* `trailingSize` は TREE フィールドを引用)。押下時の grab 算術は順写像 ctx 対を構築し、隅を
|
|
@@ -48,10 +61,10 @@
|
|
|
48
61
|
|
|
49
62
|
import type { VirtualGridHandle } from "@aiquants/virtualscroll"
|
|
50
63
|
import { type RefObject, useCallback, useEffect, useMemo, useRef } from "react"
|
|
51
|
-
import { estimateFillCellCount, expandFill, fillSourceEdit, readFillSource } from "../clipboard/tiling.ts"
|
|
64
|
+
import { estimateFillCellCount, expandFill, fillExtensionRects, fillSourceEdit, readFillSource } from "../clipboard/tiling.ts"
|
|
52
65
|
import type { AxisModel } from "../model/axisModel.ts"
|
|
53
66
|
import { type CellCoord, type CellRange, clampCoord, makeRange, type SpreadsheetOperation } from "../model/cellCoord.ts"
|
|
54
|
-
import type
|
|
67
|
+
import { type CellEdit, type EditRejection, type ReadOnlyRejection, rangesContainReadOnly, type SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
55
68
|
import { type AxisForwardCtx, colToViewportX, rowToViewportY, viewportXToCol, viewportYToRow } from "../model/frozenPartition.ts"
|
|
56
69
|
import type { SelectionState } from "../selection/selectionState.ts"
|
|
57
70
|
import type { AutoScroller } from "../selection/useAutoScroller.ts"
|
|
@@ -101,6 +114,14 @@ type UseFillHandleParams = {
|
|
|
101
114
|
commitEdits: (edits: readonly CellEdit[], rejection: EditRejection) => number | null
|
|
102
115
|
maxOperationCells: number
|
|
103
116
|
onOperationRejected?: (op: SpreadsheetOperation, cellCount: number, reason: "limit" | "bounds") => void
|
|
117
|
+
/**
|
|
118
|
+
* v0.10 read-only refusal funnel (required seam — the root always wires it; identity-stable,
|
|
119
|
+
* read through paramsRef): G7 fires it once per refused confirm. Capability absent ⇒ never
|
|
120
|
+
* called (byte identity).
|
|
121
|
+
* v0.10 読み取り専用拒否漏斗 (必須 seam — root が常に渡す。identity 安定、paramsRef 読み):
|
|
122
|
+
* G7 が拒否された確定毎に 1 回発火する。能力未宣言なら一度も呼ばれない (バイト恒等)。
|
|
123
|
+
*/
|
|
124
|
+
rejectReadOnly: (rejection: ReadOnlyRejection) => void
|
|
104
125
|
/** Preview writer relay (overlay-owned positioner + liveFillPreviewRef). / プレビュー writer リレー。 */
|
|
105
126
|
writeFillPreview: (range: CellRange | null) => void
|
|
106
127
|
/** §8.2 repaint edges (press after flag + cleanup end). / §8.2 の repaint エッジ。 */
|
|
@@ -265,6 +286,18 @@ export const useFillHandle = (params: UseFillHandleParams): SelectionEngine => {
|
|
|
265
286
|
p.onOperationRejected?.("fill", count, "limit")
|
|
266
287
|
return
|
|
267
288
|
}
|
|
289
|
+
// G7 (v0.10 §6.8 / 設計 ADR-28 rule 5): 読み取り専用壁 — walk は拡張域 (box ∖ source) の
|
|
290
|
+
// ≤2 非交矩形分解 fillExtensionRects (斜めドラッグの L 字 2 レッグ対応)。ソース行 / 列は
|
|
291
|
+
// 書込対象ではないため walk 外 = 読み取り専用ソースからの fill は合法 (Excel 同等)。座席は
|
|
292
|
+
// 限度ゲート後・expandFill (実体化) 前。cellCount = box 面積 − source 面積 (正確 — 限度
|
|
293
|
+
// チャネルの見積り estimateFillCellCount とは別の数、登記済み非対称)。後始末は限度拒否と
|
|
294
|
+
// 同一 (cleanupDrag 済み — 選択不伸長・bumpEdit なし)
|
|
295
|
+
if (rangesContainReadOnly(p.adapter, fillExtensionRects(source, box))) {
|
|
296
|
+
const boxArea = (box.endRow - box.startRow + 1) * (box.endCol - box.startCol + 1)
|
|
297
|
+
const sourceArea = (source.endRow - source.startRow + 1) * (source.endCol - source.startCol + 1)
|
|
298
|
+
p.rejectReadOnly({ kind: "operation", op: "fill", cellCount: boxArea - sourceArea })
|
|
299
|
+
return
|
|
300
|
+
}
|
|
268
301
|
// 展開は fillDown / fillRight と同じソース毎規則 (分冊 08 §4 の fill 行): 種 (数式ソースか正準
|
|
269
302
|
// 文字列) + 宣言済み側面を 1 回読み、対象毎に数式を相対シフト — v0.7 アダプタは { row, col, text } ちょうど
|
|
270
303
|
const edits = expandFill((row, col) => readFillSource(p.adapter, row, col), source, box, fillSourceEdit)
|
|
@@ -6,12 +6,17 @@
|
|
|
6
6
|
* Ctrl+Enter posts (retries after a rejection), Escape closes. Leaving by focus closes without
|
|
7
7
|
* posting. DOM hooks: `data-aqsp-popover="comment"`, `data-aqsp-popover-title`,
|
|
8
8
|
* `data-aqsp-comment-thread`, `data-aqsp-comment-id`, `data-aqsp-comment-input`,
|
|
9
|
-
* `data-aqsp-popover-post`, `data-aqsp-popover-close`, `data-aqsp-popover-rejected`.
|
|
9
|
+
* `data-aqsp-popover-post`, `data-aqsp-popover-close`, `data-aqsp-popover-rejected`. v0.10 viewer
|
|
10
|
+
* mode (`ctx.cellReadOnly === true`): the Post button is `disabled` + `aria-disabled` and
|
|
11
|
+
* Ctrl+Enter does nothing — the thread stays readable (the wall is G14a; key absent =
|
|
12
|
+
* v0.9-identical conduct).
|
|
10
13
|
*
|
|
11
14
|
* 既定のコメントスレッド (v0.8 §4.1.16): 既存スレッド (著者 / 時刻 / 本文 — 各件は consumer の id を
|
|
12
15
|
* 保つ) + 返信 textarea + 投稿ボタン。投稿はスレッド全体 `[...current (id つき), { text }]` を
|
|
13
16
|
* commit する — id / 著者 / 時計はパッケージが採番しない (設計 v5「Comments」)。空の返信は投稿
|
|
14
17
|
* しない。キー: Ctrl+Enter 投稿 (拒否後は再試行)、Escape で閉じる。フォーカス離脱は投稿せず閉じる。
|
|
18
|
+
* v0.10 ビューアモード (`ctx.cellReadOnly === true`): 投稿ボタンは `disabled` + `aria-disabled`、
|
|
19
|
+
* Ctrl+Enter は何もしない — スレッドは読めるまま (壁は G14a。キー不在 = v0.9 恒等)。
|
|
15
20
|
*/
|
|
16
21
|
|
|
17
22
|
import { type FocusEvent, type KeyboardEvent, type ReactNode, useId, useRef, useState } from "react"
|
|
@@ -26,6 +31,9 @@ export type CommentPopoverProps = { ctx: CommentPopoverContext; labels: Spreadsh
|
|
|
26
31
|
*/
|
|
27
32
|
export const CommentPopover = ({ ctx, labels }: CommentPopoverProps): ReactNode => {
|
|
28
33
|
const [reply, setReply] = useState("")
|
|
34
|
+
// v0.10 (設計 ADR-30): 宣言ゲートキーが true のときだけ確定操作を disabled に (キー不在 = v0.9 恒等)。
|
|
35
|
+
// 壁は G14a — ここは表示であって防壁ではない
|
|
36
|
+
const readOnly = ctx.cellReadOnly === true
|
|
29
37
|
const titleId = useId()
|
|
30
38
|
const settlingRef = useRef(false)
|
|
31
39
|
|
|
@@ -38,6 +46,10 @@ export const CommentPopover = ({ ctx, labels }: CommentPopoverProps): ReactNode
|
|
|
38
46
|
}
|
|
39
47
|
}
|
|
40
48
|
const post = (): void => {
|
|
49
|
+
// 読み取り専用ビューア: 投稿は不可 (disabled ボタンと同じ品行 — Ctrl+Enter も何もしない)
|
|
50
|
+
if (readOnly) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
41
53
|
// 空 (空白のみ) の返信は投稿しない
|
|
42
54
|
if (reply.trim() === "") {
|
|
43
55
|
return
|
|
@@ -97,7 +109,7 @@ export const CommentPopover = ({ ctx, labels }: CommentPopoverProps): ReactNode
|
|
|
97
109
|
onKeyDown={onKeyDown}
|
|
98
110
|
/>
|
|
99
111
|
<div className="aqsp-popover-actions">
|
|
100
|
-
<button type="button" className="aqsp-popover-button" data-aqsp-popover-post="" onClick={post}>
|
|
112
|
+
<button type="button" className="aqsp-popover-button" data-aqsp-popover-post="" disabled={readOnly} aria-disabled={readOnly ? "true" : undefined} onClick={post}>
|
|
101
113
|
{resolveLabel(labels, "commentPost")}
|
|
102
114
|
</button>
|
|
103
115
|
<button type="button" className="aqsp-popover-button" data-aqsp-popover-close="" aria-label={resolveLabel(labels, "popoverClose")} onClick={() => settle(ctx.close)}>
|