@bendyline/squisq-editor-react 2.11.14 → 2.11.16

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,
@@ -41,7 +41,7 @@ import {
41
41
  documentTitleFromFileName,
42
42
  resolveBlockVisual,
43
43
  useModalDialog
44
- } from "./chunk-BJ3TXAIQ.js";
44
+ } from "./chunk-4D7QMRTZ.js";
45
45
  import {
46
46
  Icon
47
47
  } from "./chunk-GS7QWYFT.js";
@@ -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,