@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
|
@@ -10,14 +10,19 @@
|
|
|
10
10
|
* without saving. DOM hooks: `data-aqsp-popover="link"`, `data-aqsp-popover-title`,
|
|
11
11
|
* `data-aqsp-link-text`, `data-aqsp-link-url`, `data-aqsp-popover-save`, `data-aqsp-link-open`
|
|
12
12
|
* (the Open button — distinct from the layer's anchor open-state `data-aqsp-popover-open`),
|
|
13
|
-
* `data-aqsp-popover-close`, `data-aqsp-popover-rejected`.
|
|
13
|
+
* `data-aqsp-popover-close`, `data-aqsp-popover-rejected`. v0.10 viewer mode
|
|
14
|
+
* (`ctx.cellReadOnly === true`): Save is `disabled` + `aria-disabled` and Enter does nothing,
|
|
15
|
+
* while the URL stays readable and Open stays live (navigation is a read — the wall is G14a; key
|
|
16
|
+
* absent = v0.9-identical conduct).
|
|
14
17
|
*
|
|
15
18
|
* 既定のハイパーリンクエディタ (v0.8 §4.1.16): text + URL 欄、保存、Open (ホストが onHyperlinkOpen を
|
|
16
19
|
* 宣言し (`ctx.canOpen`) セルにリンクがあるときだけ)、閉じる。URL は打鍵毎に normalizeHyperlink で
|
|
17
20
|
* 検証し、正規化不能な非空値は不正表示 (`data-aqsp-link-invalid` / `aria-invalid`) で保存を拒む —
|
|
18
21
|
* commit は常に正規化済み href か null (空欄 = リンク消去) だけを運ぶ。空の text 下書き + 有効 URL の
|
|
19
22
|
* 保存は href を表示テキストとして commit する (Sheets / Excel 同等 — 空セルのリンクを見えないままに
|
|
20
|
-
* しない)。キー: Enter 保存、Escape で閉じる。フォーカス離脱は保存せず閉じる。
|
|
23
|
+
* しない)。キー: Enter 保存、Escape で閉じる。フォーカス離脱は保存せず閉じる。v0.10 ビューアモード
|
|
24
|
+
* (`ctx.cellReadOnly === true`): 保存は `disabled` + `aria-disabled` で Enter も何もしないが、URL は
|
|
25
|
+
* 読めたまま Open も生きている (ナビゲーションは読み — 壁は G14a。キー不在 = v0.9 恒等)。
|
|
21
26
|
*/
|
|
22
27
|
|
|
23
28
|
import { type FocusEvent, type KeyboardEvent, type ReactNode, useId, useRef, useState } from "react"
|
|
@@ -34,6 +39,9 @@ export type LinkPopoverProps = { ctx: LinkPopoverContext; labels: SpreadsheetLab
|
|
|
34
39
|
export const LinkPopover = ({ ctx, labels }: LinkPopoverProps): ReactNode => {
|
|
35
40
|
const [text, setText] = useState(ctx.current.text)
|
|
36
41
|
const [url, setUrl] = useState(ctx.current.url === undefined ? "" : ctx.current.url)
|
|
42
|
+
// v0.10 (設計 ADR-30): 宣言ゲートキーが true のときだけ確定操作を disabled に (キー不在 = v0.9 恒等)。
|
|
43
|
+
// Open は読み (ナビゲーション) なので生きたまま。壁は G14a — ここは表示であって防壁ではない
|
|
44
|
+
const readOnly = ctx.cellReadOnly === true
|
|
37
45
|
const titleId = useId()
|
|
38
46
|
const settlingRef = useRef(false)
|
|
39
47
|
// 空欄はリンク消去、それ以外は正規化 (null = 不正)
|
|
@@ -49,6 +57,10 @@ export const LinkPopover = ({ ctx, labels }: LinkPopoverProps): ReactNode => {
|
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
const save = (): void => {
|
|
60
|
+
// 読み取り専用ビューア: 保存は不可 (disabled ボタンと同じ品行 — Enter も何もしない)
|
|
61
|
+
if (readOnly) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
52
64
|
// 不正 URL は保存しない (表示で知らせる — 代替値は作らない)
|
|
53
65
|
if (invalid) {
|
|
54
66
|
return
|
|
@@ -91,7 +103,7 @@ export const LinkPopover = ({ ctx, labels }: LinkPopoverProps): ReactNode => {
|
|
|
91
103
|
<input className="aqsp-popover-input" data-aqsp-link-url="" data-aqsp-popover-autofocus="" type="url" value={url} aria-invalid={invalid || ctx.rejected ? true : undefined} onChange={(event) => setUrl(event.currentTarget.value)} onKeyDown={onKeyDown} />
|
|
92
104
|
</label>
|
|
93
105
|
<div className="aqsp-popover-actions">
|
|
94
|
-
<button type="button" className="aqsp-popover-button" data-aqsp-popover-save="" disabled={invalid} onClick={save}>
|
|
106
|
+
<button type="button" className="aqsp-popover-button" data-aqsp-popover-save="" disabled={invalid || readOnly} aria-disabled={readOnly ? "true" : undefined} onClick={save}>
|
|
95
107
|
{resolveLabel(labels, "linkSave")}
|
|
96
108
|
</button>
|
|
97
109
|
{ctx.canOpen && ctx.current.url !== undefined ? (
|
|
@@ -5,17 +5,26 @@
|
|
|
5
5
|
* focus saves-if-changed — an anchor scrolled under a frozen band is clipped, not un-rendered
|
|
6
6
|
* (SelectionOverlay `placeAnchor`), so the textarea keeps focus and that is NOT a leave: the
|
|
7
7
|
* editor hides and comes back with its draft. The textarea's `aria-describedby` points at the hint paragraph, which
|
|
8
|
-
* swaps
|
|
8
|
+
* swaps among `labels.noteEscapeHint` (normal), `labels.noteRejectedHint` (rejected) and
|
|
9
|
+
* `labels.noteViewerHint` (viewer mode — precedence over the rejected wording, since neither save
|
|
10
|
+
* nor retry is possible there and the hint must not promise either).
|
|
9
11
|
* DOM hooks: `data-aqsp-popover="note"`, `data-aqsp-popover-title`, `data-aqsp-note-input`,
|
|
10
12
|
* `data-aqsp-popover-save`, `data-aqsp-popover-close`, `data-aqsp-popover-hint`,
|
|
11
|
-
* `data-aqsp-popover-rejected`.
|
|
13
|
+
* `data-aqsp-popover-rejected`. v0.10 viewer mode (`ctx.cellReadOnly === true`): the Save button
|
|
14
|
+
* is `disabled` + `aria-disabled` (native only — no new attribute, no package CSS, design ADR-31),
|
|
15
|
+
* Ctrl+Enter does nothing, Escape / focus-leave close WITHOUT saving (the wall is G14a; key
|
|
16
|
+
* absent = v0.9-identical conduct), and the hint swaps to `noteViewerHint`.
|
|
12
17
|
*
|
|
13
18
|
* 既定の note エディタ (v0.8 §4.1.16): textarea の下書き + 保存 / 閉じるボタン。キー: Escape は
|
|
14
19
|
* 通常状態で「変更があれば保存して閉じる」、拒否状態では保存せず閉じる。Ctrl+Enter は保存 (拒否後は
|
|
15
20
|
* 再試行)。フォーカスがポップオーバーを離れたら変更があれば保存 — 凍結帯の下へ潜ったアンカーは
|
|
16
21
|
* clip されるだけで描画から外れない (SelectionOverlay の placeAnchor) ので textarea はフォーカスを保ち、
|
|
17
22
|
* 離脱には当たらない: エディタは隠れて下書きごと戻る。textarea の aria-describedby は
|
|
18
|
-
*
|
|
23
|
+
* ヒント段落を指し、`labels.noteEscapeHint` (通常) ↔ `labels.noteRejectedHint` (拒否状態) ↔
|
|
24
|
+
* `labels.noteViewerHint` (ビューアモード — 保存も再試行も不能のため拒否文言より優先) が差し替わる。
|
|
25
|
+
* v0.10 ビューアモード (`ctx.cellReadOnly === true`): 保存ボタンは `disabled` + `aria-disabled`
|
|
26
|
+
* (ネイティブのみ — 新属性 / パッケージ CSS なし、設計 ADR-31)、Ctrl+Enter は何もせず、Escape /
|
|
27
|
+
* フォーカス離脱は保存せず閉じ、ヒントは `noteViewerHint` に差し替わる (壁は G14a。キー不在 = v0.9 恒等)。
|
|
19
28
|
*/
|
|
20
29
|
|
|
21
30
|
import { type FocusEvent, type KeyboardEvent, type ReactNode, useId, useRef, useState } from "react"
|
|
@@ -31,6 +40,9 @@ export type NotePopoverProps = { ctx: NotePopoverContext; labels: SpreadsheetLab
|
|
|
31
40
|
export const NotePopover = ({ ctx, labels }: NotePopoverProps): ReactNode => {
|
|
32
41
|
const initial = ctx.current === undefined ? "" : ctx.current
|
|
33
42
|
const [draft, setDraft] = useState(initial)
|
|
43
|
+
// v0.10 (設計 ADR-30): 宣言ゲートキーが true のときだけ確定操作を disabled に (キー不在 = v0.9 恒等)。
|
|
44
|
+
// 壁は G14a — ここは表示であって防壁ではない
|
|
45
|
+
const readOnly = ctx.cellReadOnly === true
|
|
34
46
|
const titleId = useId()
|
|
35
47
|
const hintId = useId()
|
|
36
48
|
/** True while THIS component drives a commit / close (its synchronous focusout must not re-fire the save). / 自分が commit / close を駆動している間 true (同期 focusout の二重保存を防ぐ)。 */
|
|
@@ -46,11 +58,16 @@ export const NotePopover = ({ ctx, labels }: NotePopoverProps): ReactNode => {
|
|
|
46
58
|
}
|
|
47
59
|
}
|
|
48
60
|
const save = (): void => {
|
|
61
|
+
// 読み取り専用ビューア: 確定操作は不可 (disabled ボタンと同じ品行 — Ctrl+Enter も何もしない)
|
|
62
|
+
if (readOnly) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
49
65
|
// 空の下書きはノート消去
|
|
50
66
|
settle(() => ctx.commit(draft === "" ? null : draft))
|
|
51
67
|
}
|
|
52
68
|
const saveIfChanged = (): void => {
|
|
53
|
-
|
|
69
|
+
// 読み取り専用ビューア: Escape / フォーカス離脱は保存せず閉じる (下書きは commit できない)
|
|
70
|
+
if (draft !== initial && !readOnly) {
|
|
54
71
|
save()
|
|
55
72
|
} else {
|
|
56
73
|
settle(ctx.close)
|
|
@@ -91,10 +108,11 @@ export const NotePopover = ({ ctx, labels }: NotePopoverProps): ReactNode => {
|
|
|
91
108
|
</div>
|
|
92
109
|
<textarea className="aqsp-popover-textarea" data-aqsp-note-input="" data-aqsp-popover-autofocus="" value={draft} aria-describedby={hintId} aria-invalid={ctx.rejected ? true : undefined} onChange={(event) => setDraft(event.currentTarget.value)} onKeyDown={onKeyDown} />
|
|
93
110
|
<p className="aqsp-popover-hint" id={hintId} data-aqsp-popover-hint="">
|
|
94
|
-
{
|
|
111
|
+
{/* ビューアは保存 (noteEscapeHint) も再試行 (noteRejectedHint) も約束できない — Escape は保存せず閉じるため noteViewerHint を最優先 */}
|
|
112
|
+
{resolveLabel(labels, readOnly ? "noteViewerHint" : ctx.rejected ? "noteRejectedHint" : "noteEscapeHint")}
|
|
95
113
|
</p>
|
|
96
114
|
<div className="aqsp-popover-actions">
|
|
97
|
-
<button type="button" className="aqsp-popover-button" data-aqsp-popover-save="" onClick={save}>
|
|
115
|
+
<button type="button" className="aqsp-popover-button" data-aqsp-popover-save="" disabled={readOnly} aria-disabled={readOnly ? "true" : undefined} onClick={save}>
|
|
98
116
|
{resolveLabel(labels, "noteSave")}
|
|
99
117
|
</button>
|
|
100
118
|
<button type="button" className="aqsp-popover-button" data-aqsp-popover-close="" aria-label={resolveLabel(labels, "popoverClose")} onClick={() => settle(ctx.close)}>
|
|
@@ -21,6 +21,13 @@
|
|
|
21
21
|
* cell binds —) and the
|
|
22
22
|
* IME guard the layer applies to every popover keydown (`isCompositionKeydown`). No DOM is
|
|
23
23
|
* rendered here — PopoverLayer.tsx renders from `context` / `tipContext` (lazy mount).
|
|
24
|
+
* v0.10 read-only (design ADR-30, refusals via the required `rejectReadOnly` funnel): S3 = ONE
|
|
25
|
+
* interactive-context builder site for the declaration-gated `cellReadOnly` key (note / comment /
|
|
26
|
+
* link builds and the G14a re-present all share it); G14a re-checks live at `commitOrReject`'s
|
|
27
|
+
* head (refusal re-presents with `cellReadOnly` recomputed, `rejected` kept false, draft
|
|
28
|
+
* preserved); G14b is the list commit's defense-in-depth; G14c refuses a list open PRE-open —
|
|
29
|
+
* after the RangeError wiring check, BEFORE the commit gate (doomed-open: no force-commit).
|
|
30
|
+
* Capability undeclared = contexts and conduct byte-identical to v0.9.
|
|
24
31
|
*
|
|
25
32
|
* ポップオーバー状態機械 (v0.8 §1.8 / §4.1.15 — ADR-15 対話層)。6 種 (note エディタ / ホバー
|
|
26
33
|
* note tip / コメントスレッド / ハイパーリンクエディタ / 検証リスト / 検証メッセージ) を overlay が
|
|
@@ -39,7 +46,12 @@
|
|
|
39
46
|
* 変化 (root の active 効果が呼ぶ) のたびに再評価し、離れれば外し、ホバー中セルへ乗れば結ぶ —)、
|
|
40
47
|
* 層が全 popover keydown に掛ける
|
|
41
48
|
* IME ガード (`isCompositionKeydown`)。DOM は描かない — PopoverLayer.tsx が `context` /
|
|
42
|
-
* `tipContext` から遅延マウントする。
|
|
49
|
+
* `tipContext` から遅延マウントする。v0.10 読み取り専用 (設計 ADR-30、拒否は必須 `rejectReadOnly`
|
|
50
|
+
* 漏斗): S3 = 宣言ゲートキー `cellReadOnly` の単一対話 context ビルダ (note / comment / link の
|
|
51
|
+
* 構築と G14a 再提示が共有)、G14a = commitOrReject 頭の live 再検査 (拒否は cellReadOnly 再計算 +
|
|
52
|
+
* rejected false のまま + 下書き保持で再提示)、G14b = list commit の縦深防御、G14c = list の
|
|
53
|
+
* pre-open 拒否 (RangeError 結線検査の後・commit ゲートの前 — doomed-open で force-commit なし)。
|
|
54
|
+
* 能力未宣言は context も品行も v0.9 とバイト恒等。
|
|
43
55
|
*/
|
|
44
56
|
|
|
45
57
|
import type { VirtualGridHandle } from "@aiquants/virtualscroll"
|
|
@@ -48,7 +60,7 @@ import { isCompositionKeydown } from "../keyboard/compositionGuard.ts"
|
|
|
48
60
|
import type { AxisModel } from "../model/axisModel.ts"
|
|
49
61
|
import { type CellComment, type CellCommentInput, normalizeAdapterHyperlink, normalizeHyperlink } from "../model/cellAnnotations.ts"
|
|
50
62
|
import { type CellCoord, clampCoord, coordEquals } from "../model/cellCoord.ts"
|
|
51
|
-
import type
|
|
63
|
+
import { type CellEdit, type EditRejection, isReadOnlyCell, type ReadOnlyRejection, readOnlyDeclared, type SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
52
64
|
import { colToViewportX, rowToViewportY, viewportXToCol, viewportYToRow } from "../model/frozenPartition.ts"
|
|
53
65
|
import { resolveLabel, type SpreadsheetLabels } from "../model/labels.ts"
|
|
54
66
|
import { NOTE_TIP_LEAVE_GRACE_MS, type PopoverRect, shouldShowNoteTip } from "./placement.ts"
|
|
@@ -62,17 +74,28 @@ export const POPOVER_KINDS: readonly PopoverKind[] = ["note", "note-tip", "comme
|
|
|
62
74
|
/** The `popovers` prop: `false` disables a kind (its commands pass through, `openPopover` throws). / `popovers` prop: `false` はその kind を無効化 (コマンドは素通し、`openPopover` は RangeError)。 */
|
|
63
75
|
export type PopoverOverrides = { readonly [K in PopoverKind]?: boolean }
|
|
64
76
|
|
|
65
|
-
/** Note editor context (v0.8 §1.8). `current` undefined = no note yet; `commit(null)` clears. / note エディタ文脈。current undefined = ノートなし、commit(null) は消去。 */
|
|
66
|
-
export type NotePopoverContext = { kind: "note"; cell: CellCoord; anchorRect: PopoverRect; current: string | undefined; commit: (next: string | null) => void; close: () => void; rejected: boolean }
|
|
77
|
+
/** Note editor context (v0.8 §1.8). `current` undefined = no note yet; `commit(null)` clears. `cellReadOnly` (v0.10, declaration-gated): key ABSENT = the `isCellReadOnly` capability is undeclared (byte-identical context), else the live predicate answer — `true` disables the default component's commit affordance (G14a walls the commit regardless). / note エディタ文脈。current undefined = ノートなし、commit(null) は消去。cellReadOnly (v0.10、宣言ゲート): キー不在 = isCellReadOnly 能力未宣言 (context はバイト恒等)、あれば live 述語の答え — true は既定コンポーネントの確定操作を disabled にする (壁は G14a)。 */
|
|
78
|
+
export type NotePopoverContext = { kind: "note"; cell: CellCoord; anchorRect: PopoverRect; current: string | undefined; commit: (next: string | null) => void; close: () => void; rejected: boolean; cellReadOnly?: boolean }
|
|
67
79
|
|
|
68
80
|
/** Hover note tip context (read-only). / ホバー note tip 文脈 (読み取り専用)。 */
|
|
69
81
|
export type NoteTipContext = { kind: "note-tip"; cell: CellCoord; anchorRect: PopoverRect; current: string; close: () => void }
|
|
70
82
|
|
|
71
|
-
/** Comment thread context: the reply UI commits `[...current (with ids), { text }]`; `null` clears the thread. / コメントスレッド文脈: 返信 UI は `[...current (id つき), { text }]` を commit、null はスレッド消去。 */
|
|
72
|
-
export type CommentPopoverContext = { kind: "comment"; cell: CellCoord; anchorRect: PopoverRect; current: readonly CellComment[]; commit: (next: readonly CellCommentInput[] | null) => void; close: () => void; rejected: boolean }
|
|
83
|
+
/** Comment thread context: the reply UI commits `[...current (with ids), { text }]`; `null` clears the thread. `cellReadOnly` (v0.10): same declaration-gated key as the note context. / コメントスレッド文脈: 返信 UI は `[...current (id つき), { text }]` を commit、null はスレッド消去。cellReadOnly (v0.10) は note 文脈と同じ宣言ゲートキー。 */
|
|
84
|
+
export type CommentPopoverContext = { kind: "comment"; cell: CellCoord; anchorRect: PopoverRect; current: readonly CellComment[]; commit: (next: readonly CellCommentInput[] | null) => void; close: () => void; rejected: boolean; cellReadOnly?: boolean }
|
|
73
85
|
|
|
74
|
-
/** Hyperlink editor context: `url` on commit is a normalised href or null (clear); `canOpen` = onHyperlinkOpen declared. / ハイパーリンクエディタ文脈: commit の url は正規化済み href か null (消去)、canOpen = onHyperlinkOpen 宣言。 */
|
|
75
|
-
export type LinkPopoverContext = {
|
|
86
|
+
/** Hyperlink editor context: `url` on commit is a normalised href or null (clear); `canOpen` = onHyperlinkOpen declared. `cellReadOnly` (v0.10): same declaration-gated key as the note context (the Open button stays live — navigation is a read). / ハイパーリンクエディタ文脈: commit の url は正規化済み href か null (消去)、canOpen = onHyperlinkOpen 宣言。cellReadOnly (v0.10) は note 文脈と同じ宣言ゲートキー (Open は読みなので生きたまま)。 */
|
|
87
|
+
export type LinkPopoverContext = {
|
|
88
|
+
kind: "link"
|
|
89
|
+
cell: CellCoord
|
|
90
|
+
anchorRect: PopoverRect
|
|
91
|
+
current: { text: string; url: string | undefined }
|
|
92
|
+
commit: (next: { text: string; url: string | null }) => void
|
|
93
|
+
close: () => void
|
|
94
|
+
canOpen: boolean
|
|
95
|
+
open: () => void
|
|
96
|
+
rejected: boolean
|
|
97
|
+
cellReadOnly?: boolean
|
|
98
|
+
}
|
|
76
99
|
|
|
77
100
|
/** Validation list picker context: `current` = the list rule's values, `typed` = the prefix that opened it. / 検証リスト文脈: current = list 規則の値、typed = 開いたときの入力プレフィックス。 */
|
|
78
101
|
export type ListPopoverContext = { kind: "list"; cell: CellCoord; anchorRect: PopoverRect; current: readonly string[]; typed: string; commit: (value: string) => void; close: () => void }
|
|
@@ -144,6 +167,15 @@ export type UsePopoversParams = {
|
|
|
144
167
|
/** Composite drag probe incl. the reference drag — the tip is never shown mid-drag. / 参照ドラッグ含む合成ドラッグ判定 — ドラッグ中は tip を出さない。 */
|
|
145
168
|
isDraggingAny: () => boolean
|
|
146
169
|
commitEdits: (edits: readonly CellEdit[], rejection: EditRejection) => number | null
|
|
170
|
+
/**
|
|
171
|
+
* v0.10 read-only refusal funnel (required seam — the root always wires it; identity-stable,
|
|
172
|
+
* read through paramsRef): G14a (interactive commit), G14b (list commit) and G14c (the list
|
|
173
|
+
* pre-open refusal) fire it once per refused gesture. Capability absent ⇒ never called.
|
|
174
|
+
* v0.10 読み取り専用拒否漏斗 (必須 seam — root が常に渡す。identity 安定、paramsRef 読み):
|
|
175
|
+
* G14a (対話 commit)・G14b (list commit)・G14c (list の pre-open 拒否) が拒否ジェスチャ毎
|
|
176
|
+
* 1 回発火する。能力未宣言なら一度も呼ばれない。
|
|
177
|
+
*/
|
|
178
|
+
rejectReadOnly: (rejection: ReadOnlyRejection) => void
|
|
147
179
|
labels: SpreadsheetLabels
|
|
148
180
|
onHyperlinkOpen?: (url: string, cell: CellCoord) => void
|
|
149
181
|
/** Root-owned anchor state the overlay positions. / root 所有のアンカー状態 (overlay が配置)。 */
|
|
@@ -451,10 +483,34 @@ export const usePopovers = (params: UsePopoversParams): PopoverController => {
|
|
|
451
483
|
[writeAnchors],
|
|
452
484
|
)
|
|
453
485
|
|
|
454
|
-
/**
|
|
486
|
+
/**
|
|
487
|
+
* S3 (v0.10): the SINGLE interactive-context builder site for the declaration-gated
|
|
488
|
+
* `cellReadOnly` key — the three build paths (note / comment / link) and the G14a re-present
|
|
489
|
+
* all spread this result, so the census stays at one `readOnlyDeclared` + one `isReadOnlyCell`
|
|
490
|
+
* call. Key absent = capability undeclared (context byte-identical), else the live answer.
|
|
491
|
+
* S3 (v0.10): 宣言ゲートキー `cellReadOnly` の**単一**対話 context ビルダサイト — 3 つの構築
|
|
492
|
+
* 経路 (note / comment / link) と G14a の再提示が全てこの戻り値を spread するため、census は
|
|
493
|
+
* readOnlyDeclared 1 + isReadOnlyCell 1 のまま。キー不在 = 能力未宣言 (context バイト恒等)、
|
|
494
|
+
* あれば live の答え。
|
|
495
|
+
*/
|
|
496
|
+
const readOnlyKeyOf = useCallback((cell: CellCoord): { cellReadOnly?: boolean } => {
|
|
497
|
+
const p = paramsRef.current
|
|
498
|
+
// 宣言時だけキーを出す (undefined キーも出さない — v0.9 形ホストの context はバイト恒等)
|
|
499
|
+
return readOnlyDeclared(p.adapter) ? { cellReadOnly: isReadOnlyCell(p.adapter, cell.row, cell.col) } : {}
|
|
500
|
+
}, [])
|
|
501
|
+
|
|
502
|
+
/** Runs an annotate commit; a G14a read-only refusal re-presents with `cellReadOnly` recomputed (`rejected` stays false), an adapter rejection re-presents with `rejected: true`. / annotate の commit を実行。G14a の読み取り専用拒否は cellReadOnly を再計算して再提示 (rejected は false のまま)、アダプタ拒否は rejected: true で再提示。 */
|
|
455
503
|
const commitOrReject = useCallback(
|
|
456
504
|
(ctx: NotePopoverContext | CommentPopoverContext | LinkPopoverContext, edits: readonly CellEdit[]) => {
|
|
457
505
|
const p = paramsRef.current
|
|
506
|
+
// G14a (v0.10 §6.10): commit 頭の live 再検査 — adapter 不到達。再提示は cellReadOnly 再計算
|
|
507
|
+
// (今は true — 既定コンポーネントが確定操作を disabled に)、rejected は false のまま
|
|
508
|
+
// (true は「adapter が投げた、再試行の余地あり」の意味 — ここでは嘘になる)、入力値は保持
|
|
509
|
+
if (isReadOnlyCell(p.adapter, ctx.cell.row, ctx.cell.col)) {
|
|
510
|
+
p.rejectReadOnly({ kind: "operation", op: "annotate", cellCount: 1 })
|
|
511
|
+
present({ ...ctx, ...readOnlyKeyOf(ctx.cell) })
|
|
512
|
+
return
|
|
513
|
+
}
|
|
458
514
|
if (p.commitEdits(edits, { source: "annotate" }) === null) {
|
|
459
515
|
// 拒否: 開いたまま rejected 状態へ (既定コンポーネントが Ctrl+Enter 再試行 / Escape 破棄を担う)
|
|
460
516
|
present({ ...ctx, rejected: true })
|
|
@@ -462,7 +518,7 @@ export const usePopovers = (params: UsePopoversParams): PopoverController => {
|
|
|
462
518
|
}
|
|
463
519
|
close()
|
|
464
520
|
},
|
|
465
|
-
[present, close],
|
|
521
|
+
[present, close, readOnlyKeyOf],
|
|
466
522
|
)
|
|
467
523
|
|
|
468
524
|
const open = useCallback(
|
|
@@ -472,6 +528,14 @@ export const usePopovers = (params: UsePopoversParams): PopoverController => {
|
|
|
472
528
|
throw new RangeError(`[spreadsheet] openPopover("${kind}") is unavailable: the kind is disabled by the popovers prop or its adapter capability is undeclared`)
|
|
473
529
|
}
|
|
474
530
|
const cell = p.getActive()
|
|
531
|
+
// G14c (v0.10 §6.10 — doomed-open 原則): list の pre-open 拒否。座席は RangeError 結線検査の
|
|
532
|
+
// 後 (結線バグはデータ状態に優先 — fail-fast 序列保存)・open 経路 commit ゲートの**前**
|
|
533
|
+
// (拒否される open が無関係セッションを force-commit しない)。picker の対話 = 書込そのもの
|
|
534
|
+
// なので popover は開かない。RangeError にはしない (読み取り専用はデータ状態で結線バグではない)
|
|
535
|
+
if (kind === "list" && isReadOnlyCell(p.adapter, cell.row, cell.col)) {
|
|
536
|
+
p.rejectReadOnly({ kind: "edit", cell, editSession: "none" })
|
|
537
|
+
return
|
|
538
|
+
}
|
|
475
539
|
if (kind === "note-tip") {
|
|
476
540
|
// 手動表示: active セルにノートがあるときだけ (ホバー述語とは独立)
|
|
477
541
|
showTip(cell)
|
|
@@ -515,6 +579,14 @@ export const usePopovers = (params: UsePopoversParams): PopoverController => {
|
|
|
515
579
|
current: rule.values,
|
|
516
580
|
typed,
|
|
517
581
|
commit: (value: string) => {
|
|
582
|
+
// G14b (v0.10 §6.10): list commit の縦深防御 — 開いている間の読み取り専用化を
|
|
583
|
+
// commitEdits 前に live 再検査 (G14c が正門、ここは第二壁)。拒否は無書込で閉じる
|
|
584
|
+
// (picker に他の対話は無い — Escape 相当の閉鎖)
|
|
585
|
+
if (isReadOnlyCell(paramsRef.current.adapter, cell.row, cell.col)) {
|
|
586
|
+
paramsRef.current.rejectReadOnly({ kind: "edit", cell, editSession: "none" })
|
|
587
|
+
close()
|
|
588
|
+
return
|
|
589
|
+
}
|
|
518
590
|
// リスト確定はセル編集 (source "edit"、セッションなし = discarded)
|
|
519
591
|
paramsRef.current.commitEdits([{ row: cell.row, col: cell.col, text: value }], { source: "edit", editSession: "discarded" })
|
|
520
592
|
close()
|
|
@@ -537,6 +609,8 @@ export const usePopovers = (params: UsePopoversParams): PopoverController => {
|
|
|
537
609
|
},
|
|
538
610
|
close,
|
|
539
611
|
rejected: false,
|
|
612
|
+
// S3: 宣言ゲートキー (単一ビルダ経由 — 未宣言はキー不在で context バイト恒等)
|
|
613
|
+
...readOnlyKeyOf(cell),
|
|
540
614
|
}
|
|
541
615
|
present(ctx)
|
|
542
616
|
return
|
|
@@ -554,6 +628,8 @@ export const usePopovers = (params: UsePopoversParams): PopoverController => {
|
|
|
554
628
|
},
|
|
555
629
|
close,
|
|
556
630
|
rejected: false,
|
|
631
|
+
// S3: 宣言ゲートキー (単一ビルダ経由)
|
|
632
|
+
...readOnlyKeyOf(cell),
|
|
557
633
|
}
|
|
558
634
|
present(ctx)
|
|
559
635
|
return
|
|
@@ -585,10 +661,12 @@ export const usePopovers = (params: UsePopoversParams): PopoverController => {
|
|
|
585
661
|
}
|
|
586
662
|
},
|
|
587
663
|
rejected: false,
|
|
664
|
+
// S3: 宣言ゲートキー (単一ビルダ経由)
|
|
665
|
+
...readOnlyKeyOf(cell),
|
|
588
666
|
}
|
|
589
667
|
present(ctx)
|
|
590
668
|
},
|
|
591
|
-
[anchorRectOf, close, commitOrReject, hideTipNow, present, showTip],
|
|
669
|
+
[anchorRectOf, close, commitOrReject, hideTipNow, present, showTip, readOnlyKeyOf],
|
|
592
670
|
)
|
|
593
671
|
|
|
594
672
|
const { enabled, bodyRef } = params
|