grist-widget-sdk 0.4.0 → 0.5.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 (37) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +79 -208
  3. package/dist/app.cjs +183 -65
  4. package/dist/app.cjs.map +1 -1
  5. package/dist/app.d.cts +5 -100
  6. package/dist/app.d.ts +5 -100
  7. package/dist/app.js +4 -2
  8. package/dist/{chunk-P6VTEKWO.js → chunk-LSALXIGM.js} +82 -10
  9. package/dist/chunk-LSALXIGM.js.map +1 -0
  10. package/dist/{chunk-O7NY46QY.js → chunk-MAPAWVQC.js} +88 -20
  11. package/dist/chunk-MAPAWVQC.js.map +1 -0
  12. package/dist/{chunk-B5ZZNLVS.js → chunk-OXA2VITC.js} +5 -1
  13. package/dist/chunk-OXA2VITC.js.map +1 -0
  14. package/dist/{use-grist-BK0eke9K.d.cts → define-widget-B1FIvI-h.d.cts} +179 -104
  15. package/dist/{use-grist-C5DMZ3wU.d.ts → define-widget-B9gEtJP0.d.ts} +179 -104
  16. package/dist/emulator/testing.cjs +2390 -2243
  17. package/dist/emulator/testing.cjs.map +1 -1
  18. package/dist/emulator/testing.d.cts +27 -9
  19. package/dist/emulator/testing.d.ts +27 -9
  20. package/dist/emulator/testing.js +74 -55
  21. package/dist/emulator/testing.js.map +1 -1
  22. package/dist/emulator.cjs +4 -0
  23. package/dist/emulator.cjs.map +1 -1
  24. package/dist/emulator.d.cts +3 -3
  25. package/dist/emulator.d.ts +3 -3
  26. package/dist/emulator.js +1 -1
  27. package/dist/index.cjs +133 -67
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +2 -2
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +5 -1
  32. package/dist/mount-grist-widget-CkB-3iG_.d.ts +137 -0
  33. package/dist/mount-grist-widget-Dtelqao_.d.cts +137 -0
  34. package/package.json +1 -1
  35. package/dist/chunk-B5ZZNLVS.js.map +0 -1
  36. package/dist/chunk-O7NY46QY.js.map +0 -1
  37. package/dist/chunk-P6VTEKWO.js.map +0 -1
@@ -20,25 +20,29 @@ 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_react11.act,
23
+ act: () => import_react13.act,
24
24
  actionsOf: () => actionsOf,
25
- cleanup: () => import_react11.cleanup,
25
+ cleanup: () => import_react13.cleanup,
26
26
  eventsOf: () => eventsOf,
27
- fireEvent: () => import_react11.fireEvent,
27
+ fireEvent: () => import_react13.fireEvent,
28
28
  installJsdomStubs: () => installJsdomStubs,
29
29
  presets: () => presets,
30
- render: () => import_react11.render,
30
+ render: () => import_react13.render,
31
31
  renderWithGrist: () => renderWithGrist,
32
- screen: () => import_react11.screen,
32
+ screen: () => import_react13.screen,
33
33
  smokeTestWidget: () => smokeTestWidget,
34
- waitFor: () => import_react11.waitFor,
34
+ waitFor: () => import_react13.waitFor,
35
35
  waitForEvent: () => waitForEvent,
36
- within: () => import_react11.within
36
+ within: () => import_react13.within
37
37
  });
38
38
  module.exports = __toCommonJS(testing_exports);
39
39
 
40
40
  // src/emulator/testing/render-with-grist.tsx
41
- var import_react2 = require("@testing-library/react");
41
+ var import_react10 = require("@testing-library/react");
42
+
43
+ // src/app/mount-grist-widget.tsx
44
+ var import_react9 = require("react");
45
+ var import_client = require("react-dom/client");
42
46
 
43
47
  // src/sdk/use-grist.ts
44
48
  var import_react = require("react");
@@ -586,16 +590,24 @@ function createGristApi(runtime) {
586
590
  throw err;
587
591
  }
588
592
  };
589
- const write = async (fn) => {
593
+ let running = 0;
594
+ let failure = null;
595
+ const write = (fn) => {
596
+ if (running++ === 0) failure = null;
590
597
  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
- }
598
+ const done = (async () => {
599
+ try {
600
+ return await fn();
601
+ } catch (err) {
602
+ failure = message(err);
603
+ throw err;
604
+ } finally {
605
+ if (--running === 0) runtime.update(failure ? { actionStatus: "error", actionError: failure } : { actionStatus: "idle" });
606
+ }
607
+ })();
608
+ done.catch(() => {
609
+ });
610
+ return done;
599
611
  };
600
612
  const tableOps = (resolve) => ({
601
613
  getTableId: () => resolve().getTableId(),
@@ -700,38 +712,6 @@ function validateColumnMappings(spec, mappings) {
700
712
  };
701
713
  }
702
714
 
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
715
  // src/sdk/runtime/cells.ts
736
716
  var ok = (value) => ({ ok: true, value });
737
717
  var bad = (message2) => ({ ok: false, message: message2 });
@@ -795,10 +775,13 @@ function checkOne(base, value) {
795
775
  return ok(value);
796
776
  }
797
777
  }
778
+ function basesOf(type) {
779
+ return (type ?? "Any").split(",").map((t) => t.trim().split(":")[0]).filter(Boolean);
780
+ }
798
781
  function checkGristCell(type, value) {
799
782
  const error = formulaError(value);
800
783
  if (error) return bad(`formula error (${error})`);
801
- const bases = (type ?? "Any").split(",").map((t) => t.trim().split(":")[0]).filter(Boolean);
784
+ const bases = basesOf(type);
802
785
  if (bases.length === 0 || bases.includes("Any")) return ok(value === void 0 ? null : value);
803
786
  let first = null;
804
787
  for (const base of bases) {
@@ -808,6 +791,19 @@ function checkGristCell(type, value) {
808
791
  }
809
792
  return first;
810
793
  }
794
+ var LIST_BASES = ["ChoiceList", "RefList", "Attachments"];
795
+ function encodeGristCell(type, value) {
796
+ if (value === null || value === void 0) return value;
797
+ const bases = basesOf(type);
798
+ const accepts = (...names) => names.some((n) => bases.includes(n));
799
+ if (value instanceof Date) return accepts("Date", "DateTime") ? Math.floor(value.getTime() / 1e3) : value;
800
+ if (Array.isArray(value)) {
801
+ if (!accepts(...LIST_BASES) || value[0] === "L") return value;
802
+ return ["L", ...value.map((item) => rowIdOf(item) ?? item)];
803
+ }
804
+ if (accepts("Ref") && typeof value === "object") return rowIdOf(value) ?? value;
805
+ return value;
806
+ }
811
807
  var nameOf = (c) => typeof c === "string" ? c : c.name;
812
808
  var typeOf = (c) => typeof c === "string" ? void 0 : c.type;
813
809
  var isOptional = (c) => typeof c === "object" && c.optional === true;
@@ -848,6 +844,38 @@ function mapAndCheck(record, columns, mappings, schema) {
848
844
  return { value: out, issues };
849
845
  }
850
846
 
847
+ // src/sdk/runtime/derive.ts
848
+ var ACCESS_RANK = { none: 0, "read table": 1, full: 2 };
849
+ function parseAccess(raw) {
850
+ return raw === "none" || raw === "read table" || raw === "full" ? raw : null;
851
+ }
852
+ function accessLabel(level) {
853
+ return level === "full" ? "Full document access" : level === "read table" ? "Read selected table" : "No document access";
854
+ }
855
+ function accessDenied(state) {
856
+ return state.grantedAccess !== null && ACCESS_RANK[state.grantedAccess] < ACCESS_RANK[state.declaredAccess];
857
+ }
858
+ function deriveStatus(state) {
859
+ switch (state.connection) {
860
+ case "connecting":
861
+ return "booting";
862
+ case "not-embedded":
863
+ case "unavailable":
864
+ return "unavailable";
865
+ case "error":
866
+ return "error";
867
+ case "ready":
868
+ return accessDenied(state) ? "error" : "ready";
869
+ }
870
+ }
871
+ var MAPPINGS_OK = Object.freeze({ ok: true, pending: false, missing: [], emptyMultiples: [] });
872
+ var MAPPINGS_PENDING = Object.freeze({ ok: true, pending: true, missing: [], emptyMultiples: [] });
873
+ function deriveColumnMappingStatus(columns, mappings) {
874
+ if (!columns?.length) return MAPPINGS_OK;
875
+ if (mappings === null) return MAPPINGS_PENDING;
876
+ return validateColumnMappings(columns, mappings);
877
+ }
878
+
851
879
  // src/sdk/helpers/grist-widget-interaction.ts
852
880
  function parseGristWidgetLinkingInfo(raw) {
853
881
  if (!raw || typeof raw !== "object" || !("asTarget" in raw)) return void 0;
@@ -1227,110 +1255,167 @@ function useNearestGristState() {
1227
1255
  return { connection, state, cellIssues: issues };
1228
1256
  }
1229
1257
 
1230
- // src/emulator/api/command-api.ts
1231
- function createCommandApiImpl(bus) {
1232
- return {
1233
- async run(name) {
1234
- bus.emit({ type: "user-action", action: ["RunCommand", name] });
1235
- }
1236
- };
1258
+ // src/sdk/define-widget.ts
1259
+ function isGristWidget(value) {
1260
+ return typeof value === "object" && value !== null && "definition" in value && "useGrist" in value;
1237
1261
  }
1238
1262
 
1239
- // src/emulator/core/mappings.ts
1240
- function resolveMappings(state) {
1241
- if (state.selection.mappings !== void 0 && state.selection.mappings !== null) {
1242
- return state.selection.mappings;
1243
- }
1244
- return null;
1263
+ // src/sdk/components/grist-widget-provider.tsx
1264
+ var import_react2 = require("react");
1265
+ var import_jsx_runtime = require("react/jsx-runtime");
1266
+ function GristWidgetProvider({ options, children }) {
1267
+ const [connection] = (0, import_react2.useState)(() => newConnection(options));
1268
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GristContextProvider, { value: connection, children });
1245
1269
  }
1246
1270
 
1247
- // src/emulator/api/custom-section-api.ts
1248
- function createCustomSectionApiImpl(state, bus) {
1249
- return {
1250
- async configure(settings) {
1251
- const config = {
1252
- requiredAccess: settings.requiredAccess,
1253
- columns: settings.columns,
1254
- allowSelectBy: settings.allowSelectBy,
1255
- hasCustomOptions: settings.hasCustomOptions
1271
+ // src/sdk/components/grist-boundary.tsx
1272
+ var import_react3 = require("react");
1273
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1274
+ var shellStyle = {
1275
+ alignItems: "center",
1276
+ background: "var(--background)",
1277
+ boxSizing: "border-box",
1278
+ color: "var(--foreground)",
1279
+ display: "flex",
1280
+ flexDirection: "column",
1281
+ justifyContent: "center",
1282
+ minHeight: "100%",
1283
+ padding: "clamp(16px, 5vh, 32px)",
1284
+ width: "100%"
1285
+ };
1286
+ var cardStyle = {
1287
+ background: "var(--card)",
1288
+ border: "1px solid var(--border)",
1289
+ borderRadius: 12,
1290
+ boxShadow: "0 1px 3px rgb(0 0 0 / 0.12)",
1291
+ maxWidth: 420,
1292
+ padding: "20px 22px",
1293
+ textAlign: "center",
1294
+ width: "100%"
1295
+ };
1296
+ var titleStyle = {
1297
+ color: "var(--foreground)",
1298
+ fontSize: 15,
1299
+ fontWeight: 600,
1300
+ lineHeight: 1.35,
1301
+ margin: 0
1302
+ };
1303
+ var messageStyle = {
1304
+ color: "var(--muted-foreground)",
1305
+ fontSize: 14,
1306
+ lineHeight: 1.5,
1307
+ margin: "10px 0 0"
1308
+ };
1309
+ var buttonStyle = {
1310
+ background: "var(--background)",
1311
+ border: "1px solid var(--border)",
1312
+ borderRadius: 8,
1313
+ color: "var(--foreground)",
1314
+ cursor: "pointer",
1315
+ fontSize: 13,
1316
+ fontWeight: 500,
1317
+ marginTop: 16,
1318
+ padding: "8px 14px"
1319
+ };
1320
+ function GristBoundaryScreen({
1321
+ title,
1322
+ message: message2,
1323
+ tone = "default",
1324
+ showRetry = false,
1325
+ actions,
1326
+ retryLabel = "Try reconnecting",
1327
+ retryBusyLabel = "Reconnecting\u2026"
1328
+ }) {
1329
+ 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: [
1330
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { style: titleStyle, children: title }),
1331
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: messageStyle, children: message2 }),
1332
+ showRetry && actions ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1333
+ "button",
1334
+ {
1335
+ disabled: actions.isReloading,
1336
+ onClick: () => void actions.reload(),
1337
+ style: {
1338
+ ...buttonStyle,
1339
+ cursor: actions.isReloading ? "not-allowed" : "pointer",
1340
+ opacity: actions.isReloading ? 0.7 : 1
1341
+ },
1342
+ type: "button",
1343
+ children: actions.isReloading ? retryBusyLabel : retryLabel
1344
+ }
1345
+ ) : null
1346
+ ] }) });
1347
+ }
1348
+ function boundaryScreen(state, columns) {
1349
+ switch (state.connection) {
1350
+ case "connecting":
1351
+ return { title: "Connecting to Grist", message: "Asking Grist for the access this widget needs\u2026" };
1352
+ case "not-embedded":
1353
+ return {
1354
+ title: "Open inside Grist",
1355
+ message: "Open this page as a custom widget inside a Grist document (it runs in an embedded panel, not on its own).",
1356
+ retry: true
1256
1357
  };
1257
- state.configuration = config;
1258
- bus.emit({ type: "ready", configuration: config });
1259
- bus.emit({ type: "mappings-change", mappings: resolveMappings(state) });
1260
- },
1261
- async mappings() {
1262
- return resolveMappings(state);
1263
- }
1264
- };
1358
+ case "unavailable":
1359
+ return {
1360
+ title: "Open inside Grist",
1361
+ message: "The Grist plugin did not load. Reload the page, or open this URL as a custom widget inside a Grist document.",
1362
+ retry: true
1363
+ };
1364
+ case "error":
1365
+ return {
1366
+ title: "Something went wrong",
1367
+ message: state.connectionError ?? "Something went wrong connecting to Grist. Try again, or reload the page.",
1368
+ tone: "error",
1369
+ retry: true
1370
+ };
1371
+ }
1372
+ if (accessDenied(state)) {
1373
+ return {
1374
+ title: "More access needed",
1375
+ message: `Set Access level to at least "${accessLabel(state.declaredAccess)}" in the Custom widget settings.`,
1376
+ tone: "error",
1377
+ retry: true
1378
+ };
1379
+ }
1380
+ const mapping = deriveColumnMappingStatus(columns, state.mappings);
1381
+ if (mapping.pending) return { title: "Connecting to Grist", message: "Waiting for Grist to report the column mapping\u2026" };
1382
+ if (!mapping.ok) {
1383
+ return {
1384
+ title: "Map your columns",
1385
+ message: "Open the widget settings in Grist (Creator Panel \u2192 Custom widget) and connect each required column to your table."
1386
+ };
1387
+ }
1388
+ return null;
1265
1389
  }
1266
-
1267
- // src/emulator/api/grist-api.ts
1268
- function createGristApiImpl() {
1269
- return {
1270
- async ping() {
1271
- return "pong";
1390
+ function GristBoundary({ children }) {
1391
+ const connection = useGristRuntime();
1392
+ if (!connection) throw new Error("<GristBoundary> must be used inside <GristWidgetProvider> (mountGristWidget renders both).");
1393
+ const state = useGristState(connection);
1394
+ const [isReloading, setIsReloading] = (0, import_react3.useState)(false);
1395
+ const reload = (0, import_react3.useCallback)(async () => {
1396
+ setIsReloading(true);
1397
+ try {
1398
+ await connection.runtime.reload();
1399
+ } catch (err) {
1400
+ console.error("[grist-widget-sdk] reload failed:", err);
1401
+ } finally {
1402
+ setIsReloading(false);
1272
1403
  }
1273
- };
1274
- }
1275
-
1276
- // src/emulator/core/display-helpers.ts
1277
- function inferDisplayHelpersByRef(columns) {
1278
- const out = {};
1279
- const helperColumns = Object.entries(columns).filter(
1280
- ([colId]) => colId.startsWith("gristHelper_")
1281
- );
1282
- const usedHelpers = /* @__PURE__ */ new Set();
1283
- const colRefToColId = /* @__PURE__ */ new Map();
1284
- for (const [colId, col] of Object.entries(columns)) {
1285
- if (typeof col?.colRef === "number" && Number.isFinite(col.colRef)) {
1286
- colRefToColId.set(col.colRef, colId);
1287
- }
1288
- }
1289
- for (const [colId, col] of Object.entries(columns)) {
1290
- const type = col?.type;
1291
- if (typeof type !== "string" || !type.startsWith("Ref:") && !type.startsWith("RefList:")) {
1292
- continue;
1293
- }
1294
- if (typeof col.displayColRef === "number" && Number.isFinite(col.displayColRef)) {
1295
- const helperColId = colRefToColId.get(col.displayColRef);
1296
- if (helperColId) {
1297
- out[colId] = helperColId;
1298
- usedHelpers.add(helperColId);
1299
- continue;
1300
- }
1301
- }
1302
- const match = helperColumns.find(([, helper]) => {
1303
- const formula = helper?.formula;
1304
- return typeof formula === "string" && (formula.includes(`$${colId}.`) || formula.includes(`$${colId}`));
1305
- });
1306
- if (match) {
1307
- out[colId] = match[0];
1308
- usedHelpers.add(match[0]);
1404
+ }, [connection]);
1405
+ const screen2 = boundaryScreen(state, connection.runtime.options.columns);
1406
+ if (!screen2) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
1407
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1408
+ GristBoundaryScreen,
1409
+ {
1410
+ title: screen2.title,
1411
+ message: screen2.message,
1412
+ tone: screen2.tone,
1413
+ showRetry: screen2.retry,
1414
+ actions: { reload, isReloading },
1415
+ retryLabel: screen2.tone === "error" ? "Try again" : "Try reconnecting",
1416
+ retryBusyLabel: screen2.tone === "error" ? "Retrying\u2026" : "Reconnecting\u2026"
1309
1417
  }
1310
- }
1311
- const unresolvedRefs = Object.entries(columns).filter(([colId, col]) => {
1312
- const type = col?.type;
1313
- return !out[colId] && typeof type === "string" && (type.startsWith("Ref:") || type.startsWith("RefList:"));
1314
- }).sort((a, b) => (a[1]?.parentPos ?? Number.MAX_SAFE_INTEGER) - (b[1]?.parentPos ?? Number.MAX_SAFE_INTEGER));
1315
- const helperByPos = helperColumns.filter(([helperId]) => !usedHelpers.has(helperId)).sort((a, b) => (a[1]?.parentPos ?? Number.MAX_SAFE_INTEGER) - (b[1]?.parentPos ?? Number.MAX_SAFE_INTEGER));
1316
- for (const [refId, refCol] of unresolvedRefs) {
1317
- const refPos = refCol?.parentPos ?? Number.MAX_SAFE_INTEGER;
1318
- const candidate = helperByPos.find(([, helperCol]) => {
1319
- const helperPos = helperCol?.parentPos ?? Number.MAX_SAFE_INTEGER;
1320
- return helperPos > refPos;
1321
- });
1322
- if (!candidate) continue;
1323
- out[refId] = candidate[0];
1324
- usedHelpers.add(candidate[0]);
1325
- }
1326
- for (const [refId] of unresolvedRefs) {
1327
- if (out[refId]) continue;
1328
- const candidate = helperByPos.find(([helperId]) => !usedHelpers.has(helperId));
1329
- if (!candidate) continue;
1330
- out[refId] = candidate[0];
1331
- usedHelpers.add(candidate[0]);
1332
- }
1333
- return out;
1418
+ );
1334
1419
  }
1335
1420
 
1336
1421
  // src/sdk/helpers/replica-normalize.ts
@@ -1346,2213 +1431,2316 @@ function normalizeReplicaDocumentInput(doc) {
1346
1431
  };
1347
1432
  }
1348
1433
 
1349
- // src/emulator/core/document.ts
1350
- var SYSTEM_COLUMN = "id";
1351
- var EMULATOR_JS_FORMULA_PLACEHOLDER = "(emulator JavaScript formula)";
1352
- function enrichEmulatorColumnMetadata(col) {
1353
- const originalTrimmed = col.formula?.trim() ?? "";
1354
- const explicitInferred = col.inferredFormula?.trim() ?? "";
1355
- if (col.formulaFn) {
1356
- const inferredFormula = explicitInferred || originalTrimmed || EMULATOR_JS_FORMULA_PLACEHOLDER;
1357
- return {
1358
- ...col,
1359
- ...originalTrimmed ? { formula: col.formula } : {},
1360
- inferredFormula
1361
- };
1362
- }
1363
- if (explicitInferred || originalTrimmed) {
1364
- return {
1365
- ...col,
1366
- inferredFormula: explicitInferred || originalTrimmed || void 0
1367
- };
1368
- }
1369
- return col;
1370
- }
1371
- function sortEmulatorColumnsByParentPos(columns) {
1372
- const entries = Object.entries(columns);
1373
- entries.sort(([ka, a], [kb, b]) => {
1374
- const pa = a.parentPos;
1375
- const pb = b.parentPos;
1376
- if (pa !== void 0 && pb !== void 0 && pa !== pb) return pa - pb;
1377
- if (pa !== void 0 && pb === void 0) return -1;
1378
- if (pa === void 0 && pb !== void 0) return 1;
1379
- return ka.localeCompare(kb);
1380
- });
1381
- return Object.fromEntries(entries);
1382
- }
1383
- function inferColumnsFromRows(rows) {
1384
- const columns = {};
1385
- for (const row of rows) {
1386
- for (const key of Object.keys(row)) {
1387
- if (key === SYSTEM_COLUMN) continue;
1388
- if (!columns[key]) columns[key] = inferColumnType(row[key]);
1389
- }
1390
- }
1391
- return columns;
1392
- }
1393
- function inferColumnType(sample) {
1394
- switch (typeof sample) {
1395
- case "number":
1396
- return { type: "Numeric" };
1397
- case "boolean":
1398
- return { type: "Bool" };
1399
- case "string":
1400
- return { type: "Text" };
1401
- default:
1402
- return { type: "Any" };
1403
- }
1404
- }
1405
- function buildTable(tableId, input) {
1406
- const rows = input.rows ?? [];
1407
- const inferred = inferColumnsFromRows(rows);
1408
- const merged = {};
1409
- for (const [colId, col] of Object.entries({ ...inferred, ...input.columns ?? {} })) {
1410
- merged[colId] = enrichEmulatorColumnMetadata(col);
1411
- }
1412
- const columns = sortEmulatorColumnsByParentPos(merged);
1413
- return {
1414
- tableId,
1415
- label: input.label,
1416
- columns,
1417
- rows: rows.map((row) => ({ ...row }))
1418
- };
1419
- }
1420
- function createDocument(input) {
1421
- if (!input.tables || Object.keys(input.tables).length === 0) {
1422
- throw new Error("createDocument: at least one table is required");
1423
- }
1424
- const normalized = normalizeReplicaDocumentInput(input);
1425
- const tables = /* @__PURE__ */ new Map();
1426
- for (const [tableId, tableInput] of Object.entries(normalized.tables)) {
1427
- tables.set(tableId, buildTable(tableId, tableInput));
1434
+ // src/sdk/use-widget-metadata.ts
1435
+ var import_react4 = require("react");
1436
+ function useWidgetMetadata(input) {
1437
+ (0, import_react4.useEffect)(() => {
1438
+ if (!input) return;
1439
+ const { title, description } = input;
1440
+ const canonicalUrl = window.location.href;
1441
+ document.title = title;
1442
+ setMetaTag("name", "title", title);
1443
+ setMetaTag("name", "description", description);
1444
+ setMetaTag("property", "og:title", title);
1445
+ setMetaTag("property", "og:description", description);
1446
+ setMetaTag("property", "og:type", "website");
1447
+ setMetaTag("property", "og:url", canonicalUrl);
1448
+ setMetaTag("name", "twitter:card", "summary");
1449
+ setMetaTag("name", "twitter:title", title);
1450
+ setMetaTag("name", "twitter:description", description);
1451
+ }, [input]);
1452
+ }
1453
+ function setMetaTag(key, value, content) {
1454
+ let meta = document.head.querySelector(
1455
+ `meta[${key}="${value}"]`
1456
+ );
1457
+ if (!meta) {
1458
+ meta = document.createElement("meta");
1459
+ meta.setAttribute(key, value);
1460
+ document.head.appendChild(meta);
1428
1461
  }
1429
- const firstTableId = tables.keys().next().value;
1430
- const requestedTableId = normalized.selection?.tableId;
1431
- const selectedTableId = requestedTableId && tables.has(requestedTableId) ? requestedTableId : firstTableId;
1432
- const selectedTable = tables.get(selectedTableId);
1433
- const fallbackRowId = typeof selectedTable.rows[0]?.id === "number" ? selectedTable.rows[0].id : null;
1434
- const selection = {
1435
- tableId: selectedTableId,
1436
- rowId: normalized.selection?.rowId ?? fallbackRowId,
1437
- mappings: normalized.selection?.mappings,
1438
- cursor: normalized.selection?.cursor
1439
- };
1440
- return {
1441
- docName: normalized.docName ?? "Untitled emulator document",
1442
- tables,
1443
- selection,
1444
- widgetOptions: { ...normalized.widgetOptions ?? {} },
1445
- widgetInteraction: { ...normalized.widgetInteraction ?? {} },
1446
- configuration: normalized.configuration ?? null,
1447
- accessToken: normalized.accessToken ?? null
1448
- };
1449
- }
1450
- function getTable(state, tableId) {
1451
- const table = state.tables.get(tableId);
1452
- if (!table) throw new Error(`Unknown table: ${tableId}`);
1453
- return table;
1454
- }
1455
- function getSelectedTable(state) {
1456
- return getTable(state, state.selection.tableId);
1462
+ meta.setAttribute("content", content);
1457
1463
  }
1458
- function nextRowId(table) {
1459
- let max = 0;
1460
- for (const row of table.rows) {
1461
- if (typeof row.id === "number" && row.id > max) max = row.id;
1462
- }
1463
- return max + 1;
1464
+
1465
+ // src/sdk/helpers/grist-user-actions.ts
1466
+ function gristUpdateRecordAction(tableId, rowId, patch) {
1467
+ return ["UpdateRecord", tableId, rowId, patch];
1464
1468
  }
1465
- function findRow(table, rowId) {
1466
- return table.rows.find((r) => r.id === rowId);
1469
+ function gristAddRecordAction(tableId, values) {
1470
+ return ["AddRecord", tableId, null, values];
1467
1471
  }
1468
- function findRowIndex(table, rowId) {
1469
- return table.rows.findIndex((r) => r.id === rowId);
1472
+ function gristRemoveRecordAction(tableId, rowId) {
1473
+ return ["RemoveRecord", tableId, rowId];
1470
1474
  }
1471
- function evaluateRowFormulas(state, table, row) {
1472
- for (const [colId, col] of Object.entries(table.columns)) {
1473
- if (col.formulaFn) {
1474
- try {
1475
- row[colId] = col.formulaFn(row, { table, document: state });
1476
- } catch (err) {
1477
- row[colId] = formulaError2(err);
1478
- }
1479
- }
1480
- }
1475
+ function gristBulkUpdateRecordAction(tableId, rowIds, columnarPatch) {
1476
+ return ["BulkUpdateRecord", tableId, rowIds, columnarPatch];
1481
1477
  }
1482
- function escapeRegExp(s) {
1483
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1478
+ function gristBulkAddRecordAction(tableId, values, rowIds) {
1479
+ const length = inferBulkLength(values);
1480
+ const ids = rowIds ?? new Array(length).fill(null);
1481
+ return ["BulkAddRecord", tableId, ids, values];
1484
1482
  }
1485
- function extractRefListIdsFromWire(raw) {
1486
- if (raw == null) return [];
1487
- if (!Array.isArray(raw)) return [];
1488
- const items = raw[0] === "L" ? raw.slice(1) : raw;
1489
- const out = [];
1490
- for (const item of items) {
1491
- if (typeof item === "number" && Number.isFinite(item) && item > 0) {
1492
- out.push(Math.trunc(item));
1493
- continue;
1494
- }
1495
- if (item && typeof item === "object" && "rowId" in item) {
1496
- const r = item.rowId;
1497
- if (typeof r === "number" && Number.isFinite(r) && r > 0) {
1498
- out.push(Math.trunc(r));
1499
- continue;
1500
- }
1501
- }
1502
- const t = coerceGristDocApiRefRowId(item);
1503
- if (t !== void 0 && t > 0) out.push(Math.trunc(t));
1504
- }
1505
- return Array.from(new Set(out));
1483
+ function gristBulkRemoveRecordAction(tableId, rowIds) {
1484
+ return ["BulkRemoveRecord", tableId, rowIds];
1506
1485
  }
1507
- function fieldFromRefListHelperFormula(formula, refColId) {
1508
- if (!formula) return null;
1509
- const re = new RegExp(`\\$${escapeRegExp(refColId)}\\.([A-Za-z_]\\w*)`);
1510
- const m = formula.match(re);
1511
- return m?.[1] ?? null;
1486
+ function inferBulkLength(values) {
1487
+ for (const arr of Object.values(values)) if (Array.isArray(arr)) return arr.length;
1488
+ return 0;
1512
1489
  }
1513
- function firstTextLikeColumnId(columns) {
1514
- let best = null;
1515
- for (const [colId, col] of Object.entries(columns)) {
1516
- if (colId === "id" || colId.startsWith("gristHelper_")) continue;
1517
- const t = col.type;
1518
- if (t !== "Text" && t !== "Choice") continue;
1519
- const pos = col.parentPos ?? 99999;
1520
- if (!best || pos < best.pos) best = { id: colId, pos };
1521
- }
1522
- return best?.id ?? null;
1490
+
1491
+ // src/app/chrome/local-dev-page.tsx
1492
+ var import_react5 = require("react");
1493
+
1494
+ // src/app/chrome/tokens.ts
1495
+ var token = (name, fallback) => `var(--${name}, ${fallback})`;
1496
+ var tokens = {
1497
+ background: token("background", "#ffffff"),
1498
+ foreground: token("foreground", "#0a0a0a"),
1499
+ card: token("card", "#ffffff"),
1500
+ mutedForeground: token("muted-foreground", "#737373"),
1501
+ border: token("border", "#e5e5e5"),
1502
+ primary: token("primary", "#171717"),
1503
+ primaryForeground: token("primary-foreground", "#fafafa"),
1504
+ destructive: token("destructive", "#dc2626"),
1505
+ radius: token("radius", "0.625rem")
1506
+ };
1507
+ var pageStyle = {
1508
+ boxSizing: "border-box",
1509
+ display: "flex",
1510
+ flexDirection: "column",
1511
+ justifyContent: "center",
1512
+ maxWidth: "28rem",
1513
+ minHeight: "100%",
1514
+ margin: "0 auto",
1515
+ padding: "4rem 1.5rem",
1516
+ color: tokens.foreground,
1517
+ font: "inherit"
1518
+ };
1519
+ var eyebrowStyle = {
1520
+ margin: "0 0 0.5rem",
1521
+ textAlign: "center",
1522
+ fontSize: "0.875rem",
1523
+ fontWeight: 500,
1524
+ color: tokens.mutedForeground
1525
+ };
1526
+ var headingStyle = {
1527
+ margin: 0,
1528
+ textAlign: "center",
1529
+ fontSize: "1.5rem",
1530
+ fontWeight: 600,
1531
+ letterSpacing: "-0.01em"
1532
+ };
1533
+ var leadStyle = {
1534
+ margin: "0.75rem auto 2rem",
1535
+ maxWidth: "24rem",
1536
+ textAlign: "center",
1537
+ fontSize: "0.875rem",
1538
+ lineHeight: 1.5,
1539
+ color: tokens.mutedForeground
1540
+ };
1541
+ var cardStyle2 = {
1542
+ boxSizing: "border-box",
1543
+ marginBottom: "1rem",
1544
+ padding: "1.25rem",
1545
+ border: `1px solid ${tokens.border}`,
1546
+ borderRadius: tokens.radius,
1547
+ background: tokens.card
1548
+ };
1549
+ var cardTitleStyle = {
1550
+ margin: "0 0 0.375rem",
1551
+ fontSize: "1rem",
1552
+ fontWeight: 600
1553
+ };
1554
+ var cardTextStyle = {
1555
+ margin: 0,
1556
+ fontSize: "0.875rem",
1557
+ lineHeight: 1.5,
1558
+ color: tokens.mutedForeground
1559
+ };
1560
+ var codeStyle = {
1561
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
1562
+ fontSize: "0.8125rem",
1563
+ color: tokens.foreground
1564
+ };
1565
+
1566
+ // src/app/chrome/widget-title.ts
1567
+ function widgetTitle() {
1568
+ const title = typeof document === "undefined" ? "" : document.title.trim();
1569
+ return title || "Grist widget";
1523
1570
  }
1524
- function formatLabelFromCell(value) {
1525
- if (value == null) return "";
1526
- if (typeof value === "string") return value.trim();
1527
- if (typeof value === "number" || typeof value === "boolean") return String(value);
1528
- if (value instanceof Date) return value.toISOString();
1529
- if (Array.isArray(value) && value[0] === "E") return "";
1530
- if (typeof value === "object" && value && "__ref" in value) {
1531
- const r = value.rowId;
1532
- return typeof r === "number" && Number.isFinite(r) ? String(r) : "";
1533
- }
1534
- return "";
1571
+
1572
+ // src/app/chrome/local-dev-page.tsx
1573
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1574
+ function useHasDemo() {
1575
+ const [has, setHas] = (0, import_react5.useState)(null);
1576
+ (0, import_react5.useEffect)(() => {
1577
+ if (typeof fetch !== "function") return;
1578
+ let live = true;
1579
+ fetch("demo/demo.grist", { method: "HEAD" }).then((res) => live && setHas(res.ok)).catch(() => live && setHas(false));
1580
+ return () => {
1581
+ live = false;
1582
+ };
1583
+ }, []);
1584
+ return has;
1535
1585
  }
1536
- function syncRefListDisplayHelpers(state, table, row) {
1537
- const displayHelpersByRef = inferDisplayHelpersByRef(table.columns);
1538
- for (const [colId, col] of Object.entries(table.columns)) {
1539
- const type = col.type;
1540
- if (typeof type !== "string" || !type.startsWith("RefList:")) continue;
1541
- const helperColId = displayHelpersByRef[colId];
1542
- if (!helperColId) continue;
1543
- const helperMeta = table.columns[helperColId];
1544
- if (!helperMeta || helperMeta.formulaFn) continue;
1545
- const targetTableId = type.slice("RefList:".length);
1546
- const targetTable = state.tables.get(targetTableId);
1547
- if (!targetTable) continue;
1548
- const ids = extractRefListIdsFromWire(row[colId]);
1549
- if (ids.length === 0) {
1550
- row[helperColId] = [];
1551
- continue;
1552
- }
1553
- const fieldId = fieldFromRefListHelperFormula(helperMeta.formula, colId) ?? firstTextLikeColumnId(targetTable.columns);
1554
- const labels = ids.map((rid) => {
1555
- if (!fieldId) return String(rid);
1556
- const trow = findRow(targetTable, rid);
1557
- if (!trow) return String(rid);
1558
- const lbl = formatLabelFromCell(trow[fieldId]);
1559
- return lbl !== "" ? lbl : String(rid);
1560
- });
1561
- row[helperColId] = labels;
1586
+ function LocalDevPage() {
1587
+ const hasDemo = useHasDemo();
1588
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("main", { style: pageStyle, children: [
1589
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { style: eyebrowStyle, children: [
1590
+ widgetTitle(),
1591
+ " \xB7 local dev"
1592
+ ] }),
1593
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { style: headingStyle, children: "Not embedded in Grist" }),
1594
+ /* @__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." }),
1595
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { style: cardStyle2, children: [
1596
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { style: cardTitleStyle, children: "Open it in Grist, in this browser" }),
1597
+ hasDemo === false ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { style: cardTextStyle, children: [
1598
+ "Run ",
1599
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "pnpm demo --save" }),
1600
+ " once (it needs Docker): it writes",
1601
+ " ",
1602
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "public/demo/" }),
1603
+ ", a document for this widget. Then",
1604
+ " ",
1605
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("a", { href: "demo/index.html", children: "demo/index.html" }),
1606
+ " opens it in real Grist, running in the browser, on this dev server's widget: edits reload in place."
1607
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { style: cardTextStyle, children: [
1608
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("a", { href: "demo/index.html", children: "Open demo/index.html" }),
1609
+ ": real Grist running in the browser (no server, nothing saved), with this dev server's widget, so edits reload in place. Its document is",
1610
+ " ",
1611
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "public/demo/demo.grist" }),
1612
+ ": after changing the declared columns, run",
1613
+ " ",
1614
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "pnpm demo --save" }),
1615
+ " (Docker) to regenerate it."
1616
+ ] })
1617
+ ] }),
1618
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { style: cardStyle2, children: [
1619
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { style: cardTitleStyle, children: "Check it against real Grist" }),
1620
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { style: cardTextStyle, children: [
1621
+ "Push to ",
1622
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "dev" }),
1623
+ ", then open its ",
1624
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "/dev/" }),
1625
+ " URL in a custom widget of a real Grist document: later pushes reload it there within seconds. Locally,",
1626
+ " ",
1627
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "pnpm test" }),
1628
+ " runs the widget against an emulated Grist."
1629
+ ] })
1630
+ ] }),
1631
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { style: cardStyle2, children: [
1632
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { style: cardTitleStyle, children: "Starting from another example?" }),
1633
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { style: cardTextStyle, children: [
1634
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "npx create-grist-widget list" }),
1635
+ " shows the examples, and",
1636
+ " ",
1637
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "npx create-grist-widget use <example>" }),
1638
+ " swaps this widget for one."
1639
+ ] })
1640
+ ] })
1641
+ ] });
1642
+ }
1643
+
1644
+ // src/app/chrome/release-page.tsx
1645
+ var import_react7 = require("react");
1646
+
1647
+ // src/app/deploy-path.ts
1648
+ var VERSION_SEGMENT = /^v(\d+\.\d+\.\d+(?:-[\w.]+)?)$/;
1649
+ function parseDeployPath(pathname) {
1650
+ const segments = pathname.replace(/\/+$/, "").split("/");
1651
+ const last = segments[segments.length - 1] ?? "";
1652
+ let channel = null;
1653
+ if (last === "latest") channel = { kind: "latest" };
1654
+ else if (last === "dev") channel = { kind: "dev" };
1655
+ else {
1656
+ const match = VERSION_SEGMENT.exec(last);
1657
+ if (match) channel = { kind: "version", version: match[1] };
1562
1658
  }
1659
+ if (!channel) return { channel: { kind: "latest" }, basePath: withTrailingSlash(pathname) };
1660
+ return { channel, basePath: withTrailingSlash(segments.slice(0, -1).join("/")) };
1563
1661
  }
1564
- function formulaError2(err) {
1565
- const message2 = err instanceof Error ? err.message : String(err);
1566
- return ["E", "Error", message2];
1662
+ function deployChannelLabel(channel) {
1663
+ return channel.kind === "version" ? `v${channel.version}` : channel.kind;
1567
1664
  }
1568
- function synthesizeTablesMeta(state) {
1569
- const ids = [];
1570
- const tableIds = [];
1571
- const labels = [];
1572
- let nextId = 1;
1573
- for (const [tableId, table] of state.tables.entries()) {
1574
- ids.push(nextId);
1575
- tableIds.push(tableId);
1576
- labels.push(table.label ?? null);
1577
- nextId += 1;
1578
- }
1579
- return {
1580
- id: ids,
1581
- tableId: tableIds,
1582
- // Optional human-readable label; real Grist keeps more fields, but these
1583
- // are sufficient for `buildReplicaDocumentFromDocApi` to resolve table ids.
1584
- label: labels
1585
- };
1665
+ function withTrailingSlash(path) {
1666
+ return path.endsWith("/") ? path : `${path}/`;
1586
1667
  }
1587
- function synthesizeTablesColumnMeta(state) {
1588
- const parentIds = [];
1589
- const colIds = [];
1590
- const columnIds = [];
1591
- const types = [];
1592
- const labels = [];
1593
- const parentPos = [];
1594
- const isFormula = [];
1595
- const formulas = [];
1596
- const descriptions = [];
1597
- const widgetOptions = [];
1598
- const displayCol = [];
1599
- const visibleCol = [];
1600
- const tableIdToInternalId = /* @__PURE__ */ new Map();
1601
- {
1602
- let nextId = 1;
1603
- for (const [tableId] of state.tables.entries()) {
1604
- tableIdToInternalId.set(tableId, nextId);
1605
- nextId += 1;
1606
- }
1607
- }
1608
- for (const [tableId, table] of state.tables.entries()) {
1609
- const parentInternalId = tableIdToInternalId.get(tableId);
1610
- if (parentInternalId === void 0) continue;
1611
- for (const [colId, col] of Object.entries(table.columns)) {
1612
- parentIds.push(parentInternalId);
1613
- colIds.push(colId);
1614
- const ref = typeof col.colRef === "number" ? col.colRef : columnIds.length + 1;
1615
- columnIds.push(ref);
1616
- types.push(col.type ?? null);
1617
- labels.push(col.label ?? null);
1618
- parentPos.push(col.parentPos ?? null);
1619
- isFormula.push(Boolean(col.isFormula));
1620
- const formula = typeof col.formula === "string" ? col.formula : null;
1621
- formulas.push(formula);
1622
- descriptions.push(col.description ?? null);
1623
- widgetOptions.push(col.widgetOptions ?? null);
1624
- displayCol.push(typeof col.displayColRef === "number" ? col.displayColRef : null);
1625
- visibleCol.push(typeof col.visibleColRef === "number" ? col.visibleColRef : null);
1626
- }
1627
- }
1628
- return {
1629
- parentId: parentIds,
1630
- colId: colIds,
1631
- id: columnIds,
1632
- type: types,
1633
- label: labels,
1634
- parentPos,
1635
- isFormula,
1636
- formula: formulas,
1637
- description: descriptions,
1638
- // Also expose legacy `descr` for robustness.
1639
- descr: descriptions,
1640
- widgetOptions,
1641
- displayCol,
1642
- visibleCol
1643
- };
1668
+
1669
+ // src/app/released-versions.ts
1670
+ var import_react6 = require("react");
1671
+ function useReleasedVersions(pathname = typeof window === "undefined" ? "/" : window.location.pathname) {
1672
+ const url = `${parseDeployPath(pathname).basePath}versions.json`;
1673
+ const [result, setResult] = (0, import_react6.useState)({ versions: null, error: false });
1674
+ (0, import_react6.useEffect)(() => {
1675
+ let cancelled = false;
1676
+ fetch(url, { cache: "no-store" }).then((res) => res.ok ? res.json() : Promise.reject(new Error(`HTTP ${res.status}`))).then((data) => {
1677
+ if (cancelled) return;
1678
+ if (!Array.isArray(data)) throw new Error("versions.json is not an array");
1679
+ setResult({ versions: data, error: false });
1680
+ }).catch(() => {
1681
+ if (!cancelled) setResult({ versions: null, error: true });
1682
+ });
1683
+ return () => {
1684
+ cancelled = true;
1685
+ };
1686
+ }, [url]);
1687
+ return result;
1644
1688
  }
1645
1689
 
1646
- // src/emulator/core/action-engine.ts
1647
- var EmulatorActionError = class extends Error {
1648
- constructor(action, message2) {
1649
- super(message2);
1650
- this.action = action;
1651
- this.name = "EmulatorActionError";
1652
- }
1653
- };
1654
- function applyUserActions(state, bus, actions) {
1655
- const retValues = [];
1656
- const changed = /* @__PURE__ */ new Set();
1657
- for (const action of actions) {
1658
- bus.emit({ type: "user-action", action });
1659
- const ret = applySingle(state, action, changed);
1660
- retValues.push(ret);
1661
- }
1662
- for (const tableId of changed) {
1663
- bus.emit({ type: "data-change", tableId });
1664
- }
1665
- return { retValues, changedTableIds: [...changed] };
1690
+ // src/app/chrome/release-page.tsx
1691
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1692
+ function ReleasePage({ channel }) {
1693
+ const label = deployChannelLabel(channel);
1694
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("main", { style: pageStyle, children: [
1695
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { style: eyebrowStyle, children: [
1696
+ widgetTitle(),
1697
+ " \xB7 ",
1698
+ label
1699
+ ] }),
1700
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h1", { style: headingStyle, children: "Grist isn't loaded here" }),
1701
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { style: leadStyle, children: [
1702
+ "This is the ",
1703
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("code", { style: codeStyle, children: label }),
1704
+ " build, opened outside Grist. Hover a version to see when it was released."
1705
+ ] }),
1706
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ChannelLinks, { current: channel }),
1707
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(InstallUrl, {})
1708
+ ] });
1666
1709
  }
1667
- function applySingle(state, action, changed) {
1668
- const [name, ...args] = action;
1669
- switch (name) {
1670
- case "AddRecord":
1671
- return addRecord(state, args, changed);
1672
- case "BulkAddRecord":
1673
- return bulkAddRecord(state, args, changed);
1674
- case "UpdateRecord":
1675
- return updateRecord(state, args, changed);
1676
- case "BulkUpdateRecord":
1677
- return bulkUpdateRecord(state, args, changed);
1678
- case "RemoveRecord":
1679
- return removeRecord(state, args, changed);
1680
- case "BulkRemoveRecord":
1681
- return bulkRemoveRecord(state, args, changed);
1682
- case "AddColumn":
1683
- return addColumn(state, args);
1684
- case "AddVisibleColumn":
1685
- return addColumn(state, args);
1686
- case "RemoveColumn":
1687
- return removeColumn(state, args, changed);
1688
- case "RenameColumn":
1689
- return renameColumn(state, args, changed);
1690
- case "ModifyColumn":
1691
- return modifyColumn(state, args, changed);
1692
- case "AddTable":
1693
- return addTable(state, args);
1694
- case "RemoveTable":
1695
- return removeTable(state, args);
1696
- case "RenameTable":
1697
- return renameTable(state, args);
1698
- default:
1699
- throw new EmulatorActionError(action, `Unsupported user action: ${String(name)}`);
1700
- }
1710
+ function ChannelLinks({ current }) {
1711
+ const { basePath } = parseDeployPath(window.location.pathname);
1712
+ const { versions } = useReleasedVersions();
1713
+ const isCurrent = (channel) => channel.kind === current.kind && (channel.kind !== "version" || current.kind === "version" && channel.version === current.version);
1714
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("nav", { "aria-label": "Versions", style: { display: "flex", flexWrap: "wrap", justifyContent: "center", gap: "0.5rem", marginBottom: "2rem" }, children: [
1715
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ChannelLink, { href: `${basePath}latest/`, active: isCurrent({ kind: "latest" }), children: "latest" }),
1716
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ChannelLink, { href: `${basePath}dev/`, active: isCurrent({ kind: "dev" }), children: "dev" }),
1717
+ versions?.map((v) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1718
+ ChannelLink,
1719
+ {
1720
+ href: `${basePath}v${v.version}/`,
1721
+ title: formatDate(v.publishedAt),
1722
+ active: isCurrent({ kind: "version", version: v.version }),
1723
+ children: `v${v.version}`
1724
+ },
1725
+ v.version
1726
+ ))
1727
+ ] });
1701
1728
  }
1702
- function addRecord(state, [tableId, rowId, values], changed) {
1703
- const table = getTable(state, requireString(tableId, "AddRecord.tableId"));
1704
- const id = typeof rowId === "number" ? rowId : nextRowId(table);
1705
- const normalizedValues = normalizeValuesForTable(
1706
- table,
1707
- values ?? {}
1708
- );
1709
- const row = { id, ...normalizedValues };
1710
- ensureColumns(table, normalizedValues);
1711
- evaluateRowFormulas(state, table, row);
1712
- syncRefListDisplayHelpers(state, table, row);
1713
- table.rows.push(row);
1714
- changed.add(table.tableId);
1715
- return id;
1729
+ function ChannelLink({
1730
+ href,
1731
+ title,
1732
+ active,
1733
+ children
1734
+ }) {
1735
+ const style = {
1736
+ padding: "0.25rem 0.75rem",
1737
+ border: `1px solid ${active ? tokens.primary : tokens.border}`,
1738
+ borderRadius: "9999px",
1739
+ background: active ? tokens.primary : "transparent",
1740
+ color: active ? tokens.primaryForeground : tokens.mutedForeground,
1741
+ fontSize: "0.75rem",
1742
+ fontWeight: 500,
1743
+ textDecoration: "none"
1744
+ };
1745
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("a", { href, title, "aria-current": active ? "page" : void 0, style, children });
1716
1746
  }
1717
- function bulkAddRecord(state, [tableId, rowIds, columnar], changed) {
1718
- const table = getTable(state, requireString(tableId, "BulkAddRecord.tableId"));
1719
- const cols = normalizeColumnarForTable(
1720
- table,
1721
- columnar ?? {}
1722
- );
1723
- const ids = Array.isArray(rowIds) ? rowIds : [];
1724
- const length = ids.length || lengthOf(cols);
1725
- const out = [];
1726
- ensureColumns(table, cols, true);
1727
- for (let i = 0; i < length; i++) {
1728
- const id = typeof ids[i] === "number" ? ids[i] : nextRowId(table);
1729
- const row = { id };
1730
- for (const colId of Object.keys(cols)) row[colId] = cols[colId][i];
1731
- evaluateRowFormulas(state, table, row);
1732
- syncRefListDisplayHelpers(state, table, row);
1733
- table.rows.push(row);
1734
- out.push(id);
1747
+ function InstallUrl() {
1748
+ const url = window.location.href;
1749
+ const [copied, setCopied] = (0, import_react7.useState)(false);
1750
+ function copy() {
1751
+ void navigator.clipboard?.writeText(url).then(() => {
1752
+ setCopied(true);
1753
+ setTimeout(() => setCopied(false), 1500);
1754
+ });
1735
1755
  }
1736
- changed.add(table.tableId);
1737
- return out;
1738
- }
1739
- function updateRecord(state, [tableId, rowId, values], changed) {
1740
- const table = getTable(state, requireString(tableId, "UpdateRecord.tableId"));
1741
- const id = requireNumber(rowId, "UpdateRecord.rowId");
1742
- const row = findRow(table, id);
1743
- if (!row) throw new EmulatorActionError(["UpdateRecord", tableId, rowId, values], `Row ${id} not found in ${table.tableId}`);
1744
- const normalizedValues = normalizeValuesForTable(
1745
- table,
1746
- values ?? {}
1747
- );
1748
- ensureColumns(table, normalizedValues);
1749
- Object.assign(row, normalizedValues);
1750
- evaluateRowFormulas(state, table, row);
1751
- syncRefListDisplayHelpers(state, table, row);
1752
- changed.add(table.tableId);
1753
- return null;
1756
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("section", { style: cardStyle2, children: [
1757
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h2", { style: cardTitleStyle, children: "Use it as a Grist custom widget" }),
1758
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: { ...cardTextStyle, marginBottom: "1rem" }, children: "Paste this URL into a custom widget's URL field in Grist." }),
1759
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", gap: "0.5rem" }, children: [
1760
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1761
+ "input",
1762
+ {
1763
+ readOnly: true,
1764
+ value: url,
1765
+ "aria-label": "Widget URL",
1766
+ onFocus: (event) => event.target.select(),
1767
+ style: {
1768
+ flex: 1,
1769
+ minWidth: 0,
1770
+ padding: "0.5rem 0.75rem",
1771
+ border: `1px solid ${tokens.border}`,
1772
+ borderRadius: tokens.radius,
1773
+ background: "transparent",
1774
+ color: tokens.foreground,
1775
+ font: "inherit",
1776
+ fontSize: "0.875rem"
1777
+ }
1778
+ }
1779
+ ),
1780
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1781
+ "button",
1782
+ {
1783
+ type: "button",
1784
+ onClick: copy,
1785
+ style: {
1786
+ padding: "0.5rem 1rem",
1787
+ border: "none",
1788
+ borderRadius: tokens.radius,
1789
+ background: tokens.primary,
1790
+ color: tokens.primaryForeground,
1791
+ font: "inherit",
1792
+ fontSize: "0.875rem",
1793
+ fontWeight: 500,
1794
+ cursor: "pointer"
1795
+ },
1796
+ children: copied ? "Copied" : "Copy"
1797
+ }
1798
+ )
1799
+ ] })
1800
+ ] });
1754
1801
  }
1755
- function bulkUpdateRecord(state, [tableId, rowIds, columnar], changed) {
1756
- const table = getTable(state, requireString(tableId, "BulkUpdateRecord.tableId"));
1757
- const ids = rowIds;
1758
- const cols = normalizeColumnarForTable(
1759
- table,
1760
- columnar ?? {}
1761
- );
1762
- ensureColumns(table, cols, true);
1763
- ids.forEach((id, idx) => {
1764
- const row = findRow(table, id);
1765
- if (!row) throw new EmulatorActionError(["BulkUpdateRecord", tableId, ids, cols], `Row ${id} not found in ${table.tableId}`);
1766
- for (const colId of Object.keys(cols)) row[colId] = cols[colId][idx];
1767
- evaluateRowFormulas(state, table, row);
1768
- syncRefListDisplayHelpers(state, table, row);
1769
- });
1770
- changed.add(table.tableId);
1771
- return null;
1802
+ function formatDate(iso) {
1803
+ const date = new Date(iso);
1804
+ return Number.isNaN(date.getTime()) ? iso : date.toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
1772
1805
  }
1773
- function removeRecord(state, [tableId, rowId], changed) {
1774
- const table = getTable(state, requireString(tableId, "RemoveRecord.tableId"));
1775
- const id = requireNumber(rowId, "RemoveRecord.rowId");
1776
- const idx = findRowIndex(table, id);
1777
- if (idx >= 0) table.rows.splice(idx, 1);
1778
- if (state.selection.tableId === table.tableId && state.selection.rowId === id) {
1779
- state.selection.rowId = table.rows[0]?.id ?? null;
1780
- }
1781
- changed.add(table.tableId);
1782
- return null;
1806
+
1807
+ // src/sdk/helpers/grist-sdk-alerts.ts
1808
+ var TITLE = {
1809
+ "column-mapping": "Column mapping",
1810
+ "mapping-pending": "Column mapping",
1811
+ "section-not-linked": "Widget linking",
1812
+ "source-not-wired": "Widget linking",
1813
+ "action-error": "Action error",
1814
+ "access-insufficient": "Access level",
1815
+ "map-back-skip": "Write skipped",
1816
+ "cell-issues": "Unexpected values"
1817
+ };
1818
+ var SEVERITY = {
1819
+ "column-mapping": "warning",
1820
+ "mapping-pending": "info",
1821
+ "section-not-linked": "warning",
1822
+ "source-not-wired": "warning",
1823
+ "action-error": "error",
1824
+ "access-insufficient": "error",
1825
+ "map-back-skip": "warning",
1826
+ "cell-issues": "warning"
1827
+ };
1828
+ var alert = (kind, message2, ariaRole = "status") => ({
1829
+ id: kind,
1830
+ kind,
1831
+ severity: SEVERITY[kind],
1832
+ title: TITLE[kind],
1833
+ ariaRole,
1834
+ message: message2
1835
+ });
1836
+ function isWidgetSectionNotLinked(interaction) {
1837
+ const linking = parseGristWidgetLinkingInfo(interaction?.linking);
1838
+ return Boolean(linking) && linking.asTarget == null;
1783
1839
  }
1784
- function bulkRemoveRecord(state, [tableId, rowIds], changed) {
1785
- const table = getTable(state, requireString(tableId, "BulkRemoveRecord.tableId"));
1786
- const ids = new Set(rowIds);
1787
- table.rows = table.rows.filter((r) => !ids.has(r.id));
1788
- if (state.selection.tableId === table.tableId && typeof state.selection.rowId === "number" && ids.has(state.selection.rowId)) {
1789
- state.selection.rowId = table.rows[0]?.id ?? null;
1840
+ function getGristSdkAlerts(state, widget, options = {}) {
1841
+ if (deriveStatus(state) !== "ready") return [];
1842
+ const out = [];
1843
+ const mapping = deriveColumnMappingStatus(widget.columns, state.mappings);
1844
+ if (mapping.pending) out.push(alert("mapping-pending", "Waiting for Grist to report column mappings."));
1845
+ else if (!mapping.ok) {
1846
+ const parts = ["Column mapping is incomplete."];
1847
+ if (mapping.missing.length) parts.push(`Required columns: ${mapping.missing.join(", ")}.`);
1848
+ if (mapping.emptyMultiples.length) parts.push(`Assign at least one sheet column for: ${mapping.emptyMultiples.join(", ")}.`);
1849
+ out.push(alert("column-mapping", parts.join(" ")));
1790
1850
  }
1791
- changed.add(table.tableId);
1792
- return null;
1793
- }
1794
- function addColumn(state, [tableId, colId, info]) {
1795
- const table = getTable(state, requireString(tableId, "AddColumn.tableId"));
1796
- const col = requireString(colId, "AddColumn.colId");
1797
- if (table.columns[col]) throw new EmulatorActionError(["AddColumn", tableId, colId], `Column ${col} already exists`);
1798
- table.columns[col] = info ?? {};
1799
- for (const row of table.rows) {
1800
- if (!(col in row)) row[col] = null;
1851
+ const failure = state.actionError ?? state.readError;
1852
+ if (failure && isAccessInsufficientError(failure)) {
1853
+ out.push(
1854
+ alert(
1855
+ "access-insufficient",
1856
+ 'This operation requires "Full document access". Set the Access level to "Full document access" in the Custom widget configuration panel.',
1857
+ "alert"
1858
+ )
1859
+ );
1860
+ } else if (state.actionError) {
1861
+ out.push(alert("action-error", state.actionError, "alert"));
1801
1862
  }
1802
- return null;
1803
- }
1804
- function removeColumn(state, [tableId, colId], changed) {
1805
- const table = getTable(state, requireString(tableId, "RemoveColumn.tableId"));
1806
- const col = requireString(colId, "RemoveColumn.colId");
1807
- delete table.columns[col];
1808
- for (const row of table.rows) delete row[col];
1809
- changed.add(table.tableId);
1810
- return null;
1811
- }
1812
- function renameColumn(state, [tableId, colId, newColId], changed) {
1813
- const table = getTable(state, requireString(tableId, "RenameColumn.tableId"));
1814
- const from = requireString(colId, "RenameColumn.colId");
1815
- const to = requireString(newColId, "RenameColumn.newColId");
1816
- if (!table.columns[from]) throw new EmulatorActionError(["RenameColumn", tableId, from, to], `Column ${from} not found`);
1817
- if (from === to) return null;
1818
- table.columns[to] = table.columns[from];
1819
- delete table.columns[from];
1820
- for (const row of table.rows) {
1821
- if (from in row) {
1822
- row[to] = row[from];
1823
- delete row[from];
1824
- }
1863
+ if (state.mapBackSkipped.length) {
1864
+ const list = state.mapBackSkipped.join(", ");
1865
+ out.push(alert("map-back-skip", `mapBack skipped multi-mapped column${state.mapBackSkipped.length === 1 ? "" : "s"}: ${list}.`));
1825
1866
  }
1826
- changed.add(table.tableId);
1827
- return null;
1828
- }
1829
- function modifyColumn(state, [tableId, colId, info], changed) {
1830
- const table = getTable(state, requireString(tableId, "ModifyColumn.tableId"));
1831
- const col = requireString(colId, "ModifyColumn.colId");
1832
- if (!table.columns[col]) throw new EmulatorActionError(["ModifyColumn", tableId, colId], `Column ${col} not found`);
1833
- table.columns[col] = { ...table.columns[col], ...info };
1834
- changed.add(table.tableId);
1835
- return null;
1836
- }
1837
- function addTable(state, [tableId, columns]) {
1838
- const id = requireString(tableId, "AddTable.tableId");
1839
- if (state.tables.has(id)) throw new EmulatorActionError(["AddTable", tableId], `Table ${id} already exists`);
1840
- const cols = {};
1841
- for (const c of columns ?? []) {
1842
- cols[c.id] = { ...c };
1867
+ const cellIssues = widget.cellIssues ?? [];
1868
+ if (cellIssues.length) {
1869
+ const first = cellIssues[0];
1870
+ const where = first.column ? `${first.column} in row ${first.rowId}` : `row ${first.rowId}`;
1871
+ const more = cellIssues.length > 1 ? ` (and ${cellIssues.length - 1} more)` : "";
1872
+ out.push(alert("cell-issues", `${where}: ${first.message}${more}. Check the column's type, or the cell, in Grist.`));
1843
1873
  }
1844
- state.tables.set(id, { tableId: id, columns: cols, rows: [] });
1845
- return id;
1846
- }
1847
- function removeTable(state, [tableId]) {
1848
- const id = requireString(tableId, "RemoveTable.tableId");
1849
- state.tables.delete(id);
1850
- if (state.selection.tableId === id) {
1851
- const fallback = state.tables.keys().next().value;
1852
- if (fallback) {
1853
- state.selection.tableId = fallback;
1854
- state.selection.rowId = null;
1874
+ const linking = parseGristWidgetLinkingInfo(state.interaction?.linking);
1875
+ if (widget.allowSelectBy) {
1876
+ if (linking && !linking.asSource) {
1877
+ out.push(
1878
+ alert(
1879
+ "source-not-wired",
1880
+ "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."
1881
+ )
1882
+ );
1855
1883
  }
1884
+ } else if (isWidgetSectionNotLinked(state.interaction)) {
1885
+ const hint = options.sectionNotLinkedHint?.trim();
1886
+ out.push(
1887
+ alert(
1888
+ "section-not-linked",
1889
+ "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}` : "")
1890
+ )
1891
+ );
1856
1892
  }
1857
- return null;
1858
- }
1859
- function renameTable(state, [from, to]) {
1860
- const fromId = requireString(from, "RenameTable.from");
1861
- const toId = requireString(to, "RenameTable.to");
1862
- const table = state.tables.get(fromId);
1863
- if (!table) throw new EmulatorActionError(["RenameTable", fromId], `Table ${fromId} not found`);
1864
- state.tables.delete(fromId);
1865
- table.tableId = toId;
1866
- state.tables.set(toId, table);
1867
- if (state.selection.tableId === fromId) state.selection.tableId = toId;
1868
- return null;
1893
+ const suppressed = widget.suppressAlerts ?? [];
1894
+ return out.filter((a) => !suppressed.includes(a.kind));
1869
1895
  }
1870
- function ensureColumns(table, values, bulk = false) {
1871
- for (const col of Object.keys(values)) {
1872
- if (!table.columns[col]) {
1873
- const sample = bulk ? values[col]?.[0] : values[col];
1874
- table.columns[col] = inferType(sample);
1875
- }
1876
- }
1896
+ function useGristSdkAlerts(options) {
1897
+ const { connection, state, cellIssues } = useNearestGristState();
1898
+ if (!connection) return [];
1899
+ const { columns, allowSelectBy, suppressAlerts } = connection.runtime.options;
1900
+ return getGristSdkAlerts(state, { columns, allowSelectBy, suppressAlerts, cellIssues }, options);
1877
1901
  }
1878
- function inferType(sample) {
1879
- switch (typeof sample) {
1880
- case "number":
1881
- return { type: "Numeric" };
1882
- case "boolean":
1883
- return { type: "Bool" };
1884
- case "string":
1885
- return { type: "Text" };
1886
- default:
1887
- return { type: "Any" };
1888
- }
1902
+
1903
+ // src/app/chrome/widget-alerts.tsx
1904
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1905
+ var ALERT_OPTIONS = {
1906
+ sectionNotLinkedHint: "In Grist, select this widget section on the page, then use Link to connect a table, card, or chart view as the selector."
1907
+ };
1908
+ function WidgetAlerts({ children }) {
1909
+ const alerts = useGristSdkAlerts(ALERT_OPTIONS);
1910
+ if (alerts.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children });
1911
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "0.75rem" }, children: [
1912
+ /* @__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)) }),
1913
+ children
1914
+ ] });
1889
1915
  }
1890
- function lengthOf(cols) {
1891
- for (const arr of Object.values(cols)) if (Array.isArray(arr)) return arr.length;
1892
- return 0;
1916
+ function WidgetAlert({ alert: alert2 }) {
1917
+ const linkIcon = alert2.kind === "section-not-linked" || alert2.kind === "source-not-wired";
1918
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { role: alert2.ariaRole, style: { ...alertStyle, ...SEVERITY2[alert2.severity] }, children: [
1919
+ /* @__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, {}) }),
1920
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { minWidth: 0 }, children: [
1921
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: { margin: 0, fontWeight: 500 }, children: alert2.title }),
1922
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: { margin: "0.25rem 0 0", color: tokens.mutedForeground }, children: alert2.message })
1923
+ ] })
1924
+ ] });
1893
1925
  }
1894
- function requireString(value, name) {
1895
- if (typeof value !== "string" || !value) throw new Error(`Expected non-empty string for ${name}`);
1896
- return value;
1926
+ var alertStyle = {
1927
+ boxSizing: "border-box",
1928
+ display: "flex",
1929
+ gap: "0.75rem",
1930
+ padding: "0.75rem 1rem",
1931
+ border: `1px solid ${tokens.border}`,
1932
+ borderRadius: tokens.radius,
1933
+ color: tokens.foreground,
1934
+ fontSize: "0.875rem",
1935
+ lineHeight: 1.5
1936
+ };
1937
+ var SEVERITY2 = {
1938
+ error: { borderColor: tokens.destructive, color: tokens.destructive },
1939
+ warning: { borderColor: "rgb(245 158 11 / 0.4)", background: "rgb(245 158 11 / 0.1)" },
1940
+ info: { background: "transparent" }
1941
+ };
1942
+ var ICON = {
1943
+ width: 16,
1944
+ height: 16,
1945
+ viewBox: "0 0 24 24",
1946
+ fill: "none",
1947
+ stroke: "currentColor",
1948
+ strokeWidth: 2,
1949
+ strokeLinecap: "round",
1950
+ strokeLinejoin: "round"
1951
+ };
1952
+ function WarningIcon() {
1953
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { ...ICON, children: [
1954
+ /* @__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" }),
1955
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 9v4" }),
1956
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 17h.01" })
1957
+ ] });
1897
1958
  }
1898
- function requireNumber(value, name) {
1899
- if (typeof value !== "number" || Number.isNaN(value)) throw new Error(`Expected number for ${name}`);
1900
- return value;
1959
+ function LinkIcon() {
1960
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { ...ICON, children: [
1961
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M9 17H7A5 5 0 0 1 7 7h2" }),
1962
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M15 7h2a5 5 0 1 1 0 10h-2" }),
1963
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "8", x2: "16", y1: "12", y2: "12" })
1964
+ ] });
1901
1965
  }
1902
- function normalizeValuesForTable(table, values) {
1903
- const out = {};
1904
- for (const [colId, value] of Object.entries(values)) {
1905
- out[colId] = normalizeCellValueForType(value, table.columns[colId]?.type);
1906
- }
1907
- return out;
1966
+
1967
+ // src/app/widget-theme.tsx
1968
+ var import_react8 = require("react");
1969
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1970
+ var STORAGE_KEY = "theme";
1971
+ var DARK_QUERY = "(prefers-color-scheme: dark)";
1972
+ var THEMES = ["light", "dark", "system"];
1973
+ var ThemeContext = (0, import_react8.createContext)(null);
1974
+ function EmbeddedThemeProvider({ children }) {
1975
+ const gristTheme = useNearestGristState().state.theme;
1976
+ const system = useSystemTheme(gristTheme === null);
1977
+ const resolvedTheme = gristTheme ?? system;
1978
+ useApplyTheme(resolvedTheme);
1979
+ const value = (0, import_react8.useMemo)(
1980
+ () => ({ theme: gristTheme ?? "system", resolvedTheme, setTheme: noop }),
1981
+ [gristTheme, resolvedTheme]
1982
+ );
1983
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ThemeContext.Provider, { value, children });
1908
1984
  }
1909
- function normalizeColumnarForTable(table, columns) {
1910
- const out = {};
1911
- for (const [colId, values] of Object.entries(columns)) {
1912
- const type = table.columns[colId]?.type;
1913
- out[colId] = Array.isArray(values) ? values.map((value) => normalizeCellValueForType(value, type)) : [];
1914
- }
1915
- return out;
1985
+ function StandaloneThemeProvider({ children }) {
1986
+ const [theme, setThemeState] = (0, import_react8.useState)(() => readStoredTheme() ?? "system");
1987
+ const system = useSystemTheme(theme === "system");
1988
+ const resolvedTheme = theme === "system" ? system : theme;
1989
+ useApplyTheme(resolvedTheme);
1990
+ const setTheme = (0, import_react8.useCallback)((next) => {
1991
+ writeStoredTheme(next);
1992
+ setThemeState(next);
1993
+ }, []);
1994
+ (0, import_react8.useEffect)(() => {
1995
+ const onKeyDown = (event) => {
1996
+ if (event.repeat || event.metaKey || event.ctrlKey || event.altKey) return;
1997
+ if (event.key.toLowerCase() !== "d" || isEditableTarget(event.target)) return;
1998
+ setThemeState((current) => {
1999
+ const currentResolved = current === "system" ? systemTheme() : current;
2000
+ const next = currentResolved === "dark" ? "light" : "dark";
2001
+ writeStoredTheme(next);
2002
+ return next;
2003
+ });
2004
+ };
2005
+ window.addEventListener("keydown", onKeyDown);
2006
+ return () => window.removeEventListener("keydown", onKeyDown);
2007
+ }, []);
2008
+ (0, import_react8.useEffect)(() => {
2009
+ const onStorage = (event) => {
2010
+ if (event.key !== STORAGE_KEY) return;
2011
+ setThemeState(isTheme(event.newValue) ? event.newValue : "system");
2012
+ };
2013
+ window.addEventListener("storage", onStorage);
2014
+ return () => window.removeEventListener("storage", onStorage);
2015
+ }, []);
2016
+ const value = (0, import_react8.useMemo)(
2017
+ () => ({ theme, resolvedTheme, setTheme }),
2018
+ [theme, resolvedTheme, setTheme]
2019
+ );
2020
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ThemeContext.Provider, { value, children });
1916
2021
  }
1917
- function normalizeCellValueForType(value, type) {
1918
- if (value === null || value === void 0 || typeof type !== "string") return value;
1919
- if (type.startsWith("Ref:")) return normalizeRefValue(value);
1920
- if (type.startsWith("RefList:")) return normalizeRefListValue(value);
1921
- return value;
2022
+ function useSystemTheme(active) {
2023
+ const [theme, setTheme] = (0, import_react8.useState)(systemTheme);
2024
+ (0, import_react8.useEffect)(() => {
2025
+ if (!active || typeof window.matchMedia !== "function") return;
2026
+ const query = window.matchMedia(DARK_QUERY);
2027
+ const onChange = () => setTheme(query.matches ? "dark" : "light");
2028
+ onChange();
2029
+ query.addEventListener("change", onChange);
2030
+ return () => query.removeEventListener("change", onChange);
2031
+ }, [active]);
2032
+ return theme;
1922
2033
  }
1923
- function extractRefRowId(value) {
1924
- if (value === null || value === void 0) return null;
1925
- if (value && typeof value === "object" && "__ref" in value) {
1926
- const inner = value.rowId;
1927
- return extractRefRowId(inner);
1928
- }
1929
- if (value && typeof value === "object" && "rowId" in value) {
1930
- return extractRefRowId(value.rowId);
1931
- }
1932
- const fromTuple = coerceGristDocApiRefRowId(value);
1933
- if (fromTuple !== void 0 && fromTuple > 0) return Math.trunc(fromTuple);
1934
- return toPositiveInt(value);
2034
+ function useApplyTheme(theme) {
2035
+ (0, import_react8.useEffect)(() => {
2036
+ const root = document.documentElement;
2037
+ if (root.classList.contains(theme) && !root.classList.contains(other(theme))) return;
2038
+ const restoreTransitions = suspendTransitions();
2039
+ root.classList.remove(other(theme));
2040
+ root.classList.add(theme);
2041
+ restoreTransitions();
2042
+ }, [theme]);
1935
2043
  }
1936
- function normalizeRefValue(value) {
1937
- return extractRefRowId(value);
2044
+ function systemTheme() {
2045
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return "light";
2046
+ return window.matchMedia(DARK_QUERY).matches ? "dark" : "light";
1938
2047
  }
1939
- function normalizeRefListValue(value) {
1940
- const toIdList = (items) => Array.from(
1941
- new Set(items.map((item) => extractRefRowId(item)).filter((item) => item !== null))
2048
+ function other(theme) {
2049
+ return theme === "dark" ? "light" : "dark";
2050
+ }
2051
+ function suspendTransitions() {
2052
+ const style = document.createElement("style");
2053
+ style.appendChild(
2054
+ document.createTextNode("*,*::before,*::after{-webkit-transition:none!important;transition:none!important}")
1942
2055
  );
1943
- if (Array.isArray(value) && value[0] === "L") {
1944
- return ["L", ...toIdList(value.slice(1))];
1945
- }
1946
- if (Array.isArray(value)) {
1947
- return ["L", ...toIdList(value)];
1948
- }
1949
- if (typeof value === "string") {
1950
- const tokens2 = value.split(",").map((t) => t.trim()).filter(Boolean);
1951
- return ["L", ...toIdList(tokens2)];
1952
- }
1953
- if (value && typeof value === "object" && Array.isArray(value.rowIds)) {
1954
- return ["L", ...toIdList(value.rowIds)];
2056
+ document.head.appendChild(style);
2057
+ return () => {
2058
+ window.getComputedStyle(document.body);
2059
+ requestAnimationFrame(() => requestAnimationFrame(() => style.remove()));
2060
+ };
2061
+ }
2062
+ function isEditableTarget(target) {
2063
+ if (!(target instanceof HTMLElement)) return false;
2064
+ return target.isContentEditable || target.closest("input, textarea, select, [contenteditable='true']") !== null;
2065
+ }
2066
+ function isTheme(value) {
2067
+ return value !== null && THEMES.includes(value);
2068
+ }
2069
+ function readStoredTheme() {
2070
+ try {
2071
+ const value = localStorage.getItem(STORAGE_KEY);
2072
+ return isTheme(value) ? value : null;
2073
+ } catch {
2074
+ return null;
1955
2075
  }
1956
- return ["L"];
1957
2076
  }
1958
- function toPositiveInt(value) {
1959
- if (typeof value === "number" && Number.isFinite(value) && value > 0) return Math.trunc(value);
1960
- if (typeof value === "string") {
1961
- const n = Number.parseInt(value.trim(), 10);
1962
- if (Number.isFinite(n) && n > 0) return n;
2077
+ function writeStoredTheme(theme) {
2078
+ try {
2079
+ localStorage.setItem(STORAGE_KEY, theme);
2080
+ } catch {
1963
2081
  }
1964
- return null;
2082
+ }
2083
+ function noop() {
1965
2084
  }
1966
2085
 
1967
- // src/emulator/core/columnar.ts
1968
- function rowsToColumnar(table, rows, options = {}) {
1969
- const cols = Object.keys(table.columns);
1970
- const out = { id: rows.map((r) => r.id) };
1971
- for (const col of cols) {
1972
- out[col] = rows.map((r) => encodeForWire(r[col], table.columns[col]?.type, options.keepEncoded));
2086
+ // src/app/mount-grist-widget.tsx
2087
+ var import_jsx_runtime7 = require("react/jsx-runtime");
2088
+ function entryProps(entry, dev = false) {
2089
+ const widget = isGristWidget(entry.widget) ? entry.widget : void 0;
2090
+ const options = widget?.options ?? entry.GRIST_OPTIONS;
2091
+ if (!options) {
2092
+ throw new Error(
2093
+ "mountGristWidget: src/app/App.tsx exports neither `widget` (defineGristWidget) nor `GRIST_OPTIONS`."
2094
+ );
1973
2095
  }
1974
- return out;
2096
+ const props = {
2097
+ App: entry.default,
2098
+ options,
2099
+ metadata: widget?.metadata ?? entry.WIDGET_METADATA,
2100
+ dev
2101
+ };
2102
+ if ("Alerts" in entry) props.Alerts = entry.Alerts;
2103
+ if (entry.LocalDevPage) props.LocalDevPage = entry.LocalDevPage;
2104
+ if (entry.ReleasePage) props.ReleasePage = entry.ReleasePage;
2105
+ return props;
1975
2106
  }
1976
- function rowToRecord(table, row, options = {}) {
1977
- const out = { id: row.id };
1978
- for (const col of Object.keys(table.columns)) {
1979
- out[col] = encodeForWire(row[col], table.columns[col]?.type, options.keepEncoded);
2107
+ function GristWidgetApp({
2108
+ App,
2109
+ options,
2110
+ metadata,
2111
+ dev = false,
2112
+ Alerts = WidgetAlerts,
2113
+ LocalDevPage: LocalDevPage2 = LocalDevPage,
2114
+ ReleasePage: ReleasePage2 = ReleasePage
2115
+ }) {
2116
+ const Wrap = Alerts ?? PassThrough;
2117
+ if (isEmbedded2()) {
2118
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(GristWidgetProvider, { options, children: [
2119
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PageMetadata, { metadata }),
2120
+ /* @__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_react9.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(App, {}) }) }) }) }) })
2121
+ ] });
1980
2122
  }
1981
- return out;
2123
+ const channel = parseDeployPath(window.location.pathname).channel;
2124
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(StandaloneThemeProvider, { children: [
2125
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PageMetadata, { metadata }),
2126
+ /* @__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 }) })
2127
+ ] });
1982
2128
  }
1983
- function encodeForWire(value, type, keepEncoded) {
1984
- if (value instanceof Date) {
1985
- const epoch = Math.floor(value.getTime() / 1e3);
1986
- if (keepEncoded) {
1987
- return type && type.startsWith("DateTime") ? ["D", epoch, "UTC"] : ["d", epoch];
1988
- }
1989
- return epoch;
1990
- }
1991
- return value;
2129
+ var FILL = { minHeight: "100%", width: "100%" };
2130
+ function isEmbedded2() {
2131
+ return typeof window !== "undefined" && window.self !== window.top;
2132
+ }
2133
+ function PageMetadata({ metadata }) {
2134
+ useWidgetMetadata(metadata?.title ? { title: metadata.title, description: metadata.description ?? "" } : null);
2135
+ return null;
2136
+ }
2137
+ function PassThrough({ children }) {
2138
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children });
1992
2139
  }
1993
2140
 
1994
- // src/emulator/api/grist-doc-api.ts
1995
- function createGristDocApiImpl(state, bus) {
2141
+ // src/emulator/testing/declared-document.ts
2142
+ function declaredColumns(options) {
2143
+ return (options.columns ?? []).map(
2144
+ (column) => typeof column === "string" ? { name: column, type: "Any", declared: void 0, allowMultiple: false } : { name: column.name, type: firstType(column.type), declared: column.type, allowMultiple: column.allowMultiple === true }
2145
+ );
2146
+ }
2147
+ function firstType(type) {
2148
+ return (type ?? "Any").split(",")[0].trim() || "Any";
2149
+ }
2150
+ function mapEveryColumn(columns) {
2151
+ return Object.fromEntries(columns.map((c) => [c.name, c.allowMultiple ? [c.name] : c.name]));
2152
+ }
2153
+ function declaredDocument(columns, rows) {
2154
+ const given = rows ?? [Object.fromEntries(columns.map((c) => [c.name, sampleValue(c.type)]))];
1996
2155
  return {
1997
- async getDocName() {
1998
- return state.docName;
1999
- },
2000
- async listTables() {
2001
- return [...state.tables.keys()];
2002
- },
2003
- async fetchTable(tableId) {
2004
- if (tableId === "_grist_Tables") {
2005
- return synthesizeTablesMeta(state);
2006
- }
2007
- if (tableId === "_grist_Tables_column") {
2008
- return synthesizeTablesColumnMeta(state);
2009
- }
2010
- const table = getTable(state, tableId);
2011
- return rowsToColumnar(table, table.rows);
2012
- },
2013
- async applyUserActions(actions) {
2014
- const result = applyUserActions(state, bus, actions);
2015
- return { retValues: result.retValues };
2016
- },
2017
- async getAccessToken() {
2018
- if (!state.accessToken) {
2019
- const err = new Error("getAccessToken not available (no token configured on emulator)");
2020
- throw err;
2156
+ generatedAt: "1970-01-01T00:00:00.000Z",
2157
+ docName: "Test document",
2158
+ mode: "schema+data",
2159
+ tables: {
2160
+ Table1: {
2161
+ columns: Object.fromEntries(columns.map((c) => [c.name, { type: c.type }])),
2162
+ rows: given.map((row, i) => ({
2163
+ id: typeof row.id === "number" ? row.id : i + 1,
2164
+ ...Object.fromEntries(columns.map((c) => [c.name, encodeGristCell(c.declared, row[c.name] ?? null)]))
2165
+ }))
2021
2166
  }
2022
- return { ...state.accessToken };
2023
2167
  }
2024
2168
  };
2025
2169
  }
2026
-
2027
- // src/emulator/api/grist-view.ts
2028
- function createGristViewImpl(state, bus) {
2029
- return {
2030
- async fetchSelectedTable(options) {
2031
- const table = getSelectedTable(state);
2032
- return rowsToColumnar(table, table.rows, { keepEncoded: options?.keepEncoded });
2033
- },
2034
- async fetchSelectedRecord(rowId, options) {
2035
- const table = getSelectedTable(state);
2036
- const row = findRow(table, rowId);
2037
- if (!row) throw new Error(`No row ${rowId} in ${table.tableId}`);
2038
- return rowToRecord(table, row, { keepEncoded: options?.keepEncoded });
2039
- },
2040
- async allowSelectBy() {
2041
- return state.configuration?.allowSelectBy ?? false;
2042
- },
2043
- async setSelectedRows(rowIds) {
2044
- if (!rowIds || rowIds.length === 0) {
2045
- state.selection.rowId = null;
2046
- } else {
2047
- state.selection.rowId = rowIds[0];
2048
- }
2049
- bus.emit({
2050
- type: "cursor-change",
2051
- tableId: state.selection.tableId,
2052
- rowId: state.selection.rowId
2053
- });
2054
- },
2055
- async setCursorPos(cursor) {
2056
- const tableId = state.selection.tableId;
2057
- if (typeof cursor.rowId === "number") state.selection.rowId = cursor.rowId;
2058
- state.selection.cursor = { ...state.selection.cursor, ...cursor };
2059
- bus.emit({
2060
- type: "cursor-change",
2061
- tableId,
2062
- rowId: state.selection.rowId,
2063
- cursor: state.selection.cursor
2064
- });
2065
- }
2066
- };
2170
+ function sampleValue(type) {
2171
+ switch (type) {
2172
+ case "Numeric":
2173
+ case "Int":
2174
+ return 42;
2175
+ case "Bool":
2176
+ return true;
2177
+ case "Date":
2178
+ case "DateTime":
2179
+ return 1767225600;
2180
+ // 2026-01-01, as Grist stores it: epoch seconds
2181
+ default:
2182
+ return "Sample";
2183
+ }
2067
2184
  }
2068
2185
 
2069
- // src/emulator/api/widget-api.ts
2070
- function createWidgetApiImpl(state, bus) {
2071
- function emit() {
2072
- bus.emit({
2073
- type: "options-change",
2074
- options: { ...state.widgetOptions },
2075
- interaction: { ...state.widgetInteraction }
2076
- });
2077
- }
2186
+ // src/emulator/api/command-api.ts
2187
+ function createCommandApiImpl(bus) {
2078
2188
  return {
2079
- async getOption(key) {
2080
- return state.widgetOptions[key];
2081
- },
2082
- async setOption(key, value) {
2083
- state.widgetOptions[key] = value;
2084
- emit();
2085
- },
2086
- async getOptions() {
2087
- return Object.keys(state.widgetOptions).length > 0 ? { ...state.widgetOptions } : null;
2088
- },
2089
- // Replaces every option, as Grist's WidgetAPI.setOptions does.
2090
- async setOptions(options) {
2091
- state.widgetOptions = { ...options };
2092
- emit();
2093
- },
2094
- async clearOptions() {
2095
- state.widgetOptions = {};
2096
- emit();
2189
+ async run(name) {
2190
+ bus.emit({ type: "user-action", action: ["RunCommand", name] });
2097
2191
  }
2098
2192
  };
2099
2193
  }
2100
2194
 
2101
- // src/emulator/api/index.ts
2102
- function createApiBundle(state, bus) {
2103
- return {
2104
- GristView: createGristViewImpl(state, bus),
2105
- GristDocAPI: createGristDocApiImpl(state, bus),
2106
- WidgetAPI: createWidgetApiImpl(state, bus),
2107
- CustomSectionAPI: createCustomSectionApiImpl(state, bus),
2108
- GristAPI: createGristApiImpl(),
2109
- CommandAPI: createCommandApiImpl(bus)
2110
- };
2195
+ // src/emulator/core/mappings.ts
2196
+ function resolveMappings(state) {
2197
+ if (state.selection.mappings !== void 0 && state.selection.mappings !== null) {
2198
+ return state.selection.mappings;
2199
+ }
2200
+ return null;
2111
2201
  }
2112
2202
 
2113
- // src/emulator/core/event-bus.ts
2114
- function createEventBus(options = {}) {
2115
- const limit = options.historyLimit ?? 200;
2116
- const listeners = /* @__PURE__ */ new Set();
2117
- const history = [];
2203
+ // src/emulator/api/custom-section-api.ts
2204
+ function createCustomSectionApiImpl(state, bus) {
2118
2205
  return {
2119
- emit(event) {
2120
- history.push(event);
2121
- if (history.length > limit) history.shift();
2122
- for (const fn of [...listeners]) {
2123
- try {
2124
- fn(event);
2125
- } catch (err) {
2126
- console.error("[grist-emulator] event listener threw:", err);
2127
- }
2128
- }
2129
- },
2130
- on(listener) {
2131
- listeners.add(listener);
2132
- return () => {
2133
- listeners.delete(listener);
2206
+ async configure(settings) {
2207
+ const config = {
2208
+ requiredAccess: settings.requiredAccess,
2209
+ columns: settings.columns,
2210
+ allowSelectBy: settings.allowSelectBy,
2211
+ hasCustomOptions: settings.hasCustomOptions
2134
2212
  };
2213
+ state.configuration = config;
2214
+ bus.emit({ type: "ready", configuration: config });
2215
+ bus.emit({ type: "mappings-change", mappings: resolveMappings(state) });
2135
2216
  },
2136
- history() {
2137
- return history;
2138
- },
2139
- clear() {
2140
- history.length = 0;
2217
+ async mappings() {
2218
+ return resolveMappings(state);
2141
2219
  }
2142
2220
  };
2143
2221
  }
2144
2222
 
2145
- // src/emulator/theme-message.ts
2146
- function buildGristThemeHostMessage(appearance, options) {
2223
+ // src/emulator/api/grist-api.ts
2224
+ function createGristApiImpl() {
2147
2225
  return {
2148
- theme: {
2149
- appearance,
2150
- name: appearance,
2151
- colors: {}
2152
- },
2153
- ...options?.fromReady ? { fromReady: true } : {}
2226
+ async ping() {
2227
+ return "pong";
2228
+ }
2154
2229
  };
2155
2230
  }
2156
2231
 
2157
- // src/sdk/helpers/grist-user-actions.ts
2158
- function gristUpdateRecordAction(tableId, rowId, patch) {
2159
- return ["UpdateRecord", tableId, rowId, patch];
2160
- }
2161
- function gristAddRecordAction(tableId, values) {
2162
- return ["AddRecord", tableId, null, values];
2163
- }
2164
- function gristRemoveRecordAction(tableId, rowId) {
2165
- return ["RemoveRecord", tableId, rowId];
2166
- }
2167
- function gristBulkUpdateRecordAction(tableId, rowIds, columnarPatch) {
2168
- return ["BulkUpdateRecord", tableId, rowIds, columnarPatch];
2169
- }
2170
- function gristBulkAddRecordAction(tableId, values, rowIds) {
2171
- const length = inferBulkLength(values);
2172
- const ids = rowIds ?? new Array(length).fill(null);
2173
- return ["BulkAddRecord", tableId, ids, values];
2174
- }
2175
- function gristBulkRemoveRecordAction(tableId, rowIds) {
2176
- return ["BulkRemoveRecord", tableId, rowIds];
2177
- }
2178
- function inferBulkLength(values) {
2179
- for (const arr of Object.values(values)) if (Array.isArray(arr)) return arr.length;
2180
- return 0;
2181
- }
2182
-
2183
- // src/emulator/transports/inline.ts
2184
- function createInlineTransport(options) {
2185
- const { state, bus, api } = options;
2186
- const target = options.target ?? globalThis;
2187
- installFakeIframeContext(target);
2188
- const recordCbs = /* @__PURE__ */ new Map();
2189
- const recordsCbs = /* @__PURE__ */ new Map();
2190
- const optionsCbs = /* @__PURE__ */ new Set();
2191
- const newRecordCbs = /* @__PURE__ */ new Set();
2192
- const generic = /* @__PURE__ */ new Map();
2193
- let lastRecord = null;
2194
- let lastRecords = null;
2195
- let lastOptions = null;
2196
- let lastNewRecord = void 0;
2197
- const buildInteraction = options.buildInteraction ?? (() => ({
2198
- access_level: state.configuration?.requiredAccess ?? "read table",
2199
- ...state.widgetInteraction
2200
- }));
2201
- async function pushRecord(rowId) {
2202
- const mappings = resolveMappings(state);
2203
- if (rowId === "new") {
2204
- lastNewRecord = mappings;
2205
- for (const cb of newRecordCbs) {
2206
- try {
2207
- cb(mappings);
2208
- } catch (err) {
2209
- logErr(err);
2210
- }
2211
- }
2212
- return;
2213
- }
2214
- if (rowId === null) {
2215
- lastRecord = { record: null, mappings };
2216
- for (const [cb] of recordCbs) {
2217
- try {
2218
- cb(null, mappings);
2219
- } catch (err) {
2220
- logErr(err);
2221
- }
2222
- }
2223
- return;
2232
+ // src/emulator/core/display-helpers.ts
2233
+ function inferDisplayHelpersByRef(columns) {
2234
+ const out = {};
2235
+ const helperColumns = Object.entries(columns).filter(
2236
+ ([colId]) => colId.startsWith("gristHelper_")
2237
+ );
2238
+ const usedHelpers = /* @__PURE__ */ new Set();
2239
+ const colRefToColId = /* @__PURE__ */ new Map();
2240
+ for (const [colId, col] of Object.entries(columns)) {
2241
+ if (typeof col?.colRef === "number" && Number.isFinite(col.colRef)) {
2242
+ colRefToColId.set(col.colRef, colId);
2224
2243
  }
2225
- let record = null;
2226
- try {
2227
- record = await api.GristView.fetchSelectedRecord(rowId);
2228
- } catch (err) {
2229
- logErr(err);
2244
+ }
2245
+ for (const [colId, col] of Object.entries(columns)) {
2246
+ const type = col?.type;
2247
+ if (typeof type !== "string" || !type.startsWith("Ref:") && !type.startsWith("RefList:")) {
2248
+ continue;
2230
2249
  }
2231
- lastRecord = { record, mappings };
2232
- for (const [cb, opts] of recordCbs) {
2233
- try {
2234
- const r = opts?.keepEncoded ? await api.GristView.fetchSelectedRecord(rowId, { keepEncoded: true }) : record;
2235
- cb(r, mappings);
2236
- } catch (err) {
2237
- logErr(err);
2250
+ if (typeof col.displayColRef === "number" && Number.isFinite(col.displayColRef)) {
2251
+ const helperColId = colRefToColId.get(col.displayColRef);
2252
+ if (helperColId) {
2253
+ out[colId] = helperColId;
2254
+ usedHelpers.add(helperColId);
2255
+ continue;
2238
2256
  }
2239
2257
  }
2258
+ const match = helperColumns.find(([, helper]) => {
2259
+ const formula = helper?.formula;
2260
+ return typeof formula === "string" && (formula.includes(`$${colId}.`) || formula.includes(`$${colId}`));
2261
+ });
2262
+ if (match) {
2263
+ out[colId] = match[0];
2264
+ usedHelpers.add(match[0]);
2265
+ }
2240
2266
  }
2241
- async function pushRecords() {
2242
- const mappings = resolveMappings(state);
2243
- let baseRows = [];
2244
- try {
2245
- const columnar = await api.GristView.fetchSelectedTable({});
2246
- baseRows = columnsToRows(columnar);
2247
- } catch (err) {
2248
- logErr(err);
2249
- }
2250
- lastRecords = { records: baseRows, mappings };
2251
- for (const [cb, opts] of recordsCbs) {
2252
- try {
2253
- const fetchOptions = coerceFetchOptions(opts);
2254
- const rows = fetchOptions?.keepEncoded ? columnsToRows(await api.GristView.fetchSelectedTable({ keepEncoded: true })) : baseRows;
2255
- cb(rows, mappings);
2256
- } catch (err) {
2257
- logErr(err);
2258
- }
2267
+ const unresolvedRefs = Object.entries(columns).filter(([colId, col]) => {
2268
+ const type = col?.type;
2269
+ return !out[colId] && typeof type === "string" && (type.startsWith("Ref:") || type.startsWith("RefList:"));
2270
+ }).sort((a, b) => (a[1]?.parentPos ?? Number.MAX_SAFE_INTEGER) - (b[1]?.parentPos ?? Number.MAX_SAFE_INTEGER));
2271
+ const helperByPos = helperColumns.filter(([helperId]) => !usedHelpers.has(helperId)).sort((a, b) => (a[1]?.parentPos ?? Number.MAX_SAFE_INTEGER) - (b[1]?.parentPos ?? Number.MAX_SAFE_INTEGER));
2272
+ for (const [refId, refCol] of unresolvedRefs) {
2273
+ const refPos = refCol?.parentPos ?? Number.MAX_SAFE_INTEGER;
2274
+ const candidate = helperByPos.find(([, helperCol]) => {
2275
+ const helperPos = helperCol?.parentPos ?? Number.MAX_SAFE_INTEGER;
2276
+ return helperPos > refPos;
2277
+ });
2278
+ if (!candidate) continue;
2279
+ out[refId] = candidate[0];
2280
+ usedHelpers.add(candidate[0]);
2281
+ }
2282
+ for (const [refId] of unresolvedRefs) {
2283
+ if (out[refId]) continue;
2284
+ const candidate = helperByPos.find(([helperId]) => !usedHelpers.has(helperId));
2285
+ if (!candidate) continue;
2286
+ out[refId] = candidate[0];
2287
+ usedHelpers.add(candidate[0]);
2288
+ }
2289
+ return out;
2290
+ }
2291
+
2292
+ // src/emulator/core/document.ts
2293
+ var SYSTEM_COLUMN = "id";
2294
+ var EMULATOR_JS_FORMULA_PLACEHOLDER = "(emulator JavaScript formula)";
2295
+ function enrichEmulatorColumnMetadata(col) {
2296
+ const originalTrimmed = col.formula?.trim() ?? "";
2297
+ const explicitInferred = col.inferredFormula?.trim() ?? "";
2298
+ if (col.formulaFn) {
2299
+ const inferredFormula = explicitInferred || originalTrimmed || EMULATOR_JS_FORMULA_PLACEHOLDER;
2300
+ return {
2301
+ ...col,
2302
+ ...originalTrimmed ? { formula: col.formula } : {},
2303
+ inferredFormula
2304
+ };
2305
+ }
2306
+ if (explicitInferred || originalTrimmed) {
2307
+ return {
2308
+ ...col,
2309
+ inferredFormula: explicitInferred || originalTrimmed || void 0
2310
+ };
2311
+ }
2312
+ return col;
2313
+ }
2314
+ function sortEmulatorColumnsByParentPos(columns) {
2315
+ const entries = Object.entries(columns);
2316
+ entries.sort(([ka, a], [kb, b]) => {
2317
+ const pa = a.parentPos;
2318
+ const pb = b.parentPos;
2319
+ if (pa !== void 0 && pb !== void 0 && pa !== pb) return pa - pb;
2320
+ if (pa !== void 0 && pb === void 0) return -1;
2321
+ if (pa === void 0 && pb !== void 0) return 1;
2322
+ return ka.localeCompare(kb);
2323
+ });
2324
+ return Object.fromEntries(entries);
2325
+ }
2326
+ function inferColumnsFromRows(rows) {
2327
+ const columns = {};
2328
+ for (const row of rows) {
2329
+ for (const key of Object.keys(row)) {
2330
+ if (key === SYSTEM_COLUMN) continue;
2331
+ if (!columns[key]) columns[key] = inferColumnType(row[key]);
2259
2332
  }
2260
2333
  }
2261
- async function fetchSelectedTableShaped(opts) {
2262
- const fetchOptions = coerceFetchOptions(opts);
2263
- const columnar = await api.GristView.fetchSelectedTable(
2264
- fetchOptions ? { keepEncoded: fetchOptions.keepEncoded } : {}
2265
- );
2266
- if (fetchOptions?.format === "rows") return columnsToRows(columnar);
2267
- return columnar;
2334
+ return columns;
2335
+ }
2336
+ function inferColumnType(sample) {
2337
+ switch (typeof sample) {
2338
+ case "number":
2339
+ return { type: "Numeric" };
2340
+ case "boolean":
2341
+ return { type: "Bool" };
2342
+ case "string":
2343
+ return { type: "Text" };
2344
+ default:
2345
+ return { type: "Any" };
2268
2346
  }
2269
- function pushOptions() {
2270
- const opts = Object.keys(state.widgetOptions).length > 0 ? { ...state.widgetOptions } : null;
2271
- const interaction = buildInteraction();
2272
- lastOptions = { options: opts, interaction };
2273
- for (const cb of optionsCbs) {
2347
+ }
2348
+ function buildTable(tableId, input) {
2349
+ const rows = input.rows ?? [];
2350
+ const inferred = inferColumnsFromRows(rows);
2351
+ const merged = {};
2352
+ for (const [colId, col] of Object.entries({ ...inferred, ...input.columns ?? {} })) {
2353
+ merged[colId] = enrichEmulatorColumnMetadata(col);
2354
+ }
2355
+ const columns = sortEmulatorColumnsByParentPos(merged);
2356
+ return {
2357
+ tableId,
2358
+ label: input.label,
2359
+ columns,
2360
+ rows: rows.map((row) => ({ ...row }))
2361
+ };
2362
+ }
2363
+ function createDocument(input) {
2364
+ if (!input.tables || Object.keys(input.tables).length === 0) {
2365
+ throw new Error("createDocument: at least one table is required");
2366
+ }
2367
+ const normalized = normalizeReplicaDocumentInput(input);
2368
+ const tables = /* @__PURE__ */ new Map();
2369
+ for (const [tableId, tableInput] of Object.entries(normalized.tables)) {
2370
+ tables.set(tableId, buildTable(tableId, tableInput));
2371
+ }
2372
+ const firstTableId = tables.keys().next().value;
2373
+ const requestedTableId = normalized.selection?.tableId;
2374
+ const selectedTableId = requestedTableId && tables.has(requestedTableId) ? requestedTableId : firstTableId;
2375
+ const selectedTable = tables.get(selectedTableId);
2376
+ const fallbackRowId = typeof selectedTable.rows[0]?.id === "number" ? selectedTable.rows[0].id : null;
2377
+ const selection = {
2378
+ tableId: selectedTableId,
2379
+ rowId: normalized.selection?.rowId ?? fallbackRowId,
2380
+ mappings: normalized.selection?.mappings,
2381
+ cursor: normalized.selection?.cursor
2382
+ };
2383
+ return {
2384
+ docName: normalized.docName ?? "Untitled emulator document",
2385
+ tables,
2386
+ selection,
2387
+ widgetOptions: { ...normalized.widgetOptions ?? {} },
2388
+ widgetInteraction: { ...normalized.widgetInteraction ?? {} },
2389
+ configuration: normalized.configuration ?? null,
2390
+ accessToken: normalized.accessToken ?? null
2391
+ };
2392
+ }
2393
+ function getTable(state, tableId) {
2394
+ const table = state.tables.get(tableId);
2395
+ if (!table) throw new Error(`Unknown table: ${tableId}`);
2396
+ return table;
2397
+ }
2398
+ function getSelectedTable(state) {
2399
+ return getTable(state, state.selection.tableId);
2400
+ }
2401
+ function nextRowId(table) {
2402
+ let max = 0;
2403
+ for (const row of table.rows) {
2404
+ if (typeof row.id === "number" && row.id > max) max = row.id;
2405
+ }
2406
+ return max + 1;
2407
+ }
2408
+ function findRow(table, rowId) {
2409
+ return table.rows.find((r) => r.id === rowId);
2410
+ }
2411
+ function findRowIndex(table, rowId) {
2412
+ return table.rows.findIndex((r) => r.id === rowId);
2413
+ }
2414
+ function evaluateRowFormulas(state, table, row) {
2415
+ for (const [colId, col] of Object.entries(table.columns)) {
2416
+ if (col.formulaFn) {
2274
2417
  try {
2275
- cb(opts, interaction);
2418
+ row[colId] = col.formulaFn(row, { table, document: state });
2276
2419
  } catch (err) {
2277
- logErr(err);
2420
+ row[colId] = formulaError2(err);
2278
2421
  }
2279
2422
  }
2280
2423
  }
2281
- const offBus = bus.on(async (event) => {
2282
- switch (event.type) {
2283
- case "data-change":
2284
- await pushRecords();
2285
- await pushRecord(state.selection.rowId);
2286
- break;
2287
- case "cursor-change":
2288
- await pushRecord(event.rowId);
2289
- break;
2290
- case "options-change":
2291
- pushOptions();
2292
- break;
2293
- case "ready":
2294
- pushOptions();
2295
- if (options.initialTheme) {
2296
- notify("message", buildGristThemeHostMessage(options.initialTheme, { fromReady: true }));
2297
- }
2298
- await pushRecords();
2299
- await pushRecord(state.selection.rowId);
2300
- break;
2301
- case "mappings-change":
2302
- await pushRecords();
2303
- await pushRecord(state.selection.rowId);
2304
- break;
2305
- case "theme-change":
2306
- notify("message", event.message);
2307
- break;
2424
+ }
2425
+ function escapeRegExp(s) {
2426
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2427
+ }
2428
+ function extractRefListIdsFromWire(raw) {
2429
+ if (raw == null) return [];
2430
+ if (!Array.isArray(raw)) return [];
2431
+ const items = raw[0] === "L" ? raw.slice(1) : raw;
2432
+ const out = [];
2433
+ for (const item of items) {
2434
+ if (typeof item === "number" && Number.isFinite(item) && item > 0) {
2435
+ out.push(Math.trunc(item));
2436
+ continue;
2308
2437
  }
2309
- });
2310
- function makeTableOps(tableId) {
2311
- return {
2312
- async getTableId() {
2313
- return tableId;
2314
- },
2315
- async create(records, options2) {
2316
- const list = Array.isArray(records) ? records : [records];
2317
- const ids = [];
2318
- for (const r of list) {
2319
- const result = applyUserActions(state, bus, [
2320
- gristAddRecordAction(tableId, parseFields(r.fields, options2))
2321
- ]);
2322
- ids.push({ id: result.retValues[0] });
2323
- }
2324
- return Array.isArray(records) ? ids : ids[0];
2325
- },
2326
- async update(records, options2) {
2327
- const list = Array.isArray(records) ? records : [records];
2328
- const actions = list.map((r) => gristUpdateRecordAction(tableId, r.id, parseFields(r.fields, options2)));
2329
- applyUserActions(state, bus, actions);
2330
- },
2331
- async upsert(records, options2) {
2332
- const list = Array.isArray(records) ? records : [records];
2333
- const allowAdd = options2?.add !== false;
2334
- const allowUpdate = options2?.update !== false;
2335
- const table = getTable(state, tableId);
2336
- const actions = [];
2337
- for (const r of list) {
2338
- const matches = table.rows.filter(
2339
- (row) => Object.entries(r.require).every(([k, v]) => row[k] === v)
2340
- );
2341
- if (matches.length > 0) {
2342
- if (!allowUpdate) continue;
2343
- const fields = parseFields(r.fields ?? {}, options2);
2344
- for (const m of matches) actions.push(gristUpdateRecordAction(tableId, m.id, fields));
2345
- } else if (allowAdd) {
2346
- actions.push(gristAddRecordAction(tableId, { ...r.require, ...parseFields(r.fields ?? {}, options2) }));
2347
- }
2348
- }
2349
- if (actions.length > 0) applyUserActions(state, bus, actions);
2350
- },
2351
- async destroy(recordIds) {
2352
- if (Array.isArray(recordIds)) {
2353
- applyUserActions(state, bus, [gristBulkRemoveRecordAction(tableId, recordIds)]);
2354
- } else {
2355
- applyUserActions(state, bus, [gristRemoveRecordAction(tableId, recordIds)]);
2356
- }
2357
- void gristBulkAddRecordAction;
2358
- void gristBulkUpdateRecordAction;
2359
- }
2360
- };
2361
- }
2362
- function notify(event, ...args) {
2363
- const set = generic.get(event);
2364
- if (!set) return;
2365
- for (const fn of set) {
2366
- try {
2367
- fn(...args);
2368
- } catch (err) {
2369
- logErr(err);
2370
- }
2371
- }
2372
- }
2373
- const grist2 = {
2374
- async ready(opts) {
2375
- await api.CustomSectionAPI.configure(opts ? { ...opts, onEditOptions: void 0 } : {});
2376
- },
2377
- onRecord(cb, opts) {
2378
- recordCbs.set(cb, opts);
2379
- if (lastRecord) {
2380
- try {
2381
- cb(lastRecord.record, lastRecord.mappings);
2382
- } catch (err) {
2383
- logErr(err);
2384
- }
2385
- }
2386
- },
2387
- onRecords(cb, opts) {
2388
- recordsCbs.set(cb, opts);
2389
- if (lastRecords) {
2390
- try {
2391
- cb(lastRecords.records, lastRecords.mappings);
2392
- } catch (err) {
2393
- logErr(err);
2394
- }
2395
- }
2396
- },
2397
- onOptions(cb) {
2398
- optionsCbs.add(cb);
2399
- if (lastOptions) {
2400
- try {
2401
- cb(lastOptions.options, lastOptions.interaction);
2402
- } catch (err) {
2403
- logErr(err);
2404
- }
2405
- }
2406
- },
2407
- onNewRecord(cb) {
2408
- newRecordCbs.add(cb);
2409
- if (lastNewRecord !== void 0) {
2410
- try {
2411
- cb(lastNewRecord);
2412
- } catch (err) {
2413
- logErr(err);
2414
- }
2415
- }
2416
- },
2417
- mapColumnNames(data) {
2418
- const mappings = resolveMappings(state);
2419
- if (!mappings) return null;
2420
- const out = {};
2421
- let allMapped = true;
2422
- for (const [logical, real] of Object.entries(mappings)) {
2423
- if (Array.isArray(real)) {
2424
- out[logical] = real.map((r) => data[r]);
2425
- } else if (typeof real === "string") {
2426
- out[logical] = data[real];
2427
- } else {
2428
- allMapped = false;
2429
- }
2430
- }
2431
- return allMapped ? out : null;
2432
- },
2433
- mapColumnNamesBack(data) {
2434
- const mappings = resolveMappings(state);
2435
- if (!mappings) return data;
2436
- const out = {};
2437
- for (const [logical, real] of Object.entries(mappings)) {
2438
- if (typeof real === "string" && logical in data) {
2439
- out[real] = data[logical];
2440
- }
2438
+ if (item && typeof item === "object" && "rowId" in item) {
2439
+ const r = item.rowId;
2440
+ if (typeof r === "number" && Number.isFinite(r) && r > 0) {
2441
+ out.push(Math.trunc(r));
2442
+ continue;
2441
2443
  }
2442
- return out;
2443
- },
2444
- getOption: (k) => api.WidgetAPI.getOption(k),
2445
- setOption: (k, v) => api.WidgetAPI.setOption(k, v),
2446
- getOptions: () => api.WidgetAPI.getOptions(),
2447
- setOptions: (o) => api.WidgetAPI.setOptions(o),
2448
- clearOptions: () => api.WidgetAPI.clearOptions(),
2449
- setCursorPos: (pos) => api.GristView.setCursorPos(pos),
2450
- setSelectedRows: (rowIds) => api.GristView.setSelectedRows(rowIds),
2451
- fetchSelectedTable: (opts) => fetchSelectedTableShaped(opts),
2452
- fetchSelectedRecord: (rowId, opts) => api.GristView.fetchSelectedRecord(rowId, coerceFetchOptions(opts)),
2453
- docApi: {
2454
- getDocName: () => api.GristDocAPI.getDocName(),
2455
- listTables: () => api.GristDocAPI.listTables(),
2456
- fetchTable: (id) => api.GristDocAPI.fetchTable(id),
2457
- applyUserActions: (actions, options2) => api.GristDocAPI.applyUserActions(actions, options2),
2458
- getAccessToken: (opts) => api.GristDocAPI.getAccessToken(opts),
2459
- fetchSelectedTable: (opts) => fetchSelectedTableShaped(opts),
2460
- fetchSelectedRecord: (rowId, opts) => api.GristView.fetchSelectedRecord(rowId, coerceFetchOptions(opts))
2461
- },
2462
- get selectedTable() {
2463
- return makeTableOps(state.selection.tableId);
2464
- },
2465
- getTable(tableId) {
2466
- return makeTableOps(tableId ?? state.selection.tableId);
2467
- },
2468
- sectionApi: {
2469
- configure: (settings) => api.CustomSectionAPI.configure(settings),
2470
- mappings: () => api.CustomSectionAPI.mappings()
2471
- },
2472
- on(eventName, listener) {
2473
- let set = generic.get(eventName);
2474
- if (!set) generic.set(eventName, set = /* @__PURE__ */ new Set());
2475
- set.add(listener);
2476
- return () => set.delete(listener);
2477
- }
2478
- };
2479
- target.grist = grist2;
2480
- return {
2481
- grist: grist2,
2482
- dispose() {
2483
- offBus();
2484
- const t = target;
2485
- if (t.grist === grist2) delete t.grist;
2486
- recordCbs.clear();
2487
- recordsCbs.clear();
2488
- optionsCbs.clear();
2489
- newRecordCbs.clear();
2490
- generic.clear();
2491
- lastRecord = null;
2492
- lastRecords = null;
2493
- lastOptions = null;
2494
- lastNewRecord = void 0;
2495
- },
2496
- fireDataChange(tableId) {
2497
- bus.emit({ type: "data-change", tableId: tableId ?? state.selection.tableId });
2498
- },
2499
- fireCursor(rowId) {
2500
- const id = rowId === void 0 ? state.selection.rowId : rowId;
2501
- bus.emit({ type: "cursor-change", tableId: state.selection.tableId, rowId: id });
2502
- },
2503
- fireOptions() {
2504
- pushOptions();
2505
- },
2506
- fireTheme(message2) {
2507
- bus.emit({ type: "theme-change", message: message2 });
2508
- },
2509
- emitInitialState() {
2510
- pushOptions();
2511
- void pushRecords().then(() => pushRecord(state.selection.rowId));
2512
2444
  }
2513
- };
2514
- }
2515
- function columnsToRows(columnar) {
2516
- const cols = Object.keys(columnar);
2517
- if (cols.length === 0) return [];
2518
- const length = (columnar[cols[0]] ?? []).length;
2519
- const rows = [];
2520
- for (let i = 0; i < length; i++) {
2521
- const row = {};
2522
- for (const c of cols) row[c] = columnar[c][i];
2523
- rows.push(row);
2445
+ const t = coerceGristDocApiRefRowId(item);
2446
+ if (t !== void 0 && t > 0) out.push(Math.trunc(t));
2524
2447
  }
2525
- return rows;
2448
+ return Array.from(new Set(out));
2526
2449
  }
2527
- function parseFields(fields, _options) {
2528
- return { ...fields };
2450
+ function fieldFromRefListHelperFormula(formula, refColId) {
2451
+ if (!formula) return null;
2452
+ const re = new RegExp(`\\$${escapeRegExp(refColId)}\\.([A-Za-z_]\\w*)`);
2453
+ const m = formula.match(re);
2454
+ return m?.[1] ?? null;
2529
2455
  }
2530
- function coerceFetchOptions(opts) {
2531
- if (!opts) return void 0;
2532
- const format = opts.format === "rows" || opts.format === "columns" ? opts.format : void 0;
2533
- return { keepEncoded: opts.keepEncoded, format };
2456
+ function firstTextLikeColumnId(columns) {
2457
+ let best = null;
2458
+ for (const [colId, col] of Object.entries(columns)) {
2459
+ if (colId === "id" || colId.startsWith("gristHelper_")) continue;
2460
+ const t = col.type;
2461
+ if (t !== "Text" && t !== "Choice") continue;
2462
+ const pos = col.parentPos ?? 99999;
2463
+ if (!best || pos < best.pos) best = { id: colId, pos };
2464
+ }
2465
+ return best?.id ?? null;
2534
2466
  }
2535
- function logErr(err) {
2536
- console.error("[grist-emulator/inline]", err);
2467
+ function formatLabelFromCell(value) {
2468
+ if (value == null) return "";
2469
+ if (typeof value === "string") return value.trim();
2470
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
2471
+ if (value instanceof Date) return value.toISOString();
2472
+ if (Array.isArray(value) && value[0] === "E") return "";
2473
+ if (typeof value === "object" && value && "__ref" in value) {
2474
+ const r = value.rowId;
2475
+ return typeof r === "number" && Number.isFinite(r) ? String(r) : "";
2476
+ }
2477
+ return "";
2537
2478
  }
2538
- function installFakeIframeContext(target) {
2539
- try {
2540
- if (target.self !== target.top) return;
2541
- Object.defineProperty(target, "top", {
2542
- configurable: true,
2543
- get: () => ({})
2479
+ function syncRefListDisplayHelpers(state, table, row) {
2480
+ const displayHelpersByRef = inferDisplayHelpersByRef(table.columns);
2481
+ for (const [colId, col] of Object.entries(table.columns)) {
2482
+ const type = col.type;
2483
+ if (typeof type !== "string" || !type.startsWith("RefList:")) continue;
2484
+ const helperColId = displayHelpersByRef[colId];
2485
+ if (!helperColId) continue;
2486
+ const helperMeta = table.columns[helperColId];
2487
+ if (!helperMeta || helperMeta.formulaFn) continue;
2488
+ const targetTableId = type.slice("RefList:".length);
2489
+ const targetTable = state.tables.get(targetTableId);
2490
+ if (!targetTable) continue;
2491
+ const ids = extractRefListIdsFromWire(row[colId]);
2492
+ if (ids.length === 0) {
2493
+ row[helperColId] = [];
2494
+ continue;
2495
+ }
2496
+ const fieldId = fieldFromRefListHelperFormula(helperMeta.formula, colId) ?? firstTextLikeColumnId(targetTable.columns);
2497
+ const labels = ids.map((rid) => {
2498
+ if (!fieldId) return String(rid);
2499
+ const trow = findRow(targetTable, rid);
2500
+ if (!trow) return String(rid);
2501
+ const lbl = formatLabelFromCell(trow[fieldId]);
2502
+ return lbl !== "" ? lbl : String(rid);
2544
2503
  });
2545
- } catch {
2504
+ row[helperColId] = labels;
2546
2505
  }
2547
2506
  }
2548
-
2549
- // src/emulator/validators.ts
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(),
2559
- formulaKind: formulaKindSchema.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()
2565
- }).strict();
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()
2574
- }).strict();
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()
2580
- }).strict();
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()
2586
- }).strict();
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()
2591
- }).strict();
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),
2598
- selection: replicaSelectionSchema.optional(),
2599
- widgetOptions: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
2600
- widgetInteraction: import_zod.z.unknown().optional(),
2601
- configuration: replicaWidgetConfigurationSchema.optional(),
2602
- accessToken: replicaAccessTokenSchema.nullable().optional()
2603
- }).strict().refine((value) => Object.keys(value.tables).length > 0, {
2604
- message: "at least one table is required",
2605
- path: ["tables"]
2606
- });
2607
- function formatZodError(prefix, error) {
2608
- const details = error.issues.map((issue) => {
2609
- const where = issue.path.length > 0 ? issue.path.join(".") : "<root>";
2610
- return `${where}: ${issue.message}`;
2611
- }).join("; ");
2612
- return new Error(`${prefix}: ${details}`);
2613
- }
2614
- function safeParseDocument(input) {
2615
- return replicaDocumentSchema.safeParse(input);
2507
+ function formulaError2(err) {
2508
+ const message2 = err instanceof Error ? err.message : String(err);
2509
+ return ["E", "Error", message2];
2616
2510
  }
2617
- function parseDocument(input) {
2618
- const result = safeParseDocument(input);
2619
- if (!result.success) {
2620
- throw formatZodError("parseDocument", result.error);
2511
+ function synthesizeTablesMeta(state) {
2512
+ const ids = [];
2513
+ const tableIds = [];
2514
+ const labels = [];
2515
+ let nextId = 1;
2516
+ for (const [tableId, table] of state.tables.entries()) {
2517
+ ids.push(nextId);
2518
+ tableIds.push(tableId);
2519
+ labels.push(table.label ?? null);
2520
+ nextId += 1;
2621
2521
  }
2622
- return result.data;
2522
+ return {
2523
+ id: ids,
2524
+ tableId: tableIds,
2525
+ // Optional human-readable label; real Grist keeps more fields, but these
2526
+ // are sufficient for `buildReplicaDocumentFromDocApi` to resolve table ids.
2527
+ label: labels
2528
+ };
2623
2529
  }
2624
-
2625
- // src/emulator/create-grist-emulator.ts
2626
- function createGristEmulator(options) {
2627
- const validatedDocument = parseDocument(options.document);
2628
- const state = createDocument({
2629
- ...validatedDocument,
2630
- widgetOptions: options.widgetOptions ?? validatedDocument.widgetOptions,
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 ?? {}
2634
- });
2635
- const bus = createEventBus();
2636
- const api = createApiBundle(state, bus);
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;
2646
- function apply(actions) {
2647
- return applyUserActions(state, bus, actions);
2530
+ function synthesizeTablesColumnMeta(state) {
2531
+ const parentIds = [];
2532
+ const colIds = [];
2533
+ const columnIds = [];
2534
+ const types = [];
2535
+ const labels = [];
2536
+ const parentPos = [];
2537
+ const isFormula = [];
2538
+ const formulas = [];
2539
+ const descriptions = [];
2540
+ const widgetOptions = [];
2541
+ const displayCol = [];
2542
+ const visibleCol = [];
2543
+ const tableIdToInternalId = /* @__PURE__ */ new Map();
2544
+ {
2545
+ let nextId = 1;
2546
+ for (const [tableId] of state.tables.entries()) {
2547
+ tableIdToInternalId.set(tableId, nextId);
2548
+ nextId += 1;
2549
+ }
2648
2550
  }
2649
- return {
2650
- state,
2651
- bus,
2652
- api,
2653
- transport,
2654
- grist: grist2,
2655
- apply,
2656
- mutate: {
2657
- addRow: (tableId, fields) => apply([["AddRecord", tableId, null, fields]]).retValues[0],
2658
- updateRow: (tableId, rowId, fields) => {
2659
- apply([["UpdateRecord", tableId, rowId, fields]]);
2660
- },
2661
- removeRow: (tableId, rowId) => {
2662
- apply([["RemoveRecord", tableId, rowId]]);
2663
- }
2664
- },
2665
- selectRow(rowId) {
2666
- state.selection.rowId = rowId;
2667
- bus.emit({ type: "cursor-change", tableId: state.selection.tableId, rowId });
2668
- },
2669
- selectTable(tableId) {
2670
- if (!state.tables.has(tableId)) throw new Error(`Unknown table: ${tableId}`);
2671
- state.selection.tableId = tableId;
2672
- const first = state.tables.get(tableId).rows[0]?.id ?? null;
2673
- state.selection.rowId = first;
2674
- bus.emit({ type: "data-change", tableId });
2675
- bus.emit({ type: "cursor-change", tableId, rowId: first });
2676
- },
2677
- setColumnMappings(mappings) {
2678
- if (mappings === void 0) {
2679
- delete state.selection.mappings;
2680
- } else {
2681
- state.selection.mappings = mappings;
2682
- }
2683
- bus.emit({ type: "mappings-change", mappings: resolveMappings(state) });
2684
- },
2685
- async setOptions(opts) {
2686
- await api.WidgetAPI.setOptions(opts);
2687
- },
2688
- setInteraction(interaction) {
2689
- state.widgetInteraction = { ...interaction };
2690
- bus.emit({ type: "options-change", options: { ...state.widgetOptions }, interaction: { ...interaction } });
2691
- },
2692
- theme: {
2693
- set(next) {
2694
- bus.emit({ type: "theme-change", message: buildGristThemeHostMessage(next) });
2695
- }
2696
- },
2697
- dispose() {
2698
- transport.dispose();
2699
- bus.clear();
2551
+ for (const [tableId, table] of state.tables.entries()) {
2552
+ const parentInternalId = tableIdToInternalId.get(tableId);
2553
+ if (parentInternalId === void 0) continue;
2554
+ for (const [colId, col] of Object.entries(table.columns)) {
2555
+ parentIds.push(parentInternalId);
2556
+ colIds.push(colId);
2557
+ const ref = typeof col.colRef === "number" ? col.colRef : columnIds.length + 1;
2558
+ columnIds.push(ref);
2559
+ types.push(col.type ?? null);
2560
+ labels.push(col.label ?? null);
2561
+ parentPos.push(col.parentPos ?? null);
2562
+ isFormula.push(Boolean(col.isFormula));
2563
+ const formula = typeof col.formula === "string" ? col.formula : null;
2564
+ formulas.push(formula);
2565
+ descriptions.push(col.description ?? null);
2566
+ widgetOptions.push(col.widgetOptions ?? null);
2567
+ displayCol.push(typeof col.displayColRef === "number" ? col.displayColRef : null);
2568
+ visibleCol.push(typeof col.visibleColRef === "number" ? col.visibleColRef : null);
2700
2569
  }
2570
+ }
2571
+ return {
2572
+ parentId: parentIds,
2573
+ colId: colIds,
2574
+ id: columnIds,
2575
+ type: types,
2576
+ label: labels,
2577
+ parentPos,
2578
+ isFormula,
2579
+ formula: formulas,
2580
+ description: descriptions,
2581
+ // Also expose legacy `descr` for robustness.
2582
+ descr: descriptions,
2583
+ widgetOptions,
2584
+ displayCol,
2585
+ visibleCol
2701
2586
  };
2702
2587
  }
2703
2588
 
2704
- // src/emulator/templates.ts
2705
- var PRESET_GENERATED_AT = "1970-01-01T00:00:00.000Z";
2706
- function replicaBase(overrides) {
2707
- return {
2708
- generatedAt: PRESET_GENERATED_AT,
2709
- ...overrides
2710
- };
2589
+ // src/emulator/core/action-engine.ts
2590
+ var EmulatorActionError = class extends Error {
2591
+ constructor(action, message2) {
2592
+ super(message2);
2593
+ this.action = action;
2594
+ this.name = "EmulatorActionError";
2595
+ }
2596
+ };
2597
+ function applyUserActions(state, bus, actions) {
2598
+ const retValues = [];
2599
+ const changed = /* @__PURE__ */ new Set();
2600
+ for (const action of actions) {
2601
+ bus.emit({ type: "user-action", action });
2602
+ const ret = applySingle(state, action, changed);
2603
+ retValues.push(ret);
2604
+ }
2605
+ for (const tableId of changed) {
2606
+ bus.emit({ type: "data-change", tableId });
2607
+ }
2608
+ return { retValues, changedTableIds: [...changed] };
2711
2609
  }
2712
- var presets = {
2713
- /**
2714
- * Default starter fixture for demos / sandboxes that need a neutral document
2715
- * without choosing a domain-specific preset.
2716
- */
2717
- defaultTemplate() {
2718
- return replicaBase({
2719
- docName: "Default",
2720
- tables: {
2721
- Table1: {
2722
- label: "Table1",
2723
- rows: [
2724
- { id: 1, Name: "Alice" },
2725
- { id: 2, Name: "Bob" }
2726
- ]
2727
- }
2728
- }
2729
- });
2730
- },
2731
- blank() {
2732
- return replicaBase({
2733
- docName: "Blank",
2734
- tables: {
2735
- Table1: { rows: [{ id: 1 }] }
2736
- }
2737
- });
2738
- },
2739
- todoList() {
2740
- return replicaBase({
2741
- docName: "Todo list",
2742
- mode: "schema+data",
2743
- tables: {
2744
- Tasks: {
2745
- label: "Tasks",
2746
- columns: {
2747
- title: { type: "Text", label: "Title" },
2748
- done: { type: "Bool", label: "Done" },
2749
- priority: { type: "Choice", label: "Priority" }
2750
- },
2751
- rows: [
2752
- { id: 1, title: "Buy milk", done: false, priority: "high" },
2753
- { id: 2, title: "Write the SOTA emulator", done: true, priority: "high" },
2754
- { id: 3, title: "Ship it", done: false, priority: "medium" }
2755
- ]
2756
- }
2757
- }
2758
- });
2759
- },
2760
- contacts() {
2761
- return replicaBase({
2762
- docName: "Contacts",
2763
- mode: "schema+data",
2764
- tables: {
2765
- Contacts: {
2766
- label: "Contacts",
2767
- columns: {
2768
- firstName: { type: "Text" },
2769
- lastName: { type: "Text" },
2770
- email: { type: "Text" },
2771
- company: { type: "Text" }
2772
- },
2773
- rows: [
2774
- { id: 1, firstName: "Ada", lastName: "Lovelace", email: "ada@analytical.engine", company: "British Crown" },
2775
- { id: 2, firstName: "Linus", lastName: "Torvalds", email: "linus@kernel.org", company: "Linux Foundation" },
2776
- { id: 3, firstName: "Margaret", lastName: "Hamilton", email: "margaret@apollo.nasa", company: "NASA" }
2777
- ]
2778
- }
2779
- }
2780
- });
2610
+ function applySingle(state, action, changed) {
2611
+ const [name, ...args] = action;
2612
+ switch (name) {
2613
+ case "AddRecord":
2614
+ return addRecord(state, args, changed);
2615
+ case "BulkAddRecord":
2616
+ return bulkAddRecord(state, args, changed);
2617
+ case "UpdateRecord":
2618
+ return updateRecord(state, args, changed);
2619
+ case "BulkUpdateRecord":
2620
+ return bulkUpdateRecord(state, args, changed);
2621
+ case "RemoveRecord":
2622
+ return removeRecord(state, args, changed);
2623
+ case "BulkRemoveRecord":
2624
+ return bulkRemoveRecord(state, args, changed);
2625
+ case "AddColumn":
2626
+ return addColumn(state, args);
2627
+ case "AddVisibleColumn":
2628
+ return addColumn(state, args);
2629
+ case "RemoveColumn":
2630
+ return removeColumn(state, args, changed);
2631
+ case "RenameColumn":
2632
+ return renameColumn(state, args, changed);
2633
+ case "ModifyColumn":
2634
+ return modifyColumn(state, args, changed);
2635
+ case "AddTable":
2636
+ return addTable(state, args);
2637
+ case "RemoveTable":
2638
+ return removeTable(state, args);
2639
+ case "RenameTable":
2640
+ return renameTable(state, args);
2641
+ default:
2642
+ throw new EmulatorActionError(action, `Unsupported user action: ${String(name)}`);
2781
2643
  }
2782
- };
2783
-
2784
- // src/emulator/testing/render-with-grist.tsx
2785
- function renderWithGrist(ui, options = {}) {
2786
- fakeIframeContext();
2787
- resetGristForTests();
2788
- const emulator = options.existing ?? createGristEmulator(options.emulator ?? { document: presets.blank() });
2789
- const result = (0, import_react2.render)(ui, options);
2790
- const originalUnmount = result.unmount;
2791
- result.unmount = () => {
2792
- originalUnmount();
2793
- if (!options.existing) emulator.dispose();
2794
- resetGristForTests();
2795
- };
2796
- return Object.assign(result, { emulator });
2797
2644
  }
2798
- function fakeIframeContext() {
2799
- if (typeof window === "undefined") return;
2800
- if (window.self !== window.top) return;
2801
- try {
2802
- Object.defineProperty(window, "top", {
2803
- configurable: true,
2804
- get: () => ({})
2805
- });
2806
- } catch {
2807
- }
2645
+ function addRecord(state, [tableId, rowId, values], changed) {
2646
+ const table = getTable(state, requireString(tableId, "AddRecord.tableId"));
2647
+ const id = typeof rowId === "number" ? rowId : nextRowId(table);
2648
+ const normalizedValues = normalizeValuesForTable(
2649
+ table,
2650
+ values ?? {}
2651
+ );
2652
+ const row = { id, ...normalizedValues };
2653
+ ensureColumns(table, normalizedValues);
2654
+ evaluateRowFormulas(state, table, row);
2655
+ syncRefListDisplayHelpers(state, table, row);
2656
+ table.rows.push(row);
2657
+ changed.add(table.tableId);
2658
+ return id;
2808
2659
  }
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 });
2660
+ function bulkAddRecord(state, [tableId, rowIds, columnar], changed) {
2661
+ const table = getTable(state, requireString(tableId, "BulkAddRecord.tableId"));
2662
+ const cols = normalizeColumnarForTable(
2663
+ table,
2664
+ columnar ?? {}
2665
+ );
2666
+ const ids = Array.isArray(rowIds) ? rowIds : [];
2667
+ const length = ids.length || lengthOf(cols);
2668
+ const out = [];
2669
+ ensureColumns(table, cols, true);
2670
+ for (let i = 0; i < length; i++) {
2671
+ const id = typeof ids[i] === "number" ? ids[i] : nextRowId(table);
2672
+ const row = { id };
2673
+ for (const colId of Object.keys(cols)) row[colId] = cols[colId][i];
2674
+ evaluateRowFormulas(state, table, row);
2675
+ syncRefListDisplayHelpers(state, table, row);
2676
+ table.rows.push(row);
2677
+ out.push(id);
2678
+ }
2679
+ changed.add(table.tableId);
2680
+ return out;
2824
2681
  }
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
- ] }) });
2682
+ function updateRecord(state, [tableId, rowId, values], changed) {
2683
+ const table = getTable(state, requireString(tableId, "UpdateRecord.tableId"));
2684
+ const id = requireNumber(rowId, "UpdateRecord.rowId");
2685
+ const row = findRow(table, id);
2686
+ if (!row) throw new EmulatorActionError(["UpdateRecord", tableId, rowId, values], `Row ${id} not found in ${table.tableId}`);
2687
+ const normalizedValues = normalizeValuesForTable(
2688
+ table,
2689
+ values ?? {}
2690
+ );
2691
+ ensureColumns(table, normalizedValues);
2692
+ Object.assign(row, normalizedValues);
2693
+ evaluateRowFormulas(state, table, row);
2694
+ syncRefListDisplayHelpers(state, table, row);
2695
+ changed.add(table.tableId);
2696
+ return null;
2902
2697
  }
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
- };
2698
+ function bulkUpdateRecord(state, [tableId, rowIds, columnar], changed) {
2699
+ const table = getTable(state, requireString(tableId, "BulkUpdateRecord.tableId"));
2700
+ const ids = rowIds;
2701
+ const cols = normalizeColumnarForTable(
2702
+ table,
2703
+ columnar ?? {}
2704
+ );
2705
+ ensureColumns(table, cols, true);
2706
+ ids.forEach((id, idx) => {
2707
+ const row = findRow(table, id);
2708
+ if (!row) throw new EmulatorActionError(["BulkUpdateRecord", tableId, ids, cols], `Row ${id} not found in ${table.tableId}`);
2709
+ for (const colId of Object.keys(cols)) row[colId] = cols[colId][idx];
2710
+ evaluateRowFormulas(state, table, row);
2711
+ syncRefListDisplayHelpers(state, table, row);
2712
+ });
2713
+ changed.add(table.tableId);
2714
+ return null;
2715
+ }
2716
+ function removeRecord(state, [tableId, rowId], changed) {
2717
+ const table = getTable(state, requireString(tableId, "RemoveRecord.tableId"));
2718
+ const id = requireNumber(rowId, "RemoveRecord.rowId");
2719
+ const idx = findRowIndex(table, id);
2720
+ if (idx >= 0) table.rows.splice(idx, 1);
2721
+ if (state.selection.tableId === table.tableId && state.selection.rowId === id) {
2722
+ state.selection.rowId = table.rows[0]?.id ?? null;
2926
2723
  }
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
- };
2724
+ changed.add(table.tableId);
2725
+ return null;
2726
+ }
2727
+ function bulkRemoveRecord(state, [tableId, rowIds], changed) {
2728
+ const table = getTable(state, requireString(tableId, "BulkRemoveRecord.tableId"));
2729
+ const ids = new Set(rowIds);
2730
+ table.rows = table.rows.filter((r) => !ids.has(r.id));
2731
+ if (state.selection.tableId === table.tableId && typeof state.selection.rowId === "number" && ids.has(state.selection.rowId)) {
2732
+ state.selection.rowId = table.rows[0]?.id ?? null;
2934
2733
  }
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
- };
2734
+ changed.add(table.tableId);
2735
+ return null;
2736
+ }
2737
+ function addColumn(state, [tableId, colId, info]) {
2738
+ const table = getTable(state, requireString(tableId, "AddColumn.tableId"));
2739
+ const col = requireString(colId, "AddColumn.colId");
2740
+ if (table.columns[col]) throw new EmulatorActionError(["AddColumn", tableId, colId], `Column ${col} already exists`);
2741
+ table.columns[col] = info ?? {};
2742
+ for (const row of table.rows) {
2743
+ if (!(col in row)) row[col] = null;
2942
2744
  }
2943
2745
  return null;
2944
2746
  }
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);
2747
+ function removeColumn(state, [tableId, colId], changed) {
2748
+ const table = getTable(state, requireString(tableId, "RemoveColumn.tableId"));
2749
+ const col = requireString(colId, "RemoveColumn.colId");
2750
+ delete table.columns[col];
2751
+ for (const row of table.rows) delete row[col];
2752
+ changed.add(table.tableId);
2753
+ return null;
2754
+ }
2755
+ function renameColumn(state, [tableId, colId, newColId], changed) {
2756
+ const table = getTable(state, requireString(tableId, "RenameColumn.tableId"));
2757
+ const from = requireString(colId, "RenameColumn.colId");
2758
+ const to = requireString(newColId, "RenameColumn.newColId");
2759
+ if (!table.columns[from]) throw new EmulatorActionError(["RenameColumn", tableId, from, to], `Column ${from} not found`);
2760
+ if (from === to) return null;
2761
+ table.columns[to] = table.columns[from];
2762
+ delete table.columns[from];
2763
+ for (const row of table.rows) {
2764
+ if (from in row) {
2765
+ row[to] = row[from];
2766
+ delete row[from];
2958
2767
  }
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"
2768
+ }
2769
+ changed.add(table.tableId);
2770
+ return null;
2771
+ }
2772
+ function modifyColumn(state, [tableId, colId, info], changed) {
2773
+ const table = getTable(state, requireString(tableId, "ModifyColumn.tableId"));
2774
+ const col = requireString(colId, "ModifyColumn.colId");
2775
+ if (!table.columns[col]) throw new EmulatorActionError(["ModifyColumn", tableId, colId], `Column ${col} not found`);
2776
+ table.columns[col] = { ...table.columns[col], ...info };
2777
+ changed.add(table.tableId);
2778
+ return null;
2779
+ }
2780
+ function addTable(state, [tableId, columns]) {
2781
+ const id = requireString(tableId, "AddTable.tableId");
2782
+ if (state.tables.has(id)) throw new EmulatorActionError(["AddTable", tableId], `Table ${id} already exists`);
2783
+ const cols = {};
2784
+ for (const c of columns ?? []) {
2785
+ cols[c.id] = { ...c };
2786
+ }
2787
+ state.tables.set(id, { tableId: id, columns: cols, rows: [] });
2788
+ return id;
2789
+ }
2790
+ function removeTable(state, [tableId]) {
2791
+ const id = requireString(tableId, "RemoveTable.tableId");
2792
+ state.tables.delete(id);
2793
+ if (state.selection.tableId === id) {
2794
+ const fallback = state.tables.keys().next().value;
2795
+ if (fallback) {
2796
+ state.selection.tableId = fallback;
2797
+ state.selection.rowId = null;
2972
2798
  }
2973
- );
2799
+ }
2800
+ return null;
2974
2801
  }
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";
2802
+ function renameTable(state, [from, to]) {
2803
+ const fromId = requireString(from, "RenameTable.from");
2804
+ const toId = requireString(to, "RenameTable.to");
2805
+ const table = state.tables.get(fromId);
2806
+ if (!table) throw new EmulatorActionError(["RenameTable", fromId], `Table ${fromId} not found`);
2807
+ state.tables.delete(fromId);
2808
+ table.tableId = toId;
2809
+ state.tables.set(toId, table);
2810
+ if (state.selection.tableId === fromId) state.selection.tableId = toId;
2811
+ return null;
3052
2812
  }
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
- ] });
2813
+ function ensureColumns(table, values, bulk = false) {
2814
+ for (const col of Object.keys(values)) {
2815
+ if (!table.columns[col]) {
2816
+ const sample = bulk ? values[col]?.[0] : values[col];
2817
+ table.columns[col] = inferType(sample);
2818
+ }
2819
+ }
3088
2820
  }
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] };
2821
+ function inferType(sample) {
2822
+ switch (typeof sample) {
2823
+ case "number":
2824
+ return { type: "Numeric" };
2825
+ case "boolean":
2826
+ return { type: "Bool" };
2827
+ case "string":
2828
+ return { type: "Text" };
2829
+ default:
2830
+ return { type: "Any" };
3104
2831
  }
3105
- if (!channel) return { channel: { kind: "latest" }, basePath: withTrailingSlash(pathname) };
3106
- return { channel, basePath: withTrailingSlash(segments.slice(0, -1).join("/")) };
3107
2832
  }
3108
- function deployChannelLabel(channel) {
3109
- return channel.kind === "version" ? `v${channel.version}` : channel.kind;
2833
+ function lengthOf(cols) {
2834
+ for (const arr of Object.values(cols)) if (Array.isArray(arr)) return arr.length;
2835
+ return 0;
3110
2836
  }
3111
- function withTrailingSlash(path) {
3112
- return path.endsWith("/") ? path : `${path}/`;
2837
+ function requireString(value, name) {
2838
+ if (typeof value !== "string" || !value) throw new Error(`Expected non-empty string for ${name}`);
2839
+ return value;
3113
2840
  }
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;
2841
+ function requireNumber(value, name) {
2842
+ if (typeof value !== "number" || Number.isNaN(value)) throw new Error(`Expected number for ${name}`);
2843
+ return value;
3134
2844
  }
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
- ] });
2845
+ function normalizeValuesForTable(table, values) {
2846
+ const out = {};
2847
+ for (const [colId, value] of Object.entries(values)) {
2848
+ out[colId] = normalizeCellValueForType(value, table.columns[colId]?.type);
2849
+ }
2850
+ return out;
3155
2851
  }
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
- ] });
2852
+ function normalizeColumnarForTable(table, columns) {
2853
+ const out = {};
2854
+ for (const [colId, values] of Object.entries(columns)) {
2855
+ const type = table.columns[colId]?.type;
2856
+ out[colId] = Array.isArray(values) ? values.map((value) => normalizeCellValueForType(value, type)) : [];
2857
+ }
2858
+ return out;
3174
2859
  }
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 });
2860
+ function normalizeCellValueForType(value, type) {
2861
+ if (value === null || value === void 0 || typeof type !== "string") return value;
2862
+ if (isListColumnType(type) && Array.isArray(value) && value[0] !== "L") return ["E", "KeyError"];
2863
+ if (type.startsWith("Ref:")) return normalizeRefValue(value);
2864
+ if (type.startsWith("RefList:")) return normalizeRefListValue(value);
2865
+ return value;
3192
2866
  }
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
- });
2867
+ function extractRefRowId(value) {
2868
+ if (value === null || value === void 0) return null;
2869
+ if (value && typeof value === "object" && "__ref" in value) {
2870
+ const inner = value.rowId;
2871
+ return extractRefRowId(inner);
3201
2872
  }
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
- ] });
2873
+ if (value && typeof value === "object" && "rowId" in value) {
2874
+ return extractRefRowId(value.rowId);
2875
+ }
2876
+ const fromTuple = coerceGristDocApiRefRowId(value);
2877
+ if (fromTuple !== void 0 && fromTuple > 0) return Math.trunc(fromTuple);
2878
+ return toPositiveInt(value);
3247
2879
  }
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" });
2880
+ function isListColumnType(type) {
2881
+ return type === "ChoiceList" || type === "Attachments" || type.startsWith("RefList:");
3251
2882
  }
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;
2883
+ function normalizeRefValue(value) {
2884
+ return extractRefRowId(value);
3285
2885
  }
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(" ")));
2886
+ function normalizeRefListValue(value) {
2887
+ const toIdList = (items) => Array.from(
2888
+ new Set(items.map((item) => extractRefRowId(item)).filter((item) => item !== null))
2889
+ );
2890
+ if (Array.isArray(value) && value[0] === "L") {
2891
+ return ["L", ...toIdList(value.slice(1))];
3296
2892
  }
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"));
2893
+ if (Array.isArray(value)) {
2894
+ return ["L", ...toIdList(value)];
3308
2895
  }
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}.`));
2896
+ if (typeof value === "string") {
2897
+ const tokens2 = value.split(",").map((t) => t.trim()).filter(Boolean);
2898
+ return ["L", ...toIdList(tokens2)];
3312
2899
  }
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.`));
2900
+ if (value && typeof value === "object" && Array.isArray(value.rowIds)) {
2901
+ return ["L", ...toIdList(value.rowIds)];
3319
2902
  }
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
- );
2903
+ return ["L"];
2904
+ }
2905
+ function toPositiveInt(value) {
2906
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return Math.trunc(value);
2907
+ if (typeof value === "string") {
2908
+ const n = Number.parseInt(value.trim(), 10);
2909
+ if (Number.isFinite(n) && n > 0) return n;
2910
+ }
2911
+ return null;
2912
+ }
2913
+
2914
+ // src/emulator/core/columnar.ts
2915
+ function rowsToColumnar(table, rows, options = {}) {
2916
+ const cols = Object.keys(table.columns);
2917
+ const out = { id: rows.map((r) => r.id) };
2918
+ for (const col of cols) {
2919
+ out[col] = rows.map((r) => encodeForWire(r[col], table.columns[col]?.type, options.keepEncoded));
2920
+ }
2921
+ return out;
2922
+ }
2923
+ function rowToRecord(table, row, options = {}) {
2924
+ const out = { id: row.id };
2925
+ for (const col of Object.keys(table.columns)) {
2926
+ out[col] = encodeForWire(row[col], table.columns[col]?.type, options.keepEncoded);
2927
+ }
2928
+ return out;
2929
+ }
2930
+ function encodeForWire(value, type, keepEncoded) {
2931
+ if (value instanceof Date) {
2932
+ const epoch = Math.floor(value.getTime() / 1e3);
2933
+ if (keepEncoded) {
2934
+ return type && type.startsWith("DateTime") ? ["D", epoch, "UTC"] : ["d", epoch];
2935
+ }
2936
+ return epoch;
2937
+ }
2938
+ return value;
2939
+ }
2940
+
2941
+ // src/emulator/api/grist-doc-api.ts
2942
+ function createGristDocApiImpl(state, bus) {
2943
+ return {
2944
+ async getDocName() {
2945
+ return state.docName;
2946
+ },
2947
+ async listTables() {
2948
+ return [...state.tables.keys()];
2949
+ },
2950
+ async fetchTable(tableId) {
2951
+ if (tableId === "_grist_Tables") {
2952
+ return synthesizeTablesMeta(state);
2953
+ }
2954
+ if (tableId === "_grist_Tables_column") {
2955
+ return synthesizeTablesColumnMeta(state);
2956
+ }
2957
+ const table = getTable(state, tableId);
2958
+ return rowsToColumnar(table, table.rows);
2959
+ },
2960
+ async applyUserActions(actions) {
2961
+ const result = applyUserActions(state, bus, actions);
2962
+ return { retValues: result.retValues };
2963
+ },
2964
+ async getAccessToken() {
2965
+ if (!state.accessToken) {
2966
+ const err = new Error("getAccessToken not available (no token configured on emulator)");
2967
+ throw err;
2968
+ }
2969
+ return { ...state.accessToken };
2970
+ }
2971
+ };
2972
+ }
2973
+
2974
+ // src/emulator/api/grist-view.ts
2975
+ function createGristViewImpl(state, bus) {
2976
+ return {
2977
+ async fetchSelectedTable(options) {
2978
+ const table = getSelectedTable(state);
2979
+ return rowsToColumnar(table, table.rows, { keepEncoded: options?.keepEncoded });
2980
+ },
2981
+ async fetchSelectedRecord(rowId, options) {
2982
+ const table = getSelectedTable(state);
2983
+ const row = findRow(table, rowId);
2984
+ if (!row) throw new Error(`No row ${rowId} in ${table.tableId}`);
2985
+ return rowToRecord(table, row, { keepEncoded: options?.keepEncoded });
2986
+ },
2987
+ async allowSelectBy() {
2988
+ return state.configuration?.allowSelectBy ?? false;
2989
+ },
2990
+ async setSelectedRows(rowIds) {
2991
+ if (!rowIds || rowIds.length === 0) {
2992
+ state.selection.rowId = null;
2993
+ } else {
2994
+ state.selection.rowId = rowIds[0];
2995
+ }
2996
+ bus.emit({
2997
+ type: "cursor-change",
2998
+ tableId: state.selection.tableId,
2999
+ rowId: state.selection.rowId
3000
+ });
3001
+ },
3002
+ async setCursorPos(cursor) {
3003
+ const tableId = state.selection.tableId;
3004
+ if (typeof cursor.rowId === "number") state.selection.rowId = cursor.rowId;
3005
+ state.selection.cursor = { ...state.selection.cursor, ...cursor };
3006
+ bus.emit({
3007
+ type: "cursor-change",
3008
+ tableId,
3009
+ rowId: state.selection.rowId,
3010
+ cursor: state.selection.cursor
3011
+ });
3012
+ }
3013
+ };
3014
+ }
3015
+
3016
+ // src/emulator/api/widget-api.ts
3017
+ function createWidgetApiImpl(state, bus) {
3018
+ function emit() {
3019
+ bus.emit({
3020
+ type: "options-change",
3021
+ options: { ...state.widgetOptions },
3022
+ interaction: { ...state.widgetInteraction }
3023
+ });
3024
+ }
3025
+ return {
3026
+ async getOption(key) {
3027
+ return state.widgetOptions[key];
3028
+ },
3029
+ async setOption(key, value) {
3030
+ state.widgetOptions[key] = value;
3031
+ emit();
3032
+ },
3033
+ async getOptions() {
3034
+ return Object.keys(state.widgetOptions).length > 0 ? { ...state.widgetOptions } : null;
3035
+ },
3036
+ // Replaces every option, as Grist's WidgetAPI.setOptions does.
3037
+ async setOptions(options) {
3038
+ state.widgetOptions = { ...options };
3039
+ emit();
3040
+ },
3041
+ async clearOptions() {
3042
+ state.widgetOptions = {};
3043
+ emit();
3044
+ }
3045
+ };
3046
+ }
3047
+
3048
+ // src/emulator/api/index.ts
3049
+ function createApiBundle(state, bus) {
3050
+ return {
3051
+ GristView: createGristViewImpl(state, bus),
3052
+ GristDocAPI: createGristDocApiImpl(state, bus),
3053
+ WidgetAPI: createWidgetApiImpl(state, bus),
3054
+ CustomSectionAPI: createCustomSectionApiImpl(state, bus),
3055
+ GristAPI: createGristApiImpl(),
3056
+ CommandAPI: createCommandApiImpl(bus)
3057
+ };
3058
+ }
3059
+
3060
+ // src/emulator/core/event-bus.ts
3061
+ function createEventBus(options = {}) {
3062
+ const limit = options.historyLimit ?? 200;
3063
+ const listeners = /* @__PURE__ */ new Set();
3064
+ const history = [];
3065
+ return {
3066
+ emit(event) {
3067
+ history.push(event);
3068
+ if (history.length > limit) history.shift();
3069
+ for (const fn of [...listeners]) {
3070
+ try {
3071
+ fn(event);
3072
+ } catch (err) {
3073
+ console.error("[grist-emulator] event listener threw:", err);
3074
+ }
3075
+ }
3076
+ },
3077
+ on(listener) {
3078
+ listeners.add(listener);
3079
+ return () => {
3080
+ listeners.delete(listener);
3081
+ };
3082
+ },
3083
+ history() {
3084
+ return history;
3085
+ },
3086
+ clear() {
3087
+ history.length = 0;
3088
+ }
3089
+ };
3090
+ }
3091
+
3092
+ // src/emulator/theme-message.ts
3093
+ function buildGristThemeHostMessage(appearance, options) {
3094
+ return {
3095
+ theme: {
3096
+ appearance,
3097
+ name: appearance,
3098
+ colors: {}
3099
+ },
3100
+ ...options?.fromReady ? { fromReady: true } : {}
3101
+ };
3102
+ }
3103
+
3104
+ // src/emulator/transports/inline.ts
3105
+ function createInlineTransport(options) {
3106
+ const { state, bus, api } = options;
3107
+ const target = options.target ?? globalThis;
3108
+ installFakeIframeContext(target);
3109
+ const recordCbs = /* @__PURE__ */ new Map();
3110
+ const recordsCbs = /* @__PURE__ */ new Map();
3111
+ const optionsCbs = /* @__PURE__ */ new Set();
3112
+ const newRecordCbs = /* @__PURE__ */ new Set();
3113
+ const generic = /* @__PURE__ */ new Map();
3114
+ let lastRecord = null;
3115
+ let lastRecords = null;
3116
+ let lastOptions = null;
3117
+ let lastNewRecord = void 0;
3118
+ const buildInteraction = options.buildInteraction ?? (() => ({
3119
+ access_level: state.configuration?.requiredAccess ?? "read table",
3120
+ ...state.widgetInteraction
3121
+ }));
3122
+ async function pushRecord(rowId) {
3123
+ const mappings = resolveMappings(state);
3124
+ if (rowId === "new") {
3125
+ lastNewRecord = mappings;
3126
+ for (const cb of newRecordCbs) {
3127
+ try {
3128
+ cb(mappings);
3129
+ } catch (err) {
3130
+ logErr(err);
3131
+ }
3132
+ }
3133
+ return;
3134
+ }
3135
+ if (rowId === null) {
3136
+ lastRecord = { record: null, mappings };
3137
+ for (const [cb] of recordCbs) {
3138
+ try {
3139
+ cb(null, mappings);
3140
+ } catch (err) {
3141
+ logErr(err);
3142
+ }
3143
+ }
3144
+ return;
3145
+ }
3146
+ let record = null;
3147
+ try {
3148
+ record = await api.GristView.fetchSelectedRecord(rowId);
3149
+ } catch (err) {
3150
+ logErr(err);
3151
+ }
3152
+ lastRecord = { record, mappings };
3153
+ for (const [cb, opts] of recordCbs) {
3154
+ try {
3155
+ const r = opts?.keepEncoded ? await api.GristView.fetchSelectedRecord(rowId, { keepEncoded: true }) : record;
3156
+ cb(r, mappings);
3157
+ } catch (err) {
3158
+ logErr(err);
3159
+ }
3160
+ }
3161
+ }
3162
+ async function pushRecords() {
3163
+ const mappings = resolveMappings(state);
3164
+ let baseRows = [];
3165
+ try {
3166
+ const columnar = await api.GristView.fetchSelectedTable({});
3167
+ baseRows = columnsToRows(columnar);
3168
+ } catch (err) {
3169
+ logErr(err);
3170
+ }
3171
+ lastRecords = { records: baseRows, mappings };
3172
+ for (const [cb, opts] of recordsCbs) {
3173
+ try {
3174
+ const fetchOptions = coerceFetchOptions(opts);
3175
+ const rows = fetchOptions?.keepEncoded ? columnsToRows(await api.GristView.fetchSelectedTable({ keepEncoded: true })) : baseRows;
3176
+ cb(rows, mappings);
3177
+ } catch (err) {
3178
+ logErr(err);
3179
+ }
3180
+ }
3181
+ }
3182
+ async function fetchSelectedTableShaped(opts) {
3183
+ const fetchOptions = coerceFetchOptions(opts);
3184
+ const columnar = await api.GristView.fetchSelectedTable(
3185
+ fetchOptions ? { keepEncoded: fetchOptions.keepEncoded } : {}
3186
+ );
3187
+ if (fetchOptions?.format === "rows") return columnsToRows(columnar);
3188
+ return columnar;
3189
+ }
3190
+ function pushOptions() {
3191
+ const opts = Object.keys(state.widgetOptions).length > 0 ? { ...state.widgetOptions } : null;
3192
+ const interaction = buildInteraction();
3193
+ lastOptions = { options: opts, interaction };
3194
+ for (const cb of optionsCbs) {
3195
+ try {
3196
+ cb(opts, interaction);
3197
+ } catch (err) {
3198
+ logErr(err);
3199
+ }
3200
+ }
3201
+ }
3202
+ const offBus = bus.on(async (event) => {
3203
+ switch (event.type) {
3204
+ case "data-change":
3205
+ await pushRecords();
3206
+ await pushRecord(state.selection.rowId);
3207
+ break;
3208
+ case "cursor-change":
3209
+ await pushRecord(event.rowId);
3210
+ break;
3211
+ case "options-change":
3212
+ pushOptions();
3213
+ break;
3214
+ case "ready":
3215
+ pushOptions();
3216
+ if (options.initialTheme) {
3217
+ notify("message", buildGristThemeHostMessage(options.initialTheme, { fromReady: true }));
3218
+ }
3219
+ await pushRecords();
3220
+ await pushRecord(state.selection.rowId);
3221
+ break;
3222
+ case "mappings-change":
3223
+ await pushRecords();
3224
+ await pushRecord(state.selection.rowId);
3225
+ break;
3226
+ case "theme-change":
3227
+ notify("message", event.message);
3228
+ break;
3229
+ }
3230
+ });
3231
+ function makeTableOps(tableId) {
3232
+ return {
3233
+ async getTableId() {
3234
+ return tableId;
3235
+ },
3236
+ async create(records, options2) {
3237
+ const list = Array.isArray(records) ? records : [records];
3238
+ const ids = [];
3239
+ for (const r of list) {
3240
+ const result = applyUserActions(state, bus, [
3241
+ gristAddRecordAction(tableId, parseFields(r.fields, options2))
3242
+ ]);
3243
+ ids.push({ id: result.retValues[0] });
3244
+ }
3245
+ return Array.isArray(records) ? ids : ids[0];
3246
+ },
3247
+ async update(records, options2) {
3248
+ const list = Array.isArray(records) ? records : [records];
3249
+ const actions = list.map((r) => gristUpdateRecordAction(tableId, r.id, parseFields(r.fields, options2)));
3250
+ applyUserActions(state, bus, actions);
3251
+ },
3252
+ async upsert(records, options2) {
3253
+ const list = Array.isArray(records) ? records : [records];
3254
+ const allowAdd = options2?.add !== false;
3255
+ const allowUpdate = options2?.update !== false;
3256
+ const table = getTable(state, tableId);
3257
+ const actions = [];
3258
+ for (const r of list) {
3259
+ const matches = table.rows.filter(
3260
+ (row) => Object.entries(r.require).every(([k, v]) => row[k] === v)
3261
+ );
3262
+ if (matches.length > 0) {
3263
+ if (!allowUpdate) continue;
3264
+ const fields = parseFields(r.fields ?? {}, options2);
3265
+ for (const m of matches) actions.push(gristUpdateRecordAction(tableId, m.id, fields));
3266
+ } else if (allowAdd) {
3267
+ actions.push(gristAddRecordAction(tableId, { ...r.require, ...parseFields(r.fields ?? {}, options2) }));
3268
+ }
3269
+ }
3270
+ if (actions.length > 0) applyUserActions(state, bus, actions);
3271
+ },
3272
+ async destroy(recordIds) {
3273
+ if (Array.isArray(recordIds)) {
3274
+ applyUserActions(state, bus, [gristBulkRemoveRecordAction(tableId, recordIds)]);
3275
+ } else {
3276
+ applyUserActions(state, bus, [gristRemoveRecordAction(tableId, recordIds)]);
3277
+ }
3278
+ void gristBulkAddRecordAction;
3279
+ void gristBulkUpdateRecordAction;
3280
+ }
3281
+ };
3282
+ }
3283
+ function notify(event, ...args) {
3284
+ const set = generic.get(event);
3285
+ if (!set) return;
3286
+ for (const fn of set) {
3287
+ try {
3288
+ fn(...args);
3289
+ } catch (err) {
3290
+ logErr(err);
3291
+ }
3292
+ }
3293
+ }
3294
+ const grist2 = {
3295
+ async ready(opts) {
3296
+ await api.CustomSectionAPI.configure(opts ? { ...opts, onEditOptions: void 0 } : {});
3297
+ },
3298
+ onRecord(cb, opts) {
3299
+ recordCbs.set(cb, opts);
3300
+ if (lastRecord) {
3301
+ try {
3302
+ cb(lastRecord.record, lastRecord.mappings);
3303
+ } catch (err) {
3304
+ logErr(err);
3305
+ }
3306
+ }
3307
+ },
3308
+ onRecords(cb, opts) {
3309
+ recordsCbs.set(cb, opts);
3310
+ if (lastRecords) {
3311
+ try {
3312
+ cb(lastRecords.records, lastRecords.mappings);
3313
+ } catch (err) {
3314
+ logErr(err);
3315
+ }
3316
+ }
3317
+ },
3318
+ onOptions(cb) {
3319
+ optionsCbs.add(cb);
3320
+ if (lastOptions) {
3321
+ try {
3322
+ cb(lastOptions.options, lastOptions.interaction);
3323
+ } catch (err) {
3324
+ logErr(err);
3325
+ }
3326
+ }
3327
+ },
3328
+ onNewRecord(cb) {
3329
+ newRecordCbs.add(cb);
3330
+ if (lastNewRecord !== void 0) {
3331
+ try {
3332
+ cb(lastNewRecord);
3333
+ } catch (err) {
3334
+ logErr(err);
3335
+ }
3336
+ }
3337
+ },
3338
+ mapColumnNames(data) {
3339
+ const mappings = resolveMappings(state);
3340
+ if (!mappings) return null;
3341
+ const out = {};
3342
+ let allMapped = true;
3343
+ for (const [logical, real] of Object.entries(mappings)) {
3344
+ if (Array.isArray(real)) {
3345
+ out[logical] = real.map((r) => data[r]);
3346
+ } else if (typeof real === "string") {
3347
+ out[logical] = data[real];
3348
+ } else {
3349
+ allMapped = false;
3350
+ }
3351
+ }
3352
+ return allMapped ? out : null;
3353
+ },
3354
+ mapColumnNamesBack(data) {
3355
+ const mappings = resolveMappings(state);
3356
+ if (!mappings) return data;
3357
+ const out = {};
3358
+ for (const [logical, real] of Object.entries(mappings)) {
3359
+ if (typeof real === "string" && logical in data) {
3360
+ out[real] = data[logical];
3361
+ }
3362
+ }
3363
+ return out;
3364
+ },
3365
+ getOption: (k) => api.WidgetAPI.getOption(k),
3366
+ setOption: (k, v) => api.WidgetAPI.setOption(k, v),
3367
+ getOptions: () => api.WidgetAPI.getOptions(),
3368
+ setOptions: (o) => api.WidgetAPI.setOptions(o),
3369
+ clearOptions: () => api.WidgetAPI.clearOptions(),
3370
+ setCursorPos: (pos) => api.GristView.setCursorPos(pos),
3371
+ setSelectedRows: (rowIds) => api.GristView.setSelectedRows(rowIds),
3372
+ fetchSelectedTable: (opts) => fetchSelectedTableShaped(opts),
3373
+ fetchSelectedRecord: (rowId, opts) => api.GristView.fetchSelectedRecord(rowId, coerceFetchOptions(opts)),
3374
+ docApi: {
3375
+ getDocName: () => api.GristDocAPI.getDocName(),
3376
+ listTables: () => api.GristDocAPI.listTables(),
3377
+ fetchTable: (id) => api.GristDocAPI.fetchTable(id),
3378
+ applyUserActions: (actions, options2) => api.GristDocAPI.applyUserActions(actions, options2),
3379
+ getAccessToken: (opts) => api.GristDocAPI.getAccessToken(opts),
3380
+ fetchSelectedTable: (opts) => fetchSelectedTableShaped(opts),
3381
+ fetchSelectedRecord: (rowId, opts) => api.GristView.fetchSelectedRecord(rowId, coerceFetchOptions(opts))
3382
+ },
3383
+ get selectedTable() {
3384
+ return makeTableOps(state.selection.tableId);
3385
+ },
3386
+ getTable(tableId) {
3387
+ return makeTableOps(tableId ?? state.selection.tableId);
3388
+ },
3389
+ sectionApi: {
3390
+ configure: (settings) => api.CustomSectionAPI.configure(settings),
3391
+ mappings: () => api.CustomSectionAPI.mappings()
3392
+ },
3393
+ on(eventName, listener) {
3394
+ let set = generic.get(eventName);
3395
+ if (!set) generic.set(eventName, set = /* @__PURE__ */ new Set());
3396
+ set.add(listener);
3397
+ return () => set.delete(listener);
3398
+ }
3399
+ };
3400
+ target.grist = grist2;
3401
+ return {
3402
+ grist: grist2,
3403
+ dispose() {
3404
+ offBus();
3405
+ const t = target;
3406
+ if (t.grist === grist2) delete t.grist;
3407
+ recordCbs.clear();
3408
+ recordsCbs.clear();
3409
+ optionsCbs.clear();
3410
+ newRecordCbs.clear();
3411
+ generic.clear();
3412
+ lastRecord = null;
3413
+ lastRecords = null;
3414
+ lastOptions = null;
3415
+ lastNewRecord = void 0;
3416
+ },
3417
+ fireDataChange(tableId) {
3418
+ bus.emit({ type: "data-change", tableId: tableId ?? state.selection.tableId });
3419
+ },
3420
+ fireCursor(rowId) {
3421
+ const id = rowId === void 0 ? state.selection.rowId : rowId;
3422
+ bus.emit({ type: "cursor-change", tableId: state.selection.tableId, rowId: id });
3423
+ },
3424
+ fireOptions() {
3425
+ pushOptions();
3426
+ },
3427
+ fireTheme(message2) {
3428
+ bus.emit({ type: "theme-change", message: message2 });
3429
+ },
3430
+ emitInitialState() {
3431
+ pushOptions();
3432
+ void pushRecords().then(() => pushRecord(state.selection.rowId));
3329
3433
  }
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));
3434
+ };
3341
3435
  }
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);
3436
+ function columnsToRows(columnar) {
3437
+ const cols = Object.keys(columnar);
3438
+ if (cols.length === 0) return [];
3439
+ const length = (columnar[cols[0]] ?? []).length;
3440
+ const rows = [];
3441
+ for (let i = 0; i < length; i++) {
3442
+ const row = {};
3443
+ for (const c of cols) row[c] = columnar[c][i];
3444
+ rows.push(row);
3445
+ }
3446
+ return rows;
3347
3447
  }
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
- ] });
3448
+ function parseFields(fields, _options) {
3449
+ return { ...fields };
3361
3450
  }
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
- ] });
3451
+ function coerceFetchOptions(opts) {
3452
+ if (!opts) return void 0;
3453
+ const format = opts.format === "rows" || opts.format === "columns" ? opts.format : void 0;
3454
+ return { keepEncoded: opts.keepEncoded, format };
3371
3455
  }
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
- ] });
3456
+ function logErr(err) {
3457
+ console.error("[grist-emulator/inline]", err);
3404
3458
  }
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
- ] });
3459
+ function installFakeIframeContext(target) {
3460
+ try {
3461
+ if (target.self !== target.top) return;
3462
+ Object.defineProperty(target, "top", {
3463
+ configurable: true,
3464
+ get: () => ({})
3465
+ });
3466
+ } catch {
3467
+ }
3411
3468
  }
3412
3469
 
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";
3470
+ // src/emulator/validators.ts
3471
+ var import_zod = require("zod");
3472
+ var emulatorRowSchema = import_zod.z.object({ id: import_zod.z.number() }).catchall(import_zod.z.unknown());
3473
+ var formulaKindSchema = import_zod.z.enum(["computed", "trigger", "none", "empty_slot"]);
3474
+ var replicaColumnSchema = import_zod.z.object({
3475
+ colRef: import_zod.z.number().optional(),
3476
+ type: import_zod.z.string().optional(),
3477
+ label: import_zod.z.string().optional(),
3478
+ parentPos: import_zod.z.number().optional(),
3479
+ isFormula: import_zod.z.boolean().optional(),
3480
+ formulaKind: formulaKindSchema.optional(),
3481
+ formula: import_zod.z.string().optional(),
3482
+ description: import_zod.z.string().optional(),
3483
+ widgetOptions: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
3484
+ displayColRef: import_zod.z.number().optional(),
3485
+ visibleColRef: import_zod.z.number().optional()
3486
+ }).strict();
3487
+ var replicaTableSchema = import_zod.z.object({
3488
+ label: import_zod.z.string().optional(),
3489
+ columns: import_zod.z.record(import_zod.z.string(), replicaColumnSchema).optional(),
3490
+ rows: import_zod.z.array(emulatorRowSchema).optional(),
3491
+ rowCount: import_zod.z.number().optional(),
3492
+ dataOmitted: import_zod.z.boolean().optional(),
3493
+ dataOmittedReason: import_zod.z.string().optional(),
3494
+ error: import_zod.z.string().optional()
3495
+ }).strict();
3496
+ var replicaSelectionSchema = import_zod.z.object({
3497
+ tableId: import_zod.z.string().optional(),
3498
+ rowId: import_zod.z.union([import_zod.z.number(), import_zod.z.literal("new"), import_zod.z.null()]).optional(),
3499
+ mappings: import_zod.z.unknown().nullable().optional(),
3500
+ cursor: import_zod.z.unknown().optional()
3501
+ }).strict();
3502
+ var replicaWidgetConfigurationSchema = import_zod.z.object({
3503
+ requiredAccess: import_zod.z.string().optional(),
3504
+ columns: import_zod.z.unknown().optional(),
3505
+ allowSelectBy: import_zod.z.boolean().optional(),
3506
+ hasCustomOptions: import_zod.z.boolean().optional()
3507
+ }).strict();
3508
+ var replicaAccessTokenSchema = import_zod.z.object({
3509
+ token: import_zod.z.string(),
3510
+ baseUrl: import_zod.z.string(),
3511
+ ttlMsecs: import_zod.z.number()
3512
+ }).strict();
3513
+ var replicaDocumentSchema = import_zod.z.object({
3514
+ generatedAt: import_zod.z.string(),
3515
+ source: import_zod.z.string().optional(),
3516
+ docName: import_zod.z.string().optional(),
3517
+ mode: import_zod.z.enum(["schema-only", "schema+samples", "schema+data"]).optional(),
3518
+ tables: import_zod.z.record(import_zod.z.string(), replicaTableSchema),
3519
+ selection: replicaSelectionSchema.optional(),
3520
+ widgetOptions: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
3521
+ widgetInteraction: import_zod.z.unknown().optional(),
3522
+ configuration: replicaWidgetConfigurationSchema.optional(),
3523
+ accessToken: replicaAccessTokenSchema.nullable().optional()
3524
+ }).strict().refine((value) => Object.keys(value.tables).length > 0, {
3525
+ message: "at least one table is required",
3526
+ path: ["tables"]
3527
+ });
3528
+ function formatZodError(prefix, error) {
3529
+ const details = error.issues.map((issue) => {
3530
+ const where = issue.path.length > 0 ? issue.path.join(".") : "<root>";
3531
+ return `${where}: ${issue.message}`;
3532
+ }).join("; ");
3533
+ return new Error(`${prefix}: ${details}`);
3493
3534
  }
3494
- function other(theme) {
3495
- return theme === "dark" ? "light" : "dark";
3535
+ function safeParseDocument(input) {
3536
+ return replicaDocumentSchema.safeParse(input);
3496
3537
  }
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
- };
3538
+ function parseDocument(input) {
3539
+ const result = safeParseDocument(input);
3540
+ if (!result.success) {
3541
+ throw formatZodError("parseDocument", result.error);
3542
+ }
3543
+ return result.data;
3507
3544
  }
3508
- function isEditableTarget(target) {
3509
- if (!(target instanceof HTMLElement)) return false;
3510
- return target.isContentEditable || target.closest("input, textarea, select, [contenteditable='true']") !== null;
3545
+
3546
+ // src/emulator/create-grist-emulator.ts
3547
+ function createGristEmulator(options) {
3548
+ const validatedDocument = parseDocument(options.document);
3549
+ const state = createDocument({
3550
+ ...validatedDocument,
3551
+ widgetOptions: options.widgetOptions ?? validatedDocument.widgetOptions,
3552
+ // No access_level by default: the emulator then grants the access the
3553
+ // widget asks for in grist.ready(), as a user accepting it in Grist would.
3554
+ widgetInteraction: options.widgetInteraction ?? validatedDocument.widgetInteraction ?? {}
3555
+ });
3556
+ const bus = createEventBus();
3557
+ const api = createApiBundle(state, bus);
3558
+ const inline = createInlineTransport({
3559
+ state,
3560
+ bus,
3561
+ api,
3562
+ initialTheme: options.initialTheme,
3563
+ target: options.transport?.target
3564
+ });
3565
+ const transport = inline;
3566
+ const grist2 = inline.grist;
3567
+ function apply(actions) {
3568
+ return applyUserActions(state, bus, actions);
3569
+ }
3570
+ return {
3571
+ state,
3572
+ bus,
3573
+ api,
3574
+ transport,
3575
+ grist: grist2,
3576
+ apply,
3577
+ mutate: {
3578
+ addRow: (tableId, fields) => apply([["AddRecord", tableId, null, fields]]).retValues[0],
3579
+ updateRow: (tableId, rowId, fields) => {
3580
+ apply([["UpdateRecord", tableId, rowId, fields]]);
3581
+ },
3582
+ removeRow: (tableId, rowId) => {
3583
+ apply([["RemoveRecord", tableId, rowId]]);
3584
+ }
3585
+ },
3586
+ selectRow(rowId) {
3587
+ state.selection.rowId = rowId;
3588
+ bus.emit({ type: "cursor-change", tableId: state.selection.tableId, rowId });
3589
+ },
3590
+ selectTable(tableId) {
3591
+ if (!state.tables.has(tableId)) throw new Error(`Unknown table: ${tableId}`);
3592
+ state.selection.tableId = tableId;
3593
+ const first = state.tables.get(tableId).rows[0]?.id ?? null;
3594
+ state.selection.rowId = first;
3595
+ bus.emit({ type: "data-change", tableId });
3596
+ bus.emit({ type: "cursor-change", tableId, rowId: first });
3597
+ },
3598
+ setColumnMappings(mappings) {
3599
+ if (mappings === void 0) {
3600
+ delete state.selection.mappings;
3601
+ } else {
3602
+ state.selection.mappings = mappings;
3603
+ }
3604
+ bus.emit({ type: "mappings-change", mappings: resolveMappings(state) });
3605
+ },
3606
+ async setOptions(opts) {
3607
+ await api.WidgetAPI.setOptions(opts);
3608
+ },
3609
+ setInteraction(interaction) {
3610
+ state.widgetInteraction = { ...interaction };
3611
+ bus.emit({ type: "options-change", options: { ...state.widgetOptions }, interaction: { ...interaction } });
3612
+ },
3613
+ theme: {
3614
+ set(next) {
3615
+ bus.emit({ type: "theme-change", message: buildGristThemeHostMessage(next) });
3616
+ }
3617
+ },
3618
+ dispose() {
3619
+ transport.dispose();
3620
+ bus.clear();
3621
+ }
3622
+ };
3511
3623
  }
3512
- function isTheme(value) {
3513
- return value !== null && THEMES.includes(value);
3624
+
3625
+ // src/emulator/templates.ts
3626
+ var PRESET_GENERATED_AT = "1970-01-01T00:00:00.000Z";
3627
+ function replicaBase(overrides) {
3628
+ return {
3629
+ generatedAt: PRESET_GENERATED_AT,
3630
+ ...overrides
3631
+ };
3514
3632
  }
3515
- function readStoredTheme() {
3516
- try {
3517
- const value = localStorage.getItem(STORAGE_KEY);
3518
- return isTheme(value) ? value : null;
3519
- } catch {
3520
- return null;
3633
+ var presets = {
3634
+ /**
3635
+ * Default starter fixture for demos / sandboxes that need a neutral document
3636
+ * without choosing a domain-specific preset.
3637
+ */
3638
+ defaultTemplate() {
3639
+ return replicaBase({
3640
+ docName: "Default",
3641
+ tables: {
3642
+ Table1: {
3643
+ label: "Table1",
3644
+ rows: [
3645
+ { id: 1, Name: "Alice" },
3646
+ { id: 2, Name: "Bob" }
3647
+ ]
3648
+ }
3649
+ }
3650
+ });
3651
+ },
3652
+ blank() {
3653
+ return replicaBase({
3654
+ docName: "Blank",
3655
+ tables: {
3656
+ Table1: { rows: [{ id: 1 }] }
3657
+ }
3658
+ });
3659
+ },
3660
+ todoList() {
3661
+ return replicaBase({
3662
+ docName: "Todo list",
3663
+ mode: "schema+data",
3664
+ tables: {
3665
+ Tasks: {
3666
+ label: "Tasks",
3667
+ columns: {
3668
+ title: { type: "Text", label: "Title" },
3669
+ done: { type: "Bool", label: "Done" },
3670
+ priority: { type: "Choice", label: "Priority" }
3671
+ },
3672
+ rows: [
3673
+ { id: 1, title: "Buy milk", done: false, priority: "high" },
3674
+ { id: 2, title: "Write the SOTA emulator", done: true, priority: "high" },
3675
+ { id: 3, title: "Ship it", done: false, priority: "medium" }
3676
+ ]
3677
+ }
3678
+ }
3679
+ });
3680
+ },
3681
+ contacts() {
3682
+ return replicaBase({
3683
+ docName: "Contacts",
3684
+ mode: "schema+data",
3685
+ tables: {
3686
+ Contacts: {
3687
+ label: "Contacts",
3688
+ columns: {
3689
+ firstName: { type: "Text" },
3690
+ lastName: { type: "Text" },
3691
+ email: { type: "Text" },
3692
+ company: { type: "Text" }
3693
+ },
3694
+ rows: [
3695
+ { id: 1, firstName: "Ada", lastName: "Lovelace", email: "ada@analytical.engine", company: "British Crown" },
3696
+ { id: 2, firstName: "Linus", lastName: "Torvalds", email: "linus@kernel.org", company: "Linux Foundation" },
3697
+ { id: 3, firstName: "Margaret", lastName: "Hamilton", email: "margaret@apollo.nasa", company: "NASA" }
3698
+ ]
3699
+ }
3700
+ }
3701
+ });
3521
3702
  }
3703
+ };
3704
+
3705
+ // src/emulator/testing/render-with-grist.tsx
3706
+ var import_jsx_runtime8 = require("react/jsx-runtime");
3707
+ function renderWithGrist(ui, options = {}) {
3708
+ fakeIframeContext();
3709
+ resetGristForTests();
3710
+ const widgetOptions = options.widget ? isGristWidget(options.widget) ? options.widget.options : options.widget : void 0;
3711
+ const columns = widgetOptions ? declaredColumns(widgetOptions) : [];
3712
+ const emulatorOptions = widgetOptions && !options.emulator?.document ? {
3713
+ ...options.emulator,
3714
+ document: declaredDocument(columns, options.rows),
3715
+ widgetInteraction: options.emulator?.widgetInteraction ?? { access_level: widgetOptions.requiredAccess ?? "read table" }
3716
+ } : options.emulator ?? { document: presets.blank() };
3717
+ const emulator = options.existing ?? createGristEmulator(emulatorOptions);
3718
+ if (widgetOptions && columns.length > 0) emulator.setColumnMappings(mapEveryColumn(columns));
3719
+ const Widget = () => ui;
3720
+ const result = (0, import_react10.render)(widgetOptions ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(GristWidgetApp, { App: Widget, options: widgetOptions }) : ui, options);
3721
+ const originalUnmount = result.unmount;
3722
+ result.unmount = () => {
3723
+ originalUnmount();
3724
+ if (!options.existing) emulator.dispose();
3725
+ resetGristForTests();
3726
+ };
3727
+ return Object.assign(result, { emulator });
3522
3728
  }
3523
- function writeStoredTheme(theme) {
3729
+ function fakeIframeContext() {
3730
+ if (typeof window === "undefined") return;
3731
+ if (window.self !== window.top) return;
3524
3732
  try {
3525
- localStorage.setItem(STORAGE_KEY, theme);
3733
+ Object.defineProperty(window, "top", {
3734
+ configurable: true,
3735
+ get: () => ({})
3736
+ });
3526
3737
  } catch {
3527
3738
  }
3528
3739
  }
3529
- function noop() {
3530
- }
3531
3740
 
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
- }
3741
+ // src/emulator/testing/smoke-test-widget.tsx
3742
+ var import_react11 = require("react");
3743
+ var import_react12 = require("@testing-library/react");
3556
3744
 
3557
3745
  // src/emulator/testing/jsdom-stubs.ts
3558
3746
  function installJsdomStubs() {
@@ -3582,21 +3770,19 @@ function installJsdomStubs() {
3582
3770
  }
3583
3771
 
3584
3772
  // 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
- }) {
3773
+ var import_jsx_runtime9 = require("react/jsx-runtime");
3774
+ async function smokeTestWidget(input, extra = {}) {
3775
+ const { App, options: given, widget, document: document2, timeout = 1e4 } = "App" in input ? input : { ...entryProps(input), ...extra };
3592
3776
  installJsdomStubs();
3777
+ const options = given ?? widget?.options;
3778
+ if (!options) throw new Error("smokeTestWidget: pass the widget's `widget` (defineGristWidget) or its `options` (GRIST_OPTIONS).");
3593
3779
  const crash = { error: void 0 };
3594
3780
  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, {}) }) });
3781
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CatchRenderError, { crash, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { "data-grist-smoke-test": "", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(App, {}) }) });
3596
3782
  }
3597
3783
  const columns = declaredColumns(options);
3598
- const gristDocument = document2 ?? smokeTestDocument(columns);
3599
- const result = renderWithGrist(/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(GristWidgetApp, { App: Probe, options }), {
3784
+ const gristDocument = document2 ?? declaredDocument(columns);
3785
+ const result = renderWithGrist(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(GristWidgetApp, { App: Probe, options }), {
3600
3786
  emulator: {
3601
3787
  document: gristDocument,
3602
3788
  // Granted as the user would in Grist's widget panel: the access the
@@ -3607,7 +3793,7 @@ async function smokeTestWidget({
3607
3793
  try {
3608
3794
  if (columns.length > 0) result.emulator.setColumnMappings(mapEveryColumn(columns));
3609
3795
  try {
3610
- await (0, import_react10.waitFor)(
3796
+ await (0, import_react12.waitFor)(
3611
3797
  () => {
3612
3798
  if (crash.error !== void 0) return;
3613
3799
  const probe = result.container.querySelector("[data-grist-smoke-test]");
@@ -3626,7 +3812,7 @@ async function smokeTestWidget({
3626
3812
  result.unmount();
3627
3813
  }
3628
3814
  }
3629
- var CatchRenderError = class extends import_react9.Component {
3815
+ var CatchRenderError = class extends import_react11.Component {
3630
3816
  constructor() {
3631
3817
  super(...arguments);
3632
3818
  this.state = { failed: false };
@@ -3641,45 +3827,6 @@ var CatchRenderError = class extends import_react9.Component {
3641
3827
  return this.state.failed ? null : this.props.children;
3642
3828
  }
3643
3829
  };
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
3830
 
3684
3831
  // src/emulator/testing/matchers.ts
3685
3832
  function actionsOf(emulator) {
@@ -3735,7 +3882,7 @@ function waitForEvent(emulator, kindOrPredicate, timeoutOrOptions = 1e3) {
3735
3882
  }
3736
3883
 
3737
3884
  // src/emulator/testing/index.ts
3738
- var import_react11 = require("@testing-library/react");
3885
+ var import_react13 = require("@testing-library/react");
3739
3886
  // Annotate the CommonJS export names for ESM import in node:
3740
3887
  0 && (module.exports = {
3741
3888
  act,