grist-widget-sdk 0.3.26 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +1 -1
  3. package/dist/app.cjs +1139 -4336
  4. package/dist/app.cjs.map +1 -1
  5. package/dist/app.d.cts +1 -3
  6. package/dist/app.d.ts +1 -3
  7. package/dist/app.js +9 -517
  8. package/dist/app.js.map +1 -1
  9. package/dist/chunk-4ZGVKGDF.js +204 -0
  10. package/dist/chunk-4ZGVKGDF.js.map +1 -0
  11. package/dist/{chunk-YQNMIL33.js → chunk-B5ZZNLVS.js} +528 -181
  12. package/dist/chunk-B5ZZNLVS.js.map +1 -0
  13. package/dist/chunk-O7NY46QY.js +1830 -0
  14. package/dist/chunk-O7NY46QY.js.map +1 -0
  15. package/dist/chunk-P6VTEKWO.js +618 -0
  16. package/dist/chunk-P6VTEKWO.js.map +1 -0
  17. package/dist/emulator/testing.cjs +2084 -391
  18. package/dist/emulator/testing.cjs.map +1 -1
  19. package/dist/emulator/testing.d.cts +55 -7
  20. package/dist/emulator/testing.d.ts +55 -7
  21. package/dist/emulator/testing.js +144 -19
  22. package/dist/emulator/testing.js.map +1 -1
  23. package/dist/emulator.cjs +59 -249
  24. package/dist/emulator.cjs.map +1 -1
  25. package/dist/emulator.d.cts +3 -5
  26. package/dist/emulator.d.ts +3 -5
  27. package/dist/emulator.js +5 -11
  28. package/dist/index.cjs +1373 -4859
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +81 -260
  31. package/dist/index.d.ts +81 -260
  32. package/dist/index.js +8 -161
  33. package/dist/{inline-C7DkvuET.d.cts → templates-CGmRIQBV.d.cts} +89 -35
  34. package/dist/{inline-DpcT8tV9.d.ts → templates-Cq94qOcf.d.ts} +89 -35
  35. package/dist/{types-D3JxUCfL.d.cts → types-DBStn0Dk.d.cts} +2 -26
  36. package/dist/{types-D3JxUCfL.d.ts → types-DBStn0Dk.d.ts} +2 -26
  37. package/dist/use-grist-BK0eke9K.d.cts +358 -0
  38. package/dist/use-grist-C5DMZ3wU.d.ts +358 -0
  39. package/package.json +8 -37
  40. package/dist/advanced.cjs +0 -2799
  41. package/dist/advanced.cjs.map +0 -1
  42. package/dist/advanced.d.cts +0 -822
  43. package/dist/advanced.d.ts +0 -822
  44. package/dist/advanced.js +0 -875
  45. package/dist/advanced.js.map +0 -1
  46. package/dist/chunk-22U7WAY3.js +0 -463
  47. package/dist/chunk-22U7WAY3.js.map +0 -1
  48. package/dist/chunk-64SVRWW7.js +0 -758
  49. package/dist/chunk-64SVRWW7.js.map +0 -1
  50. package/dist/chunk-ELGVOHFD.js +0 -32
  51. package/dist/chunk-ELGVOHFD.js.map +0 -1
  52. package/dist/chunk-EPEBKXVN.js +0 -470
  53. package/dist/chunk-EPEBKXVN.js.map +0 -1
  54. package/dist/chunk-GTTD5HPO.js +0 -36
  55. package/dist/chunk-GTTD5HPO.js.map +0 -1
  56. package/dist/chunk-JASMM2YC.js +0 -243
  57. package/dist/chunk-JASMM2YC.js.map +0 -1
  58. package/dist/chunk-N3X46JW2.js +0 -37
  59. package/dist/chunk-N3X46JW2.js.map +0 -1
  60. package/dist/chunk-PDGEZ24B.js +0 -2366
  61. package/dist/chunk-PDGEZ24B.js.map +0 -1
  62. package/dist/chunk-PM3B4WYV.js +0 -12
  63. package/dist/chunk-PM3B4WYV.js.map +0 -1
  64. package/dist/chunk-REV5AUXD.js +0 -25
  65. package/dist/chunk-REV5AUXD.js.map +0 -1
  66. package/dist/chunk-TZB3OZA6.js +0 -1630
  67. package/dist/chunk-TZB3OZA6.js.map +0 -1
  68. package/dist/chunk-YQNMIL33.js.map +0 -1
  69. package/dist/create-grist-emulator-BCqp1BF9.d.ts +0 -81
  70. package/dist/create-grist-emulator-DBM8OSIR.d.cts +0 -81
  71. package/dist/emulator/devtools.cjs +0 -1404
  72. package/dist/emulator/devtools.cjs.map +0 -1
  73. package/dist/emulator/devtools.d.cts +0 -48
  74. package/dist/emulator/devtools.d.ts +0 -48
  75. package/dist/emulator/devtools.js +0 -508
  76. package/dist/emulator/devtools.js.map +0 -1
  77. package/dist/emulator/transports.cjs +0 -1132
  78. package/dist/emulator/transports.cjs.map +0 -1
  79. package/dist/emulator/transports.d.cts +0 -38
  80. package/dist/emulator/transports.d.ts +0 -38
  81. package/dist/emulator/transports.js +0 -12
  82. package/dist/emulator/transports.js.map +0 -1
  83. package/dist/templates-8k2LKoO-.d.cts +0 -24
  84. package/dist/templates-DIwi_mvI.d.ts +0 -24
  85. package/dist/use-grist-reads-DUMvJgV7.d.ts +0 -480
  86. package/dist/use-grist-reads-DpjVuI0H.d.cts +0 -480
  87. package/dist/use-grist-types-DdOLEJXN.d.cts +0 -499
  88. package/dist/use-grist-types-kwxoFeub.d.ts +0 -499
  89. package/dist/use-grist-widget-options-from-context-CGOlQ5Vc.d.cts +0 -43
  90. package/dist/use-grist-widget-options-from-context-CGOlQ5Vc.d.ts +0 -43
@@ -20,23 +20,833 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/emulator/testing/index.ts
21
21
  var testing_exports = {};
22
22
  __export(testing_exports, {
23
- act: () => import_react2.act,
23
+ act: () => import_react11.act,
24
24
  actionsOf: () => actionsOf,
25
- cleanup: () => import_react2.cleanup,
25
+ cleanup: () => import_react11.cleanup,
26
26
  eventsOf: () => eventsOf,
27
- fireEvent: () => import_react2.fireEvent,
27
+ fireEvent: () => import_react11.fireEvent,
28
+ installJsdomStubs: () => installJsdomStubs,
28
29
  presets: () => presets,
29
- render: () => import_react2.render,
30
+ render: () => import_react11.render,
30
31
  renderWithGrist: () => renderWithGrist,
31
- screen: () => import_react2.screen,
32
- waitFor: () => import_react2.waitFor,
32
+ screen: () => import_react11.screen,
33
+ smokeTestWidget: () => smokeTestWidget,
34
+ waitFor: () => import_react11.waitFor,
33
35
  waitForEvent: () => waitForEvent,
34
- within: () => import_react2.within
36
+ within: () => import_react11.within
35
37
  });
36
38
  module.exports = __toCommonJS(testing_exports);
37
39
 
38
40
  // src/emulator/testing/render-with-grist.tsx
39
- var import_react = require("@testing-library/react");
41
+ var import_react2 = require("@testing-library/react");
42
+
43
+ // src/sdk/use-grist.ts
44
+ var import_react = require("react");
45
+
46
+ // src/sdk/helpers/grist-value-codec.ts
47
+ var MARSHALLED_TAGS = /* @__PURE__ */ new Set(["E", "L", "D", "d", "R", "r", "P"]);
48
+ function isMarshalledTuple(value) {
49
+ return Array.isArray(value) && typeof value[0] === "string" && value[0].length <= 2 && MARSHALLED_TAGS.has(value[0]);
50
+ }
51
+ function coerceGristDocApiRefRowId(value) {
52
+ if (typeof value === "number" && Number.isFinite(value)) return value;
53
+ if (!Array.isArray(value) || value.length < 2) return void 0;
54
+ const tag = value[0];
55
+ if (tag === "R" && typeof value[2] === "number" && Number.isFinite(value[2])) return value[2];
56
+ if (tag === "r" && typeof value[1] === "number" && Number.isFinite(value[1])) return value[1];
57
+ return void 0;
58
+ }
59
+ function looksLikeAttachmentColumn(column) {
60
+ return column?.type === "Attachments";
61
+ }
62
+ function looksLikeChoiceListColumn(column) {
63
+ return column?.type === "ChoiceList";
64
+ }
65
+ function looksLikeRefListColumn(column) {
66
+ return typeof column?.type === "string" && column.type.startsWith("RefList:");
67
+ }
68
+ function looksLikeRefColumn(column) {
69
+ return typeof column?.type === "string" && column.type.startsWith("Ref:");
70
+ }
71
+ function looksLikeDateColumn(column) {
72
+ return column?.type === "Date";
73
+ }
74
+ function looksLikeDateTimeColumn(column) {
75
+ return typeof column?.type === "string" && column.type.startsWith("DateTime");
76
+ }
77
+ function decodeGristValue(value, column) {
78
+ if (value === null || value === void 0) return null;
79
+ if (isMarshalledTuple(value)) {
80
+ const tag = value[0];
81
+ switch (tag) {
82
+ case "E":
83
+ return {
84
+ __error: true,
85
+ type: typeof value[1] === "string" ? value[1] : "Error",
86
+ message: typeof value[2] === "string" ? value[2] : void 0,
87
+ traceback: typeof value[3] === "string" ? value[3] : void 0
88
+ };
89
+ case "L":
90
+ return value.slice(1).map((v) => decodeGristValue(v, void 0));
91
+ case "D":
92
+ if (typeof value[1] === "number") return new Date(value[1] * 1e3);
93
+ return null;
94
+ case "d":
95
+ if (typeof value[1] === "number") return new Date(value[1] * 1e3);
96
+ return null;
97
+ case "R":
98
+ return {
99
+ __ref: true,
100
+ tableId: typeof value[1] === "string" ? value[1] : void 0,
101
+ rowId: typeof value[2] === "number" ? value[2] : 0
102
+ };
103
+ case "r":
104
+ return { __ref: true, rowId: typeof value[1] === "number" ? value[1] : 0 };
105
+ case "P":
106
+ return null;
107
+ default:
108
+ return null;
109
+ }
110
+ }
111
+ if (looksLikeDateColumn(column) || looksLikeDateTimeColumn(column)) {
112
+ if (typeof value === "number" && Number.isFinite(value)) return new Date(value * 1e3);
113
+ }
114
+ if (looksLikeRefColumn(column) && typeof value === "number") {
115
+ return { __ref: true, rowId: value };
116
+ }
117
+ if ((looksLikeRefListColumn(column) || looksLikeChoiceListColumn(column) || looksLikeAttachmentColumn(column)) && Array.isArray(value)) {
118
+ return value.map((v) => decodeGristValue(v, void 0));
119
+ }
120
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
121
+ if (Array.isArray(value)) return value.map((v) => decodeGristValue(v, void 0));
122
+ return null;
123
+ }
124
+
125
+ // src/sdk/helpers/grist-table-data.ts
126
+ function computeRowCount(data) {
127
+ const idCol = data.id;
128
+ if (Array.isArray(idCol)) return idCol.length;
129
+ for (const key of Object.keys(data)) {
130
+ const col = data[key];
131
+ if (Array.isArray(col)) return col.length;
132
+ }
133
+ return 0;
134
+ }
135
+ function decodeColumnarToReplicaRows(data, columnIds, columns, maxRows) {
136
+ const nFull = computeRowCount(data);
137
+ const n = maxRows !== void 0 ? Math.min(nFull, maxRows) : nFull;
138
+ const out = [];
139
+ for (let i = 0; i < n; i++) {
140
+ const row = {};
141
+ for (const k of columnIds) {
142
+ const arr = data[k];
143
+ const raw = Array.isArray(arr) ? arr[i] : void 0;
144
+ if (k === "id") {
145
+ row[k] = raw;
146
+ } else {
147
+ row[k] = decodeGristValue(raw, columns[k]);
148
+ }
149
+ }
150
+ out.push(row);
151
+ }
152
+ return out;
153
+ }
154
+ function tableDataToRows(data) {
155
+ const idCol = data.id;
156
+ if (!Array.isArray(idCol) || idCol.length === 0) return [];
157
+ const keys = Object.keys(data);
158
+ const n = idCol.length;
159
+ const rows = [];
160
+ for (let i = 0; i < n; i++) {
161
+ const row = {};
162
+ for (const k of keys) {
163
+ const col = data[k];
164
+ row[k] = Array.isArray(col) ? col[i] : void 0;
165
+ }
166
+ rows.push(row);
167
+ }
168
+ return rows;
169
+ }
170
+ function manualSortKey(row) {
171
+ const v = row.manualSort;
172
+ if (typeof v === "number" && !Number.isNaN(v)) return v;
173
+ if (typeof v === "string" && v.trim() !== "") {
174
+ const n = Number(v);
175
+ if (!Number.isNaN(n)) return n;
176
+ }
177
+ return null;
178
+ }
179
+ function sortRowsByGristOrder(rows) {
180
+ const hasManual = rows.some((r) => manualSortKey(r) !== null);
181
+ if (!hasManual) {
182
+ return [...rows].sort((a, b) => {
183
+ const ida = typeof a.id === "number" ? a.id : 0;
184
+ const idb = typeof b.id === "number" ? b.id : 0;
185
+ return ida - idb;
186
+ });
187
+ }
188
+ return [...rows].sort((a, b) => {
189
+ const ma = manualSortKey(a);
190
+ const mb = manualSortKey(b);
191
+ if (ma !== null && mb !== null && ma !== mb) return ma - mb;
192
+ if (ma !== null && mb === null) return -1;
193
+ if (ma === null && mb !== null) return 1;
194
+ const ida = typeof a.id === "number" ? a.id : 0;
195
+ const idb = typeof b.id === "number" ? b.id : 0;
196
+ return ida - idb;
197
+ });
198
+ }
199
+ function materializeGristRowsFromColumnar(data, options) {
200
+ const order = options?.order ?? "grist";
201
+ let rows;
202
+ if (options?.columns) {
203
+ const columnIds = Object.keys(data);
204
+ rows = decodeColumnarToReplicaRows(data, columnIds, options.columns);
205
+ } else {
206
+ rows = tableDataToRows(data);
207
+ }
208
+ if (order === "none") return rows;
209
+ if (order === "id") {
210
+ return [...rows].sort((a, b) => {
211
+ const ida = typeof a.id === "number" ? a.id : 0;
212
+ const idb = typeof b.id === "number" ? b.id : 0;
213
+ return ida - idb;
214
+ });
215
+ }
216
+ return sortRowsByGristOrder(rows);
217
+ }
218
+ async function fetchGristTableRows(docApi, tableId, options) {
219
+ const data = await docApi.fetchTable(tableId);
220
+ return materializeGristRowsFromColumnar(data, options);
221
+ }
222
+
223
+ // src/sdk/helpers/grist-widget-options.ts
224
+ function parseGristWidgetOptionsCell(raw) {
225
+ if (raw == null || raw === "") return void 0;
226
+ if (typeof raw === "string") {
227
+ try {
228
+ const parsed = JSON.parse(raw);
229
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
230
+ return parsed;
231
+ }
232
+ } catch {
233
+ return void 0;
234
+ }
235
+ return void 0;
236
+ }
237
+ if (typeof raw === "object" && !Array.isArray(raw)) {
238
+ return raw;
239
+ }
240
+ return void 0;
241
+ }
242
+
243
+ // src/sdk/helpers/build-replica-document.ts
244
+ function readCellStr(table, key, rowIndex) {
245
+ const col = table[key];
246
+ if (!Array.isArray(col)) return void 0;
247
+ const v = col[rowIndex];
248
+ if (v === null || v === void 0) return void 0;
249
+ if (typeof v === "string") return v;
250
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
251
+ return void 0;
252
+ }
253
+ function readCellNum(table, key, rowIndex) {
254
+ const col = table[key];
255
+ if (!Array.isArray(col)) return void 0;
256
+ const v = col[rowIndex];
257
+ if (typeof v === "number" && !Number.isNaN(v)) return v;
258
+ if (typeof v === "string" && v.trim() !== "") {
259
+ const n = Number(v);
260
+ if (!Number.isNaN(n)) return n;
261
+ }
262
+ return void 0;
263
+ }
264
+ async function loadColumnMetadataByTable(docApi) {
265
+ const byTable = /* @__PURE__ */ new Map();
266
+ try {
267
+ const tables = await docApi.fetchTable("_grist_Tables");
268
+ const columns = await docApi.fetchTable("_grist_Tables_column");
269
+ const tableIds = tables.tableId;
270
+ const tableInternalIds = tables.id;
271
+ if (!Array.isArray(tableIds) || !Array.isArray(tableInternalIds)) return byTable;
272
+ const internalIdToTableId = /* @__PURE__ */ new Map();
273
+ for (let i = 0; i < tableInternalIds.length; i++) {
274
+ const tid = tableIds[i];
275
+ const iid = coerceGristDocApiRefRowId(tableInternalIds[i]);
276
+ if (typeof tid === "string" && iid !== void 0) internalIdToTableId.set(iid, tid);
277
+ }
278
+ const parentIds = columns.parentId;
279
+ const colIds = columns.colId;
280
+ const columnIds = columns.id;
281
+ if (!Array.isArray(parentIds) || !Array.isArray(colIds) || !Array.isArray(columnIds) || parentIds.length !== colIds.length || colIds.length !== columnIds.length) {
282
+ return byTable;
283
+ }
284
+ const n = parentIds.length;
285
+ for (let i = 0; i < n; i++) {
286
+ const parentRowId = coerceGristDocApiRefRowId(parentIds[i]);
287
+ const colRef = coerceGristDocApiRefRowId(columnIds[i]);
288
+ const colId = colIds[i];
289
+ if (parentRowId === void 0 || colRef === void 0 || typeof colId !== "string") continue;
290
+ const tableId = internalIdToTableId.get(parentRowId);
291
+ if (!tableId) continue;
292
+ let colMap = byTable.get(tableId);
293
+ if (!colMap) {
294
+ colMap = /* @__PURE__ */ new Map();
295
+ byTable.set(tableId, colMap);
296
+ }
297
+ const parentPos = readCellNum(columns, "parentPos", i);
298
+ const isFormulaCol = columns.isFormula;
299
+ const isFormula = Array.isArray(isFormulaCol) ? Boolean(isFormulaCol[i]) : false;
300
+ const woCol = columns.widgetOptions;
301
+ const widgetOptions = Array.isArray(woCol) && i < woCol.length ? parseGristWidgetOptionsCell(woCol[i]) : void 0;
302
+ const displayColRef = coerceGristDocApiRefRowId(
303
+ Array.isArray(columns.displayCol) ? columns.displayCol[i] : void 0
304
+ );
305
+ const visibleColRef = coerceGristDocApiRefRowId(
306
+ Array.isArray(columns.visibleCol) ? columns.visibleCol[i] : void 0
307
+ );
308
+ colMap.set(colId, {
309
+ colRef,
310
+ type: readCellStr(columns, "type", i),
311
+ label: readCellStr(columns, "label", i),
312
+ parentPos: parentPos !== void 0 ? parentPos : void 0,
313
+ isFormula,
314
+ formula: readCellStr(columns, "formula", i),
315
+ description: readCellStr(columns, "description", i) ?? readCellStr(columns, "descr", i),
316
+ widgetOptions,
317
+ displayColRef,
318
+ visibleColRef
319
+ });
320
+ }
321
+ } catch {
322
+ }
323
+ return byTable;
324
+ }
325
+
326
+ // src/sdk/helpers/grist-columns-filters.ts
327
+ var NOISE_COLUMN_FORMULAS = /* @__PURE__ */ new Set(["now()"]);
328
+ function normalizeFormulaForComparison(formula) {
329
+ return formula.trim().toLowerCase().replace(/\s+/g, "");
330
+ }
331
+ function isGristNoiseColumn(colId, col, options) {
332
+ const excludeId = options?.excludeId ?? true;
333
+ const excludeGristHelpers = options?.excludeGristHelpers ?? true;
334
+ const excludeLoggingColumns = options?.excludeLoggingColumns ?? true;
335
+ const excludeManualSort = options?.excludeManualSort ?? false;
336
+ if (excludeId && colId === "id") return true;
337
+ if (excludeManualSort && colId === "manualSort") return true;
338
+ if (excludeGristHelpers && colId.startsWith("gristHelper_")) return true;
339
+ if (excludeLoggingColumns) {
340
+ const formula = typeof col?.formula === "string" ? normalizeFormulaForComparison(col.formula) : "";
341
+ if (formula && NOISE_COLUMN_FORMULAS.has(formula)) return true;
342
+ if (formula.startsWith("user.")) return true;
343
+ }
344
+ return false;
345
+ }
346
+
347
+ // src/sdk/helpers/grist-attachment-download.ts
348
+ async function getAttachmentDownloadUrl(docApi, attachmentId, options) {
349
+ const getAccessToken = docApi.getAccessToken;
350
+ if (!getAccessToken) {
351
+ throw new Error("getAccessToken is not available on docApi");
352
+ }
353
+ const { token: token2, baseUrl } = await getAccessToken({ readOnly: options?.readOnly ?? false });
354
+ return `${baseUrl}/attachments/${attachmentId}/download?auth=${token2}`;
355
+ }
356
+ async function fetchBytesFromSignedAttachmentUrl(url) {
357
+ const response = await fetch(url);
358
+ const contentType = response.headers.get("content-type")?.trim() || "application/octet-stream";
359
+ if (!response.ok) {
360
+ throw new Error(`Attachment download failed (${response.status})`);
361
+ }
362
+ const buffer = await response.arrayBuffer();
363
+ return { buffer, contentType };
364
+ }
365
+ async function fetchAttachmentBlob(docApi, attachmentId, options) {
366
+ const url = await getAttachmentDownloadUrl(docApi, attachmentId, options);
367
+ const { buffer, contentType } = await fetchBytesFromSignedAttachmentUrl(url);
368
+ return { blob: new Blob([buffer], { type: contentType }), contentType };
369
+ }
370
+
371
+ // src/sdk/helpers/grist-fetch-with-auth.ts
372
+ var TOKEN_TTL_MS = 45e3;
373
+ var cache = /* @__PURE__ */ new Map();
374
+ var inFlight = /* @__PURE__ */ new Map();
375
+ function cacheKey(readOnly) {
376
+ return readOnly ? "ro" : "rw";
377
+ }
378
+ async function getGristAccessToken(docApi, options) {
379
+ if (!docApi.getAccessToken) {
380
+ throw new Error("getAccessToken is not available on docApi (need higher access level)");
381
+ }
382
+ const readOnly = options?.readOnly ?? false;
383
+ const key = cacheKey(readOnly);
384
+ const now = Date.now();
385
+ const hit = cache.get(key);
386
+ if (hit && now - hit.fetchedAt < TOKEN_TTL_MS) {
387
+ return hit.token;
388
+ }
389
+ const pending = inFlight.get(key);
390
+ if (pending) return pending;
391
+ const next = docApi.getAccessToken({ readOnly }).then((token2) => {
392
+ cache.set(key, { token: token2, fetchedAt: Date.now() });
393
+ inFlight.delete(key);
394
+ return token2;
395
+ });
396
+ inFlight.set(key, next);
397
+ return next;
398
+ }
399
+ function clearGristAccessTokenCache() {
400
+ cache.clear();
401
+ inFlight.clear();
402
+ }
403
+ async function fetchWithGristAuth(docApi, path, init) {
404
+ const readOnly = init?.readOnly ?? false;
405
+ let token2;
406
+ try {
407
+ token2 = await getGristAccessToken(docApi, { readOnly });
408
+ } catch (err) {
409
+ clearGristAccessTokenCache();
410
+ throw err;
411
+ }
412
+ const isAbsolute = /^https?:\/\//i.test(path);
413
+ const cleanPath = isAbsolute ? path : path.startsWith("/") ? path : `/${path}`;
414
+ const url = isAbsolute ? new URL(path) : new URL(`${token2.baseUrl.replace(/\/$/, "")}${cleanPath}`);
415
+ const gristOrigin = new URL(token2.baseUrl).origin;
416
+ if (url.origin !== gristOrigin) {
417
+ throw new Error(
418
+ `fetchWithGristAuth: refusing to send the Grist access token to ${url.origin}; absolute URLs must point at this document's Grist instance (${gristOrigin}). Use plain fetch() for other hosts.`
419
+ );
420
+ }
421
+ if (!url.searchParams.has("auth")) url.searchParams.set("auth", token2.token);
422
+ const headers = new Headers(init?.headers ?? {});
423
+ headers.delete("Authorization");
424
+ const response = await fetch(url.toString(), { ...init, headers });
425
+ if (response.status === 401) {
426
+ clearGristAccessTokenCache();
427
+ const retryToken = await getGristAccessToken(docApi, { readOnly });
428
+ url.searchParams.set("auth", retryToken.token);
429
+ return fetch(url.toString(), { ...init, headers });
430
+ }
431
+ return response;
432
+ }
433
+
434
+ // src/sdk/helpers/grist-attachment-upload.ts
435
+ function parseGristAttachmentUploadResponseIds(raw, options) {
436
+ const trimmed = raw.trim();
437
+ if (!trimmed) {
438
+ throw new Error("Attachment upload returned an empty response.");
439
+ }
440
+ let parsed;
441
+ try {
442
+ parsed = JSON.parse(trimmed);
443
+ } catch {
444
+ if (options?.strict) {
445
+ throw new Error("Attachment upload returned non-JSON response.");
446
+ }
447
+ parsed = trimmed;
448
+ }
449
+ return extractAttachmentIdsFromUploadPayload(parsed, options);
450
+ }
451
+ function extractAttachmentIdsFromUploadPayload(parsed, options) {
452
+ if (options?.strict) {
453
+ return extractAttachmentIdsFromUploadPayloadStrict(parsed);
454
+ }
455
+ const first = extractFirstAttachmentIdFromUploadPayload(parsed);
456
+ if (first !== void 0) {
457
+ if (Array.isArray(parsed)) {
458
+ const ids = [];
459
+ const seen = /* @__PURE__ */ new Set();
460
+ for (const item of parsed) {
461
+ const id = coerceAttachmentId(item);
462
+ if (id && !seen.has(id)) {
463
+ seen.add(id);
464
+ ids.push(id);
465
+ }
466
+ }
467
+ return ids.length > 0 ? ids : [first];
468
+ }
469
+ return [first];
470
+ }
471
+ throw new Error("Attachment upload returned an unexpected response shape.");
472
+ }
473
+ function extractAttachmentIdsFromUploadPayloadStrict(parsed) {
474
+ if (typeof parsed === "number" && Number.isFinite(parsed)) {
475
+ return [String(parsed)];
476
+ }
477
+ if (Array.isArray(parsed)) {
478
+ const ids = [];
479
+ const seen = /* @__PURE__ */ new Set();
480
+ for (const item of parsed) {
481
+ if (typeof item !== "number" || !Number.isFinite(item)) {
482
+ throw new Error("Attachment upload returned an unexpected response shape.");
483
+ }
484
+ const id = String(item);
485
+ if (!seen.has(id)) {
486
+ seen.add(id);
487
+ ids.push(id);
488
+ }
489
+ }
490
+ if (ids.length === 0) {
491
+ throw new Error("Attachment upload returned an empty id list.");
492
+ }
493
+ return ids;
494
+ }
495
+ throw new Error("Attachment upload returned an unexpected response shape.");
496
+ }
497
+ function extractFirstAttachmentIdFromUploadPayload(parsed) {
498
+ if (typeof parsed === "number" && Number.isFinite(parsed)) {
499
+ return String(parsed);
500
+ }
501
+ if (typeof parsed === "string" && parsed.trim()) {
502
+ return parsed.trim();
503
+ }
504
+ if (Array.isArray(parsed)) {
505
+ for (const item of parsed) {
506
+ const id = coerceAttachmentId(item);
507
+ if (id) return id;
508
+ }
509
+ return void 0;
510
+ }
511
+ if (parsed && typeof parsed === "object") {
512
+ const record = parsed;
513
+ for (const key of ["id", "attachmentId", "fileId"]) {
514
+ const id = coerceAttachmentId(record[key]);
515
+ if (id) return id;
516
+ }
517
+ }
518
+ return void 0;
519
+ }
520
+ function coerceAttachmentId(value) {
521
+ if (typeof value === "number" && Number.isFinite(value)) {
522
+ return String(value);
523
+ }
524
+ if (typeof value === "string" && value.trim()) {
525
+ return value.trim();
526
+ }
527
+ return void 0;
528
+ }
529
+ var UPLOAD_HEADERS = {
530
+ "X-Requested-With": "XMLHttpRequest",
531
+ Accept: "application/json"
532
+ };
533
+ async function uploadGristAttachment(docApi, file, options) {
534
+ const form = new FormData();
535
+ const fileName = options?.fileName ?? (file instanceof File && file.name ? file.name : "upload");
536
+ form.append("upload", file, fileName);
537
+ let response;
538
+ try {
539
+ response = await fetchWithGristAuth(docApi, "/attachments", {
540
+ method: "POST",
541
+ body: form,
542
+ readOnly: false,
543
+ headers: UPLOAD_HEADERS
544
+ });
545
+ } catch (err) {
546
+ const msg = err instanceof Error ? err.message : String(err);
547
+ if (/failed to fetch/i.test(msg)) {
548
+ const widgetOrigin = typeof globalThis.location !== "undefined" ? globalThis.location.origin : "this widget origin";
549
+ throw new Error(
550
+ `Attachment upload was blocked by the Grist host (CORS). The browser could not POST from ${widgetOrigin} to the document API. If upload worked from a dev URL (e.g. ngrok) but fails after deploy, add the new widget URL to your Grist server CORS allowlist (see grist-core#2040).`,
551
+ { cause: err }
552
+ );
553
+ }
554
+ throw err;
555
+ }
556
+ const raw = await response.text();
557
+ if (!response.ok) {
558
+ throw new Error(
559
+ raw.trim() ? `Attachment upload failed (${response.status}): ${raw.slice(0, 200)}` : `Attachment upload failed (${response.status})`
560
+ );
561
+ }
562
+ const ids = parseGristAttachmentUploadResponseIds(raw);
563
+ return { ids, firstId: ids[0] };
564
+ }
565
+
566
+ // src/sdk/runtime/api.ts
567
+ var ACCESS_ERROR = [/access not granted/i, /need higher access level/i, /requires.*full.*access/i, /RPC_ONCALL_ERROR Access/i];
568
+ function isAccessInsufficientError(message2) {
569
+ return ACCESS_ERROR.some((re) => re.test(message2));
570
+ }
571
+ var message = (err) => err instanceof Error ? err.message : String(err);
572
+ function createGristApi(runtime) {
573
+ const docApi = () => {
574
+ if (runtime.getState().connection !== "ready" || typeof grist === "undefined" || !grist.docApi) {
575
+ throw new Error('Grist is not ready: wait for useGrist().status === "ready".');
576
+ }
577
+ return grist.docApi;
578
+ };
579
+ const read = async (fn) => {
580
+ try {
581
+ const out = await fn();
582
+ if (runtime.getState().readError) runtime.update({ readError: null });
583
+ return out;
584
+ } catch (err) {
585
+ if (isAccessInsufficientError(message(err))) runtime.update({ readError: message(err) });
586
+ throw err;
587
+ }
588
+ };
589
+ const write = async (fn) => {
590
+ runtime.update({ actionStatus: "running", actionError: null });
591
+ try {
592
+ const out = await fn();
593
+ runtime.update({ actionStatus: "idle" });
594
+ return out;
595
+ } catch (err) {
596
+ runtime.update({ actionStatus: "error", actionError: message(err) });
597
+ throw err;
598
+ }
599
+ };
600
+ const tableOps = (resolve) => ({
601
+ getTableId: () => resolve().getTableId(),
602
+ create: ((records, opts) => write(() => resolve().create(records, opts))),
603
+ update: (records, opts) => write(() => resolve().update(records, opts)),
604
+ upsert: (records, opts) => write(() => resolve().upsert(records, opts)),
605
+ destroy: (ids) => write(() => resolve().destroy(ids))
606
+ });
607
+ const tables = /* @__PURE__ */ new Map();
608
+ return {
609
+ reload: () => runtime.reload(),
610
+ fetchSelectedTable: (options) => read(async () => {
611
+ const impl = docApi().fetchSelectedTable ?? grist.fetchSelectedTable;
612
+ if (!impl) throw new Error("fetchSelectedTable is not available in this Grist version");
613
+ return await impl({ keepEncoded: false, ...options });
614
+ }),
615
+ fetchTable: (tableId) => read(async () => await docApi().fetchTable(tableId)),
616
+ fetchTableRows: (tableId, options) => read(() => fetchGristTableRows(docApi(), tableId, options ?? {})),
617
+ listTables: (options) => read(async () => {
618
+ const all = [...await docApi().listTables()];
619
+ return options?.includeSystem ? all : all.filter((t) => !t.startsWith("_grist") && !t.startsWith("GristHidden_"));
620
+ }),
621
+ listColumns: (tableId, options) => read(async () => {
622
+ const meta = (await loadColumnMetadataByTable(docApi())).get(tableId);
623
+ if (!meta) return [];
624
+ const noise = {
625
+ excludeId: options?.excludeId ?? true,
626
+ excludeGristHelpers: options?.excludeGristHelpers ?? true,
627
+ excludeLoggingColumns: options?.excludeLoggingColumns ?? true,
628
+ excludeManualSort: options?.excludeManualSort ?? true
629
+ };
630
+ return [...meta].filter(([colId, m]) => options?.includeSystem || !isGristNoiseColumn(colId, m, noise)).map(([colId, m]) => ({ colId, type: m.type, label: m.label, isFormula: m.isFormula, formula: m.formula, description: m.description }));
631
+ }),
632
+ table: tableOps(() => {
633
+ if (!grist.selectedTable) throw new Error("grist.selectedTable is not available in this Grist version");
634
+ return grist.selectedTable;
635
+ }),
636
+ getTable: (tableId) => {
637
+ let ops = tables.get(tableId);
638
+ if (!ops) {
639
+ tables.set(
640
+ tableId,
641
+ ops = tableOps(() => {
642
+ if (typeof grist.getTable !== "function") throw new Error("grist.getTable is not available in this Grist version");
643
+ return grist.getTable(tableId);
644
+ })
645
+ );
646
+ }
647
+ return ops;
648
+ },
649
+ applyActions: (actions, options) => write(() => docApi().applyUserActions(actions, options)),
650
+ setWidgetOptions: async (options) => {
651
+ await grist.setOptions(options);
652
+ },
653
+ patchWidgetOptions: async (patch) => {
654
+ const current = await grist.getOptions() ?? {};
655
+ await grist.setOptions({ ...current, ...patch });
656
+ },
657
+ setCursorPosition: async (pos) => {
658
+ if (!grist.setCursorPos) throw new Error("setCursorPos is not available in this Grist version");
659
+ await grist.setCursorPos(pos);
660
+ },
661
+ setLinkedRowSelection: async (rowIds) => {
662
+ if (!grist.setSelectedRows) throw new Error("setSelectedRows is not available in this Grist version");
663
+ await grist.setSelectedRows(rowIds);
664
+ },
665
+ getAttachmentUrl: (id, options) => read(() => getAttachmentDownloadUrl(docApi(), id, options)),
666
+ fetchAttachmentBlob: (id, options) => read(() => fetchAttachmentBlob(docApi(), id, options)),
667
+ uploadAttachment: (file, options) => write(() => uploadGristAttachment(docApi(), file, options)),
668
+ fetchWithAuth: (path, init) => read(() => fetchWithGristAuth(docApi(), path, init))
669
+ };
670
+ }
671
+
672
+ // src/sdk/helpers/grist-mappings.ts
673
+ function asColumnSpec(entry) {
674
+ return typeof entry === "string" ? { name: entry } : entry;
675
+ }
676
+ function validateColumnMappings(spec, mappings) {
677
+ const missing = [];
678
+ const emptyMultiples = [];
679
+ if (!spec) return { ok: true, pending: false, missing, emptyMultiples };
680
+ const m = mappings ?? {};
681
+ for (const raw of spec) {
682
+ const col = asColumnSpec(raw);
683
+ if (col.optional) continue;
684
+ const value = m[col.name];
685
+ if (col.allowMultiple) {
686
+ if (value === void 0) {
687
+ missing.push(col.name);
688
+ } else if (!Array.isArray(value) || value.length === 0) {
689
+ emptyMultiples.push(col.name);
690
+ }
691
+ } else {
692
+ if (value === void 0 || value === null || value === "") missing.push(col.name);
693
+ }
694
+ }
695
+ return {
696
+ ok: missing.length === 0 && emptyMultiples.length === 0,
697
+ pending: false,
698
+ missing,
699
+ emptyMultiples
700
+ };
701
+ }
702
+
703
+ // src/sdk/runtime/derive.ts
704
+ var ACCESS_RANK = { none: 0, "read table": 1, full: 2 };
705
+ function parseAccess(raw) {
706
+ return raw === "none" || raw === "read table" || raw === "full" ? raw : null;
707
+ }
708
+ function accessLabel(level) {
709
+ return level === "full" ? "Full document access" : level === "read table" ? "Read selected table" : "No document access";
710
+ }
711
+ function accessDenied(state) {
712
+ return state.grantedAccess !== null && ACCESS_RANK[state.grantedAccess] < ACCESS_RANK[state.declaredAccess];
713
+ }
714
+ function deriveStatus(state) {
715
+ switch (state.connection) {
716
+ case "connecting":
717
+ return "booting";
718
+ case "not-embedded":
719
+ case "unavailable":
720
+ return "unavailable";
721
+ case "error":
722
+ return "error";
723
+ case "ready":
724
+ return accessDenied(state) ? "error" : "ready";
725
+ }
726
+ }
727
+ var MAPPINGS_OK = Object.freeze({ ok: true, pending: false, missing: [], emptyMultiples: [] });
728
+ var MAPPINGS_PENDING = Object.freeze({ ok: true, pending: true, missing: [], emptyMultiples: [] });
729
+ function deriveColumnMappingStatus(columns, mappings) {
730
+ if (!columns?.length) return MAPPINGS_OK;
731
+ if (mappings === null) return MAPPINGS_PENDING;
732
+ return validateColumnMappings(columns, mappings);
733
+ }
734
+
735
+ // src/sdk/runtime/cells.ts
736
+ var ok = (value) => ({ ok: true, value });
737
+ var bad = (message2) => ({ ok: false, message: message2 });
738
+ function formulaError(value) {
739
+ if (Array.isArray(value) && value[0] === "E") return String(value[1] ?? "error");
740
+ if (value && typeof value === "object" && value.constructor?.name === "RaisedException") {
741
+ return String(value.name ?? "error");
742
+ }
743
+ return null;
744
+ }
745
+ function rowIdOf(value) {
746
+ if (typeof value === "number" && Number.isInteger(value)) return value === 0 ? null : value;
747
+ if (value && typeof value === "object" && typeof value.rowId === "number") {
748
+ const id = value.rowId;
749
+ return id === 0 ? null : id;
750
+ }
751
+ if (Array.isArray(value) && value[0] === "R" && typeof value[2] === "number") return value[2] === 0 ? null : value[2];
752
+ return void 0;
753
+ }
754
+ function idsOf(value) {
755
+ if (value === null || value === void 0) return [];
756
+ const list = Array.isArray(value) ? value[0] === "L" ? value.slice(1) : value : value && typeof value === "object" && Array.isArray(value.rowIds) ? value.rowIds : void 0;
757
+ return list?.every((v) => typeof v === "number") ? list : void 0;
758
+ }
759
+ function checkOne(base, value) {
760
+ switch (base) {
761
+ case "Any":
762
+ return ok(value);
763
+ case "Text":
764
+ return value === null || typeof value === "string" ? ok(value ?? "") : bad("not text");
765
+ case "Numeric":
766
+ return value === null || typeof value === "number" && Number.isFinite(value) ? ok(value) : bad("not a number");
767
+ case "Int":
768
+ return value === null || typeof value === "number" && Number.isInteger(value) ? ok(value) : bad("not an integer");
769
+ case "Bool":
770
+ return value === null || typeof value === "boolean" ? ok(value ?? false) : bad("not true or false");
771
+ case "Date":
772
+ case "DateTime":
773
+ if (value === null) return ok(null);
774
+ if (value instanceof Date && !Number.isNaN(value.getTime())) return ok(value);
775
+ if (typeof value === "number" && Number.isFinite(value)) return ok(new Date(value * 1e3));
776
+ return bad("not a date");
777
+ case "Choice":
778
+ return value === null || typeof value === "string" ? ok(value) : bad("not a choice");
779
+ case "ChoiceList": {
780
+ if (value === null || value === void 0) return ok([]);
781
+ const list = Array.isArray(value) ? value[0] === "L" ? value.slice(1) : value : void 0;
782
+ return list?.every((v) => typeof v === "string") ? ok(list) : bad("not a list of choices");
783
+ }
784
+ case "Ref": {
785
+ if (value === null) return ok(null);
786
+ const id = rowIdOf(value);
787
+ return id === void 0 ? bad("not a reference") : ok(id);
788
+ }
789
+ case "RefList":
790
+ case "Attachments": {
791
+ const ids = idsOf(value);
792
+ return ids === void 0 ? bad(base === "RefList" ? "not a list of references" : "not a list of attachments") : ok(ids);
793
+ }
794
+ default:
795
+ return ok(value);
796
+ }
797
+ }
798
+ function checkGristCell(type, value) {
799
+ const error = formulaError(value);
800
+ if (error) return bad(`formula error (${error})`);
801
+ const bases = (type ?? "Any").split(",").map((t) => t.trim().split(":")[0]).filter(Boolean);
802
+ if (bases.length === 0 || bases.includes("Any")) return ok(value === void 0 ? null : value);
803
+ let first = null;
804
+ for (const base of bases) {
805
+ const check = checkOne(base, value);
806
+ if (check.ok) return check;
807
+ first ?? (first = check);
808
+ }
809
+ return first;
810
+ }
811
+ var nameOf = (c) => typeof c === "string" ? c : c.name;
812
+ var typeOf = (c) => typeof c === "string" ? void 0 : c.type;
813
+ var isOptional = (c) => typeof c === "object" && c.optional === true;
814
+ function mapAndCheck(record, columns, mappings, schema) {
815
+ if (!record) return { value: null, issues: [] };
816
+ if (!columns?.length) return { value: record, issues: [] };
817
+ if (mappings === null) return { value: null, issues: [] };
818
+ const rowId = typeof record.id === "number" ? record.id : 0;
819
+ const out = { id: record.id };
820
+ const issues = [];
821
+ for (const col of columns) {
822
+ const name = nameOf(col);
823
+ const real = mappings[name];
824
+ const take = (raw) => {
825
+ const check = checkGristCell(typeOf(col), raw);
826
+ if (check.ok) return check.value;
827
+ issues.push({ rowId, column: name, message: check.message, value: raw });
828
+ return null;
829
+ };
830
+ if (Array.isArray(real) && real.length > 0) out[name] = real.map((r) => take(record[r]));
831
+ else if (typeof real === "string" && real) out[name] = take(record[real]);
832
+ else if (!isOptional(col)) return { value: null, issues: [] };
833
+ }
834
+ if (schema) {
835
+ const result = schema["~standard"].validate(out);
836
+ if (result instanceof Promise) {
837
+ issues.push({ rowId, column: "", message: "the schema validates asynchronously; useGrist() needs a synchronous one", value: out });
838
+ } else if ("issues" in result && result.issues) {
839
+ for (const issue of result.issues) {
840
+ const key = issue.path?.[0];
841
+ const column = key === void 0 ? "" : String(typeof key === "object" ? key.key : key);
842
+ issues.push({ rowId, column, message: issue.message, value: column ? out[column] : out });
843
+ }
844
+ } else if ("value" in result) {
845
+ return { value: result.value, issues };
846
+ }
847
+ }
848
+ return { value: out, issues };
849
+ }
40
850
 
41
851
  // src/sdk/helpers/grist-widget-interaction.ts
42
852
  function parseGristWidgetLinkingInfo(raw) {
@@ -59,152 +869,363 @@ function normalizeWidgetInteraction(interaction) {
59
869
  };
60
870
  }
61
871
 
62
- // src/sdk/internal/subscriptions.ts
63
- var GristEvent = class {
64
- constructor(bindOnce) {
65
- this.bindOnce = bindOnce;
872
+ // src/sdk/runtime/host.ts
873
+ var HostStream = class {
874
+ constructor(bind) {
875
+ this.bind = bind;
66
876
  this.listeners = /* @__PURE__ */ new Set();
67
- this.hasValue = false;
68
- this.bind = null;
877
+ this.bound = false;
878
+ this.has = false;
69
879
  }
70
880
  subscribe(listener) {
71
881
  this.listeners.add(listener);
72
- if (!this.bind) {
73
- this.bind = () => {
74
- this.bind = () => {
75
- };
76
- try {
77
- this.bindOnce((value) => {
78
- this.hasValue = true;
79
- this.last = value;
80
- for (const l of this.listeners) l(value);
81
- });
82
- } catch (err) {
83
- if (typeof console !== "undefined") {
84
- console.error("[lib/grist] failed to subscribe to Grist event:", err);
85
- }
86
- }
87
- };
88
- this.bind();
89
- }
90
- if (this.hasValue && this.last !== void 0) {
91
- listener(this.last);
882
+ if (!this.bound) {
883
+ this.bound = true;
884
+ try {
885
+ this.bind((value) => this.emit(value));
886
+ } catch (err) {
887
+ console.error("[grist-widget-sdk] could not subscribe to a Grist event:", err);
888
+ }
92
889
  }
890
+ if (this.has) listener(this.last);
93
891
  return () => {
94
892
  this.listeners.delete(listener);
95
893
  };
96
894
  }
97
- /** Last cached value (or `undefined` if no event has fired yet). */
98
895
  peek() {
99
896
  return this.last;
100
897
  }
101
- /** True once at least one event has fired. */
102
- get ready() {
103
- return this.hasValue;
104
- }
105
- /**
106
- * Test helper: fan out a payload without calling `grist.on*`.
107
- * Used to simulate hosts that reuse one record object and mutate it in place.
108
- */
109
- deliverForTests(value) {
110
- this.hasValue = true;
898
+ /** Tests: deliver a value as if Grist had sent it. */
899
+ emit(value) {
900
+ this.has = true;
111
901
  this.last = value;
112
- for (const l of this.listeners) l(value);
902
+ for (const l of [...this.listeners]) l(value);
113
903
  }
114
- /**
115
- * Test helper: forget the bound state and cached value. Lets a test swap
116
- * `window.grist` (e.g. via the emulator's `dispose()` + `createGristEmulator()`)
117
- * and re-bind cleanly on the next subscribe.
118
- */
119
- _resetForTests() {
904
+ /** Tests: forget the binding, so a new `window.grist` binds afresh. */
905
+ reset() {
120
906
  this.listeners.clear();
121
- this.hasValue = false;
907
+ this.bound = false;
908
+ this.has = false;
122
909
  this.last = void 0;
123
- this.bind = null;
124
910
  }
125
911
  };
126
- var recordEvent = new GristEvent((emit) => {
127
- const deliver = (record, mappings) => {
128
- emit({ record, mappings });
129
- };
130
- grist.onRecord(
131
- (record, mappings) => {
132
- deliver(record, mappings ?? null);
133
- },
134
- { keepEncoded: false }
135
- );
912
+ var asMappings = (m) => m && typeof m === "object" ? m : null;
913
+ var recordStream = new HostStream((emit) => {
914
+ grist.onRecord((record, mappings) => emit({ record, mappings: asMappings(mappings) }), { keepEncoded: false });
136
915
  const fetchSelectedRecord = grist.fetchSelectedRecord ?? grist.docApi?.fetchSelectedRecord;
137
- if (typeof grist.on !== "function" || typeof fetchSelectedRecord !== "function") {
138
- return;
139
- }
916
+ if (typeof grist.on !== "function" || typeof fetchSelectedRecord !== "function") return;
140
917
  let lastRowId;
141
918
  grist.on("message", async (msg) => {
142
- if (!msg || typeof msg !== "object") return;
143
919
  const host = msg;
144
- if (!host.tableId || typeof host.rowId !== "number") return;
145
- if (host.rowId === lastRowId) return;
920
+ if (!host?.tableId || typeof host.rowId !== "number" || host.rowId === lastRowId) return;
146
921
  lastRowId = host.rowId;
147
922
  try {
148
923
  const record = await fetchSelectedRecord(host.rowId, { keepEncoded: false });
149
- deliver(record, recordEvent.peek()?.mappings ?? null);
924
+ emit({ record, mappings: recordStream.peek()?.mappings ?? null });
150
925
  } catch (err) {
151
- if (typeof console !== "undefined") {
152
- console.error("[grist-widget-sdk] fetchSelectedRecord on cursor message failed:", err);
153
- }
926
+ console.error("[grist-widget-sdk] fetchSelectedRecord on a cursor move failed:", err);
154
927
  }
155
928
  });
156
929
  });
157
- var recordsEvent = new GristEvent((emit) => {
158
- grist.onRecords(
159
- (records, mappings) => {
160
- emit({ records, mappings: mappings ?? null });
161
- },
162
- { keepEncoded: false, format: "rows" }
163
- );
930
+ var recordsStream = new HostStream((emit) => {
931
+ grist.onRecords((records, mappings) => emit({ records, mappings: asMappings(mappings) }), {
932
+ keepEncoded: false,
933
+ format: "rows"
934
+ });
164
935
  });
165
- var optionsEvent = new GristEvent((emit) => {
166
- grist.onOptions((options, interaction) => {
167
- const normalized = normalizeWidgetInteraction(
168
- interaction ?? null
169
- );
170
- emit({
936
+ var optionsStream = new HostStream((emit) => {
937
+ grist.onOptions(
938
+ (options, interaction) => emit({
171
939
  options: options ? { ...options } : null,
172
- interaction: normalized
173
- });
174
- });
940
+ interaction: normalizeWidgetInteraction(interaction ?? null)
941
+ })
942
+ );
175
943
  });
176
- var newRecordEvent = new GristEvent((emit) => {
944
+ var newRecordStream = new HostStream((emit) => {
177
945
  if (typeof grist.onNewRecord !== "function") return;
178
- grist.onNewRecord((mappings) => {
179
- emit({ mappings: mappings ?? null });
180
- });
946
+ grist.onNewRecord((mappings) => emit({ mappings: asMappings(mappings) }));
181
947
  });
182
- function resetGristEventsForTests() {
183
- recordEvent._resetForTests();
184
- recordsEvent._resetForTests();
185
- optionsEvent._resetForTests();
186
- newRecordEvent._resetForTests();
187
- }
188
-
189
- // src/sdk/internal/embedded-handshake-manager.ts
190
- var embedded = null;
191
- function resetEmbeddedHandshakeManagerForTests() {
192
- if (embedded) {
193
- embedded.manager.stop();
194
- embedded = null;
195
- }
196
- }
948
+ var messageStream = new HostStream((emit) => {
949
+ if (typeof grist.on !== "function") return;
950
+ grist.on("message", (msg) => emit(msg));
951
+ });
952
+ var ALL_STREAMS = [recordStream, recordsStream, optionsStream, newRecordStream, messageStream];
197
953
 
198
- // src/sdk/internal/ready-singleton.ts
199
- var inFlight = null;
954
+ // src/sdk/runtime/ready.ts
955
+ var ACCESS_RANK2 = {
956
+ none: 0,
957
+ "read table": 1,
958
+ full: 2
959
+ };
960
+ var inFlight2 = null;
200
961
  var lastAccess = null;
201
962
  var lastColumnNames = null;
202
- function resetGristReadySingleton() {
203
- inFlight = null;
963
+ function columnNamesOf(columns) {
964
+ return new Set((columns ?? []).map((c) => typeof c === "string" ? c : c.name));
965
+ }
966
+ function shouldReinit(next, nextColumns) {
967
+ if (!lastAccess) return true;
968
+ if (ACCESS_RANK2[next] > ACCESS_RANK2[lastAccess]) return true;
969
+ for (const name of nextColumns) {
970
+ if (!lastColumnNames?.has(name)) return true;
971
+ }
972
+ return false;
973
+ }
974
+ function ensureGristReady(payload) {
975
+ const access = payload.requiredAccess ?? "read table";
976
+ const columns = columnNamesOf(payload.columns);
977
+ if (inFlight2 && !shouldReinit(access, columns)) return inFlight2;
978
+ lastAccess = access;
979
+ lastColumnNames = columns;
980
+ const next = Promise.resolve().then(() => {
981
+ const result = grist.ready(payload);
982
+ return result instanceof Promise ? result : Promise.resolve();
983
+ });
984
+ inFlight2 = next.catch((err) => {
985
+ inFlight2 = null;
986
+ lastAccess = null;
987
+ lastColumnNames = null;
988
+ throw err;
989
+ });
990
+ return inFlight2;
991
+ }
992
+ function resetGristReady() {
993
+ inFlight2 = null;
204
994
  lastAccess = null;
205
995
  lastColumnNames = null;
206
996
  }
207
- var resetGristReadyCacheForTests = resetGristReadySingleton;
997
+
998
+ // src/sdk/helpers/grist-theme-payload.ts
999
+ function parseGristThemePayload(payload) {
1000
+ if (!payload || typeof payload !== "object") return null;
1001
+ const raw = payload;
1002
+ const top = raw.theme;
1003
+ if (top === "light" || top === "dark") return top;
1004
+ if (top && typeof top === "object") {
1005
+ const appearance = top.appearance;
1006
+ if (appearance === "light" || appearance === "dark") return appearance;
1007
+ }
1008
+ return null;
1009
+ }
1010
+
1011
+ // src/sdk/runtime/runtime.ts
1012
+ var READY_TIMEOUT_MS = 3e4;
1013
+ function initialRuntimeState(options = {}) {
1014
+ return {
1015
+ connection: "connecting",
1016
+ connectionError: null,
1017
+ declaredAccess: options.requiredAccess ?? "read table",
1018
+ grantedAccess: null,
1019
+ record: null,
1020
+ records: null,
1021
+ isNewRecord: false,
1022
+ mappings: null,
1023
+ widgetOptions: null,
1024
+ interaction: null,
1025
+ currentTableId: null,
1026
+ theme: null,
1027
+ actionStatus: "idle",
1028
+ actionError: null,
1029
+ readError: null,
1030
+ mapBackSkipped: []
1031
+ };
1032
+ }
1033
+ function isEmbedded() {
1034
+ try {
1035
+ return typeof window !== "undefined" && window.self !== window.top;
1036
+ } catch {
1037
+ return true;
1038
+ }
1039
+ }
1040
+ var hasGrist = () => typeof grist !== "undefined" && grist !== null;
1041
+ function createGristRuntime(options = {}) {
1042
+ let state = initialRuntimeState(options);
1043
+ const listeners = /* @__PURE__ */ new Set();
1044
+ let starts = 0;
1045
+ let stopStreams = null;
1046
+ let attempt = 0;
1047
+ const update = (patch) => {
1048
+ let changed = false;
1049
+ for (const key of Object.keys(patch)) {
1050
+ if (!Object.is(state[key], patch[key])) changed = true;
1051
+ }
1052
+ if (!changed) return;
1053
+ state = { ...state, ...patch };
1054
+ for (const l of [...listeners]) l();
1055
+ };
1056
+ const payload = {
1057
+ requiredAccess: options.requiredAccess ?? "read table",
1058
+ columns: options.columns ?? [],
1059
+ ...options.allowSelectBy !== void 0 ? { allowSelectBy: options.allowSelectBy } : {},
1060
+ ...options.hasCustomOptions !== void 0 ? { hasCustomOptions: options.hasCustomOptions } : {},
1061
+ ...options.onEditOptions ? { onEditOptions: options.onEditOptions } : {}
1062
+ };
1063
+ const refreshTable = async () => {
1064
+ try {
1065
+ const tableId = await grist.selectedTable?.getTableId();
1066
+ if (tableId) update({ currentTableId: tableId });
1067
+ } catch {
1068
+ }
1069
+ };
1070
+ const refreshMappings = async () => {
1071
+ if (typeof grist.sectionApi?.mappings !== "function") return;
1072
+ try {
1073
+ const mappings = await grist.sectionApi.mappings();
1074
+ if (state.mappings === null) update({ mappings: mappings ?? {} });
1075
+ } catch {
1076
+ }
1077
+ };
1078
+ const connect = async () => {
1079
+ const mine = ++attempt;
1080
+ update({ connection: "connecting", connectionError: null });
1081
+ let timer;
1082
+ try {
1083
+ await Promise.race([
1084
+ ensureGristReady(payload),
1085
+ new Promise((_, reject) => {
1086
+ timer = setTimeout(() => reject(new Error(`Grist did not answer grist.ready() within ${READY_TIMEOUT_MS / 1e3} s.`)), READY_TIMEOUT_MS);
1087
+ })
1088
+ ]);
1089
+ } catch (err) {
1090
+ if (mine === attempt) update({ connection: "error", connectionError: err instanceof Error ? err.message : String(err) });
1091
+ return;
1092
+ } finally {
1093
+ clearTimeout(timer);
1094
+ }
1095
+ if (mine !== attempt) return;
1096
+ update({ connection: "ready" });
1097
+ await Promise.all([refreshMappings(), refreshTable()]);
1098
+ };
1099
+ const listen = () => {
1100
+ const offs = [
1101
+ // Copies: a host may reuse one record object and mutate it in place.
1102
+ recordStream.subscribe(
1103
+ ({ record, mappings }) => update({
1104
+ record: record ? { ...record } : null,
1105
+ isNewRecord: false,
1106
+ ...mappings ? { mappings } : {}
1107
+ })
1108
+ ),
1109
+ recordsStream.subscribe(({ records, mappings }) => {
1110
+ update({ records: records.map((r) => ({ ...r })), ...mappings ? { mappings } : {} });
1111
+ void refreshTable();
1112
+ }),
1113
+ optionsStream.subscribe(
1114
+ ({ options: widgetOptions, interaction }) => update({
1115
+ widgetOptions,
1116
+ interaction,
1117
+ grantedAccess: parseAccess(interaction?.access_level) ?? state.grantedAccess
1118
+ })
1119
+ ),
1120
+ newRecordStream.subscribe(({ mappings }) => update({ isNewRecord: true, ...mappings ? { mappings } : {} })),
1121
+ messageStream.subscribe((msg) => {
1122
+ const theme = parseGristThemePayload(msg);
1123
+ if (theme) update({ theme });
1124
+ })
1125
+ ];
1126
+ return () => offs.forEach((off) => off());
1127
+ };
1128
+ const begin = () => {
1129
+ if (!isEmbedded()) return update({ connection: "not-embedded" });
1130
+ if (!hasGrist()) {
1131
+ if (typeof document !== "undefined" && document.readyState !== "complete" && typeof window !== "undefined") {
1132
+ window.addEventListener("load", () => hasGrist() ? begin() : update({ connection: "unavailable" }), { once: true });
1133
+ return;
1134
+ }
1135
+ return update({ connection: "unavailable" });
1136
+ }
1137
+ stopStreams ?? (stopStreams = listen());
1138
+ void connect();
1139
+ };
1140
+ return {
1141
+ options,
1142
+ getState: () => state,
1143
+ subscribe(listener) {
1144
+ listeners.add(listener);
1145
+ return () => listeners.delete(listener);
1146
+ },
1147
+ start() {
1148
+ if (starts++ === 0) begin();
1149
+ return () => {
1150
+ if (--starts > 0) return;
1151
+ stopStreams?.();
1152
+ stopStreams = null;
1153
+ };
1154
+ },
1155
+ async reload() {
1156
+ if (!hasGrist() || !isEmbedded()) return begin();
1157
+ resetGristReady();
1158
+ update({ mappings: null });
1159
+ stopStreams ?? (stopStreams = listen());
1160
+ await connect();
1161
+ },
1162
+ update
1163
+ };
1164
+ }
1165
+
1166
+ // src/sdk/use-grist.ts
1167
+ var GristContext = (0, import_react.createContext)(null);
1168
+ GristContext.displayName = "GristContext";
1169
+ function useGristRuntime() {
1170
+ return (0, import_react.useContext)(GristContext);
1171
+ }
1172
+ function newConnection(options) {
1173
+ const runtime = createGristRuntime(options);
1174
+ return { runtime, api: createGristApi(runtime) };
1175
+ }
1176
+ var pageConnection = null;
1177
+ function resetGristForTests() {
1178
+ pageConnection = null;
1179
+ resetGristReady();
1180
+ for (const s of ALL_STREAMS) s.reset();
1181
+ }
1182
+ var GristContextProvider = GristContext.Provider;
1183
+ var serverState = () => initialRuntimeState();
1184
+ function useGristState(connection) {
1185
+ const { runtime } = connection;
1186
+ (0, import_react.useEffect)(() => runtime.start(), [runtime]);
1187
+ return (0, import_react.useSyncExternalStore)(runtime.subscribe, runtime.getState, serverState);
1188
+ }
1189
+ var NO_ISSUES = Object.freeze([]);
1190
+ function useMapped(state, options) {
1191
+ const { record, records, mappings } = state;
1192
+ const { columns, schema } = options;
1193
+ return (0, import_react.useMemo)(() => {
1194
+ const issues = [];
1195
+ const rows = records && (columns?.length ? mappings !== null : true) ? records.map((r) => {
1196
+ const out = mapAndCheck(r, columns, mappings, schema);
1197
+ issues.push(...out.issues);
1198
+ return out.value;
1199
+ }) : null;
1200
+ const selected = mapAndCheck(record, columns, mappings, schema);
1201
+ if (record && !records?.some((r) => r.id === record.id)) issues.push(...selected.issues);
1202
+ const mappedRecords = rows && rows.every((r) => r !== null) ? rows : null;
1203
+ return { record: selected.value, records: mappedRecords, issues: issues.length ? issues : NO_ISSUES };
1204
+ }, [record, records, mappings, columns, schema]);
1205
+ }
1206
+ var EMPTY_MAPPINGS = Object.freeze({});
1207
+ var DETACHED = (() => {
1208
+ const state = initialRuntimeState();
1209
+ const runtime = {
1210
+ options: {},
1211
+ getState: () => state,
1212
+ subscribe: () => () => {
1213
+ },
1214
+ start: () => () => {
1215
+ },
1216
+ reload: async () => {
1217
+ },
1218
+ update: () => {
1219
+ }
1220
+ };
1221
+ return { runtime, api: createGristApi(runtime) };
1222
+ })();
1223
+ function useNearestGristState() {
1224
+ const connection = (0, import_react.useContext)(GristContext);
1225
+ const state = useGristState(connection ?? DETACHED);
1226
+ const { issues } = useMapped(state, (connection ?? DETACHED).runtime.options);
1227
+ return { connection, state, cellIssues: issues };
1228
+ }
208
1229
 
209
1230
  // src/emulator/api/command-api.ts
210
1231
  function createCommandApiImpl(bus) {
@@ -252,36 +1273,7 @@ function createGristApiImpl() {
252
1273
  };
253
1274
  }
254
1275
 
255
- // src/sdk/helpers/grist-value-codec.ts
256
- function coerceGristDocApiRefRowId(value) {
257
- if (typeof value === "number" && Number.isFinite(value)) return value;
258
- if (!Array.isArray(value) || value.length < 2) return void 0;
259
- const tag = value[0];
260
- if (tag === "R" && typeof value[2] === "number" && Number.isFinite(value[2])) return value[2];
261
- if (tag === "r" && typeof value[1] === "number" && Number.isFinite(value[1])) return value[1];
262
- return void 0;
263
- }
264
-
265
- // src/sdk/helpers/grist-safe-parse.ts
266
- var import_zod = require("zod");
267
- var gristSafeRefValueSchema = import_zod.z.object({
268
- rowId: import_zod.z.number(),
269
- display: import_zod.z.string().nullable().optional()
270
- });
271
- var gristSafeRefListValueSchema = import_zod.z.object({
272
- rowIds: import_zod.z.array(import_zod.z.number()),
273
- displays: import_zod.z.array(import_zod.z.string()).optional()
274
- });
275
- var gristCellValueSchema = import_zod.z.union([
276
- import_zod.z.null(),
277
- import_zod.z.string(),
278
- import_zod.z.number(),
279
- import_zod.z.boolean(),
280
- import_zod.z.date(),
281
- import_zod.z.array(import_zod.z.unknown()),
282
- gristSafeRefValueSchema,
283
- gristSafeRefListValueSchema
284
- ]);
1276
+ // src/emulator/core/display-helpers.ts
285
1277
  function inferDisplayHelpersByRef(columns) {
286
1278
  const out = {};
287
1279
  const helperColumns = Object.entries(columns).filter(
@@ -482,7 +1474,7 @@ function evaluateRowFormulas(state, table, row) {
482
1474
  try {
483
1475
  row[colId] = col.formulaFn(row, { table, document: state });
484
1476
  } catch (err) {
485
- row[colId] = formulaError(err);
1477
+ row[colId] = formulaError2(err);
486
1478
  }
487
1479
  }
488
1480
  }
@@ -569,9 +1561,9 @@ function syncRefListDisplayHelpers(state, table, row) {
569
1561
  row[helperColId] = labels;
570
1562
  }
571
1563
  }
572
- function formulaError(err) {
573
- const message = err instanceof Error ? err.message : String(err);
574
- return ["E", "Error", message];
1564
+ function formulaError2(err) {
1565
+ const message2 = err instanceof Error ? err.message : String(err);
1566
+ return ["E", "Error", message2];
575
1567
  }
576
1568
  function synthesizeTablesMeta(state) {
577
1569
  const ids = [];
@@ -653,8 +1645,8 @@ function synthesizeTablesColumnMeta(state) {
653
1645
 
654
1646
  // src/emulator/core/action-engine.ts
655
1647
  var EmulatorActionError = class extends Error {
656
- constructor(action, message) {
657
- super(message);
1648
+ constructor(action, message2) {
1649
+ super(message2);
658
1650
  this.action = action;
659
1651
  this.name = "EmulatorActionError";
660
1652
  }
@@ -955,8 +1947,8 @@ function normalizeRefListValue(value) {
955
1947
  return ["L", ...toIdList(value)];
956
1948
  }
957
1949
  if (typeof value === "string") {
958
- const tokens = value.split(",").map((t) => t.trim()).filter(Boolean);
959
- return ["L", ...toIdList(tokens)];
1950
+ const tokens2 = value.split(",").map((t) => t.trim()).filter(Boolean);
1951
+ return ["L", ...toIdList(tokens2)];
960
1952
  }
961
1953
  if (value && typeof value === "object" && Array.isArray(value.rowIds)) {
962
1954
  return ["L", ...toIdList(value.rowIds)];
@@ -1094,8 +2086,9 @@ function createWidgetApiImpl(state, bus) {
1094
2086
  async getOptions() {
1095
2087
  return Object.keys(state.widgetOptions).length > 0 ? { ...state.widgetOptions } : null;
1096
2088
  },
2089
+ // Replaces every option, as Grist's WidgetAPI.setOptions does.
1097
2090
  async setOptions(options) {
1098
- state.widgetOptions = { ...state.widgetOptions, ...options };
2091
+ state.widgetOptions = { ...options };
1099
2092
  emit();
1100
2093
  },
1101
2094
  async clearOptions() {
@@ -1161,157 +2154,6 @@ function buildGristThemeHostMessage(appearance, options) {
1161
2154
  };
1162
2155
  }
1163
2156
 
1164
- // src/emulator/transports/iframe.ts
1165
- var import_grain_rpc = require("grain-rpc");
1166
- function createIframeTransport(options) {
1167
- const { iframe, state, bus, api, targetOrigin = "*" } = options;
1168
- const timers = /* @__PURE__ */ new Set();
1169
- const rpc = new import_grain_rpc.Rpc({
1170
- sendMessage: (msg) => {
1171
- const target = iframe.contentWindow;
1172
- if (!target) return;
1173
- target.postMessage(msg, targetOrigin);
1174
- },
1175
- logger: { info: () => {
1176
- }, warn: () => {
1177
- } }
1178
- });
1179
- rpc.queueOutgoingUntilReadyMessage();
1180
- const receiveFromWidget = rpc.receiveMessage.bind(rpc);
1181
- rpc.receiveMessage = (msg) => {
1182
- if (msg && typeof msg === "object" && msg.mtype === import_grain_rpc.MsgType.RpcCall && msg.mdest === "grist") {
1183
- const copy = { ...msg };
1184
- delete copy.mdest;
1185
- receiveFromWidget(copy);
1186
- return;
1187
- }
1188
- receiveFromWidget(msg);
1189
- };
1190
- const onMessage = (event) => {
1191
- if (event.source !== iframe.contentWindow) return;
1192
- if (!isRpcEnvelope(event.data)) return;
1193
- rpc.receiveMessage(event.data);
1194
- };
1195
- window.addEventListener("message", onMessage);
1196
- rpc.registerImpl("GristView", api.GristView);
1197
- rpc.registerImpl("GristDocAPI", api.GristDocAPI);
1198
- rpc.registerImpl("WidgetAPI", api.WidgetAPI);
1199
- rpc.registerImpl("CustomSectionAPI", api.CustomSectionAPI);
1200
- rpc.registerImpl("_grist_api", api.GristAPI);
1201
- rpc.registerImpl("CommandAPI", api.CommandAPI);
1202
- const buildInteraction = options.buildInteraction ?? defaultInteraction(state);
1203
- const cursorOf = options.cursor ?? (() => state.selection.cursor);
1204
- const initialCursorOf = options.initialCursor ?? (() => state.selection.rowId);
1205
- function fireDataChange(tableId, opts) {
1206
- safePostMessage({
1207
- tableId: tableId ?? state.selection.tableId,
1208
- dataChange: true,
1209
- ...opts?.mappingsChange ? { mappingsChange: true } : {}
1210
- });
1211
- }
1212
- function fireCursor(rowId, opts) {
1213
- const id = rowId === void 0 ? initialCursorOf() : rowId;
1214
- if (id === null) return;
1215
- safePostMessage({
1216
- tableId: state.selection.tableId,
1217
- rowId: id,
1218
- cursor: cursorOf(),
1219
- ...opts?.mappingsChange ? { mappingsChange: true } : {}
1220
- });
1221
- }
1222
- function fireOptions() {
1223
- safePostMessage({
1224
- options: hasOptions(state.widgetOptions) ? { ...state.widgetOptions } : null,
1225
- settings: { ...buildInteraction() }
1226
- });
1227
- }
1228
- function fireTheme(message) {
1229
- safePostMessage(message);
1230
- }
1231
- function emitInitialState(opts) {
1232
- fireOptions();
1233
- if (options.initialTheme) {
1234
- fireTheme(buildGristThemeHostMessage(options.initialTheme, { fromReady: true }));
1235
- }
1236
- fireDataChange(void 0, opts);
1237
- fireCursor(void 0, opts);
1238
- }
1239
- function emitInitialStateWithReplay(opts) {
1240
- emitInitialState(opts);
1241
- queueMicrotask(() => emitInitialState(opts));
1242
- const timer = setTimeout(() => {
1243
- timers.delete(timer);
1244
- emitInitialState(opts);
1245
- }, 0);
1246
- timers.add(timer);
1247
- }
1248
- const offBus = bus.on((event) => {
1249
- switch (event.type) {
1250
- case "ready":
1251
- emitInitialStateWithReplay();
1252
- break;
1253
- case "data-change":
1254
- fireDataChange(event.tableId);
1255
- break;
1256
- case "cursor-change": {
1257
- const rowId = event.rowId === null ? state.selection.rowId : event.rowId;
1258
- if (rowId !== null) {
1259
- safePostMessage({
1260
- tableId: event.tableId,
1261
- rowId,
1262
- cursor: cursorOf()
1263
- });
1264
- }
1265
- fireDataChange(event.tableId);
1266
- break;
1267
- }
1268
- case "options-change":
1269
- fireOptions();
1270
- break;
1271
- case "mappings-change":
1272
- emitInitialStateWithReplay({ mappingsChange: true });
1273
- break;
1274
- case "theme-change":
1275
- fireTheme(event.message);
1276
- break;
1277
- }
1278
- });
1279
- rpc.processIncoming();
1280
- function safePostMessage(payload) {
1281
- try {
1282
- rpc.postMessage(payload);
1283
- } catch (err) {
1284
- console.warn("[grist-emulator/iframe] failed to deliver postMessage", err);
1285
- }
1286
- }
1287
- return {
1288
- dispose() {
1289
- window.removeEventListener("message", onMessage);
1290
- offBus();
1291
- for (const timer of timers) clearTimeout(timer);
1292
- timers.clear();
1293
- },
1294
- fireDataChange,
1295
- fireCursor,
1296
- fireOptions,
1297
- fireTheme,
1298
- emitInitialState
1299
- };
1300
- }
1301
- function hasOptions(options) {
1302
- return Object.keys(options).length > 0;
1303
- }
1304
- function isRpcEnvelope(data) {
1305
- if (!data || typeof data !== "object") return false;
1306
- return typeof data.mtype === "number";
1307
- }
1308
- function defaultInteraction(state) {
1309
- return () => ({
1310
- access_level: state.configuration?.requiredAccess ?? "read table",
1311
- ...state.widgetInteraction
1312
- });
1313
- }
1314
-
1315
2157
  // src/sdk/helpers/grist-user-actions.ts
1316
2158
  function gristUpdateRecordAction(tableId, rowId, patch) {
1317
2159
  return ["UpdateRecord", tableId, rowId, patch];
@@ -1367,14 +2209,6 @@ function createInlineTransport(options) {
1367
2209
  logErr(err);
1368
2210
  }
1369
2211
  }
1370
- lastRecord = { record: null, mappings };
1371
- for (const [cb] of recordCbs) {
1372
- try {
1373
- cb(null, mappings);
1374
- } catch (err) {
1375
- logErr(err);
1376
- }
1377
- }
1378
2212
  return;
1379
2213
  }
1380
2214
  if (rowId === null) {
@@ -1669,8 +2503,8 @@ function createInlineTransport(options) {
1669
2503
  fireOptions() {
1670
2504
  pushOptions();
1671
2505
  },
1672
- fireTheme(message) {
1673
- bus.emit({ type: "theme-change", message });
2506
+ fireTheme(message2) {
2507
+ bus.emit({ type: "theme-change", message: message2 });
1674
2508
  },
1675
2509
  emitInitialState() {
1676
2510
  pushOptions();
@@ -1713,57 +2547,57 @@ function installFakeIframeContext(target) {
1713
2547
  }
1714
2548
 
1715
2549
  // src/emulator/validators.ts
1716
- var import_zod2 = require("zod");
1717
- var emulatorRowSchema = import_zod2.z.object({ id: import_zod2.z.number() }).catchall(import_zod2.z.unknown());
1718
- var formulaKindSchema = import_zod2.z.enum(["computed", "trigger", "none", "empty_slot"]);
1719
- var replicaColumnSchema = import_zod2.z.object({
1720
- colRef: import_zod2.z.number().optional(),
1721
- type: import_zod2.z.string().optional(),
1722
- label: import_zod2.z.string().optional(),
1723
- parentPos: import_zod2.z.number().optional(),
1724
- isFormula: import_zod2.z.boolean().optional(),
2550
+ var import_zod = require("zod");
2551
+ var emulatorRowSchema = import_zod.z.object({ id: import_zod.z.number() }).catchall(import_zod.z.unknown());
2552
+ var formulaKindSchema = import_zod.z.enum(["computed", "trigger", "none", "empty_slot"]);
2553
+ var replicaColumnSchema = import_zod.z.object({
2554
+ colRef: import_zod.z.number().optional(),
2555
+ type: import_zod.z.string().optional(),
2556
+ label: import_zod.z.string().optional(),
2557
+ parentPos: import_zod.z.number().optional(),
2558
+ isFormula: import_zod.z.boolean().optional(),
1725
2559
  formulaKind: formulaKindSchema.optional(),
1726
- formula: import_zod2.z.string().optional(),
1727
- description: import_zod2.z.string().optional(),
1728
- widgetOptions: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).optional(),
1729
- displayColRef: import_zod2.z.number().optional(),
1730
- visibleColRef: import_zod2.z.number().optional()
2560
+ formula: import_zod.z.string().optional(),
2561
+ description: import_zod.z.string().optional(),
2562
+ widgetOptions: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
2563
+ displayColRef: import_zod.z.number().optional(),
2564
+ visibleColRef: import_zod.z.number().optional()
1731
2565
  }).strict();
1732
- var replicaTableSchema = import_zod2.z.object({
1733
- label: import_zod2.z.string().optional(),
1734
- columns: import_zod2.z.record(import_zod2.z.string(), replicaColumnSchema).optional(),
1735
- rows: import_zod2.z.array(emulatorRowSchema).optional(),
1736
- rowCount: import_zod2.z.number().optional(),
1737
- dataOmitted: import_zod2.z.boolean().optional(),
1738
- dataOmittedReason: import_zod2.z.string().optional(),
1739
- error: import_zod2.z.string().optional()
2566
+ var replicaTableSchema = import_zod.z.object({
2567
+ label: import_zod.z.string().optional(),
2568
+ columns: import_zod.z.record(import_zod.z.string(), replicaColumnSchema).optional(),
2569
+ rows: import_zod.z.array(emulatorRowSchema).optional(),
2570
+ rowCount: import_zod.z.number().optional(),
2571
+ dataOmitted: import_zod.z.boolean().optional(),
2572
+ dataOmittedReason: import_zod.z.string().optional(),
2573
+ error: import_zod.z.string().optional()
1740
2574
  }).strict();
1741
- var replicaSelectionSchema = import_zod2.z.object({
1742
- tableId: import_zod2.z.string().optional(),
1743
- rowId: import_zod2.z.union([import_zod2.z.number(), import_zod2.z.literal("new"), import_zod2.z.null()]).optional(),
1744
- mappings: import_zod2.z.unknown().nullable().optional(),
1745
- cursor: import_zod2.z.unknown().optional()
2575
+ var replicaSelectionSchema = import_zod.z.object({
2576
+ tableId: import_zod.z.string().optional(),
2577
+ rowId: import_zod.z.union([import_zod.z.number(), import_zod.z.literal("new"), import_zod.z.null()]).optional(),
2578
+ mappings: import_zod.z.unknown().nullable().optional(),
2579
+ cursor: import_zod.z.unknown().optional()
1746
2580
  }).strict();
1747
- var replicaWidgetConfigurationSchema = import_zod2.z.object({
1748
- requiredAccess: import_zod2.z.string().optional(),
1749
- columns: import_zod2.z.unknown().optional(),
1750
- allowSelectBy: import_zod2.z.boolean().optional(),
1751
- hasCustomOptions: import_zod2.z.boolean().optional()
2581
+ var replicaWidgetConfigurationSchema = import_zod.z.object({
2582
+ requiredAccess: import_zod.z.string().optional(),
2583
+ columns: import_zod.z.unknown().optional(),
2584
+ allowSelectBy: import_zod.z.boolean().optional(),
2585
+ hasCustomOptions: import_zod.z.boolean().optional()
1752
2586
  }).strict();
1753
- var replicaAccessTokenSchema = import_zod2.z.object({
1754
- token: import_zod2.z.string(),
1755
- baseUrl: import_zod2.z.string(),
1756
- ttlMsecs: import_zod2.z.number()
2587
+ var replicaAccessTokenSchema = import_zod.z.object({
2588
+ token: import_zod.z.string(),
2589
+ baseUrl: import_zod.z.string(),
2590
+ ttlMsecs: import_zod.z.number()
1757
2591
  }).strict();
1758
- var replicaDocumentSchema = import_zod2.z.object({
1759
- generatedAt: import_zod2.z.string(),
1760
- source: import_zod2.z.string().optional(),
1761
- docName: import_zod2.z.string().optional(),
1762
- mode: import_zod2.z.enum(["schema-only", "schema+samples", "schema+data"]).optional(),
1763
- tables: import_zod2.z.record(import_zod2.z.string(), replicaTableSchema),
2592
+ var replicaDocumentSchema = import_zod.z.object({
2593
+ generatedAt: import_zod.z.string(),
2594
+ source: import_zod.z.string().optional(),
2595
+ docName: import_zod.z.string().optional(),
2596
+ mode: import_zod.z.enum(["schema-only", "schema+samples", "schema+data"]).optional(),
2597
+ tables: import_zod.z.record(import_zod.z.string(), replicaTableSchema),
1764
2598
  selection: replicaSelectionSchema.optional(),
1765
- widgetOptions: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).optional(),
1766
- widgetInteraction: import_zod2.z.unknown().optional(),
2599
+ widgetOptions: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
2600
+ widgetInteraction: import_zod.z.unknown().optional(),
1767
2601
  configuration: replicaWidgetConfigurationSchema.optional(),
1768
2602
  accessToken: replicaAccessTokenSchema.nullable().optional()
1769
2603
  }).strict().refine((value) => Object.keys(value.tables).length > 0, {
@@ -1794,34 +2628,21 @@ function createGristEmulator(options) {
1794
2628
  const state = createDocument({
1795
2629
  ...validatedDocument,
1796
2630
  widgetOptions: options.widgetOptions ?? validatedDocument.widgetOptions,
1797
- widgetInteraction: options.widgetInteraction ?? validatedDocument.widgetInteraction ?? { access_level: "read table" }
2631
+ // No access_level by default: the emulator then grants the access the
2632
+ // widget asks for in grist.ready(), as a user accepting it in Grist would.
2633
+ widgetInteraction: options.widgetInteraction ?? validatedDocument.widgetInteraction ?? {}
1798
2634
  });
1799
2635
  const bus = createEventBus();
1800
2636
  const api = createApiBundle(state, bus);
1801
- const transportKind = options.transport?.kind ?? "inline";
1802
- let transport;
1803
- let grist2;
1804
- if (transportKind === "iframe") {
1805
- if (options.transport?.kind !== "iframe") throw new Error("transport mismatch");
1806
- transport = createIframeTransport({
1807
- iframe: options.transport.iframe,
1808
- targetOrigin: options.transport.targetOrigin,
1809
- state,
1810
- bus,
1811
- api,
1812
- initialTheme: options.initialTheme
1813
- });
1814
- } else {
1815
- const inline = createInlineTransport({
1816
- state,
1817
- bus,
1818
- api,
1819
- initialTheme: options.initialTheme,
1820
- target: options.transport?.kind === "inline" ? options.transport.target : void 0
1821
- });
1822
- transport = inline;
1823
- grist2 = inline.grist;
1824
- }
2637
+ const inline = createInlineTransport({
2638
+ state,
2639
+ bus,
2640
+ api,
2641
+ initialTheme: options.initialTheme,
2642
+ target: options.transport?.target
2643
+ });
2644
+ const transport = inline;
2645
+ const grist2 = inline.grist;
1825
2646
  function apply(actions) {
1826
2647
  return applyUserActions(state, bus, actions);
1827
2648
  }
@@ -1963,18 +2784,14 @@ var presets = {
1963
2784
  // src/emulator/testing/render-with-grist.tsx
1964
2785
  function renderWithGrist(ui, options = {}) {
1965
2786
  fakeIframeContext();
1966
- resetGristEventsForTests();
1967
- resetGristReadyCacheForTests();
1968
- resetEmbeddedHandshakeManagerForTests();
2787
+ resetGristForTests();
1969
2788
  const emulator = options.existing ?? createGristEmulator(options.emulator ?? { document: presets.blank() });
1970
- const result = (0, import_react.render)(ui, options);
2789
+ const result = (0, import_react2.render)(ui, options);
1971
2790
  const originalUnmount = result.unmount;
1972
2791
  result.unmount = () => {
1973
2792
  originalUnmount();
1974
2793
  if (!options.existing) emulator.dispose();
1975
- resetGristReadyCacheForTests();
1976
- resetGristEventsForTests();
1977
- resetEmbeddedHandshakeManagerForTests();
2794
+ resetGristForTests();
1978
2795
  };
1979
2796
  return Object.assign(result, { emulator });
1980
2797
  }
@@ -1990,6 +2807,880 @@ function fakeIframeContext() {
1990
2807
  }
1991
2808
  }
1992
2809
 
2810
+ // src/emulator/testing/smoke-test-widget.tsx
2811
+ var import_react9 = require("react");
2812
+ var import_react10 = require("@testing-library/react");
2813
+
2814
+ // src/app/mount-grist-widget.tsx
2815
+ var import_react8 = require("react");
2816
+ var import_client = require("react-dom/client");
2817
+
2818
+ // src/sdk/components/grist-widget-provider.tsx
2819
+ var import_react3 = require("react");
2820
+ var import_jsx_runtime = require("react/jsx-runtime");
2821
+ function GristWidgetProvider({ options, children }) {
2822
+ const [connection] = (0, import_react3.useState)(() => newConnection(options));
2823
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GristContextProvider, { value: connection, children });
2824
+ }
2825
+
2826
+ // src/sdk/components/grist-boundary.tsx
2827
+ var import_react4 = require("react");
2828
+ var import_jsx_runtime2 = require("react/jsx-runtime");
2829
+ var shellStyle = {
2830
+ alignItems: "center",
2831
+ background: "var(--background)",
2832
+ boxSizing: "border-box",
2833
+ color: "var(--foreground)",
2834
+ display: "flex",
2835
+ flexDirection: "column",
2836
+ justifyContent: "center",
2837
+ minHeight: "100%",
2838
+ padding: "clamp(16px, 5vh, 32px)",
2839
+ width: "100%"
2840
+ };
2841
+ var cardStyle = {
2842
+ background: "var(--card)",
2843
+ border: "1px solid var(--border)",
2844
+ borderRadius: 12,
2845
+ boxShadow: "0 1px 3px rgb(0 0 0 / 0.12)",
2846
+ maxWidth: 420,
2847
+ padding: "20px 22px",
2848
+ textAlign: "center",
2849
+ width: "100%"
2850
+ };
2851
+ var titleStyle = {
2852
+ color: "var(--foreground)",
2853
+ fontSize: 15,
2854
+ fontWeight: 600,
2855
+ lineHeight: 1.35,
2856
+ margin: 0
2857
+ };
2858
+ var messageStyle = {
2859
+ color: "var(--muted-foreground)",
2860
+ fontSize: 14,
2861
+ lineHeight: 1.5,
2862
+ margin: "10px 0 0"
2863
+ };
2864
+ var buttonStyle = {
2865
+ background: "var(--background)",
2866
+ border: "1px solid var(--border)",
2867
+ borderRadius: 8,
2868
+ color: "var(--foreground)",
2869
+ cursor: "pointer",
2870
+ fontSize: 13,
2871
+ fontWeight: 500,
2872
+ marginTop: 16,
2873
+ padding: "8px 14px"
2874
+ };
2875
+ function GristBoundaryScreen({
2876
+ title,
2877
+ message: message2,
2878
+ tone = "default",
2879
+ showRetry = false,
2880
+ actions,
2881
+ retryLabel = "Try reconnecting",
2882
+ retryBusyLabel = "Reconnecting\u2026"
2883
+ }) {
2884
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: shellStyle, role: tone === "error" ? "alert" : "status", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: cardStyle, children: [
2885
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { style: titleStyle, children: title }),
2886
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: messageStyle, children: message2 }),
2887
+ showRetry && actions ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2888
+ "button",
2889
+ {
2890
+ disabled: actions.isReloading,
2891
+ onClick: () => void actions.reload(),
2892
+ style: {
2893
+ ...buttonStyle,
2894
+ cursor: actions.isReloading ? "not-allowed" : "pointer",
2895
+ opacity: actions.isReloading ? 0.7 : 1
2896
+ },
2897
+ type: "button",
2898
+ children: actions.isReloading ? retryBusyLabel : retryLabel
2899
+ }
2900
+ ) : null
2901
+ ] }) });
2902
+ }
2903
+ function boundaryScreen(state, columns) {
2904
+ switch (state.connection) {
2905
+ case "connecting":
2906
+ return { title: "Connecting to Grist", message: "Asking Grist for the access this widget needs\u2026" };
2907
+ case "not-embedded":
2908
+ return {
2909
+ title: "Open inside Grist",
2910
+ message: "Open this page as a custom widget inside a Grist document (it runs in an embedded panel, not on its own).",
2911
+ retry: true
2912
+ };
2913
+ case "unavailable":
2914
+ return {
2915
+ title: "Open inside Grist",
2916
+ message: "The Grist plugin did not load. Reload the page, or open this URL as a custom widget inside a Grist document.",
2917
+ retry: true
2918
+ };
2919
+ case "error":
2920
+ return {
2921
+ title: "Something went wrong",
2922
+ message: state.connectionError ?? "Something went wrong connecting to Grist. Try again, or reload the page.",
2923
+ tone: "error",
2924
+ retry: true
2925
+ };
2926
+ }
2927
+ if (accessDenied(state)) {
2928
+ return {
2929
+ title: "More access needed",
2930
+ message: `Set Access level to at least "${accessLabel(state.declaredAccess)}" in the Custom widget settings.`,
2931
+ tone: "error",
2932
+ retry: true
2933
+ };
2934
+ }
2935
+ const mapping = deriveColumnMappingStatus(columns, state.mappings);
2936
+ if (mapping.pending) return { title: "Connecting to Grist", message: "Waiting for Grist to report the column mapping\u2026" };
2937
+ if (!mapping.ok) {
2938
+ return {
2939
+ title: "Map your columns",
2940
+ message: "Open the widget settings in Grist (Creator Panel \u2192 Custom widget) and connect each required column to your table."
2941
+ };
2942
+ }
2943
+ return null;
2944
+ }
2945
+ function GristBoundary({ children }) {
2946
+ const connection = useGristRuntime();
2947
+ if (!connection) throw new Error("<GristBoundary> must be used inside <GristWidgetProvider> (mountGristWidget renders both).");
2948
+ const state = useGristState(connection);
2949
+ const [isReloading, setIsReloading] = (0, import_react4.useState)(false);
2950
+ const reload = (0, import_react4.useCallback)(async () => {
2951
+ setIsReloading(true);
2952
+ try {
2953
+ await connection.runtime.reload();
2954
+ } catch (err) {
2955
+ console.error("[grist-widget-sdk] reload failed:", err);
2956
+ } finally {
2957
+ setIsReloading(false);
2958
+ }
2959
+ }, [connection]);
2960
+ const screen2 = boundaryScreen(state, connection.runtime.options.columns);
2961
+ if (!screen2) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
2962
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2963
+ GristBoundaryScreen,
2964
+ {
2965
+ title: screen2.title,
2966
+ message: screen2.message,
2967
+ tone: screen2.tone,
2968
+ showRetry: screen2.retry,
2969
+ actions: { reload, isReloading },
2970
+ retryLabel: screen2.tone === "error" ? "Try again" : "Try reconnecting",
2971
+ retryBusyLabel: screen2.tone === "error" ? "Retrying\u2026" : "Reconnecting\u2026"
2972
+ }
2973
+ );
2974
+ }
2975
+
2976
+ // src/app/chrome/tokens.ts
2977
+ var token = (name, fallback) => `var(--${name}, ${fallback})`;
2978
+ var tokens = {
2979
+ background: token("background", "#ffffff"),
2980
+ foreground: token("foreground", "#0a0a0a"),
2981
+ card: token("card", "#ffffff"),
2982
+ mutedForeground: token("muted-foreground", "#737373"),
2983
+ border: token("border", "#e5e5e5"),
2984
+ primary: token("primary", "#171717"),
2985
+ primaryForeground: token("primary-foreground", "#fafafa"),
2986
+ destructive: token("destructive", "#dc2626"),
2987
+ radius: token("radius", "0.625rem")
2988
+ };
2989
+ var pageStyle = {
2990
+ boxSizing: "border-box",
2991
+ display: "flex",
2992
+ flexDirection: "column",
2993
+ justifyContent: "center",
2994
+ maxWidth: "28rem",
2995
+ minHeight: "100%",
2996
+ margin: "0 auto",
2997
+ padding: "4rem 1.5rem",
2998
+ color: tokens.foreground,
2999
+ font: "inherit"
3000
+ };
3001
+ var eyebrowStyle = {
3002
+ margin: "0 0 0.5rem",
3003
+ textAlign: "center",
3004
+ fontSize: "0.875rem",
3005
+ fontWeight: 500,
3006
+ color: tokens.mutedForeground
3007
+ };
3008
+ var headingStyle = {
3009
+ margin: 0,
3010
+ textAlign: "center",
3011
+ fontSize: "1.5rem",
3012
+ fontWeight: 600,
3013
+ letterSpacing: "-0.01em"
3014
+ };
3015
+ var leadStyle = {
3016
+ margin: "0.75rem auto 2rem",
3017
+ maxWidth: "24rem",
3018
+ textAlign: "center",
3019
+ fontSize: "0.875rem",
3020
+ lineHeight: 1.5,
3021
+ color: tokens.mutedForeground
3022
+ };
3023
+ var cardStyle2 = {
3024
+ boxSizing: "border-box",
3025
+ marginBottom: "1rem",
3026
+ padding: "1.25rem",
3027
+ border: `1px solid ${tokens.border}`,
3028
+ borderRadius: tokens.radius,
3029
+ background: tokens.card
3030
+ };
3031
+ var cardTitleStyle = {
3032
+ margin: "0 0 0.375rem",
3033
+ fontSize: "1rem",
3034
+ fontWeight: 600
3035
+ };
3036
+ var cardTextStyle = {
3037
+ margin: 0,
3038
+ fontSize: "0.875rem",
3039
+ lineHeight: 1.5,
3040
+ color: tokens.mutedForeground
3041
+ };
3042
+ var codeStyle = {
3043
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
3044
+ fontSize: "0.8125rem",
3045
+ color: tokens.foreground
3046
+ };
3047
+
3048
+ // src/app/chrome/widget-title.ts
3049
+ function widgetTitle() {
3050
+ const title = typeof document === "undefined" ? "" : document.title.trim();
3051
+ return title || "Grist widget";
3052
+ }
3053
+
3054
+ // src/app/chrome/local-dev-page.tsx
3055
+ var import_jsx_runtime3 = require("react/jsx-runtime");
3056
+ function LocalDevPage() {
3057
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("main", { style: pageStyle, children: [
3058
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { style: eyebrowStyle, children: [
3059
+ widgetTitle(),
3060
+ " \xB7 local dev"
3061
+ ] }),
3062
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { style: headingStyle, children: "Not embedded in Grist" }),
3063
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: leadStyle, children: "This is the local dev server: there's no Grist here to render the widget against." }),
3064
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { style: cardStyle2, children: [
3065
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { style: cardTitleStyle, children: "Check it against real Grist" }),
3066
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { style: cardTextStyle, children: [
3067
+ "Push to ",
3068
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "dev" }),
3069
+ ", then open its ",
3070
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "/dev/" }),
3071
+ " URL in a custom widget of a real Grist document: later pushes reload it there within seconds. Locally,",
3072
+ " ",
3073
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "pnpm test" }),
3074
+ " runs the widget against an emulated Grist."
3075
+ ] })
3076
+ ] }),
3077
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { style: cardStyle2, children: [
3078
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { style: cardTitleStyle, children: "Starting from another example?" }),
3079
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { style: cardTextStyle, children: [
3080
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "npx create-grist-widget list" }),
3081
+ " shows the examples, and",
3082
+ " ",
3083
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "npx create-grist-widget use <example>" }),
3084
+ " swaps this widget for one."
3085
+ ] })
3086
+ ] })
3087
+ ] });
3088
+ }
3089
+
3090
+ // src/app/chrome/release-page.tsx
3091
+ var import_react6 = require("react");
3092
+
3093
+ // src/app/deploy-path.ts
3094
+ var VERSION_SEGMENT = /^v(\d+\.\d+\.\d+(?:-[\w.]+)?)$/;
3095
+ function parseDeployPath(pathname) {
3096
+ const segments = pathname.replace(/\/+$/, "").split("/");
3097
+ const last = segments[segments.length - 1] ?? "";
3098
+ let channel = null;
3099
+ if (last === "latest") channel = { kind: "latest" };
3100
+ else if (last === "dev") channel = { kind: "dev" };
3101
+ else {
3102
+ const match = VERSION_SEGMENT.exec(last);
3103
+ if (match) channel = { kind: "version", version: match[1] };
3104
+ }
3105
+ if (!channel) return { channel: { kind: "latest" }, basePath: withTrailingSlash(pathname) };
3106
+ return { channel, basePath: withTrailingSlash(segments.slice(0, -1).join("/")) };
3107
+ }
3108
+ function deployChannelLabel(channel) {
3109
+ return channel.kind === "version" ? `v${channel.version}` : channel.kind;
3110
+ }
3111
+ function withTrailingSlash(path) {
3112
+ return path.endsWith("/") ? path : `${path}/`;
3113
+ }
3114
+
3115
+ // src/app/released-versions.ts
3116
+ var import_react5 = require("react");
3117
+ function useReleasedVersions(pathname = typeof window === "undefined" ? "/" : window.location.pathname) {
3118
+ const url = `${parseDeployPath(pathname).basePath}versions.json`;
3119
+ const [result, setResult] = (0, import_react5.useState)({ versions: null, error: false });
3120
+ (0, import_react5.useEffect)(() => {
3121
+ let cancelled = false;
3122
+ fetch(url, { cache: "no-store" }).then((res) => res.ok ? res.json() : Promise.reject(new Error(`HTTP ${res.status}`))).then((data) => {
3123
+ if (cancelled) return;
3124
+ if (!Array.isArray(data)) throw new Error("versions.json is not an array");
3125
+ setResult({ versions: data, error: false });
3126
+ }).catch(() => {
3127
+ if (!cancelled) setResult({ versions: null, error: true });
3128
+ });
3129
+ return () => {
3130
+ cancelled = true;
3131
+ };
3132
+ }, [url]);
3133
+ return result;
3134
+ }
3135
+
3136
+ // src/app/chrome/release-page.tsx
3137
+ var import_jsx_runtime4 = require("react/jsx-runtime");
3138
+ function ReleasePage({ channel }) {
3139
+ const label = deployChannelLabel(channel);
3140
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("main", { style: pageStyle, children: [
3141
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { style: eyebrowStyle, children: [
3142
+ widgetTitle(),
3143
+ " \xB7 ",
3144
+ label
3145
+ ] }),
3146
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h1", { style: headingStyle, children: "Grist isn't loaded here" }),
3147
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { style: leadStyle, children: [
3148
+ "This is the ",
3149
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("code", { style: codeStyle, children: label }),
3150
+ " build, opened outside Grist. Hover a version to see when it was released."
3151
+ ] }),
3152
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ChannelLinks, { current: channel }),
3153
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(InstallUrl, {})
3154
+ ] });
3155
+ }
3156
+ function ChannelLinks({ current }) {
3157
+ const { basePath } = parseDeployPath(window.location.pathname);
3158
+ const { versions } = useReleasedVersions();
3159
+ const isCurrent = (channel) => channel.kind === current.kind && (channel.kind !== "version" || current.kind === "version" && channel.version === current.version);
3160
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("nav", { "aria-label": "Versions", style: { display: "flex", flexWrap: "wrap", justifyContent: "center", gap: "0.5rem", marginBottom: "2rem" }, children: [
3161
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ChannelLink, { href: `${basePath}latest/`, active: isCurrent({ kind: "latest" }), children: "latest" }),
3162
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ChannelLink, { href: `${basePath}dev/`, active: isCurrent({ kind: "dev" }), children: "dev" }),
3163
+ versions?.map((v) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3164
+ ChannelLink,
3165
+ {
3166
+ href: `${basePath}v${v.version}/`,
3167
+ title: formatDate(v.publishedAt),
3168
+ active: isCurrent({ kind: "version", version: v.version }),
3169
+ children: `v${v.version}`
3170
+ },
3171
+ v.version
3172
+ ))
3173
+ ] });
3174
+ }
3175
+ function ChannelLink({
3176
+ href,
3177
+ title,
3178
+ active,
3179
+ children
3180
+ }) {
3181
+ const style = {
3182
+ padding: "0.25rem 0.75rem",
3183
+ border: `1px solid ${active ? tokens.primary : tokens.border}`,
3184
+ borderRadius: "9999px",
3185
+ background: active ? tokens.primary : "transparent",
3186
+ color: active ? tokens.primaryForeground : tokens.mutedForeground,
3187
+ fontSize: "0.75rem",
3188
+ fontWeight: 500,
3189
+ textDecoration: "none"
3190
+ };
3191
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("a", { href, title, "aria-current": active ? "page" : void 0, style, children });
3192
+ }
3193
+ function InstallUrl() {
3194
+ const url = window.location.href;
3195
+ const [copied, setCopied] = (0, import_react6.useState)(false);
3196
+ function copy() {
3197
+ void navigator.clipboard?.writeText(url).then(() => {
3198
+ setCopied(true);
3199
+ setTimeout(() => setCopied(false), 1500);
3200
+ });
3201
+ }
3202
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("section", { style: cardStyle2, children: [
3203
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h2", { style: cardTitleStyle, children: "Use it as a Grist custom widget" }),
3204
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: { ...cardTextStyle, marginBottom: "1rem" }, children: "Paste this URL into a custom widget's URL field in Grist." }),
3205
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", gap: "0.5rem" }, children: [
3206
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3207
+ "input",
3208
+ {
3209
+ readOnly: true,
3210
+ value: url,
3211
+ "aria-label": "Widget URL",
3212
+ onFocus: (event) => event.target.select(),
3213
+ style: {
3214
+ flex: 1,
3215
+ minWidth: 0,
3216
+ padding: "0.5rem 0.75rem",
3217
+ border: `1px solid ${tokens.border}`,
3218
+ borderRadius: tokens.radius,
3219
+ background: "transparent",
3220
+ color: tokens.foreground,
3221
+ font: "inherit",
3222
+ fontSize: "0.875rem"
3223
+ }
3224
+ }
3225
+ ),
3226
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3227
+ "button",
3228
+ {
3229
+ type: "button",
3230
+ onClick: copy,
3231
+ style: {
3232
+ padding: "0.5rem 1rem",
3233
+ border: "none",
3234
+ borderRadius: tokens.radius,
3235
+ background: tokens.primary,
3236
+ color: tokens.primaryForeground,
3237
+ font: "inherit",
3238
+ fontSize: "0.875rem",
3239
+ fontWeight: 500,
3240
+ cursor: "pointer"
3241
+ },
3242
+ children: copied ? "Copied" : "Copy"
3243
+ }
3244
+ )
3245
+ ] })
3246
+ ] });
3247
+ }
3248
+ function formatDate(iso) {
3249
+ const date = new Date(iso);
3250
+ return Number.isNaN(date.getTime()) ? iso : date.toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
3251
+ }
3252
+
3253
+ // src/sdk/helpers/grist-sdk-alerts.ts
3254
+ var TITLE = {
3255
+ "column-mapping": "Column mapping",
3256
+ "mapping-pending": "Column mapping",
3257
+ "section-not-linked": "Widget linking",
3258
+ "source-not-wired": "Widget linking",
3259
+ "action-error": "Action error",
3260
+ "access-insufficient": "Access level",
3261
+ "map-back-skip": "Write skipped",
3262
+ "cell-issues": "Unexpected values"
3263
+ };
3264
+ var SEVERITY = {
3265
+ "column-mapping": "warning",
3266
+ "mapping-pending": "info",
3267
+ "section-not-linked": "warning",
3268
+ "source-not-wired": "warning",
3269
+ "action-error": "error",
3270
+ "access-insufficient": "error",
3271
+ "map-back-skip": "warning",
3272
+ "cell-issues": "warning"
3273
+ };
3274
+ var alert = (kind, message2, ariaRole = "status") => ({
3275
+ id: kind,
3276
+ kind,
3277
+ severity: SEVERITY[kind],
3278
+ title: TITLE[kind],
3279
+ ariaRole,
3280
+ message: message2
3281
+ });
3282
+ function isWidgetSectionNotLinked(interaction) {
3283
+ const linking = parseGristWidgetLinkingInfo(interaction?.linking);
3284
+ return Boolean(linking) && linking.asTarget == null;
3285
+ }
3286
+ function getGristSdkAlerts(state, widget, options = {}) {
3287
+ if (deriveStatus(state) !== "ready") return [];
3288
+ const out = [];
3289
+ const mapping = deriveColumnMappingStatus(widget.columns, state.mappings);
3290
+ if (mapping.pending) out.push(alert("mapping-pending", "Waiting for Grist to report column mappings."));
3291
+ else if (!mapping.ok) {
3292
+ const parts = ["Column mapping is incomplete."];
3293
+ if (mapping.missing.length) parts.push(`Required columns: ${mapping.missing.join(", ")}.`);
3294
+ if (mapping.emptyMultiples.length) parts.push(`Assign at least one sheet column for: ${mapping.emptyMultiples.join(", ")}.`);
3295
+ out.push(alert("column-mapping", parts.join(" ")));
3296
+ }
3297
+ const failure = state.actionError ?? state.readError;
3298
+ if (failure && isAccessInsufficientError(failure)) {
3299
+ out.push(
3300
+ alert(
3301
+ "access-insufficient",
3302
+ 'This operation requires "Full document access". Set the Access level to "Full document access" in the Custom widget configuration panel.',
3303
+ "alert"
3304
+ )
3305
+ );
3306
+ } else if (state.actionError) {
3307
+ out.push(alert("action-error", state.actionError, "alert"));
3308
+ }
3309
+ if (state.mapBackSkipped.length) {
3310
+ const list = state.mapBackSkipped.join(", ");
3311
+ out.push(alert("map-back-skip", `mapBack skipped multi-mapped column${state.mapBackSkipped.length === 1 ? "" : "s"}: ${list}.`));
3312
+ }
3313
+ const cellIssues = widget.cellIssues ?? [];
3314
+ if (cellIssues.length) {
3315
+ const first = cellIssues[0];
3316
+ const where = first.column ? `${first.column} in row ${first.rowId}` : `row ${first.rowId}`;
3317
+ const more = cellIssues.length > 1 ? ` (and ${cellIssues.length - 1} more)` : "";
3318
+ out.push(alert("cell-issues", `${where}: ${first.message}${more}. Check the column's type, or the cell, in Grist.`));
3319
+ }
3320
+ const linking = parseGristWidgetLinkingInfo(state.interaction?.linking);
3321
+ if (widget.allowSelectBy) {
3322
+ if (linking && !linking.asSource) {
3323
+ out.push(
3324
+ alert(
3325
+ "source-not-wired",
3326
+ "This widget is configured as a selector (allowSelectBy) but no other section is using it as a link source yet. Link another section to read from this widget."
3327
+ )
3328
+ );
3329
+ }
3330
+ } else if (isWidgetSectionNotLinked(state.interaction)) {
3331
+ const hint = options.sectionNotLinkedHint?.trim();
3332
+ out.push(
3333
+ alert(
3334
+ "section-not-linked",
3335
+ "This widget section is not linked to a table or selector. Link it from the page settings so the selected row is sent to the widget." + (hint ? ` ${hint}` : "")
3336
+ )
3337
+ );
3338
+ }
3339
+ const suppressed = widget.suppressAlerts ?? [];
3340
+ return out.filter((a) => !suppressed.includes(a.kind));
3341
+ }
3342
+ function useGristSdkAlerts(options) {
3343
+ const { connection, state, cellIssues } = useNearestGristState();
3344
+ if (!connection) return [];
3345
+ const { columns, allowSelectBy, suppressAlerts } = connection.runtime.options;
3346
+ return getGristSdkAlerts(state, { columns, allowSelectBy, suppressAlerts, cellIssues }, options);
3347
+ }
3348
+
3349
+ // src/app/chrome/widget-alerts.tsx
3350
+ var import_jsx_runtime5 = require("react/jsx-runtime");
3351
+ var ALERT_OPTIONS = {
3352
+ sectionNotLinkedHint: "In Grist, select this widget section on the page, then use Link to connect a table, card, or chart view as the selector."
3353
+ };
3354
+ function WidgetAlerts({ children }) {
3355
+ const alerts = useGristSdkAlerts(ALERT_OPTIONS);
3356
+ if (alerts.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children });
3357
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "0.75rem" }, children: [
3358
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: "0.75rem", padding: "0.75rem 0.75rem 0" }, children: alerts.map((alert2) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(WidgetAlert, { alert: alert2 }, alert2.id)) }),
3359
+ children
3360
+ ] });
3361
+ }
3362
+ function WidgetAlert({ alert: alert2 }) {
3363
+ const linkIcon = alert2.kind === "section-not-linked" || alert2.kind === "source-not-wired";
3364
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { role: alert2.ariaRole, style: { ...alertStyle, ...SEVERITY2[alert2.severity] }, children: [
3365
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { "aria-hidden": true, style: { flexShrink: 0, marginTop: "0.125rem" }, children: linkIcon ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(LinkIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(WarningIcon, {}) }),
3366
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { minWidth: 0 }, children: [
3367
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: { margin: 0, fontWeight: 500 }, children: alert2.title }),
3368
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: { margin: "0.25rem 0 0", color: tokens.mutedForeground }, children: alert2.message })
3369
+ ] })
3370
+ ] });
3371
+ }
3372
+ var alertStyle = {
3373
+ boxSizing: "border-box",
3374
+ display: "flex",
3375
+ gap: "0.75rem",
3376
+ padding: "0.75rem 1rem",
3377
+ border: `1px solid ${tokens.border}`,
3378
+ borderRadius: tokens.radius,
3379
+ color: tokens.foreground,
3380
+ fontSize: "0.875rem",
3381
+ lineHeight: 1.5
3382
+ };
3383
+ var SEVERITY2 = {
3384
+ error: { borderColor: tokens.destructive, color: tokens.destructive },
3385
+ warning: { borderColor: "rgb(245 158 11 / 0.4)", background: "rgb(245 158 11 / 0.1)" },
3386
+ info: { background: "transparent" }
3387
+ };
3388
+ var ICON = {
3389
+ width: 16,
3390
+ height: 16,
3391
+ viewBox: "0 0 24 24",
3392
+ fill: "none",
3393
+ stroke: "currentColor",
3394
+ strokeWidth: 2,
3395
+ strokeLinecap: "round",
3396
+ strokeLinejoin: "round"
3397
+ };
3398
+ function WarningIcon() {
3399
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { ...ICON, children: [
3400
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }),
3401
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 9v4" }),
3402
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 17h.01" })
3403
+ ] });
3404
+ }
3405
+ function LinkIcon() {
3406
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { ...ICON, children: [
3407
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M9 17H7A5 5 0 0 1 7 7h2" }),
3408
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M15 7h2a5 5 0 1 1 0 10h-2" }),
3409
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "8", x2: "16", y1: "12", y2: "12" })
3410
+ ] });
3411
+ }
3412
+
3413
+ // src/app/widget-theme.tsx
3414
+ var import_react7 = require("react");
3415
+ var import_jsx_runtime6 = require("react/jsx-runtime");
3416
+ var STORAGE_KEY = "theme";
3417
+ var DARK_QUERY = "(prefers-color-scheme: dark)";
3418
+ var THEMES = ["light", "dark", "system"];
3419
+ var ThemeContext = (0, import_react7.createContext)(null);
3420
+ function EmbeddedThemeProvider({ children }) {
3421
+ const gristTheme = useNearestGristState().state.theme;
3422
+ const system = useSystemTheme(gristTheme === null);
3423
+ const resolvedTheme = gristTheme ?? system;
3424
+ useApplyTheme(resolvedTheme);
3425
+ const value = (0, import_react7.useMemo)(
3426
+ () => ({ theme: gristTheme ?? "system", resolvedTheme, setTheme: noop }),
3427
+ [gristTheme, resolvedTheme]
3428
+ );
3429
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ThemeContext.Provider, { value, children });
3430
+ }
3431
+ function StandaloneThemeProvider({ children }) {
3432
+ const [theme, setThemeState] = (0, import_react7.useState)(() => readStoredTheme() ?? "system");
3433
+ const system = useSystemTheme(theme === "system");
3434
+ const resolvedTheme = theme === "system" ? system : theme;
3435
+ useApplyTheme(resolvedTheme);
3436
+ const setTheme = (0, import_react7.useCallback)((next) => {
3437
+ writeStoredTheme(next);
3438
+ setThemeState(next);
3439
+ }, []);
3440
+ (0, import_react7.useEffect)(() => {
3441
+ const onKeyDown = (event) => {
3442
+ if (event.repeat || event.metaKey || event.ctrlKey || event.altKey) return;
3443
+ if (event.key.toLowerCase() !== "d" || isEditableTarget(event.target)) return;
3444
+ setThemeState((current) => {
3445
+ const currentResolved = current === "system" ? systemTheme() : current;
3446
+ const next = currentResolved === "dark" ? "light" : "dark";
3447
+ writeStoredTheme(next);
3448
+ return next;
3449
+ });
3450
+ };
3451
+ window.addEventListener("keydown", onKeyDown);
3452
+ return () => window.removeEventListener("keydown", onKeyDown);
3453
+ }, []);
3454
+ (0, import_react7.useEffect)(() => {
3455
+ const onStorage = (event) => {
3456
+ if (event.key !== STORAGE_KEY) return;
3457
+ setThemeState(isTheme(event.newValue) ? event.newValue : "system");
3458
+ };
3459
+ window.addEventListener("storage", onStorage);
3460
+ return () => window.removeEventListener("storage", onStorage);
3461
+ }, []);
3462
+ const value = (0, import_react7.useMemo)(
3463
+ () => ({ theme, resolvedTheme, setTheme }),
3464
+ [theme, resolvedTheme, setTheme]
3465
+ );
3466
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ThemeContext.Provider, { value, children });
3467
+ }
3468
+ function useSystemTheme(active) {
3469
+ const [theme, setTheme] = (0, import_react7.useState)(systemTheme);
3470
+ (0, import_react7.useEffect)(() => {
3471
+ if (!active || typeof window.matchMedia !== "function") return;
3472
+ const query = window.matchMedia(DARK_QUERY);
3473
+ const onChange = () => setTheme(query.matches ? "dark" : "light");
3474
+ onChange();
3475
+ query.addEventListener("change", onChange);
3476
+ return () => query.removeEventListener("change", onChange);
3477
+ }, [active]);
3478
+ return theme;
3479
+ }
3480
+ function useApplyTheme(theme) {
3481
+ (0, import_react7.useEffect)(() => {
3482
+ const root = document.documentElement;
3483
+ if (root.classList.contains(theme) && !root.classList.contains(other(theme))) return;
3484
+ const restoreTransitions = suspendTransitions();
3485
+ root.classList.remove(other(theme));
3486
+ root.classList.add(theme);
3487
+ restoreTransitions();
3488
+ }, [theme]);
3489
+ }
3490
+ function systemTheme() {
3491
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return "light";
3492
+ return window.matchMedia(DARK_QUERY).matches ? "dark" : "light";
3493
+ }
3494
+ function other(theme) {
3495
+ return theme === "dark" ? "light" : "dark";
3496
+ }
3497
+ function suspendTransitions() {
3498
+ const style = document.createElement("style");
3499
+ style.appendChild(
3500
+ document.createTextNode("*,*::before,*::after{-webkit-transition:none!important;transition:none!important}")
3501
+ );
3502
+ document.head.appendChild(style);
3503
+ return () => {
3504
+ window.getComputedStyle(document.body);
3505
+ requestAnimationFrame(() => requestAnimationFrame(() => style.remove()));
3506
+ };
3507
+ }
3508
+ function isEditableTarget(target) {
3509
+ if (!(target instanceof HTMLElement)) return false;
3510
+ return target.isContentEditable || target.closest("input, textarea, select, [contenteditable='true']") !== null;
3511
+ }
3512
+ function isTheme(value) {
3513
+ return value !== null && THEMES.includes(value);
3514
+ }
3515
+ function readStoredTheme() {
3516
+ try {
3517
+ const value = localStorage.getItem(STORAGE_KEY);
3518
+ return isTheme(value) ? value : null;
3519
+ } catch {
3520
+ return null;
3521
+ }
3522
+ }
3523
+ function writeStoredTheme(theme) {
3524
+ try {
3525
+ localStorage.setItem(STORAGE_KEY, theme);
3526
+ } catch {
3527
+ }
3528
+ }
3529
+ function noop() {
3530
+ }
3531
+
3532
+ // src/app/mount-grist-widget.tsx
3533
+ var import_jsx_runtime7 = require("react/jsx-runtime");
3534
+ function GristWidgetApp({
3535
+ App,
3536
+ options,
3537
+ dev = false,
3538
+ Alerts = WidgetAlerts,
3539
+ LocalDevPage: LocalDevPage2 = LocalDevPage,
3540
+ ReleasePage: ReleasePage2 = ReleasePage
3541
+ }) {
3542
+ const Wrap = Alerts ?? PassThrough;
3543
+ if (isEmbedded2()) {
3544
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(GristWidgetProvider, { options, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(EmbeddedThemeProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(GristBoundary, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: FILL, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Wrap, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react8.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(App, {}) }) }) }) }) }) });
3545
+ }
3546
+ const channel = parseDeployPath(window.location.pathname).channel;
3547
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(StandaloneThemeProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: FILL, children: dev ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(LocalDevPage2, {}) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ReleasePage2, { channel }) }) });
3548
+ }
3549
+ var FILL = { minHeight: "100%", width: "100%" };
3550
+ function isEmbedded2() {
3551
+ return typeof window !== "undefined" && window.self !== window.top;
3552
+ }
3553
+ function PassThrough({ children }) {
3554
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children });
3555
+ }
3556
+
3557
+ // src/emulator/testing/jsdom-stubs.ts
3558
+ function installJsdomStubs() {
3559
+ if (typeof window === "undefined") return;
3560
+ globalThis.ResizeObserver ?? (globalThis.ResizeObserver = class ResizeObserver {
3561
+ observe() {
3562
+ }
3563
+ unobserve() {
3564
+ }
3565
+ disconnect() {
3566
+ }
3567
+ });
3568
+ window.matchMedia ?? (window.matchMedia = (query) => ({
3569
+ matches: false,
3570
+ media: query,
3571
+ onchange: null,
3572
+ addEventListener() {
3573
+ },
3574
+ removeEventListener() {
3575
+ },
3576
+ addListener() {
3577
+ },
3578
+ removeListener() {
3579
+ },
3580
+ dispatchEvent: () => false
3581
+ }));
3582
+ }
3583
+
3584
+ // src/emulator/testing/smoke-test-widget.tsx
3585
+ var import_jsx_runtime8 = require("react/jsx-runtime");
3586
+ async function smokeTestWidget({
3587
+ App,
3588
+ options,
3589
+ document: document2,
3590
+ timeout = 1e4
3591
+ }) {
3592
+ installJsdomStubs();
3593
+ const crash = { error: void 0 };
3594
+ function Probe() {
3595
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CatchRenderError, { crash, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { "data-grist-smoke-test": "", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(App, {}) }) });
3596
+ }
3597
+ const columns = declaredColumns(options);
3598
+ const gristDocument = document2 ?? smokeTestDocument(columns);
3599
+ const result = renderWithGrist(/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(GristWidgetApp, { App: Probe, options }), {
3600
+ emulator: {
3601
+ document: gristDocument,
3602
+ // Granted as the user would in Grist's widget panel: the access the
3603
+ // widget asks for, unless the given document says otherwise.
3604
+ widgetInteraction: gristDocument.widgetInteraction ?? { access_level: options.requiredAccess ?? "read table" }
3605
+ }
3606
+ });
3607
+ try {
3608
+ if (columns.length > 0) result.emulator.setColumnMappings(mapEveryColumn(columns));
3609
+ try {
3610
+ await (0, import_react10.waitFor)(
3611
+ () => {
3612
+ if (crash.error !== void 0) return;
3613
+ const probe = result.container.querySelector("[data-grist-smoke-test]");
3614
+ if (!probe || !probe.hasChildNodes()) throw new Error("not rendered yet");
3615
+ },
3616
+ { timeout }
3617
+ );
3618
+ } catch {
3619
+ const shown = result.container.textContent?.trim().replace(/\s+/g, " ") || "nothing";
3620
+ throw new Error(
3621
+ `smokeTestWidget: the widget didn't render past GristBoundary within ${timeout} ms. The page showed: "${shown.slice(0, 300)}".`
3622
+ );
3623
+ }
3624
+ if (crash.error !== void 0) throw crash.error;
3625
+ } finally {
3626
+ result.unmount();
3627
+ }
3628
+ }
3629
+ var CatchRenderError = class extends import_react9.Component {
3630
+ constructor() {
3631
+ super(...arguments);
3632
+ this.state = { failed: false };
3633
+ }
3634
+ static getDerivedStateFromError() {
3635
+ return { failed: true };
3636
+ }
3637
+ componentDidCatch(error) {
3638
+ this.props.crash.error = error;
3639
+ }
3640
+ render() {
3641
+ return this.state.failed ? null : this.props.children;
3642
+ }
3643
+ };
3644
+ function declaredColumns(options) {
3645
+ return (options.columns ?? []).map(
3646
+ (column) => typeof column === "string" ? { name: column, type: "Any", allowMultiple: false } : { name: column.name, type: firstType(column.type), allowMultiple: column.allowMultiple === true }
3647
+ );
3648
+ }
3649
+ function firstType(type) {
3650
+ return (type ?? "Any").split(",")[0].trim() || "Any";
3651
+ }
3652
+ function mapEveryColumn(columns) {
3653
+ return Object.fromEntries(columns.map((c) => [c.name, c.allowMultiple ? [c.name] : c.name]));
3654
+ }
3655
+ function smokeTestDocument(columns) {
3656
+ return {
3657
+ generatedAt: "1970-01-01T00:00:00.000Z",
3658
+ docName: "Smoke test",
3659
+ mode: "schema+data",
3660
+ tables: {
3661
+ Table1: {
3662
+ columns: Object.fromEntries(columns.map((c) => [c.name, { type: c.type }])),
3663
+ rows: [{ id: 1, ...Object.fromEntries(columns.map((c) => [c.name, sampleValue(c.type)])) }]
3664
+ }
3665
+ }
3666
+ };
3667
+ }
3668
+ function sampleValue(type) {
3669
+ switch (type) {
3670
+ case "Numeric":
3671
+ case "Int":
3672
+ return 42;
3673
+ case "Bool":
3674
+ return true;
3675
+ case "Date":
3676
+ case "DateTime":
3677
+ return 1767225600;
3678
+ // 2026-01-01, as Grist stores it: epoch seconds
3679
+ default:
3680
+ return "Sample";
3681
+ }
3682
+ }
3683
+
1993
3684
  // src/emulator/testing/matchers.ts
1994
3685
  function actionsOf(emulator) {
1995
3686
  return emulator.bus.history().filter((e) => e.type === "user-action").map((e) => e.action);
@@ -2044,7 +3735,7 @@ function waitForEvent(emulator, kindOrPredicate, timeoutOrOptions = 1e3) {
2044
3735
  }
2045
3736
 
2046
3737
  // src/emulator/testing/index.ts
2047
- var import_react2 = require("@testing-library/react");
3738
+ var import_react11 = require("@testing-library/react");
2048
3739
  // Annotate the CommonJS export names for ESM import in node:
2049
3740
  0 && (module.exports = {
2050
3741
  act,
@@ -2052,10 +3743,12 @@ var import_react2 = require("@testing-library/react");
2052
3743
  cleanup,
2053
3744
  eventsOf,
2054
3745
  fireEvent,
3746
+ installJsdomStubs,
2055
3747
  presets,
2056
3748
  render,
2057
3749
  renderWithGrist,
2058
3750
  screen,
3751
+ smokeTestWidget,
2059
3752
  waitFor,
2060
3753
  waitForEvent,
2061
3754
  within