@bendyline/squisq-editor-react 2.11.14 → 2.11.15

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.
@@ -33,7 +33,7 @@ import {
33
33
  ImageEditor,
34
34
  ImageViewer,
35
35
  ShapePalette
36
- } from "./chunk-V44VP242.js";
36
+ } from "./chunk-K4Z2GIX4.js";
37
37
  import {
38
38
  BlockThumbnail,
39
39
  RecorderPanel,
@@ -1026,7 +1026,7 @@ function VersionHistoryPanel() {
1026
1026
  }
1027
1027
  ),
1028
1028
  visibleVersions.map((v2) => {
1029
- const pending = state.pendingRevert !== null && state.pendingRevert.path === v2.path;
1029
+ const pending2 = state.pendingRevert !== null && state.pendingRevert.path === v2.path;
1030
1030
  const selected = state.selected?.version.path === v2.path;
1031
1031
  return /* @__PURE__ */ jsxs(
1032
1032
  "li",
@@ -1054,7 +1054,7 @@ function VersionHistoryPanel() {
1054
1054
  children: "Revert"
1055
1055
  }
1056
1056
  ) }),
1057
- pending && /* @__PURE__ */ jsxs("div", { className: "squisq-version-history-confirm", children: [
1057
+ pending2 && /* @__PURE__ */ jsxs("div", { className: "squisq-version-history-confirm", children: [
1058
1058
  /* @__PURE__ */ jsx("span", { children: "Revert to this version? Your current draft will be saved as a new snapshot first. Reverting will reset your editor undo state." }),
1059
1059
  /* @__PURE__ */ jsxs("span", { className: "squisq-version-history-confirm-actions", children: [
1060
1060
  /* @__PURE__ */ jsx(
@@ -27541,15 +27541,15 @@ async function loadDataPreview(bytes, ext, opts) {
27541
27541
  var previewCache = /* @__PURE__ */ new Map();
27542
27542
  function loadDataPreviewCached(path, revision, load) {
27543
27543
  const key = `${revision}\0${path}`;
27544
- let pending = previewCache.get(key);
27545
- if (!pending) {
27544
+ let pending2 = previewCache.get(key);
27545
+ if (!pending2) {
27546
27546
  for (const existing of previewCache.keys()) {
27547
27547
  if (existing.endsWith(`\0${path}`) && existing !== key) previewCache.delete(existing);
27548
27548
  }
27549
- pending = load();
27550
- previewCache.set(key, pending);
27549
+ pending2 = load();
27550
+ previewCache.set(key, pending2);
27551
27551
  }
27552
- return pending;
27552
+ return pending2;
27553
27553
  }
27554
27554
 
27555
27555
  // src/dataCard/ingestAdapters.ts
@@ -27591,20 +27591,25 @@ async function ingestSidecarBytes(bytes, ext, params) {
27591
27591
  };
27592
27592
  }
27593
27593
  if (ext === "xlsx") {
27594
- const { xlsxToCellGrids, gridToTables, parseCellRef } = await import("@bendyline/squisq-formats/xlsx");
27595
- const grids = await xlsxToCellGrids(bytes, {
27596
- ...params.sheet !== void 0 ? { sheet: params.sheet } : {}
27597
- });
27594
+ const { xlsxToCellGrids, gridToTables, parseCellRef, formatCellRef } = await import("@bendyline/squisq-formats/xlsx");
27595
+ const grids = await xlsxToCellGrids(bytes);
27598
27596
  const bySheet = new Map(grids.sheets.map((sheet) => [sheet.name, sheet]));
27599
- const tables = grids.sheets.flatMap(
27600
- (sheet) => gridToTables(sheet.name, sheet.cells, sheet.merges)
27601
- );
27597
+ const tables = grids.sheets.filter((sheet) => params.sheet === void 0 || sheet.name === params.sheet).flatMap((sheet) => gridToTables(sheet.name, sheet.cells, sheet.merges));
27602
27598
  const table = params.anchor && tables.find((t) => t.anchor.toUpperCase() === params.anchor.toUpperCase()) || tables.reduce(
27603
27599
  (best, candidate) => !best || candidate.rows.length * candidate.columns.length > best.rows.length * best.columns.length ? candidate : best,
27604
27600
  null
27605
27601
  );
27606
27602
  if (!table) throw new Error("no tabular data found in workbook");
27607
27603
  const anchor = parseCellRef(table.anchor);
27604
+ if (params.headerRow === false && table.hasHeader) {
27605
+ table.rows.unshift(table.columns.map((column) => column.name));
27606
+ table.columns = table.columns.map((column, col) => ({
27607
+ ...column,
27608
+ name: formatCellRef(0, (anchor?.col ?? 0) + col).slice(0, -1),
27609
+ kind: "mixed"
27610
+ }));
27611
+ table.hasHeader = false;
27612
+ }
27608
27613
  const sheetGrid = bySheet.get(table.sheet);
27609
27614
  const locked = /* @__PURE__ */ new Set();
27610
27615
  const dateLocked = /* @__PURE__ */ new Set();
@@ -27834,6 +27839,43 @@ import { EMPTY_TABLE_VIEW_STATE } from "@bendyline/squisq/table";
27834
27839
 
27835
27840
  // src/dataCard/gridSave.ts
27836
27841
  import { DATA_BACKUP_PREFIX, buildDataBackupPath } from "@bendyline/squisq/versions";
27842
+
27843
+ // src/dataCard/saveSession.ts
27844
+ var sessions = /* @__PURE__ */ new WeakMap();
27845
+ function withDataSave(options, save) {
27846
+ const owner = options.container ?? options.mediaProvider;
27847
+ let files = sessions.get(owner);
27848
+ if (!files) {
27849
+ files = /* @__PURE__ */ new Map();
27850
+ sessions.set(owner, files);
27851
+ }
27852
+ let session = files.get(options.path);
27853
+ if (!session) {
27854
+ session = { tail: Promise.resolve() };
27855
+ files.set(options.path, session);
27856
+ }
27857
+ const current = session;
27858
+ const result = current.tail.catch(() => void 0).then(async () => {
27859
+ let latest = await options.container?.readFile(options.path);
27860
+ if (!latest && !options.container) {
27861
+ const url = await options.mediaProvider.resolveUrl(options.path);
27862
+ if (url !== options.path) {
27863
+ const response = await fetch(url);
27864
+ if (!response.ok)
27865
+ throw new Error(`Could not read the current file before saving (${response.status})`);
27866
+ latest = await response.arrayBuffer();
27867
+ }
27868
+ }
27869
+ const baseline = latest ?? current.bytes ?? options.originalBytes;
27870
+ const saved = await save(baseline);
27871
+ if (saved.ok && saved.bytes) current.bytes = saved.bytes;
27872
+ return saved;
27873
+ });
27874
+ current.tail = result;
27875
+ return result;
27876
+ }
27877
+
27878
+ // src/dataCard/gridSave.ts
27837
27879
  var BACKUPS_KEPT_PER_SIDECAR = 3;
27838
27880
  var FORMULA_PREFIX = /^[\t\r\n \uFEFF]*[=+\-@]/;
27839
27881
  function renderCell(value) {
@@ -27856,96 +27898,111 @@ async function backupOriginal(container, path, original, mime) {
27856
27898
  }
27857
27899
  }
27858
27900
  async function saveCsvEdits(options) {
27859
- const { path, originalBytes, csv, journal, mediaProvider, container } = options;
27860
- const notices = [];
27861
- const mime = path.toLowerCase().endsWith(".tsv") ? "text/tab-separated-values" : "text/csv";
27862
- try {
27863
- if (container) {
27864
- await backupOriginal(container, path, originalBytes, mime);
27865
- } else {
27866
- notices.push("no workspace container \u2014 saved without a backup copy");
27867
- }
27868
- const rows = csv.rows.map((row) => [...row]);
27869
- const offset = csv.hasHeader ? 1 : 0;
27870
- for (const entry of journal.entries()) {
27871
- const targetRow = entry.rowId + offset;
27872
- while (rows.length <= targetRow) rows.push([]);
27873
- const row = rows[targetRow];
27874
- while (row.length <= entry.col) row.push("");
27875
- row[entry.col] = neutralizeEdited(renderCell(entry.next));
27876
- }
27877
- const { serializeCsvRows } = await import("@bendyline/squisq-formats/csv");
27878
- const text = serializeCsvRows(rows, {
27879
- delimiter: csv.delimiter,
27880
- newline: csv.newline,
27881
- trailingNewline: csv.trailingNewline,
27882
- formulaHandling: "preserve"
27883
- });
27884
- const bytes = new TextEncoder().encode(csv.bom ? `\uFEFF${text}` : text).buffer;
27885
- const savedPath = await mediaProvider.addMedia(path, bytes, mime);
27886
- if (savedPath !== path) {
27901
+ const { path, csv, journal, mediaProvider, container } = options;
27902
+ const snapshot = journal.entries();
27903
+ return withDataSave(options, async (originalBytes) => {
27904
+ const notices = [];
27905
+ const mime = path.toLowerCase().endsWith(".tsv") ? "text/tab-separated-values" : "text/csv";
27906
+ try {
27907
+ if (container) {
27908
+ await backupOriginal(container, path, originalBytes, mime);
27909
+ } else {
27910
+ notices.push("no workspace container \u2014 saved without a backup copy");
27911
+ }
27912
+ const { parseCsv, serializeCsvRows } = await import("@bendyline/squisq-formats/csv");
27913
+ const rows = parseCsv(new TextDecoder().decode(originalBytes), csv.delimiter);
27914
+ const offset = csv.hasHeader ? 1 : 0;
27915
+ for (const entry of snapshot) {
27916
+ const targetRow = entry.rowId + offset;
27917
+ while (rows.length <= targetRow) rows.push([]);
27918
+ const row = rows[targetRow];
27919
+ while (row.length <= entry.col) row.push("");
27920
+ row[entry.col] = neutralizeEdited(renderCell(entry.next));
27921
+ }
27922
+ const text = serializeCsvRows(rows, {
27923
+ delimiter: csv.delimiter,
27924
+ newline: csv.newline,
27925
+ trailingNewline: csv.trailingNewline,
27926
+ formulaHandling: "preserve"
27927
+ });
27928
+ const bytes = new TextEncoder().encode(csv.bom ? `\uFEFF${text}` : text).buffer;
27929
+ const savedPath = await mediaProvider.addMedia(path, bytes, mime);
27930
+ if (savedPath !== path) {
27931
+ return {
27932
+ ok: false,
27933
+ savedPath,
27934
+ notices,
27935
+ error: `the host saved a copy at "${savedPath}" instead of overwriting "${path}" \u2014 the document still references the original`
27936
+ };
27937
+ }
27938
+ journal.acknowledge(snapshot);
27939
+ return { ok: true, savedPath, notices, bytes };
27940
+ } catch (err) {
27887
27941
  return {
27888
27942
  ok: false,
27889
- savedPath,
27890
27943
  notices,
27891
- error: `the host saved a copy at "${savedPath}" instead of overwriting "${path}" \u2014 the document still references the original`
27944
+ error: err instanceof Error ? err.message : String(err)
27892
27945
  };
27893
27946
  }
27894
- journal.clear();
27895
- return { ok: true, savedPath, notices };
27896
- } catch (err) {
27897
- return {
27898
- ok: false,
27899
- notices,
27900
- error: err instanceof Error ? err.message : String(err)
27901
- };
27902
- }
27947
+ }).catch((error) => ({
27948
+ ok: false,
27949
+ notices: [],
27950
+ error: error instanceof Error ? error.message : String(error)
27951
+ }));
27903
27952
  }
27904
27953
  var XLSX_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
27905
27954
  async function saveXlsxEdits(options) {
27906
- const { path, originalBytes, xlsx, journal, formulaEdits, mediaProvider, container } = options;
27907
- const notices = [];
27908
- try {
27909
- if (container) {
27910
- await backupOriginal(container, path, originalBytes, XLSX_MIME);
27911
- } else {
27912
- notices.push("no workspace container \u2014 saved without a backup copy");
27913
- }
27914
- const { patchXlsxCellValues, formatCellRef } = await import("@bendyline/squisq-formats/xlsx");
27915
- const bodyTop = xlsx.anchorRow + (xlsx.hasHeader ? 1 : 0);
27916
- const patches = journal.entries().map((entry) => ({
27917
- sheet: xlsx.sheet,
27918
- ref: formatCellRef(bodyTop + entry.rowId, xlsx.anchorCol + entry.col),
27919
- value: entry.next
27920
- }));
27921
- for (const [key, record] of formulaEdits ?? []) {
27922
- const [row, col] = key.split(":").map(Number);
27923
- patches.push({
27955
+ const { path, xlsx, journal, mediaProvider, container } = options;
27956
+ const snapshot = journal.entries();
27957
+ const formulaEdits = new Map(options.formulaEdits);
27958
+ return withDataSave(options, async (originalBytes) => {
27959
+ const notices = [];
27960
+ try {
27961
+ if (container) {
27962
+ await backupOriginal(container, path, originalBytes, XLSX_MIME);
27963
+ } else {
27964
+ notices.push("no workspace container \u2014 saved without a backup copy");
27965
+ }
27966
+ const { patchXlsxCellValues, formatCellRef } = await import("@bendyline/squisq-formats/xlsx");
27967
+ const bodyTop = xlsx.anchorRow + (xlsx.hasHeader ? 1 : 0);
27968
+ const patches = snapshot.filter((entry) => !formulaEdits.has(`${entry.rowId}:${entry.col}`)).map((entry) => ({
27924
27969
  sheet: xlsx.sheet,
27925
- ref: formatCellRef(bodyTop + row, xlsx.anchorCol + col),
27926
- formula: record.formula,
27927
- ...record.cachedValue !== void 0 ? { cachedValue: record.cachedValue } : {}
27928
- });
27929
- }
27930
- const bytes = await patchXlsxCellValues(originalBytes, patches);
27931
- const savedPath = await mediaProvider.addMedia(path, bytes, XLSX_MIME);
27932
- if (savedPath !== path) {
27970
+ ref: formatCellRef(bodyTop + entry.rowId, xlsx.anchorCol + entry.col),
27971
+ value: entry.next
27972
+ }));
27973
+ for (const [key, record] of formulaEdits ?? []) {
27974
+ const [row, col] = key.split(":").map(Number);
27975
+ patches.push({
27976
+ sheet: xlsx.sheet,
27977
+ ref: formatCellRef(bodyTop + row, xlsx.anchorCol + col),
27978
+ ...record.formula === null ? { value: record.value ?? null, removeFormula: true } : { formula: record.formula },
27979
+ ...record.cachedValue !== void 0 ? { cachedValue: record.cachedValue } : {}
27980
+ });
27981
+ }
27982
+ const bytes = await patchXlsxCellValues(originalBytes, patches);
27983
+ const savedPath = await mediaProvider.addMedia(path, bytes, XLSX_MIME);
27984
+ if (savedPath !== path) {
27985
+ return {
27986
+ ok: false,
27987
+ savedPath,
27988
+ notices,
27989
+ error: `the host saved a copy at "${savedPath}" instead of overwriting "${path}" \u2014 the document still references the original`
27990
+ };
27991
+ }
27992
+ journal.acknowledge(snapshot);
27993
+ return { ok: true, savedPath, notices, bytes };
27994
+ } catch (err) {
27933
27995
  return {
27934
27996
  ok: false,
27935
- savedPath,
27936
27997
  notices,
27937
- error: `the host saved a copy at "${savedPath}" instead of overwriting "${path}" \u2014 the document still references the original`
27998
+ error: err instanceof Error ? err.message : String(err)
27938
27999
  };
27939
28000
  }
27940
- journal.clear();
27941
- return { ok: true, savedPath, notices };
27942
- } catch (err) {
27943
- return {
27944
- ok: false,
27945
- notices,
27946
- error: err instanceof Error ? err.message : String(err)
27947
- };
27948
- }
28001
+ }).catch((error) => ({
28002
+ ok: false,
28003
+ notices: [],
28004
+ error: error instanceof Error ? error.message : String(error)
28005
+ }));
27949
28006
  }
27950
28007
 
27951
28008
  // src/dataCard/formulaSupport.ts
@@ -28002,8 +28059,15 @@ async function createXlsxFormulaSession(meta, options = {}) {
28002
28059
  engine = calc.createInHouseEngine(engineConfig);
28003
28060
  }
28004
28061
  }
28005
- await engine.loadWorkbook(buildSeed(calc, meta));
28006
- const initial = await engine.evaluateAll();
28062
+ const initial = await (async () => {
28063
+ try {
28064
+ await engine.loadWorkbook(buildSeed(calc, meta));
28065
+ return await engine.evaluateAll();
28066
+ } catch (error) {
28067
+ engine.dispose();
28068
+ throw error;
28069
+ }
28070
+ })();
28007
28071
  if (initial.status === "budget-exceeded") {
28008
28072
  engine.dispose();
28009
28073
  return null;
@@ -28015,7 +28079,7 @@ async function createXlsxFormulaSession(meta, options = {}) {
28015
28079
  col: meta.anchorCol + col
28016
28080
  });
28017
28081
  const formulas = new Map(meta.formulas);
28018
- const edits = /* @__PURE__ */ new Map();
28082
+ const edits = options.edits ?? /* @__PURE__ */ new Map();
28019
28083
  let disposed = false;
28020
28084
  const readValues = async (keys) => {
28021
28085
  const coords = keys.map((key) => key.split(":").map(Number));
@@ -28032,6 +28096,13 @@ async function createXlsxFormulaSession(meta, options = {}) {
28032
28096
  const updates = [];
28033
28097
  keyList.forEach((key, index2) => {
28034
28098
  const raw = states[index2].value;
28099
+ const formula = formulas.get(key);
28100
+ if (formula !== void 0 && (edits.has(key) || before.get(key) !== displayValue(calc, raw))) {
28101
+ edits.set(key, {
28102
+ formula,
28103
+ ...!calc.isCalcError(raw) && raw !== null ? { cachedValue: raw } : {}
28104
+ });
28105
+ }
28035
28106
  if (calc.isCalcError(raw) && raw.code === "#NAME?") return;
28036
28107
  const value = displayValue(calc, raw);
28037
28108
  if (before.get(key) !== value) {
@@ -28041,7 +28112,13 @@ async function createXlsxFormulaSession(meta, options = {}) {
28041
28112
  });
28042
28113
  return updates;
28043
28114
  };
28044
- return {
28115
+ let mutationTail = Promise.resolve();
28116
+ const serial = (operation) => {
28117
+ const result = mutationTail.catch(() => void 0).then(operation);
28118
+ mutationTail = result;
28119
+ return result;
28120
+ };
28121
+ const session = {
28045
28122
  getFormula: (bodyRow, col) => formulas.get(`${bodyRow}:${col}`),
28046
28123
  isCellLocked: (bodyRow, col) => {
28047
28124
  const key = `${bodyRow}:${col}`;
@@ -28095,7 +28172,8 @@ async function createXlsxFormulaSession(meta, options = {}) {
28095
28172
  if (disposed) return [];
28096
28173
  const key = `${bodyRow}:${col}`;
28097
28174
  formulas.delete(key);
28098
- edits.delete(key);
28175
+ if (meta.formulas.has(key)) edits.set(key, { formula: null, value });
28176
+ else edits.delete(key);
28099
28177
  const before = await snapshotRegion();
28100
28178
  engine.setCellValue(toAddress(bodyRow, col), value);
28101
28179
  const result = await engine.evaluateAll();
@@ -28105,7 +28183,7 @@ async function createXlsxFormulaSession(meta, options = {}) {
28105
28183
  get dirtyCount() {
28106
28184
  return edits.size;
28107
28185
  },
28108
- formulaEdits: () => edits,
28186
+ formulaEdits: () => new Map(edits),
28109
28187
  async discard() {
28110
28188
  const before = await snapshotRegion();
28111
28189
  const staleKeys = new Set(formulas.keys());
@@ -28115,29 +28193,49 @@ async function createXlsxFormulaSession(meta, options = {}) {
28115
28193
  await engine.loadWorkbook(buildSeed(calc, meta));
28116
28194
  await engine.evaluateAll();
28117
28195
  for (const key of formulas.keys()) staleKeys.add(key);
28118
- return diffAgainst(before, staleKeys);
28119
- },
28120
- markSaved() {
28196
+ const updates = await diffAgainst(before, staleKeys);
28121
28197
  edits.clear();
28198
+ return updates;
28199
+ },
28200
+ markSaved(snapshot = new Map(edits), baseline) {
28201
+ if (baseline) meta = baseline;
28202
+ for (const [key, record] of snapshot) {
28203
+ if (edits.get(key) === record) edits.delete(key);
28204
+ }
28122
28205
  },
28123
28206
  dispose() {
28124
28207
  disposed = true;
28125
28208
  engine.dispose();
28209
+ },
28210
+ flush: async () => {
28211
+ await mutationTail;
28126
28212
  }
28127
28213
  };
28214
+ return {
28215
+ ...session,
28216
+ get dirtyCount() {
28217
+ return session.dirtyCount;
28218
+ },
28219
+ commitFormula: (...args) => serial(() => session.commitFormula(...args)),
28220
+ noteValueEdit: (...args) => serial(() => session.noteValueEdit(...args)),
28221
+ discard: () => serial(() => session.discard())
28222
+ };
28128
28223
  }
28129
28224
 
28130
- // src/dataCard/DataCardWidget.tsx
28131
- import { jsx as jsx54, jsxs as jsxs43 } from "react/jsx-runtime";
28132
- var XLSX_SESSION_LOCKED_REASON = "Date cells and shared-formula masters stay locked; other formulas are editable";
28133
- var gridModulePromise = null;
28225
+ // src/dataCard/loadGridModule.ts
28226
+ var pending = null;
28134
28227
  function loadGridModule() {
28135
- gridModulePromise ?? (gridModulePromise = import("@bendyline/squisq-grid-react").catch(() => {
28136
- gridModulePromise = null;
28228
+ pending ?? (pending = import("@bendyline/squisq-grid-react").catch(() => {
28229
+ pending = null;
28137
28230
  return null;
28138
28231
  }));
28139
- return gridModulePromise;
28232
+ return pending;
28140
28233
  }
28234
+
28235
+ // src/dataCard/DataCardWidget.tsx
28236
+ import { jsx as jsx54, jsxs as jsxs43 } from "react/jsx-runtime";
28237
+ var XLSX_SESSION_LOCKED_REASON = "Date cells and shared-formula masters stay locked; other formulas are editable";
28238
+ var pendingFormulas = /* @__PURE__ */ new WeakMap();
28141
28239
  function useCardLink(editor, blockId) {
28142
28240
  const [version, setVersion] = useState47(0);
28143
28241
  useEffect38(() => {
@@ -28190,14 +28288,23 @@ function DataCardWidget({
28190
28288
  const [view, setView] = useState47(EMPTY_TABLE_VIEW_STATE);
28191
28289
  const [viewPersisted, setViewPersisted] = useState47(false);
28192
28290
  const [saving, setSaving] = useState47(false);
28291
+ const savingRef = useRef39(false);
28193
28292
  const [saveNotice, setSaveNotice] = useState47(null);
28194
28293
  const [formulaDirty, setFormulaDirty] = useState47(0);
28195
28294
  const gridRef = useRef39(null);
28196
28295
  const href = link?.href ?? null;
28197
28296
  const cardPos = link?.pos ?? null;
28297
+ const activeProvider = getMediaProvider();
28298
+ const activeContainer = getContainer?.() ?? null;
28299
+ const regionParams = href && cardPos !== null ? readHeadingViewBinding(editor, cardPos, href).params : {};
28300
+ const regionIdentity = JSON.stringify([
28301
+ regionParams.sheet,
28302
+ regionParams.anchor,
28303
+ regionParams.headerRow
28304
+ ]);
28198
28305
  useEffect38(() => {
28199
28306
  if (!href || cardPos === null) return;
28200
- const provider = getMediaProvider();
28307
+ const provider = activeProvider;
28201
28308
  if (!provider) {
28202
28309
  setState3({ kind: "meta", size: null, preview: null });
28203
28310
  return;
@@ -28206,6 +28313,9 @@ function DataCardWidget({
28206
28313
  setState3({ kind: "loading" });
28207
28314
  void (async () => {
28208
28315
  let size2 = null;
28316
+ let loadingStore = null;
28317
+ let loadingSession = null;
28318
+ let retained = false;
28209
28319
  try {
28210
28320
  const entries = await provider.listMedia();
28211
28321
  const entry = entries.find((e2) => e2.name === href);
@@ -28249,26 +28359,61 @@ function DataCardWidget({
28249
28359
  ...binding.params.headerRow !== void 0 ? { headerRow: binding.params.headerRow !== "false" } : {}
28250
28360
  });
28251
28361
  if (cancelled) return;
28362
+ const container = activeContainer;
28363
+ const region = JSON.stringify(
28364
+ ingested.xlsx ? [
28365
+ ingested.xlsx.sheet,
28366
+ ingested.xlsx.anchorRow,
28367
+ ingested.xlsx.anchorCol,
28368
+ ingested.xlsx.hasHeader
28369
+ ] : [ingested.csv?.hasHeader ?? true]
28370
+ );
28371
+ const journal = module.journalFor(href, revision, {
28372
+ owner: container ?? provider,
28373
+ region
28374
+ });
28375
+ let formulaEdits = pendingFormulas.get(journal);
28376
+ if (!formulaEdits) {
28377
+ formulaEdits = /* @__PURE__ */ new Map();
28378
+ pendingFormulas.set(journal, formulaEdits);
28379
+ }
28380
+ const restoredFormulas = new Map(formulaEdits);
28252
28381
  let formulaSession = null;
28253
28382
  if (ingested.xlsx) {
28254
28383
  try {
28255
28384
  const engineFactory = getCalcEngineFactory?.();
28256
- formulaSession = await createXlsxFormulaSession(
28257
- ingested.xlsx,
28258
- engineFactory ? { engineFactory } : {}
28259
- );
28385
+ formulaSession = await createXlsxFormulaSession(ingested.xlsx, {
28386
+ ...engineFactory ? { engineFactory } : {},
28387
+ edits: formulaEdits
28388
+ });
28389
+ loadingSession = formulaSession;
28260
28390
  } catch {
28261
28391
  formulaSession = null;
28262
28392
  }
28263
28393
  }
28264
28394
  if (cancelled) {
28265
- formulaSession?.dispose();
28266
28395
  return;
28267
28396
  }
28268
- gridRef.current?.formulaSession?.dispose();
28269
- gridRef.current?.provider.dispose();
28270
28397
  const storeProvider = new module.TableStoreClient(ingested.ingest);
28271
- const journal = module.journalFor(href, revision);
28398
+ loadingStore = storeProvider;
28399
+ await storeProvider.describe();
28400
+ const restoredValues = journal.entries().map(({ rowId, col, next }) => ({ rowId, col, value: next }));
28401
+ if (restoredValues.length) await storeProvider.applyEdits(restoredValues);
28402
+ if (formulaSession) {
28403
+ for (const edit of restoredValues) {
28404
+ if (restoredFormulas.has(`${edit.rowId}:${edit.col}`)) continue;
28405
+ const updates = await formulaSession.noteValueEdit(edit.rowId, edit.col, edit.value);
28406
+ if (updates.length) await storeProvider.applyEdits(updates);
28407
+ }
28408
+ for (const [key, record] of restoredFormulas) {
28409
+ const [row, col] = key.split(":").map(Number);
28410
+ const updates = record.formula === null ? await formulaSession.noteValueEdit(row, col, record.value ?? null) : (await formulaSession.commitFormula(row, col, record.formula)).updates ?? [];
28411
+ if (updates.length) await storeProvider.applyEdits(updates);
28412
+ }
28413
+ }
28414
+ if (cancelled) {
28415
+ return;
28416
+ }
28272
28417
  const grid2 = {
28273
28418
  module,
28274
28419
  provider: storeProvider,
@@ -28276,13 +28421,19 @@ function DataCardWidget({
28276
28421
  ingested,
28277
28422
  bytes,
28278
28423
  headers: ingested.ingest.headers,
28279
- formulaSession
28424
+ formulaSession,
28425
+ mediaProvider: provider,
28426
+ container,
28427
+ path: href
28280
28428
  };
28429
+ gridRef.current?.formulaSession?.dispose();
28430
+ gridRef.current?.provider.dispose();
28281
28431
  gridRef.current = grid2;
28432
+ retained = true;
28282
28433
  setView(viewStateFromBinding(binding, ingested.ingest.headers));
28283
28434
  setViewPersisted(binding.persisted);
28284
28435
  setSaveNotice(null);
28285
- setFormulaDirty(0);
28436
+ setFormulaDirty(formulaSession?.dirtyCount ?? 0);
28286
28437
  setState3({ kind: "grid", size: size2, grid: grid2 });
28287
28438
  return;
28288
28439
  }
@@ -28302,12 +28453,17 @@ function DataCardWidget({
28302
28453
  reason: err instanceof Error ? err.message : String(err)
28303
28454
  });
28304
28455
  }
28456
+ } finally {
28457
+ if (!retained) {
28458
+ loadingStore?.dispose();
28459
+ loadingSession?.dispose();
28460
+ }
28305
28461
  }
28306
28462
  })();
28307
28463
  return () => {
28308
28464
  cancelled = true;
28309
28465
  };
28310
- }, [href, revision, getMediaProvider, editor]);
28466
+ }, [href, revision, activeProvider, activeContainer, regionIdentity, editor]);
28311
28467
  useEffect38(
28312
28468
  () => () => {
28313
28469
  gridRef.current?.formulaSession?.dispose();
@@ -28327,34 +28483,61 @@ function DataCardWidget({
28327
28483
  );
28328
28484
  const handleSave = useCallback36(async () => {
28329
28485
  const grid2 = gridRef.current;
28330
- const provider = getMediaProvider();
28486
+ const provider = grid2?.mediaProvider;
28331
28487
  if (!grid2 || !provider || !href || !grid2.ingested.csv && !grid2.ingested.xlsx) return;
28488
+ if (savingRef.current) return;
28489
+ savingRef.current = true;
28332
28490
  setSaving(true);
28333
28491
  try {
28492
+ await grid2.formulaSession?.flush();
28334
28493
  const shared = {
28335
- path: href,
28494
+ path: grid2.path,
28336
28495
  originalBytes: grid2.bytes,
28337
28496
  journal: grid2.journal,
28338
28497
  mediaProvider: provider,
28339
- container: getContainer?.() ?? null
28498
+ container: grid2.container
28340
28499
  };
28500
+ const formulaSnapshot = grid2.formulaSession?.formulaEdits();
28341
28501
  const result = grid2.ingested.csv ? await saveCsvEdits({ ...shared, csv: grid2.ingested.csv }) : await saveXlsxEdits({
28342
28502
  ...shared,
28343
28503
  xlsx: grid2.ingested.xlsx,
28344
- ...grid2.formulaSession ? { formulaEdits: grid2.formulaSession.formulaEdits() } : {}
28504
+ ...formulaSnapshot ? { formulaEdits: formulaSnapshot } : {}
28345
28505
  });
28346
- setSaveNotice(
28347
- result.ok ? result.notices.length > 0 ? `saved (${result.notices.join("; ")})` : null : result.error ?? "save failed"
28348
- );
28506
+ if (gridRef.current === grid2)
28507
+ setSaveNotice(
28508
+ result.ok ? result.notices.length > 0 ? `saved (${result.notices.join("; ")})` : null : result.error ?? "save failed"
28509
+ );
28349
28510
  if (result.ok) {
28350
- gridRef.current?.formulaSession?.markSaved();
28351
- setFormulaDirty(0);
28352
- onMediaSaved?.();
28511
+ if (result.bytes) {
28512
+ grid2.bytes = result.bytes;
28513
+ const baseline = await ingestSidecarBytes(
28514
+ result.bytes,
28515
+ dataExtensionOf(grid2.path),
28516
+ grid2.ingested.xlsx ? {
28517
+ sheet: grid2.ingested.xlsx.sheet,
28518
+ anchor: (await import("@bendyline/squisq-formats/xlsx")).formatCellRef(
28519
+ grid2.ingested.xlsx.anchorRow,
28520
+ grid2.ingested.xlsx.anchorCol
28521
+ ),
28522
+ headerRow: grid2.ingested.xlsx.hasHeader
28523
+ } : { headerRow: grid2.ingested.csv?.hasHeader }
28524
+ );
28525
+ grid2.ingested = baseline;
28526
+ }
28527
+ grid2.formulaSession?.markSaved(formulaSnapshot, grid2.ingested.xlsx);
28528
+ if (gridRef.current === grid2) {
28529
+ setFormulaDirty(grid2.formulaSession?.dirtyCount ?? 0);
28530
+ onMediaSaved?.();
28531
+ }
28353
28532
  }
28533
+ } catch (error) {
28534
+ if (gridRef.current === grid2)
28535
+ setSaveNotice(error instanceof Error ? error.message : String(error));
28354
28536
  } finally {
28537
+ savingRef.current = false;
28355
28538
  setSaving(false);
28356
28539
  }
28357
- }, [getContainer, getMediaProvider, href, onMediaSaved]);
28540
+ }, [href, onMediaSaved]);
28358
28541
  if (!link || !href) return null;
28359
28542
  const fileName = href.split("/").pop() ?? href;
28360
28543
  const size = state.kind === "meta" || state.kind === "grid" ? state.size : null;
@@ -34764,8 +34947,8 @@ function playTimelineMediaAt(root, time) {
34764
34947
  const end = Number(el.dataset.absEnd);
34765
34948
  if (!Number.isFinite(start) || !Number.isFinite(end) || time < start || time >= end) return;
34766
34949
  try {
34767
- const pending = el.play();
34768
- pending?.catch(() => {
34950
+ const pending2 = el.play();
34951
+ pending2?.catch(() => {
34769
34952
  });
34770
34953
  } catch {
34771
34954
  }
@@ -37836,7 +38019,6 @@ import {
37836
38019
  useState as useState68
37837
38020
  } from "react";
37838
38021
  import { parseTableViewState as parseTableViewState2 } from "@bendyline/squisq/table";
37839
- import { DataGrid, TableStoreClient } from "@bendyline/squisq-grid-react";
37840
38022
 
37841
38023
  // src/slideshow/slideshowDataSources.ts
37842
38024
  import { isDataFilePath as isDataFilePath2, resolveTemplateName as resolveTemplateName3 } from "@bendyline/squisq/doc";
@@ -37938,8 +38120,18 @@ function SlideshowDataGridRenderer({
37938
38120
  const source = sourceForBlock(sources, block);
37939
38121
  const activeSourceKey = sourceKey(source);
37940
38122
  const [remote, setRemote] = useState68({ key: "", status: "idle" });
38123
+ const [gridModule, setGridModule] = useState68(null);
37941
38124
  useEffect59(() => {
37942
- if (!container || !source) {
38125
+ let active = true;
38126
+ void loadGridModule().then((module) => {
38127
+ if (active) setGridModule(module);
38128
+ });
38129
+ return () => {
38130
+ active = false;
38131
+ };
38132
+ }, []);
38133
+ useEffect59(() => {
38134
+ if (!container || !source || !gridModule) {
37943
38135
  setRemote({ key: activeSourceKey, status: "idle" });
37944
38136
  return;
37945
38137
  }
@@ -37955,7 +38147,8 @@ function SlideshowDataGridRenderer({
37955
38147
  ...source.anchor ? { anchor: source.anchor } : {},
37956
38148
  ...source.headerRow !== void 0 ? { headerRow: source.headerRow } : {}
37957
38149
  });
37958
- const provider2 = new TableStoreClient(ingest2);
38150
+ if (cancelled) return;
38151
+ const provider2 = new gridModule.TableStoreClient(ingest2);
37959
38152
  ownedProvider = provider2;
37960
38153
  await provider2.describe();
37961
38154
  if (cancelled) {
@@ -37973,7 +38166,7 @@ function SlideshowDataGridRenderer({
37973
38166
  cancelled = true;
37974
38167
  ownedProvider?.dispose();
37975
38168
  };
37976
- }, [activeSourceKey, container, mediaRevision]);
38169
+ }, [activeSourceKey, container, mediaRevision, gridModule]);
37977
38170
  const currentRemote = remote.key === activeSourceKey ? remote : void 0;
37978
38171
  const previewRows = useMemo52(
37979
38172
  () => Array.isArray(layer.content.rows) ? layer.content.rows : [],
@@ -37985,8 +38178,8 @@ function SlideshowDataGridRenderer({
37985
38178
  );
37986
38179
  const useLocalGrid = (!source || currentRemote?.status === "error") && previewRows.length > (layer.content.maxVisibleRows ?? 10);
37987
38180
  const localProvider = useMemo52(
37988
- () => useLocalGrid ? new TableStoreClient({ headers: previewHeaders, cells: previewRows }) : null,
37989
- [previewHeaders, previewRows, useLocalGrid]
38181
+ () => useLocalGrid && gridModule ? new gridModule.TableStoreClient({ headers: previewHeaders, cells: previewRows }) : null,
38182
+ [previewHeaders, previewRows, useLocalGrid, gridModule]
37990
38183
  );
37991
38184
  useEffect59(() => () => localProvider?.dispose(), [localProvider]);
37992
38185
  const ready = currentRemote?.status === "ready" ? currentRemote : void 0;
@@ -37995,7 +38188,7 @@ function SlideshowDataGridRenderer({
37995
38188
  const initialView = ready?.initialView ?? EMPTY_VIEW;
37996
38189
  const [view, setView] = useState68(initialView);
37997
38190
  useEffect59(() => setView(initialView), [initialView, provider]);
37998
- if (!provider || !ingest) return /* @__PURE__ */ jsx77(Fragment22, { children: fallback });
38191
+ if (!provider || !ingest || !gridModule) return /* @__PURE__ */ jsx77(Fragment22, { children: fallback });
37999
38192
  const { style } = layer.content;
38000
38193
  const rowHeight = Math.max(32, style.fontSize * 2.1);
38001
38194
  const footerHeight = Math.max(28, style.fontSize * 1.5);
@@ -38025,7 +38218,7 @@ function SlideshowDataGridRenderer({
38025
38218
  "--squisq-grid-resize-handle": style.headerBackground
38026
38219
  };
38027
38220
  return /* @__PURE__ */ jsx77("div", { className: "squisq-slideshow-data-grid", style: gridStyle, children: /* @__PURE__ */ jsx77(
38028
- DataGrid,
38221
+ gridModule.DataGrid,
38029
38222
  {
38030
38223
  provider,
38031
38224
  view,
@@ -294,13 +294,19 @@ function useImageEditor(options) {
294
294
  versioningAutoSaveIdleMs = 5e3,
295
295
  persistDebounceMs = 500
296
296
  } = options;
297
- const [state, dispatch] = useReducer(
297
+ const target = useMemo(
298
+ () => ({ container, stateFilename, initialSrc, resourcePolicy }),
299
+ [container, stateFilename, initialSrc, resourcePolicy]
300
+ );
301
+ const [loadedTarget, setLoadedTarget] = useState2(null);
302
+ const [loadedState, dispatch] = useReducer(
298
303
  (s, a) => {
299
304
  if (s === null) return a.type === "load" ? initialImageEditorState(a.doc) : null;
300
305
  return imageEditorReducer(s, a);
301
306
  },
302
307
  null
303
308
  );
309
+ const state = loadedTarget === target ? loadedState : null;
304
310
  const [ready, setReady] = useState2(false);
305
311
  const [error, setError] = useState2(null);
306
312
  const seededOnLoadRef = useRef2(false);
@@ -309,12 +315,15 @@ function useImageEditor(options) {
309
315
  const controller = new AbortController();
310
316
  setReady(false);
311
317
  setError(null);
318
+ seededOnLoadRef.current = false;
312
319
  (async () => {
313
320
  try {
321
+ await writeQueueRef.current;
314
322
  const existing = await readImageEditDoc(container, stateFilename);
315
323
  if (cancelled) return;
316
324
  if (existing) {
317
325
  dispatch({ type: "load", doc: existing });
326
+ setLoadedTarget(target);
318
327
  setReady(true);
319
328
  return;
320
329
  }
@@ -328,6 +337,7 @@ function useImageEditor(options) {
328
337
  await writeImageEditDoc(container, seeded, stateFilename);
329
338
  if (cancelled) return;
330
339
  dispatch({ type: "load", doc: seeded });
340
+ setLoadedTarget(target);
331
341
  setReady(true);
332
342
  seededOnLoadRef.current = true;
333
343
  } catch (err) {
@@ -340,33 +350,35 @@ function useImageEditor(options) {
340
350
  cancelled = true;
341
351
  controller.abort();
342
352
  };
343
- }, [container, stateFilename, initialSrc, resourcePolicy]);
353
+ }, [container, stateFilename, initialSrc, resourcePolicy, target]);
344
354
  const persistTimerRef = useRef2(null);
345
355
  const docRef = useRef2(null);
346
356
  const dirtyRef = useRef2(false);
347
357
  const revisionRef = useRef2(0);
348
358
  const previousDocRef = useRef2(null);
349
- const nextDoc = state?.doc ?? null;
359
+ const nextDoc = loadedState?.doc ?? null;
350
360
  if (nextDoc !== previousDocRef.current) {
351
361
  previousDocRef.current = nextDoc;
352
362
  revisionRef.current += 1;
353
363
  }
354
- docRef.current = nextDoc;
355
- dirtyRef.current = state?.dirty ?? false;
364
+ if (loadedTarget === target) {
365
+ docRef.current = nextDoc;
366
+ dirtyRef.current = state?.dirty ?? false;
367
+ }
356
368
  const writeQueueRef = useRef2(Promise.resolve());
357
369
  const persistTargetRef = useRef2({ container, stateFilename });
358
- persistTargetRef.current = { container, stateFilename };
370
+ if (loadedTarget === target) persistTargetRef.current = target;
359
371
  const enqueueWrite = useCallback2(
360
372
  (doc, revision, markClean) => {
361
373
  const write = writeQueueRef.current.catch(() => void 0).then(() => writeImageEditDoc(container, doc, stateFilename));
362
374
  writeQueueRef.current = write.catch(() => void 0);
363
375
  return write.then(() => {
364
- if (markClean && revision === revisionRef.current && docRef.current === doc) {
376
+ if (markClean && persistTargetRef.current === target && revision === revisionRef.current && docRef.current === doc) {
365
377
  dispatch({ type: "mark-clean" });
366
378
  }
367
379
  });
368
380
  },
369
- [container, stateFilename]
381
+ [container, stateFilename, target]
370
382
  );
371
383
  useEffect2(() => {
372
384
  if (!state?.dirty) return;
@@ -387,6 +399,7 @@ function useImageEditor(options) {
387
399
  };
388
400
  }, [state?.dirty, state?.doc, persistDebounceMs, enqueueWrite]);
389
401
  const flush = useCallback2(async () => {
402
+ if (loadedTarget !== target) return;
390
403
  const doc = docRef.current;
391
404
  if (!doc) return;
392
405
  if (persistTimerRef.current) {
@@ -394,14 +407,14 @@ function useImageEditor(options) {
394
407
  persistTimerRef.current = null;
395
408
  }
396
409
  await enqueueWrite(doc, revisionRef.current, true);
397
- }, [enqueueWrite]);
410
+ }, [enqueueWrite, loadedTarget, target]);
398
411
  useEffect2(
399
412
  () => () => {
400
413
  if (persistTimerRef.current) clearTimeout(persistTimerRef.current);
401
414
  const doc = docRef.current;
402
415
  if (!dirtyRef.current || !doc) return;
403
- const target = persistTargetRef.current;
404
- const write = writeQueueRef.current.catch(() => void 0).then(() => writeImageEditDoc(target.container, doc, target.stateFilename));
416
+ const target2 = persistTargetRef.current;
417
+ const write = writeQueueRef.current.catch(() => void 0).then(() => writeImageEditDoc(target2.container, doc, target2.stateFilename));
405
418
  writeQueueRef.current = write.catch((err) => {
406
419
  console.warn(
407
420
  "[squisq-editor] image-edit final persist failed:",
@@ -409,7 +422,7 @@ function useImageEditor(options) {
409
422
  );
410
423
  });
411
424
  },
412
- []
425
+ [target]
413
426
  );
414
427
  const versioning = useMemo(
415
428
  () => allowVersioning ? new ImageEditVersionManager(container, { stateFilename }) : null,
@@ -487,12 +500,14 @@ function useImageEditor(options) {
487
500
  );
488
501
  return {
489
502
  state,
490
- dispatch,
503
+ dispatch: (action) => {
504
+ if (loadedTarget === target) dispatch(action);
505
+ },
491
506
  flush,
492
507
  resolveAssetUrl,
493
508
  uploadAsset,
494
509
  versioning,
495
- ready,
510
+ ready: loadedTarget === target && ready,
496
511
  error
497
512
  };
498
513
  }
@@ -4,7 +4,7 @@ import {
4
4
  imageEditorReducer,
5
5
  initialImageEditorState,
6
6
  useImageEditor
7
- } from "../chunk-V44VP242.js";
7
+ } from "../chunk-K4Z2GIX4.js";
8
8
  import "../chunk-GS7QWYFT.js";
9
9
  export {
10
10
  ImageEditor,
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as CodeContext, E as EditorHostMode, S as SceneTextChannel, a as CalcEngineFactory, b as CsvSourceMeta, X as XlsxSourceMeta, F as FormulaEditRecord } from './shell-3UAmXcYE.js';
2
- export { B as BlockRange, c as BlockSlice, d as BlockTagVisibility, e as CodeContextSection, f as CoverImageSaveOutput, D as DashboardImageSaveOutput, g as DocumentLinkCandidate, h as DocumentLinkProvider, i as EditorActions, j as EditorColorScheme, k as EditorContextMenuContext, l as EditorContextMenuItem, m as EditorContextMenuItemVisibility, n as EditorContextMenuProvider, o as EditorContextMenuProviderProps, p as EditorContextValue, q as EditorMode, r as EditorProvider, s as EditorProviderProps, t as EditorSelectionInfo, u as EditorShell, v as EditorShellProps, w as EditorState, x as EditorView, y as FormulaSessionOptions, I as ImageDisplayMode, z as IngestedSidecar, L as LayoutMode, M as MentionCandidate, A as MentionProvider, P as PreviewPanel, G as PreviewPanelProps, R as RawEditor, H as RawEditorProps, T as ThemeInheritance, V as ViewPreferences, W as WriteCanvasSettings, J as WysiwygEditor, K as WysiwygEditorProps, N as XLSX_LOCKED_REASON, O as XlsxFormulaSession, Q as createXlsxFormulaSession, U as getBlockSlices, Y as ingestSidecarBytes, Z as lineToOffset, _ as offsetToLine, $ as sliceIndexAtOffset, a0 as spliceBlock, a1 as useEditorContext, a2 as useEditorContextMenuItems } from './shell-3UAmXcYE.js';
1
+ import { C as CodeContext, E as EditorHostMode, S as SceneTextChannel, a as CalcEngineFactory, b as CsvSourceMeta, X as XlsxSourceMeta, F as FormulaEditRecord } from './shell-X0EoQbbN.js';
2
+ export { B as BlockRange, c as BlockSlice, d as BlockTagVisibility, e as CodeContextSection, f as CoverImageSaveOutput, D as DashboardImageSaveOutput, g as DocumentLinkCandidate, h as DocumentLinkProvider, i as EditorActions, j as EditorColorScheme, k as EditorContextMenuContext, l as EditorContextMenuItem, m as EditorContextMenuItemVisibility, n as EditorContextMenuProvider, o as EditorContextMenuProviderProps, p as EditorContextValue, q as EditorMode, r as EditorProvider, s as EditorProviderProps, t as EditorSelectionInfo, u as EditorShell, v as EditorShellProps, w as EditorState, x as EditorView, y as FormulaSessionOptions, I as ImageDisplayMode, z as IngestedSidecar, L as LayoutMode, M as MentionCandidate, A as MentionProvider, P as PreviewPanel, G as PreviewPanelProps, R as RawEditor, H as RawEditorProps, T as ThemeInheritance, V as ViewPreferences, W as WriteCanvasSettings, J as WysiwygEditor, K as WysiwygEditorProps, N as XLSX_LOCKED_REASON, O as XlsxFormulaSession, Q as createXlsxFormulaSession, U as getBlockSlices, Y as ingestSidecarBytes, Z as lineToOffset, _ as offsetToLine, $ as sliceIndexAtOffset, a0 as spliceBlock, a1 as useEditorContext, a2 as useEditorContextMenuItems } from './shell-X0EoQbbN.js';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import * as react from 'react';
5
5
  import { ReactNode, CSSProperties, ReactElement } from 'react';
@@ -3022,17 +3022,16 @@ declare function viewStateFromBinding(binding: HeadingViewBinding, headers: read
3022
3022
  * 1. BACKUP the original bytes first (`buildDataBackupPath`, pruned to the
3023
3023
  * newest 3 per sidecar) — the only cross-save undo the grid offers.
3024
3024
  * No container = no backup; the result says so rather than hiding it.
3025
- * 2. Serialize from the ORIGINAL parsed rows with journal edits splatted
3026
- * in — unedited cells reproduce the parse product byte-for-byte (values,
3027
- * not necessarily quoting style), sidestepping float-rendering drift.
3025
+ * 2. Serialize from the latest persisted bytes with a snapshot of this
3026
+ * region's journal, preserving saves made by other regions of the file.
3028
3027
  * Formula neutralization applies ONLY to journal-edited, non-numeric
3029
3028
  * cells (`serializeCsvRows` defaults `preserve`; blanket escaping would
3030
3029
  * corrupt every negative number in a re-saved file).
3031
3030
  * 3. `addMedia(samePath)` with FORK DETECTION: `addMedia` returns the path
3032
3031
  * the host actually used, and a collision-renaming provider would
3033
3032
  * silently fork the document's reference — surface it instead.
3034
- * 4. The caller bumps mediaRevision (previews re-resolve, journal cache
3035
- * rotates) only on success.
3033
+ * 4. Acknowledge only the saved snapshot. The caller refreshes the formula
3034
+ * baseline and bumps mediaRevision so previews re-resolve on success.
3036
3035
  */
3037
3036
 
3038
3037
  interface GridSaveResult {
@@ -3042,6 +3041,8 @@ interface GridSaveResult {
3042
3041
  /** Human-readable caveats: fork detected, backup skipped, … */
3043
3042
  notices: string[];
3044
3043
  error?: string;
3044
+ /** Persisted baseline for subsequent saves, including other regions' edits. */
3045
+ bytes?: ArrayBuffer;
3045
3046
  }
3046
3047
  interface SaveCsvEditsOptions {
3047
3048
  path: string;
package/dist/index.js CHANGED
@@ -219,7 +219,7 @@ import {
219
219
  useTreeViewData,
220
220
  viewStateFromBinding,
221
221
  writeHeadingViewState
222
- } from "./chunk-QY2ZFRJE.js";
222
+ } from "./chunk-7B46MJNW.js";
223
223
  import {
224
224
  EditorProvider,
225
225
  detectLanguageFromFileName,
@@ -249,7 +249,7 @@ import {
249
249
  imageEditorReducer,
250
250
  initialImageEditorState,
251
251
  useImageEditor
252
- } from "./chunk-V44VP242.js";
252
+ } from "./chunk-K4Z2GIX4.js";
253
253
  import {
254
254
  RecorderButton
255
255
  } from "./chunk-DN6XFOY2.js";
@@ -1,4 +1,4 @@
1
- export { d as BlockTagVisibility, g as DocumentLinkCandidate, h as DocumentLinkProvider, i as EditorActions, j as EditorColorScheme, k as EditorContextMenuContext, l as EditorContextMenuItem, m as EditorContextMenuItemVisibility, n as EditorContextMenuProvider, o as EditorContextMenuProviderProps, p as EditorContextValue, E as EditorHostMode, q as EditorMode, r as EditorProvider, s as EditorProviderProps, u as EditorShell, v as EditorShellProps, w as EditorState, x as EditorView, I as ImageDisplayMode, L as LayoutMode, M as MentionCandidate, A as MentionProvider, P as PreviewPanel, G as PreviewPanelProps, R as RawEditor, H as RawEditorProps, T as ThemeInheritance, V as ViewPreferences, J as WysiwygEditor, K as WysiwygEditorProps, a1 as useEditorContext, a2 as useEditorContextMenuItems } from '../shell-3UAmXcYE.js';
1
+ export { d as BlockTagVisibility, g as DocumentLinkCandidate, h as DocumentLinkProvider, i as EditorActions, j as EditorColorScheme, k as EditorContextMenuContext, l as EditorContextMenuItem, m as EditorContextMenuItemVisibility, n as EditorContextMenuProvider, o as EditorContextMenuProviderProps, p as EditorContextValue, E as EditorHostMode, q as EditorMode, r as EditorProvider, s as EditorProviderProps, u as EditorShell, v as EditorShellProps, w as EditorState, x as EditorView, I as ImageDisplayMode, L as LayoutMode, M as MentionCandidate, A as MentionProvider, P as PreviewPanel, G as PreviewPanelProps, R as RawEditor, H as RawEditorProps, T as ThemeInheritance, V as ViewPreferences, J as WysiwygEditor, K as WysiwygEditorProps, a1 as useEditorContext, a2 as useEditorContextMenuItems } from '../shell-X0EoQbbN.js';
2
2
  import 'react/jsx-runtime';
3
3
  import '@bendyline/squisq/table';
4
4
  import '@bendyline/squisq-calc';
@@ -5,7 +5,7 @@ import {
5
5
  RawEditor,
6
6
  WysiwygEditor,
7
7
  useEditorContextMenuItems
8
- } from "../chunk-QY2ZFRJE.js";
8
+ } from "../chunk-7B46MJNW.js";
9
9
  import {
10
10
  EditorProvider,
11
11
  useEditorContext
@@ -13,7 +13,7 @@ import {
13
13
  import "../chunk-V4NBQF5C.js";
14
14
  import "../chunk-ZDWZRVAY.js";
15
15
  import "../chunk-EN5BYZOE.js";
16
- import "../chunk-V44VP242.js";
16
+ import "../chunk-K4Z2GIX4.js";
17
17
  import "../chunk-BJ3TXAIQ.js";
18
18
  import "../chunk-GS7QWYFT.js";
19
19
  import "../chunk-ENDRPV6T.js";
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { TableCellEdit, TableCellValue } from '@bendyline/squisq/table';
2
+ import { TableCellValue, TableCellEdit } from '@bendyline/squisq/table';
3
3
  import { CalcEngineConfig, CalcEngine } from '@bendyline/squisq-calc';
4
4
  import { IngestTable } from '@bendyline/squisq-grid-react';
5
5
  import { XlsxWorkbookGrids } from '@bendyline/squisq-formats/xlsx';
@@ -114,9 +114,13 @@ declare function ingestSidecarBytes(bytes: ArrayBuffer, ext: string, params: {
114
114
  type CalcEngineFactory = (config: CalcEngineConfig) => Promise<CalcEngine>;
115
115
  interface FormulaSessionOptions {
116
116
  engineFactory?: CalcEngineFactory;
117
+ /** Caller-owned edits survive view remounts; scoped to the same journal. */
118
+ edits?: Map<string, FormulaEditRecord>;
117
119
  }
118
120
  interface FormulaEditRecord {
119
- formula: string;
121
+ /** null explicitly removes a formula. */
122
+ formula: string | null;
123
+ value?: TableCellValue;
120
124
  /** Engine result, when scalar — persisted as the cached `<v>`. */
121
125
  cachedValue?: number | boolean | string;
122
126
  }
@@ -141,7 +145,9 @@ interface XlsxFormulaSession {
141
145
  /** Revert to the loaded workbook; returns the display updates to re-apply. */
142
146
  discard(): Promise<TableCellEdit[]>;
143
147
  /** Forget the edits after a successful save (values stay current). */
144
- markSaved(): void;
148
+ markSaved(snapshot?: ReadonlyMap<string, FormulaEditRecord>, baseline?: XlsxSourceMeta): void;
149
+ /** Wait for mutations already submitted to this session. */
150
+ flush(): Promise<void>;
145
151
  dispose(): void;
146
152
  }
147
153
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-editor-react",
3
- "version": "2.11.14",
3
+ "version": "2.11.15",
4
4
  "description": "React editor shell with raw/WYSIWYG/preview modes for Squisq documents",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -98,12 +98,12 @@
98
98
  }
99
99
  },
100
100
  "dependencies": {
101
- "@bendyline/squisq": "2.11.11",
102
- "@bendyline/squisq-calc": "2.11.0",
103
- "@bendyline/squisq-formats": "2.6.11",
104
- "@bendyline/squisq-grid-react": "2.11.12",
105
- "@bendyline/squisq-react": "2.11.13",
106
- "@bendyline/squisq-video-react": "2.4.17",
101
+ "@bendyline/squisq": "2.11.12",
102
+ "@bendyline/squisq-calc": "2.11.1",
103
+ "@bendyline/squisq-formats": "2.6.12",
104
+ "@bendyline/squisq-grid-react": "2.11.13",
105
+ "@bendyline/squisq-react": "2.11.14",
106
+ "@bendyline/squisq-video-react": "2.4.18",
107
107
  "@fortawesome/fontawesome-free": "7.2.0",
108
108
  "@tiptap/core": "2.27.3",
109
109
  "@tiptap/extension-heading": "2.27.3",