superdoc 2.24.0-next.1 → 2.24.0-next.3

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.
@@ -5939,6 +5939,7 @@ function buildTableCommandInput(action, context) {
5939
5939
  };
5940
5940
  case "delete-table": return { nodeId };
5941
5941
  case "merge-cells": {
5942
+ if (!context.canMerge) return null;
5942
5943
  const range = mergeRange ?? {
5943
5944
  start: {
5944
5945
  rowIndex,
@@ -6074,6 +6075,18 @@ function createSuperDocUI(options) {
6074
6075
  const host = getEditor()?.host;
6075
6076
  return host && typeof host === "object" ? host : null;
6076
6077
  };
6078
+ /**
6079
+ * The host selection snapshot a selection read, slice, or capture was
6080
+ * resolved from. The host replaces its snapshot object on every selection
6081
+ * change, so an identical object means the selection has not moved since.
6082
+ */
6083
+ const selectionHostSnapshots = /* @__PURE__ */ new WeakMap();
6084
+ const readHostSelectionSnapshot = () => {
6085
+ const selectionHandle = (safeCall(() => getHost()?.getHandles?.() ?? null, null)?.editing)?.selection;
6086
+ if (typeof selectionHandle?.getSnapshot !== "function") return null;
6087
+ const snapshot = safeCall(() => selectionHandle.getSnapshot(), null);
6088
+ return snapshot && typeof snapshot === "object" ? snapshot : null;
6089
+ };
6077
6090
  const createFirstPartyCommandDoc = () => {
6078
6091
  const editor = getEditor();
6079
6092
  const doc = editor?.doc;
@@ -6245,7 +6258,7 @@ function createSuperDocUI(options) {
6245
6258
  };
6246
6259
  const resolveTableContext = () => {
6247
6260
  const snapshot = readHostTableContext();
6248
- if (!snapshot || snapshot.inTable !== true) return null;
6261
+ if (!snapshot || snapshot.inTable !== true || snapshot.status && snapshot.status !== "ready") return null;
6249
6262
  const table = snapshot.table;
6250
6263
  const tableNodeId = typeof table?.nodeId === "string" && table.nodeId.length > 0 ? table.nodeId : null;
6251
6264
  if (!tableNodeId) return null;
@@ -6257,12 +6270,15 @@ function createSuperDocUI(options) {
6257
6270
  if (rowIndex == null || columnIndex == null) return null;
6258
6271
  const cellNodeId = typeof cell?.nodeId === "string" && cell.nodeId.length > 0 ? cell.nodeId : null;
6259
6272
  const range = snapshot.cellRange;
6273
+ const mergeRange = readMergeRange(range);
6274
+ const metadata = snapshot.tableSelection;
6260
6275
  return {
6261
6276
  tableNodeId,
6262
6277
  rowIndex,
6263
6278
  columnIndex,
6264
6279
  cellNodeId,
6265
- mergeRange: readMergeRange(range)
6280
+ mergeRange,
6281
+ canMerge: metadata == null || metadata.selectionMode === "cells" && metadata.isRectangular === true
6266
6282
  };
6267
6283
  };
6268
6284
  const customCommands = /* @__PURE__ */ new Map();
@@ -7278,12 +7294,21 @@ function createSuperDocUI(options) {
7278
7294
  ...EMPTY_SELECTION,
7279
7295
  status
7280
7296
  };
7297
+ const slice = buildSelectionSlice(info, status);
7298
+ const hostSelection = selectionHostSnapshots.get(info);
7299
+ if (hostSelection) selectionHostSnapshots.set(slice, hostSelection);
7300
+ return slice;
7301
+ };
7302
+ const buildSelectionSlice = (info, status) => {
7281
7303
  const target = info.target ?? null;
7282
7304
  const quotedText = typeof info.text === "string" ? info.text : "";
7283
7305
  return {
7284
7306
  status,
7285
7307
  empty: info.empty === true,
7286
7308
  target,
7309
+ trackedTarget: info.trackedTarget ?? null,
7310
+ trackedChangeRangeDecisionSupported: info.trackedChangeRangeDecisionSupported,
7311
+ trackedChangesWhollySelected: info.trackedChangesWhollySelected,
7287
7312
  selectionTarget: info.selectionTarget ?? null,
7288
7313
  tableSelection: info.tableSelection ?? null,
7289
7314
  activeMarks: Array.isArray(info.activeMarks) ? info.activeMarks : [],
@@ -7292,18 +7317,22 @@ function createSuperDocUI(options) {
7292
7317
  quotedText
7293
7318
  };
7294
7319
  };
7295
- const readSelectionInfoLive = () => {
7296
- const selectionApi = getDoc()?.selection;
7297
- return readAsync("selection", selectionReadToken(), () => selectionApi?.current ? selectionApi.current({ includeText: true }) : void 0, normalizeSelectionInfo);
7298
- };
7320
+ const readSelectionInfoLive = () => readAsync("selection", selectionReadToken(), selectionCurrentRun, normalizeSelectionInfo);
7299
7321
  const computeSelection = () => {
7300
7322
  const { value: info, status } = readSelectionInfoLive();
7301
7323
  return selectionSliceFromInfo(info, status);
7302
7324
  };
7303
- const selectionCurrentRun = () => {
7325
+ function selectionCurrentRun() {
7304
7326
  const selectionApi = getDoc()?.selection;
7305
- return selectionApi?.current ? selectionApi.current({ includeText: true }) : void 0;
7306
- };
7327
+ if (!selectionApi?.current) return void 0;
7328
+ const hostSelection = readHostSelectionSnapshot();
7329
+ const tag = (info) => {
7330
+ if (hostSelection && info && typeof info === "object") selectionHostSnapshots.set(info, hostSelection);
7331
+ return info;
7332
+ };
7333
+ const result = selectionApi.current({ includeText: true });
7334
+ return isPromiseLike(result) ? Promise.resolve(result).then(tag) : tag(result);
7335
+ }
7307
7336
  /** True when a host sync snapshot is a resolved collapsed caret (not a range). */
7308
7337
  const isCollapsedCaretSnapshot = (info) => {
7309
7338
  if (info.empty !== true) return false;
@@ -8283,6 +8312,10 @@ function createSuperDocUI(options) {
8283
8312
  if (!supported) return command.scope === "all" ? SUPERDOC_UI_REASONS.bulkDecisionsDisabled : unavailableRouteReason(doc);
8284
8313
  if (disabledReason) return disabledReason;
8285
8314
  if (command.scope !== "all" && selection.activeChangeIds.length === 0) return SUPERDOC_UI_REASONS.selectionRequired;
8315
+ if (command.scope !== "all") {
8316
+ const plan = planSelectionTrackDecision(selection, true);
8317
+ if (plan?.kind === "blocked") return plan.reason;
8318
+ }
8286
8319
  };
8287
8320
  const findTrackChangeForPermission = (id) => {
8288
8321
  syncCoordinatorEditor();
@@ -8302,20 +8335,22 @@ function createSuperDocUI(options) {
8302
8335
  if (typeof canPerform !== "function" || ids.length === 0) return void 0;
8303
8336
  const currentUser = superdoc.config && typeof superdoc.config === "object" ? superdoc.config.user : null;
8304
8337
  for (const id of ids) {
8305
- const trackedChange = findTrackChangeForPermission(id) ?? { id };
8306
- const isOwn = trackedChangeOwnedByCurrentUser(trackedChange, currentUser);
8307
- const permission = kind === "accept" ? isOwn ? "RESOLVE_OWN" : "RESOLVE_OTHER" : isOwn ? "REJECT_OWN" : "REJECT_OTHER";
8308
- let allowed;
8309
- try {
8310
- allowed = canPerform.call(superdoc, {
8311
- permission,
8312
- trackedChange,
8313
- comment: null
8314
- });
8315
- } catch {
8316
- return;
8338
+ const trackedChange = findTrackChangeForPermission(id);
8339
+ const isOwn = trackedChange ? trackedChangeOwnedByCurrentUser(trackedChange, currentUser) : false;
8340
+ const permissions = trackedChange ? [kind === "accept" ? isOwn ? "RESOLVE_OWN" : "RESOLVE_OTHER" : isOwn ? "REJECT_OWN" : "REJECT_OTHER"] : kind === "accept" ? ["RESOLVE_OWN", "RESOLVE_OTHER"] : ["REJECT_OWN", "REJECT_OTHER"];
8341
+ for (const candidate of permissions) {
8342
+ let allowed;
8343
+ try {
8344
+ allowed = canPerform.call(superdoc, {
8345
+ permission: candidate,
8346
+ trackedChange: trackedChange ?? { id },
8347
+ comment: null
8348
+ });
8349
+ } catch {
8350
+ return trackedChange ? void 0 : SUPERDOC_UI_REASONS.permissionDenied;
8351
+ }
8352
+ if (allowed === false || !trackedChange && allowed !== true) return SUPERDOC_UI_REASONS.permissionDenied;
8317
8353
  }
8318
- if (allowed === false) return SUPERDOC_UI_REASONS.permissionDenied;
8319
8354
  }
8320
8355
  };
8321
8356
  const hostCommandSupport = (command) => {
@@ -9156,7 +9191,7 @@ function createSuperDocUI(options) {
9156
9191
  reason: SUPERDOC_UI_REASONS.documentReadonly
9157
9192
  }, "builtin");
9158
9193
  const context = resolveTableContext();
9159
- if (!(context != null && (!descriptor.table.requiresCell || context.cellNodeId != null))) return normalizeCommandState({
9194
+ if (!(context != null && (!descriptor.table.requiresCell || context.cellNodeId != null) && (descriptor.table.action !== "merge-cells" || context.canMerge))) return normalizeCommandState({
9160
9195
  enabled: false,
9161
9196
  active: false,
9162
9197
  supported: true,
@@ -9995,14 +10030,22 @@ function createSuperDocUI(options) {
9995
10030
  * ordinary/insertion selections are visible-space, while a selection that
9996
10031
  * reaches painted deleted text is projected in tracked space by the host.
9997
10032
  *
9998
- * A cross-block or malformed selection fails closed. Its intermediate block
9999
- * coverage cannot be reconstructed from two endpoints without guessing.
10033
+ * Cross-block coverage must come from the model-backed tracked target;
10034
+ * intermediate block lengths cannot be inferred from the endpoints.
10000
10035
  */
10001
10036
  function trackDecisionRangeFromSelection(selection) {
10002
10037
  if (selection.empty) return null;
10003
10038
  const explicit = selection.selectionTarget;
10004
10039
  const start = explicit?.start;
10005
10040
  const end = explicit?.end;
10041
+ if (start?.kind === "text" && end?.kind === "text" && start.blockId !== end.blockId) {
10042
+ const range = selection.trackedTarget;
10043
+ return range ? {
10044
+ kind: "range",
10045
+ coordinateSpace: "tracked",
10046
+ range
10047
+ } : null;
10048
+ }
10006
10049
  if (explicit?.kind !== "selection" || start?.kind !== "text" || end?.kind !== "text" || typeof start.blockId !== "string" || start.blockId !== end.blockId || !Number.isInteger(start.offset) || !Number.isInteger(end.offset) || start.offset < 0 || end.offset < 0 || start.offset === end.offset) return null;
10007
10050
  const rangeStart = Math.min(start.offset, end.offset);
10008
10051
  const rangeEnd = Math.max(start.offset, end.offset);
@@ -10047,32 +10090,20 @@ function createSuperDocUI(options) {
10047
10090
  function normalizeTrackDecisionText(value) {
10048
10091
  return typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
10049
10092
  }
10050
- function activeChangePartialDecisionRoute() {
10051
- const activeId = state.selection.activeChangeIds[0];
10093
+ function activeChangePartialDecisionRoute(selection = state.selection) {
10094
+ const activeId = selection.activeChangeIds[0];
10052
10095
  if (!activeId) return "unavailable";
10053
- const story = selectionStoryLocator(state.selection);
10054
- const item = (state.trackChanges.status === "ready" ? state.trackChanges.items.find((candidate) => entityRowMatchesRequest(candidate, activeId, story)) : void 0) ?? readBodyTrackChangeForDecision(activeId, story) ?? readAllStoryTrackChanges()?.find((candidate) => entityRowMatchesRequest(candidate, activeId, story));
10096
+ const story = selectionStoryLocator(selection);
10097
+ const trackChanges = state?.trackChanges;
10098
+ const item = (trackChanges?.status === "ready" ? trackChanges.items.find((candidate) => entityRowMatchesRequest(candidate, activeId, story)) : void 0) ?? readBodyTrackChangeForDecision(activeId, story) ?? readAllStoryTrackChanges()?.find((candidate) => entityRowMatchesRequest(candidate, activeId, story));
10055
10099
  if (!item) return "unavailable";
10056
10100
  const change = trackChangesItemPayload(item);
10057
10101
  const type = change.type;
10058
10102
  const grouping = change.grouping;
10059
- if (selectedTextCoversTrackChangeText(state.selection, item)) return "id";
10103
+ if (selectedTextCoversTrackChangeText(selection, item)) return "id";
10060
10104
  if (type === "replacement" && grouping === "replacement-pair") return "range";
10061
10105
  return (type === "insertion" || type === "deletion" || type === "insert" || type === "delete") && grouping === "standalone" ? "range" : "id";
10062
10106
  }
10063
- function executeTrackDecisionRangeWithIdFallback(kind, rangeTarget, changeId, story) {
10064
- const fallback = () => callTrackDecisionTarget(kind, trackDecisionIdTarget(changeId, story), changeId, story);
10065
- try {
10066
- const result = callTrackDecisionTarget(kind, rangeTarget, changeId, story);
10067
- if (isPromiseLike(result)) return settleCommandExecution(Promise.resolve(result).then((settled) => {
10068
- return commandResultSucceeded(commandResultFromOperationResult(settled)) ? settled : fallback();
10069
- }));
10070
- const commandResult = commandResultFromOperationResult(result);
10071
- return settleCommandExecution(commandResultSucceeded(commandResult) ? result : fallback());
10072
- } catch {
10073
- return false;
10074
- }
10075
- }
10076
10107
  /** A decision target carried by the payload itself, independent of the selection. */
10077
10108
  function explicitTrackDecisionTarget(payload) {
10078
10109
  if (typeof payload === "string" && payload.length > 0) return {
@@ -10116,29 +10147,88 @@ function createSuperDocUI(options) {
10116
10147
  story
10117
10148
  } : null;
10118
10149
  }
10150
+ /**
10151
+ * The selection-derived decision for a review command. The command gate and
10152
+ * execution share these rules so an enabled command always has a target.
10153
+ * `null` means the selection does not drive the decision (no active change
10154
+ * or a collapsed caret) and the caller falls back to the active change id.
10155
+ * `gateOnly` stops after the blocking rules and returns only a block or null.
10156
+ */
10157
+ function planSelectionTrackDecision(selection, gateOnly = false) {
10158
+ if (selection.tableSelection?.kind === "tableCells" && (selection.tableSelection.type === "cells" || selection.tableSelection.type === "table")) return {
10159
+ kind: "blocked",
10160
+ reason: SUPERDOC_UI_REASONS.operationUnavailable
10161
+ };
10162
+ const ids = selection.activeChangeIds;
10163
+ if (ids.length > 1) {
10164
+ if (selection.empty || selection.trackedChangesWhollySelected === true) return {
10165
+ kind: "ids",
10166
+ ids
10167
+ };
10168
+ if (selection.trackedChangeRangeDecisionSupported === false) return {
10169
+ kind: "blocked",
10170
+ reason: SUPERDOC_UI_REASONS.operationUnavailable
10171
+ };
10172
+ const target = trackDecisionRangeFromSelection(selection);
10173
+ return target ? {
10174
+ kind: "range",
10175
+ target,
10176
+ changeId: null,
10177
+ permissionIds: ids
10178
+ } : {
10179
+ kind: "blocked",
10180
+ reason: SUPERDOC_UI_REASONS.targetUnresolved
10181
+ };
10182
+ }
10183
+ if (ids.length !== 1 || selection.empty) return null;
10184
+ const activeId = ids[0];
10185
+ let routeValue;
10186
+ const route = () => routeValue ??= selection.trackedChangesWhollySelected === true ? "id" : activeChangePartialDecisionRoute(selection);
10187
+ if (selection.trackedChangeRangeDecisionSupported === false && route() !== "id") return {
10188
+ kind: "blocked",
10189
+ reason: SUPERDOC_UI_REASONS.operationUnavailable
10190
+ };
10191
+ if (!selection.selectionTarget) return null;
10192
+ const target = trackDecisionRangeFromSelection(selection);
10193
+ if (!target && route() !== "id") return {
10194
+ kind: "blocked",
10195
+ reason: SUPERDOC_UI_REASONS.targetUnresolved
10196
+ };
10197
+ if (gateOnly) return null;
10198
+ if (route() === "id") {
10199
+ const selectedStory = selectionStoryLocator(selection);
10200
+ return {
10201
+ kind: "id",
10202
+ id: activeId,
10203
+ story: selectedStory && storyLocatorSignature(selectedStory) !== storyLocatorSignature(null) ? selectedStory : void 0
10204
+ };
10205
+ }
10206
+ if (route() === "unavailable" && !selection.trackedTarget) return { kind: "unavailable" };
10207
+ return target ? {
10208
+ kind: "range",
10209
+ target,
10210
+ changeId: activeId,
10211
+ story: nonBodySelectionStoryLocator(selection)
10212
+ } : {
10213
+ kind: "blocked",
10214
+ reason: SUPERDOC_UI_REASONS.targetUnresolved
10215
+ };
10216
+ }
10119
10217
  function executeTrackDecisionCommand(command, payload) {
10120
10218
  if (command.scope === "id" && payload == null) {
10121
- if (state.selection.activeChangeIds.length > 1) return executeTrackDecisionTarget(command.kind, {
10122
- kind: "ids",
10123
- ids: state.selection.activeChangeIds
10124
- }, null);
10125
- if (state.selection.activeChangeIds.length === 1 && !state.selection.empty && state.selection.selectionTarget) {
10126
- const route = activeChangePartialDecisionRoute();
10127
- if (route === "unavailable") return false;
10128
- if (route === "range") {
10129
- const target = trackDecisionRangeFromSelection(state.selection);
10130
- const story = nonBodySelectionStoryLocator(state.selection);
10131
- return target ? executeTrackDecisionRangeWithIdFallback(command.kind, target, state.selection.activeChangeIds[0], story) : false;
10132
- }
10133
- if (route === "id") {
10134
- const activeId = state.selection.activeChangeIds[0];
10135
- const selectedStory = selectionStoryLocator(state.selection);
10136
- const story = selectedStory && storyLocatorSignature(selectedStory) !== storyLocatorSignature(null) ? selectedStory : void 0;
10137
- return executeTrackDecision(command.kind, story ? {
10138
- id: activeId,
10139
- story
10140
- } : activeId);
10141
- }
10219
+ const plan = planSelectionTrackDecision(state.selection);
10220
+ switch (plan?.kind) {
10221
+ case "blocked":
10222
+ case "unavailable": return false;
10223
+ case "ids": return executeTrackDecisionTarget(command.kind, {
10224
+ kind: "ids",
10225
+ ids: plan.ids
10226
+ }, null, void 0, plan.ids);
10227
+ case "range": return executeTrackDecisionTarget(command.kind, plan.target, plan.changeId, plan.story, plan.permissionIds);
10228
+ case "id": return executeTrackDecision(command.kind, plan.story ? {
10229
+ id: plan.id,
10230
+ story: plan.story
10231
+ } : plan.id);
10142
10232
  }
10143
10233
  }
10144
10234
  const resolved = resolveTrackDecisionTarget(command, payload);
@@ -11807,11 +11897,14 @@ function createSuperDocUI(options) {
11807
11897
  if (snapshot.empty) return null;
11808
11898
  const cells = currentCellSelectionTarget();
11809
11899
  if (!snapshot.target && !snapshot.selectionTarget && !cells) return null;
11810
- return {
11900
+ const capture = {
11811
11901
  ...snapshot,
11812
11902
  capturedAt: Date.now(),
11813
11903
  ...cells ? { _tableCellSelectionTarget: cells } : {}
11814
11904
  };
11905
+ const hostSelection = selectionHostSnapshots.get(snapshot);
11906
+ if (hostSelection) selectionHostSnapshots.set(capture, hostSelection);
11907
+ return capture;
11815
11908
  },
11816
11909
  restore: (capture) => {
11817
11910
  const done = (result) => {
@@ -11840,6 +11933,8 @@ function createSuperDocUI(options) {
11840
11933
  ok: false,
11841
11934
  reason: SUPERDOC_UI_REASONS.targetUnresolved
11842
11935
  });
11936
+ const heldHostSelection = selectionHostSnapshots.get(capture);
11937
+ if (!hasCellTarget && target.coordinateSpace === "tracked" && heldHostSelection !== void 0 && readHostSelectionSnapshot() === heldHostSelection) return done({ ok: true });
11843
11938
  const resolved = getSelectionApplyHelper();
11844
11939
  if (!("helper" in resolved)) return done({
11845
11940
  ok: false,
@@ -12676,20 +12771,20 @@ function createSuperDocUI(options) {
12676
12771
  if (run() === false) return Promise.resolve(false);
12677
12772
  return lastCommandSettlement;
12678
12773
  };
12679
- const executeTrackDecisionTarget = (kind, target, changeId, story) => {
12774
+ const executeTrackDecisionTarget = (kind, target, changeId, story, selectionChangeIds) => {
12680
12775
  try {
12681
- return settleCommandExecution(callTrackDecisionTarget(kind, target, changeId, story));
12776
+ return settleCommandExecution(callTrackDecisionTarget(kind, target, changeId, story, selectionChangeIds));
12682
12777
  } catch {
12683
12778
  return false;
12684
12779
  }
12685
12780
  };
12686
- const callTrackDecisionTarget = (kind, target, changeId, story) => {
12781
+ const callTrackDecisionTarget = (kind, target, changeId, story, selectionChangeIds) => {
12687
12782
  if (trackedChangeDecisionsAreDisabled()) return false;
12688
12783
  const tcApi = getDoc()?.trackChanges;
12689
12784
  const isAllTarget = target.kind === "all" || target.scope === "all";
12690
12785
  const isRangeTarget = target.kind === "range";
12691
12786
  const isMultiIdTarget = target.kind === "ids";
12692
- const permissionIds = isAllTarget ? [] : isMultiIdTarget && Array.isArray(target.ids) ? target.ids.filter((id) => typeof id === "string" && id.length > 0) : changeId ? [changeId] : [];
12787
+ const permissionIds = isAllTarget ? [] : selectionChangeIds ?? (isMultiIdTarget && Array.isArray(target.ids) ? target.ids.filter((id) => typeof id === "string" && id.length > 0) : changeId ? [changeId] : []);
12693
12788
  if (permissionIds.length > 0 && trackedChangeDecisionPermissionReason(kind, permissionIds)) return false;
12694
12789
  if (bulkTrackDecisionBlockedReason({
12695
12790
  kind,
@@ -13061,7 +13156,13 @@ function createSuperDocUI(options) {
13061
13156
  return {
13062
13157
  point,
13063
13158
  entities: validInput ? entityAt(point) : [],
13064
- selection: state.selection,
13159
+ selection: (() => {
13160
+ const current = computeSelection();
13161
+ return current.status === "ready" ? current : {
13162
+ ...current,
13163
+ tableSelection: null
13164
+ };
13165
+ })(),
13065
13166
  position: null,
13066
13167
  insideSelection: false
13067
13168
  };
@@ -13278,21 +13379,67 @@ function createSuperDocUI(options) {
13278
13379
  columnIndex: null,
13279
13380
  cellNodeId: null,
13280
13381
  rows: null,
13281
- columns: null
13382
+ columns: null,
13383
+ status: "ready",
13384
+ cellRange: null,
13385
+ tableSelection: null
13386
+ };
13387
+ const publicTableContext = (snapshot) => {
13388
+ const status = snapshot?.status === "pending" || snapshot?.status === "stale" ? snapshot.status : "ready";
13389
+ if (!snapshot || snapshot.inTable !== true || status !== "ready") return {
13390
+ ...EMPTY_TABLE_CONTEXT,
13391
+ status
13392
+ };
13393
+ const table = snapshot.table;
13394
+ const row = snapshot.row;
13395
+ const column = snapshot.column;
13396
+ const cell = snapshot.cell;
13397
+ if (typeof table?.nodeId !== "string") return {
13398
+ ...EMPTY_TABLE_CONTEXT,
13399
+ status
13400
+ };
13401
+ return {
13402
+ inTable: true,
13403
+ status,
13404
+ tableNodeId: table.nodeId,
13405
+ rowIndex: typeof row?.index === "number" ? row.index : null,
13406
+ columnIndex: typeof column?.index === "number" ? column.index : null,
13407
+ cellNodeId: typeof cell?.nodeId === "string" ? cell.nodeId : null,
13408
+ rows: typeof table.rows === "number" ? table.rows : null,
13409
+ columns: typeof table.columns === "number" ? table.columns : null,
13410
+ cellRange: readMergeRange(snapshot.cellRange),
13411
+ tableSelection: snapshot.tableSelection ?? null
13412
+ };
13282
13413
  };
13283
13414
  const tables = {
13284
- getContext: () => {
13285
- const context = resolveTableContext();
13286
- if (!context) return { ...EMPTY_TABLE_CONTEXT };
13287
- const table = readHostTableContext()?.table;
13415
+ getContext: () => publicTableContext(readHostTableContext()),
13416
+ getContextAsync: async () => {
13417
+ for (let attempt = 0; attempt < 2; attempt += 1) {
13418
+ const token = selectionReadToken();
13419
+ const host = getHost();
13420
+ const read = host?.getTableContextAsync;
13421
+ if (typeof read !== "function") return publicTableContext(readHostTableContext());
13422
+ let snapshot = null;
13423
+ try {
13424
+ snapshot = await read.call(host);
13425
+ } catch {
13426
+ return {
13427
+ ...EMPTY_TABLE_CONTEXT,
13428
+ status: "pending"
13429
+ };
13430
+ }
13431
+ if (disposed) return {
13432
+ ...EMPTY_TABLE_CONTEXT,
13433
+ status: "pending"
13434
+ };
13435
+ if (token !== selectionReadToken() || host !== getHost()) continue;
13436
+ const context = publicTableContext(snapshot);
13437
+ if (context.status === "stale") continue;
13438
+ return context;
13439
+ }
13288
13440
  return {
13289
- inTable: true,
13290
- tableNodeId: context.tableNodeId,
13291
- rowIndex: context.rowIndex,
13292
- columnIndex: context.columnIndex,
13293
- cellNodeId: context.cellNodeId,
13294
- rows: typeof table?.rows === "number" ? table.rows : null,
13295
- columns: typeof table?.columns === "number" ? table.columns : null
13441
+ ...EMPTY_TABLE_CONTEXT,
13442
+ status: "stale"
13296
13443
  };
13297
13444
  },
13298
13445
  isInTable: () => resolveTableContext() != null
@@ -19,7 +19,7 @@ var COLLABORATION_UPGRADE_ENGINE_MINIMUM_NODE_MAJOR = 20;
19
19
  var PRIVATE_ENGINE_INFO = (0, _superdoc_docx_engine_collaboration_upgrade_engine.getCollaborationUpgradeEngineInfo)();
20
20
  var ENGINE_INFO = Object.freeze({
21
21
  ...PRIVATE_ENGINE_INFO,
22
- superdocVersion: "2.24.0-next.1",
22
+ superdocVersion: "2.24.0-next.3",
23
23
  roomSchemaVersion: Object.freeze({ ...PRIVATE_ENGINE_INFO.roomSchemaVersion }),
24
24
  supportedBundleVersions: SUPPORTED_COLLABORATION_UPGRADE_BUNDLE_VERSIONS,
25
25
  supportedV1ReaderContractVersions: SUPPORTED_V1_READER_CONTRACT_VERSIONS
@@ -18,7 +18,7 @@ var COLLABORATION_UPGRADE_ENGINE_MINIMUM_NODE_MAJOR = 20;
18
18
  var PRIVATE_ENGINE_INFO = getCollaborationUpgradeEngineInfo$1();
19
19
  var ENGINE_INFO = Object.freeze({
20
20
  ...PRIVATE_ENGINE_INFO,
21
- superdocVersion: "2.24.0-next.1",
21
+ superdocVersion: "2.24.0-next.3",
22
22
  roomSchemaVersion: Object.freeze({ ...PRIVATE_ENGINE_INFO.roomSchemaVersion }),
23
23
  supportedBundleVersions: SUPPORTED_COLLABORATION_UPGRADE_BUNDLE_VERSIONS,
24
24
  supportedV1ReaderContractVersions: SUPPORTED_V1_READER_CONTRACT_VERSIONS
@@ -1,6 +1,8 @@
1
1
  import { SelectionTarget, TextTarget } from '../types/address.js';
2
2
  export interface SelectionTableCellsTarget {
3
3
  kind: 'tableCells';
4
+ selectionMode?: 'cells';
5
+ isRectangular?: true;
4
6
  tableId: string;
5
7
  start: {
6
8
  rowIndex: number;
@@ -12,15 +14,33 @@ export interface SelectionTableCellsTarget {
12
14
  };
13
15
  story?: SelectionTarget['story'];
14
16
  }
15
- /** Table identity for a caret, rectangular cell selection, or whole-grid selection. */
16
- export interface TableSelectionInfo extends SelectionTableCellsTarget {
17
- /** `cell` includes a caret or text range contained in one cell. */
17
+ /** Table/cell context for the selection; bounds alone are not an editing target. */
18
+ export interface TableSelectionInfo {
19
+ kind: 'tableCells';
20
+ tableId: string;
21
+ story?: SelectionTarget['story'];
22
+ /** Inclusive logical bounds, including each selected owner's span. */
23
+ start: {
24
+ rowIndex: number;
25
+ columnIndex: number;
26
+ };
27
+ end: {
28
+ rowIndex: number;
29
+ columnIndex: number;
30
+ };
31
+ /** Whole-table identity requires whole-cell selection intent. */
18
32
  type: 'table' | 'cell' | 'cells';
19
- /** Selected cells in row-major order, using opaque Document API node IDs. */
33
+ /** Text context preserves exact character targets; cells denotes whole-cell intent. */
34
+ selectionMode: 'text' | 'cells';
35
+ /** Whether the exact cells fill the bounding rectangle without holes. */
36
+ isRectangular: boolean;
37
+ /** Exact logical owners in row-major order; vertical continuations are deduplicated. */
20
38
  cells: {
21
39
  nodeId: string;
22
40
  rowIndex: number;
23
41
  columnIndex: number;
42
+ rowspan: number;
43
+ colspan: number;
24
44
  }[];
25
45
  }
26
46
  export type SelectionExtractOoxmlInput = {
@@ -123,6 +143,12 @@ export interface SelectionInfo {
123
143
  * full selection.
124
144
  */
125
145
  target: TextTarget | null;
146
+ /** Deletion-inclusive multi-block text coverage for range review decisions, when resolvable. */
147
+ trackedTarget?: TextTarget | null;
148
+ /** Whether this exact text range supports tracked-change decisions; excludes permissions. */
149
+ trackedChangeRangeDecisionSupported?: boolean;
150
+ /** Whether the selection covers every tracked-text fragment of each active change. */
151
+ trackedChangesWhollySelected?: boolean;
126
152
  /**
127
153
  * Explicit selection envelope for the current live selection, or `null` when
128
154
  * the runtime cannot project one truthfully.
@@ -132,10 +158,11 @@ export interface SelectionInfo {
132
158
  */
133
159
  selectionTarget?: SelectionTarget | null;
134
160
  /**
135
- * V2 browser table selection with inclusive, zero-based grid coordinates.
136
- * Rectangles in unmerged tables clear `target` and `selectionTarget`.
137
- * Rectangles in tables containing merged cells return null and empty text.
138
- * A single-cell caret/text range preserves its text targets and `empty` value.
161
+ * V2 browser cell context with inclusive, zero-based logical bounds.
162
+ * Whole-cell selections clear text targets. Character selections preserve
163
+ * their targets and include the containing cells, including endpoint cells.
164
+ * Exact cell IDs and spans describe merged or nonrectangular selections;
165
+ * only rectangular whole-cell metadata can become a table-cells target.
139
166
  */
140
167
  tableSelection?: TableSelectionInfo | null;
141
168
  /**
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_create_super_doc_ui = require("../chunks/create-super-doc-ui-D_FbXpOQ.cjs");
2
+ const require_create_super_doc_ui = require("../chunks/create-super-doc-ui-bTFGKVRb.cjs");
3
3
  const require_slice_source = require("../chunks/slice-source-CFaqq0Vo.cjs");
4
4
  let react = require("react");
5
5
  //#region src/public/ui/react.ts
@@ -1,4 +1,4 @@
1
- import { t as createSuperDocUI } from "../chunks/create-super-doc-ui-DFNXiKvQ.es.js";
1
+ import { t as createSuperDocUI } from "../chunks/create-super-doc-ui-8UTu16Lw.es.js";
2
2
  import { t as toSliceSource } from "../chunks/slice-source-gfOhG2MW.es.js";
3
3
  import { createContext, createElement, useCallback, useContext, useEffect, useRef, useState } from "react";
4
4
  //#region src/public/ui/react.ts
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_create_super_doc_ui = require("../chunks/create-super-doc-ui-D_FbXpOQ.cjs");
2
+ const require_create_super_doc_ui = require("../chunks/create-super-doc-ui-bTFGKVRb.cjs");
3
3
  const require_slice_source = require("../chunks/slice-source-CFaqq0Vo.cjs");
4
4
  let superdoc_entry_vue = require("vue");
5
5
  //#region src/public/ui/vue.ts
@@ -1,4 +1,4 @@
1
- import { t as createSuperDocUI } from "../chunks/create-super-doc-ui-DFNXiKvQ.es.js";
1
+ import { t as createSuperDocUI } from "../chunks/create-super-doc-ui-8UTu16Lw.es.js";
2
2
  import { t as toSliceSource } from "../chunks/slice-source-gfOhG2MW.es.js";
3
3
  import { computed, inject, onScopeDispose, provide, shallowRef, toRaw, toValue, watch } from "vue";
4
4
  //#region src/public/ui/vue.ts
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_create_super_doc_ui = require("../chunks/create-super-doc-ui-D_FbXpOQ.cjs");
2
+ const require_create_super_doc_ui = require("../chunks/create-super-doc-ui-bTFGKVRb.cjs");
3
3
  exports.BUILT_IN_COMMAND_IDS = require_create_super_doc_ui.BUILT_IN_COMMAND_IDS;
4
4
  exports.createSuperDocUI = require_create_super_doc_ui.createSuperDocUI;
5
5
  exports.shallowEqual = require_create_super_doc_ui.shallowEqual;
@@ -1,2 +1,2 @@
1
- import { a as BUILT_IN_COMMAND_IDS, t as createSuperDocUI, u as shallowEqual } from "../chunks/create-super-doc-ui-DFNXiKvQ.es.js";
1
+ import { a as BUILT_IN_COMMAND_IDS, t as createSuperDocUI, u as shallowEqual } from "../chunks/create-super-doc-ui-8UTu16Lw.es.js";
2
2
  export { BUILT_IN_COMMAND_IDS, createSuperDocUI, shallowEqual };
@@ -551,6 +551,12 @@ export interface SelectionSlice {
551
551
  empty: boolean;
552
552
  /** Resolved text target for the selection, when available. */
553
553
  target: TextTarget | null;
554
+ /** Deletion-inclusive multi-block coverage for range review decisions, when available. */
555
+ trackedTarget?: TextTarget | null;
556
+ /** Whether this exact text range supports tracked-change decisions; excludes permissions. */
557
+ trackedChangeRangeDecisionSupported?: boolean;
558
+ /** Whether the selection covers every tracked-text fragment of each active change. */
559
+ trackedChangesWhollySelected?: boolean;
554
560
  /** Explicit start/end selection target, when available. */
555
561
  selectionTarget: SelectionTarget | null;
556
562
  /** Table/cell identities from the settled public selection read, when supported. */
@@ -1270,6 +1276,20 @@ export interface MetadataHandle {
1270
1276
  * through; custom UIs read it to enable / label their own table controls.
1271
1277
  */
1272
1278
  export interface TableContextInfo {
1279
+ /** Runtime always supplies status; stale/pending snapshots carry no previous cell IDs. */
1280
+ status?: SliceStatus;
1281
+ /** Inclusive logical bounding geometry; use tableSelection for exact membership. */
1282
+ cellRange?: {
1283
+ start: {
1284
+ rowIndex: number;
1285
+ columnIndex: number;
1286
+ };
1287
+ end: {
1288
+ rowIndex: number;
1289
+ columnIndex: number;
1290
+ };
1291
+ } | null;
1292
+ tableSelection?: SelectionInfo['tableSelection'];
1273
1293
  /** Whether the current selection is inside a table. */
1274
1294
  inTable: boolean;
1275
1295
  /** Stable node id of the enclosing table, when resolved. */
@@ -1289,6 +1309,8 @@ export interface TableContextInfo {
1289
1309
  export interface TablesHandle {
1290
1310
  /** Read the current table context snapshot. */
1291
1311
  getContext(): TableContextInfo;
1312
+ /** Resolve fresh, bounded table context for immediate context-menu callbacks. */
1313
+ getContextAsync(): Promise<TableContextInfo>;
1292
1314
  /** Convenience: whether the current selection is inside a table. */
1293
1315
  isInTable(): boolean;
1294
1316
  }