grist-widget-sdk 0.4.0 → 0.5.1

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 +47 -0
  2. package/README.md +79 -208
  3. package/dist/app.cjs +186 -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-O7NY46QY.js → chunk-MAPAWVQC.js} +88 -20
  9. package/dist/chunk-MAPAWVQC.js.map +1 -0
  10. package/dist/{chunk-B5ZZNLVS.js → chunk-OXA2VITC.js} +5 -1
  11. package/dist/chunk-OXA2VITC.js.map +1 -0
  12. package/dist/{chunk-P6VTEKWO.js → chunk-P5I5MRC6.js} +86 -10
  13. package/dist/chunk-P5I5MRC6.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 +2393 -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 +75 -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,2319 @@ 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));
1973
- }
1974
- return out;
2086
+ // src/app/mount-grist-widget.tsx
2087
+ var import_jsx_runtime7 = require("react/jsx-runtime");
2088
+ function isGristWidgetEntry(value) {
2089
+ return "default" in value && value.default != null;
1975
2090
  }
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);
2091
+ function entryProps(entry, dev = false) {
2092
+ const widget = isGristWidget(entry.widget) ? entry.widget : void 0;
2093
+ const options = widget?.options ?? entry.GRIST_OPTIONS;
2094
+ if (!options) {
2095
+ throw new Error(
2096
+ "mountGristWidget: src/app/App.tsx exports neither `widget` (defineGristWidget) nor `GRIST_OPTIONS`."
2097
+ );
1980
2098
  }
1981
- return out;
2099
+ const props = {
2100
+ App: entry.default,
2101
+ options,
2102
+ metadata: widget?.metadata ?? entry.WIDGET_METADATA,
2103
+ dev
2104
+ };
2105
+ if ("Alerts" in entry) props.Alerts = entry.Alerts;
2106
+ if (entry.LocalDevPage) props.LocalDevPage = entry.LocalDevPage;
2107
+ if (entry.ReleasePage) props.ReleasePage = entry.ReleasePage;
2108
+ return props;
1982
2109
  }
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;
2110
+ function GristWidgetApp({
2111
+ App,
2112
+ options,
2113
+ metadata,
2114
+ dev = false,
2115
+ Alerts = WidgetAlerts,
2116
+ LocalDevPage: LocalDevPage2 = LocalDevPage,
2117
+ ReleasePage: ReleasePage2 = ReleasePage
2118
+ }) {
2119
+ const Wrap = Alerts ?? PassThrough;
2120
+ if (isEmbedded2()) {
2121
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(GristWidgetProvider, { options, children: [
2122
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PageMetadata, { metadata }),
2123
+ /* @__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, {}) }) }) }) }) })
2124
+ ] });
1990
2125
  }
1991
- return value;
2126
+ const channel = parseDeployPath(window.location.pathname).channel;
2127
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(StandaloneThemeProvider, { children: [
2128
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PageMetadata, { metadata }),
2129
+ /* @__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 }) })
2130
+ ] });
2131
+ }
2132
+ var FILL = { minHeight: "100%", width: "100%" };
2133
+ function isEmbedded2() {
2134
+ return typeof window !== "undefined" && window.self !== window.top;
2135
+ }
2136
+ function PageMetadata({ metadata }) {
2137
+ useWidgetMetadata(metadata?.title ? { title: metadata.title, description: metadata.description ?? "" } : null);
2138
+ return null;
2139
+ }
2140
+ function PassThrough({ children }) {
2141
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children });
1992
2142
  }
1993
2143
 
1994
- // src/emulator/api/grist-doc-api.ts
1995
- function createGristDocApiImpl(state, bus) {
2144
+ // src/emulator/testing/declared-document.ts
2145
+ function declaredColumns(options) {
2146
+ return (options.columns ?? []).map(
2147
+ (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 }
2148
+ );
2149
+ }
2150
+ function firstType(type) {
2151
+ return (type ?? "Any").split(",")[0].trim() || "Any";
2152
+ }
2153
+ function mapEveryColumn(columns) {
2154
+ return Object.fromEntries(columns.map((c) => [c.name, c.allowMultiple ? [c.name] : c.name]));
2155
+ }
2156
+ function declaredDocument(columns, rows) {
2157
+ const given = rows ?? [Object.fromEntries(columns.map((c) => [c.name, sampleValue(c.type)]))];
1996
2158
  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;
2159
+ generatedAt: "1970-01-01T00:00:00.000Z",
2160
+ docName: "Test document",
2161
+ mode: "schema+data",
2162
+ tables: {
2163
+ Table1: {
2164
+ columns: Object.fromEntries(columns.map((c) => [c.name, { type: c.type }])),
2165
+ rows: given.map((row, i) => ({
2166
+ id: typeof row.id === "number" ? row.id : i + 1,
2167
+ ...Object.fromEntries(columns.map((c) => [c.name, encodeGristCell(c.declared, row[c.name] ?? null)]))
2168
+ }))
2021
2169
  }
2022
- return { ...state.accessToken };
2023
2170
  }
2024
2171
  };
2025
2172
  }
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
- };
2173
+ function sampleValue(type) {
2174
+ switch (type) {
2175
+ case "Numeric":
2176
+ case "Int":
2177
+ return 42;
2178
+ case "Bool":
2179
+ return true;
2180
+ case "Date":
2181
+ case "DateTime":
2182
+ return 1767225600;
2183
+ // 2026-01-01, as Grist stores it: epoch seconds
2184
+ default:
2185
+ return "Sample";
2186
+ }
2067
2187
  }
2068
2188
 
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
- }
2189
+ // src/emulator/api/command-api.ts
2190
+ function createCommandApiImpl(bus) {
2078
2191
  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();
2192
+ async run(name) {
2193
+ bus.emit({ type: "user-action", action: ["RunCommand", name] });
2097
2194
  }
2098
2195
  };
2099
2196
  }
2100
2197
 
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
- };
2198
+ // src/emulator/core/mappings.ts
2199
+ function resolveMappings(state) {
2200
+ if (state.selection.mappings !== void 0 && state.selection.mappings !== null) {
2201
+ return state.selection.mappings;
2202
+ }
2203
+ return null;
2111
2204
  }
2112
2205
 
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 = [];
2206
+ // src/emulator/api/custom-section-api.ts
2207
+ function createCustomSectionApiImpl(state, bus) {
2118
2208
  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);
2209
+ async configure(settings) {
2210
+ const config = {
2211
+ requiredAccess: settings.requiredAccess,
2212
+ columns: settings.columns,
2213
+ allowSelectBy: settings.allowSelectBy,
2214
+ hasCustomOptions: settings.hasCustomOptions
2134
2215
  };
2216
+ state.configuration = config;
2217
+ bus.emit({ type: "ready", configuration: config });
2218
+ bus.emit({ type: "mappings-change", mappings: resolveMappings(state) });
2135
2219
  },
2136
- history() {
2137
- return history;
2138
- },
2139
- clear() {
2140
- history.length = 0;
2220
+ async mappings() {
2221
+ return resolveMappings(state);
2141
2222
  }
2142
2223
  };
2143
2224
  }
2144
2225
 
2145
- // src/emulator/theme-message.ts
2146
- function buildGristThemeHostMessage(appearance, options) {
2226
+ // src/emulator/api/grist-api.ts
2227
+ function createGristApiImpl() {
2147
2228
  return {
2148
- theme: {
2149
- appearance,
2150
- name: appearance,
2151
- colors: {}
2152
- },
2153
- ...options?.fromReady ? { fromReady: true } : {}
2229
+ async ping() {
2230
+ return "pong";
2231
+ }
2154
2232
  };
2155
2233
  }
2156
2234
 
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;
2235
+ // src/emulator/core/display-helpers.ts
2236
+ function inferDisplayHelpersByRef(columns) {
2237
+ const out = {};
2238
+ const helperColumns = Object.entries(columns).filter(
2239
+ ([colId]) => colId.startsWith("gristHelper_")
2240
+ );
2241
+ const usedHelpers = /* @__PURE__ */ new Set();
2242
+ const colRefToColId = /* @__PURE__ */ new Map();
2243
+ for (const [colId, col] of Object.entries(columns)) {
2244
+ if (typeof col?.colRef === "number" && Number.isFinite(col.colRef)) {
2245
+ colRefToColId.set(col.colRef, colId);
2224
2246
  }
2225
- let record = null;
2226
- try {
2227
- record = await api.GristView.fetchSelectedRecord(rowId);
2228
- } catch (err) {
2229
- logErr(err);
2247
+ }
2248
+ for (const [colId, col] of Object.entries(columns)) {
2249
+ const type = col?.type;
2250
+ if (typeof type !== "string" || !type.startsWith("Ref:") && !type.startsWith("RefList:")) {
2251
+ continue;
2230
2252
  }
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);
2253
+ if (typeof col.displayColRef === "number" && Number.isFinite(col.displayColRef)) {
2254
+ const helperColId = colRefToColId.get(col.displayColRef);
2255
+ if (helperColId) {
2256
+ out[colId] = helperColId;
2257
+ usedHelpers.add(helperColId);
2258
+ continue;
2238
2259
  }
2239
2260
  }
2261
+ const match = helperColumns.find(([, helper]) => {
2262
+ const formula = helper?.formula;
2263
+ return typeof formula === "string" && (formula.includes(`$${colId}.`) || formula.includes(`$${colId}`));
2264
+ });
2265
+ if (match) {
2266
+ out[colId] = match[0];
2267
+ usedHelpers.add(match[0]);
2268
+ }
2240
2269
  }
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
- }
2270
+ const unresolvedRefs = Object.entries(columns).filter(([colId, col]) => {
2271
+ const type = col?.type;
2272
+ return !out[colId] && typeof type === "string" && (type.startsWith("Ref:") || type.startsWith("RefList:"));
2273
+ }).sort((a, b) => (a[1]?.parentPos ?? Number.MAX_SAFE_INTEGER) - (b[1]?.parentPos ?? Number.MAX_SAFE_INTEGER));
2274
+ 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));
2275
+ for (const [refId, refCol] of unresolvedRefs) {
2276
+ const refPos = refCol?.parentPos ?? Number.MAX_SAFE_INTEGER;
2277
+ const candidate = helperByPos.find(([, helperCol]) => {
2278
+ const helperPos = helperCol?.parentPos ?? Number.MAX_SAFE_INTEGER;
2279
+ return helperPos > refPos;
2280
+ });
2281
+ if (!candidate) continue;
2282
+ out[refId] = candidate[0];
2283
+ usedHelpers.add(candidate[0]);
2284
+ }
2285
+ for (const [refId] of unresolvedRefs) {
2286
+ if (out[refId]) continue;
2287
+ const candidate = helperByPos.find(([helperId]) => !usedHelpers.has(helperId));
2288
+ if (!candidate) continue;
2289
+ out[refId] = candidate[0];
2290
+ usedHelpers.add(candidate[0]);
2291
+ }
2292
+ return out;
2293
+ }
2294
+
2295
+ // src/emulator/core/document.ts
2296
+ var SYSTEM_COLUMN = "id";
2297
+ var EMULATOR_JS_FORMULA_PLACEHOLDER = "(emulator JavaScript formula)";
2298
+ function enrichEmulatorColumnMetadata(col) {
2299
+ const originalTrimmed = col.formula?.trim() ?? "";
2300
+ const explicitInferred = col.inferredFormula?.trim() ?? "";
2301
+ if (col.formulaFn) {
2302
+ const inferredFormula = explicitInferred || originalTrimmed || EMULATOR_JS_FORMULA_PLACEHOLDER;
2303
+ return {
2304
+ ...col,
2305
+ ...originalTrimmed ? { formula: col.formula } : {},
2306
+ inferredFormula
2307
+ };
2308
+ }
2309
+ if (explicitInferred || originalTrimmed) {
2310
+ return {
2311
+ ...col,
2312
+ inferredFormula: explicitInferred || originalTrimmed || void 0
2313
+ };
2314
+ }
2315
+ return col;
2316
+ }
2317
+ function sortEmulatorColumnsByParentPos(columns) {
2318
+ const entries = Object.entries(columns);
2319
+ entries.sort(([ka, a], [kb, b]) => {
2320
+ const pa = a.parentPos;
2321
+ const pb = b.parentPos;
2322
+ if (pa !== void 0 && pb !== void 0 && pa !== pb) return pa - pb;
2323
+ if (pa !== void 0 && pb === void 0) return -1;
2324
+ if (pa === void 0 && pb !== void 0) return 1;
2325
+ return ka.localeCompare(kb);
2326
+ });
2327
+ return Object.fromEntries(entries);
2328
+ }
2329
+ function inferColumnsFromRows(rows) {
2330
+ const columns = {};
2331
+ for (const row of rows) {
2332
+ for (const key of Object.keys(row)) {
2333
+ if (key === SYSTEM_COLUMN) continue;
2334
+ if (!columns[key]) columns[key] = inferColumnType(row[key]);
2259
2335
  }
2260
2336
  }
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;
2337
+ return columns;
2338
+ }
2339
+ function inferColumnType(sample) {
2340
+ switch (typeof sample) {
2341
+ case "number":
2342
+ return { type: "Numeric" };
2343
+ case "boolean":
2344
+ return { type: "Bool" };
2345
+ case "string":
2346
+ return { type: "Text" };
2347
+ default:
2348
+ return { type: "Any" };
2268
2349
  }
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) {
2350
+ }
2351
+ function buildTable(tableId, input) {
2352
+ const rows = input.rows ?? [];
2353
+ const inferred = inferColumnsFromRows(rows);
2354
+ const merged = {};
2355
+ for (const [colId, col] of Object.entries({ ...inferred, ...input.columns ?? {} })) {
2356
+ merged[colId] = enrichEmulatorColumnMetadata(col);
2357
+ }
2358
+ const columns = sortEmulatorColumnsByParentPos(merged);
2359
+ return {
2360
+ tableId,
2361
+ label: input.label,
2362
+ columns,
2363
+ rows: rows.map((row) => ({ ...row }))
2364
+ };
2365
+ }
2366
+ function createDocument(input) {
2367
+ if (!input.tables || Object.keys(input.tables).length === 0) {
2368
+ throw new Error("createDocument: at least one table is required");
2369
+ }
2370
+ const normalized = normalizeReplicaDocumentInput(input);
2371
+ const tables = /* @__PURE__ */ new Map();
2372
+ for (const [tableId, tableInput] of Object.entries(normalized.tables)) {
2373
+ tables.set(tableId, buildTable(tableId, tableInput));
2374
+ }
2375
+ const firstTableId = tables.keys().next().value;
2376
+ const requestedTableId = normalized.selection?.tableId;
2377
+ const selectedTableId = requestedTableId && tables.has(requestedTableId) ? requestedTableId : firstTableId;
2378
+ const selectedTable = tables.get(selectedTableId);
2379
+ const fallbackRowId = typeof selectedTable.rows[0]?.id === "number" ? selectedTable.rows[0].id : null;
2380
+ const selection = {
2381
+ tableId: selectedTableId,
2382
+ rowId: normalized.selection?.rowId ?? fallbackRowId,
2383
+ mappings: normalized.selection?.mappings,
2384
+ cursor: normalized.selection?.cursor
2385
+ };
2386
+ return {
2387
+ docName: normalized.docName ?? "Untitled emulator document",
2388
+ tables,
2389
+ selection,
2390
+ widgetOptions: { ...normalized.widgetOptions ?? {} },
2391
+ widgetInteraction: { ...normalized.widgetInteraction ?? {} },
2392
+ configuration: normalized.configuration ?? null,
2393
+ accessToken: normalized.accessToken ?? null
2394
+ };
2395
+ }
2396
+ function getTable(state, tableId) {
2397
+ const table = state.tables.get(tableId);
2398
+ if (!table) throw new Error(`Unknown table: ${tableId}`);
2399
+ return table;
2400
+ }
2401
+ function getSelectedTable(state) {
2402
+ return getTable(state, state.selection.tableId);
2403
+ }
2404
+ function nextRowId(table) {
2405
+ let max = 0;
2406
+ for (const row of table.rows) {
2407
+ if (typeof row.id === "number" && row.id > max) max = row.id;
2408
+ }
2409
+ return max + 1;
2410
+ }
2411
+ function findRow(table, rowId) {
2412
+ return table.rows.find((r) => r.id === rowId);
2413
+ }
2414
+ function findRowIndex(table, rowId) {
2415
+ return table.rows.findIndex((r) => r.id === rowId);
2416
+ }
2417
+ function evaluateRowFormulas(state, table, row) {
2418
+ for (const [colId, col] of Object.entries(table.columns)) {
2419
+ if (col.formulaFn) {
2274
2420
  try {
2275
- cb(opts, interaction);
2421
+ row[colId] = col.formulaFn(row, { table, document: state });
2276
2422
  } catch (err) {
2277
- logErr(err);
2423
+ row[colId] = formulaError2(err);
2278
2424
  }
2279
2425
  }
2280
2426
  }
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;
2427
+ }
2428
+ function escapeRegExp(s) {
2429
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2430
+ }
2431
+ function extractRefListIdsFromWire(raw) {
2432
+ if (raw == null) return [];
2433
+ if (!Array.isArray(raw)) return [];
2434
+ const items = raw[0] === "L" ? raw.slice(1) : raw;
2435
+ const out = [];
2436
+ for (const item of items) {
2437
+ if (typeof item === "number" && Number.isFinite(item) && item > 0) {
2438
+ out.push(Math.trunc(item));
2439
+ continue;
2308
2440
  }
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
- }
2441
+ if (item && typeof item === "object" && "rowId" in item) {
2442
+ const r = item.rowId;
2443
+ if (typeof r === "number" && Number.isFinite(r) && r > 0) {
2444
+ out.push(Math.trunc(r));
2445
+ continue;
2441
2446
  }
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
2447
  }
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);
2448
+ const t = coerceGristDocApiRefRowId(item);
2449
+ if (t !== void 0 && t > 0) out.push(Math.trunc(t));
2524
2450
  }
2525
- return rows;
2451
+ return Array.from(new Set(out));
2526
2452
  }
2527
- function parseFields(fields, _options) {
2528
- return { ...fields };
2453
+ function fieldFromRefListHelperFormula(formula, refColId) {
2454
+ if (!formula) return null;
2455
+ const re = new RegExp(`\\$${escapeRegExp(refColId)}\\.([A-Za-z_]\\w*)`);
2456
+ const m = formula.match(re);
2457
+ return m?.[1] ?? null;
2529
2458
  }
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 };
2459
+ function firstTextLikeColumnId(columns) {
2460
+ let best = null;
2461
+ for (const [colId, col] of Object.entries(columns)) {
2462
+ if (colId === "id" || colId.startsWith("gristHelper_")) continue;
2463
+ const t = col.type;
2464
+ if (t !== "Text" && t !== "Choice") continue;
2465
+ const pos = col.parentPos ?? 99999;
2466
+ if (!best || pos < best.pos) best = { id: colId, pos };
2467
+ }
2468
+ return best?.id ?? null;
2534
2469
  }
2535
- function logErr(err) {
2536
- console.error("[grist-emulator/inline]", err);
2470
+ function formatLabelFromCell(value) {
2471
+ if (value == null) return "";
2472
+ if (typeof value === "string") return value.trim();
2473
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
2474
+ if (value instanceof Date) return value.toISOString();
2475
+ if (Array.isArray(value) && value[0] === "E") return "";
2476
+ if (typeof value === "object" && value && "__ref" in value) {
2477
+ const r = value.rowId;
2478
+ return typeof r === "number" && Number.isFinite(r) ? String(r) : "";
2479
+ }
2480
+ return "";
2537
2481
  }
2538
- function installFakeIframeContext(target) {
2539
- try {
2540
- if (target.self !== target.top) return;
2541
- Object.defineProperty(target, "top", {
2542
- configurable: true,
2543
- get: () => ({})
2482
+ function syncRefListDisplayHelpers(state, table, row) {
2483
+ const displayHelpersByRef = inferDisplayHelpersByRef(table.columns);
2484
+ for (const [colId, col] of Object.entries(table.columns)) {
2485
+ const type = col.type;
2486
+ if (typeof type !== "string" || !type.startsWith("RefList:")) continue;
2487
+ const helperColId = displayHelpersByRef[colId];
2488
+ if (!helperColId) continue;
2489
+ const helperMeta = table.columns[helperColId];
2490
+ if (!helperMeta || helperMeta.formulaFn) continue;
2491
+ const targetTableId = type.slice("RefList:".length);
2492
+ const targetTable = state.tables.get(targetTableId);
2493
+ if (!targetTable) continue;
2494
+ const ids = extractRefListIdsFromWire(row[colId]);
2495
+ if (ids.length === 0) {
2496
+ row[helperColId] = [];
2497
+ continue;
2498
+ }
2499
+ const fieldId = fieldFromRefListHelperFormula(helperMeta.formula, colId) ?? firstTextLikeColumnId(targetTable.columns);
2500
+ const labels = ids.map((rid) => {
2501
+ if (!fieldId) return String(rid);
2502
+ const trow = findRow(targetTable, rid);
2503
+ if (!trow) return String(rid);
2504
+ const lbl = formatLabelFromCell(trow[fieldId]);
2505
+ return lbl !== "" ? lbl : String(rid);
2544
2506
  });
2545
- } catch {
2507
+ row[helperColId] = labels;
2546
2508
  }
2547
2509
  }
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);
2510
+ function formulaError2(err) {
2511
+ const message2 = err instanceof Error ? err.message : String(err);
2512
+ return ["E", "Error", message2];
2616
2513
  }
2617
- function parseDocument(input) {
2618
- const result = safeParseDocument(input);
2619
- if (!result.success) {
2620
- throw formatZodError("parseDocument", result.error);
2514
+ function synthesizeTablesMeta(state) {
2515
+ const ids = [];
2516
+ const tableIds = [];
2517
+ const labels = [];
2518
+ let nextId = 1;
2519
+ for (const [tableId, table] of state.tables.entries()) {
2520
+ ids.push(nextId);
2521
+ tableIds.push(tableId);
2522
+ labels.push(table.label ?? null);
2523
+ nextId += 1;
2621
2524
  }
2622
- return result.data;
2525
+ return {
2526
+ id: ids,
2527
+ tableId: tableIds,
2528
+ // Optional human-readable label; real Grist keeps more fields, but these
2529
+ // are sufficient for `buildReplicaDocumentFromDocApi` to resolve table ids.
2530
+ label: labels
2531
+ };
2623
2532
  }
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);
2533
+ function synthesizeTablesColumnMeta(state) {
2534
+ const parentIds = [];
2535
+ const colIds = [];
2536
+ const columnIds = [];
2537
+ const types = [];
2538
+ const labels = [];
2539
+ const parentPos = [];
2540
+ const isFormula = [];
2541
+ const formulas = [];
2542
+ const descriptions = [];
2543
+ const widgetOptions = [];
2544
+ const displayCol = [];
2545
+ const visibleCol = [];
2546
+ const tableIdToInternalId = /* @__PURE__ */ new Map();
2547
+ {
2548
+ let nextId = 1;
2549
+ for (const [tableId] of state.tables.entries()) {
2550
+ tableIdToInternalId.set(tableId, nextId);
2551
+ nextId += 1;
2552
+ }
2648
2553
  }
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();
2554
+ for (const [tableId, table] of state.tables.entries()) {
2555
+ const parentInternalId = tableIdToInternalId.get(tableId);
2556
+ if (parentInternalId === void 0) continue;
2557
+ for (const [colId, col] of Object.entries(table.columns)) {
2558
+ parentIds.push(parentInternalId);
2559
+ colIds.push(colId);
2560
+ const ref = typeof col.colRef === "number" ? col.colRef : columnIds.length + 1;
2561
+ columnIds.push(ref);
2562
+ types.push(col.type ?? null);
2563
+ labels.push(col.label ?? null);
2564
+ parentPos.push(col.parentPos ?? null);
2565
+ isFormula.push(Boolean(col.isFormula));
2566
+ const formula = typeof col.formula === "string" ? col.formula : null;
2567
+ formulas.push(formula);
2568
+ descriptions.push(col.description ?? null);
2569
+ widgetOptions.push(col.widgetOptions ?? null);
2570
+ displayCol.push(typeof col.displayColRef === "number" ? col.displayColRef : null);
2571
+ visibleCol.push(typeof col.visibleColRef === "number" ? col.visibleColRef : null);
2700
2572
  }
2573
+ }
2574
+ return {
2575
+ parentId: parentIds,
2576
+ colId: colIds,
2577
+ id: columnIds,
2578
+ type: types,
2579
+ label: labels,
2580
+ parentPos,
2581
+ isFormula,
2582
+ formula: formulas,
2583
+ description: descriptions,
2584
+ // Also expose legacy `descr` for robustness.
2585
+ descr: descriptions,
2586
+ widgetOptions,
2587
+ displayCol,
2588
+ visibleCol
2701
2589
  };
2702
2590
  }
2703
2591
 
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
- };
2592
+ // src/emulator/core/action-engine.ts
2593
+ var EmulatorActionError = class extends Error {
2594
+ constructor(action, message2) {
2595
+ super(message2);
2596
+ this.action = action;
2597
+ this.name = "EmulatorActionError";
2598
+ }
2599
+ };
2600
+ function applyUserActions(state, bus, actions) {
2601
+ const retValues = [];
2602
+ const changed = /* @__PURE__ */ new Set();
2603
+ for (const action of actions) {
2604
+ bus.emit({ type: "user-action", action });
2605
+ const ret = applySingle(state, action, changed);
2606
+ retValues.push(ret);
2607
+ }
2608
+ for (const tableId of changed) {
2609
+ bus.emit({ type: "data-change", tableId });
2610
+ }
2611
+ return { retValues, changedTableIds: [...changed] };
2711
2612
  }
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
- });
2613
+ function applySingle(state, action, changed) {
2614
+ const [name, ...args] = action;
2615
+ switch (name) {
2616
+ case "AddRecord":
2617
+ return addRecord(state, args, changed);
2618
+ case "BulkAddRecord":
2619
+ return bulkAddRecord(state, args, changed);
2620
+ case "UpdateRecord":
2621
+ return updateRecord(state, args, changed);
2622
+ case "BulkUpdateRecord":
2623
+ return bulkUpdateRecord(state, args, changed);
2624
+ case "RemoveRecord":
2625
+ return removeRecord(state, args, changed);
2626
+ case "BulkRemoveRecord":
2627
+ return bulkRemoveRecord(state, args, changed);
2628
+ case "AddColumn":
2629
+ return addColumn(state, args);
2630
+ case "AddVisibleColumn":
2631
+ return addColumn(state, args);
2632
+ case "RemoveColumn":
2633
+ return removeColumn(state, args, changed);
2634
+ case "RenameColumn":
2635
+ return renameColumn(state, args, changed);
2636
+ case "ModifyColumn":
2637
+ return modifyColumn(state, args, changed);
2638
+ case "AddTable":
2639
+ return addTable(state, args);
2640
+ case "RemoveTable":
2641
+ return removeTable(state, args);
2642
+ case "RenameTable":
2643
+ return renameTable(state, args);
2644
+ default:
2645
+ throw new EmulatorActionError(action, `Unsupported user action: ${String(name)}`);
2781
2646
  }
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
2647
  }
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
- }
2648
+ function addRecord(state, [tableId, rowId, values], changed) {
2649
+ const table = getTable(state, requireString(tableId, "AddRecord.tableId"));
2650
+ const id = typeof rowId === "number" ? rowId : nextRowId(table);
2651
+ const normalizedValues = normalizeValuesForTable(
2652
+ table,
2653
+ values ?? {}
2654
+ );
2655
+ const row = { id, ...normalizedValues };
2656
+ ensureColumns(table, normalizedValues);
2657
+ evaluateRowFormulas(state, table, row);
2658
+ syncRefListDisplayHelpers(state, table, row);
2659
+ table.rows.push(row);
2660
+ changed.add(table.tableId);
2661
+ return id;
2808
2662
  }
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 });
2663
+ function bulkAddRecord(state, [tableId, rowIds, columnar], changed) {
2664
+ const table = getTable(state, requireString(tableId, "BulkAddRecord.tableId"));
2665
+ const cols = normalizeColumnarForTable(
2666
+ table,
2667
+ columnar ?? {}
2668
+ );
2669
+ const ids = Array.isArray(rowIds) ? rowIds : [];
2670
+ const length = ids.length || lengthOf(cols);
2671
+ const out = [];
2672
+ ensureColumns(table, cols, true);
2673
+ for (let i = 0; i < length; i++) {
2674
+ const id = typeof ids[i] === "number" ? ids[i] : nextRowId(table);
2675
+ const row = { id };
2676
+ for (const colId of Object.keys(cols)) row[colId] = cols[colId][i];
2677
+ evaluateRowFormulas(state, table, row);
2678
+ syncRefListDisplayHelpers(state, table, row);
2679
+ table.rows.push(row);
2680
+ out.push(id);
2681
+ }
2682
+ changed.add(table.tableId);
2683
+ return out;
2824
2684
  }
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
- ] }) });
2685
+ function updateRecord(state, [tableId, rowId, values], changed) {
2686
+ const table = getTable(state, requireString(tableId, "UpdateRecord.tableId"));
2687
+ const id = requireNumber(rowId, "UpdateRecord.rowId");
2688
+ const row = findRow(table, id);
2689
+ if (!row) throw new EmulatorActionError(["UpdateRecord", tableId, rowId, values], `Row ${id} not found in ${table.tableId}`);
2690
+ const normalizedValues = normalizeValuesForTable(
2691
+ table,
2692
+ values ?? {}
2693
+ );
2694
+ ensureColumns(table, normalizedValues);
2695
+ Object.assign(row, normalizedValues);
2696
+ evaluateRowFormulas(state, table, row);
2697
+ syncRefListDisplayHelpers(state, table, row);
2698
+ changed.add(table.tableId);
2699
+ return null;
2902
2700
  }
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
- };
2701
+ function bulkUpdateRecord(state, [tableId, rowIds, columnar], changed) {
2702
+ const table = getTable(state, requireString(tableId, "BulkUpdateRecord.tableId"));
2703
+ const ids = rowIds;
2704
+ const cols = normalizeColumnarForTable(
2705
+ table,
2706
+ columnar ?? {}
2707
+ );
2708
+ ensureColumns(table, cols, true);
2709
+ ids.forEach((id, idx) => {
2710
+ const row = findRow(table, id);
2711
+ if (!row) throw new EmulatorActionError(["BulkUpdateRecord", tableId, ids, cols], `Row ${id} not found in ${table.tableId}`);
2712
+ for (const colId of Object.keys(cols)) row[colId] = cols[colId][idx];
2713
+ evaluateRowFormulas(state, table, row);
2714
+ syncRefListDisplayHelpers(state, table, row);
2715
+ });
2716
+ changed.add(table.tableId);
2717
+ return null;
2718
+ }
2719
+ function removeRecord(state, [tableId, rowId], changed) {
2720
+ const table = getTable(state, requireString(tableId, "RemoveRecord.tableId"));
2721
+ const id = requireNumber(rowId, "RemoveRecord.rowId");
2722
+ const idx = findRowIndex(table, id);
2723
+ if (idx >= 0) table.rows.splice(idx, 1);
2724
+ if (state.selection.tableId === table.tableId && state.selection.rowId === id) {
2725
+ state.selection.rowId = table.rows[0]?.id ?? null;
2926
2726
  }
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
- };
2727
+ changed.add(table.tableId);
2728
+ return null;
2729
+ }
2730
+ function bulkRemoveRecord(state, [tableId, rowIds], changed) {
2731
+ const table = getTable(state, requireString(tableId, "BulkRemoveRecord.tableId"));
2732
+ const ids = new Set(rowIds);
2733
+ table.rows = table.rows.filter((r) => !ids.has(r.id));
2734
+ if (state.selection.tableId === table.tableId && typeof state.selection.rowId === "number" && ids.has(state.selection.rowId)) {
2735
+ state.selection.rowId = table.rows[0]?.id ?? null;
2934
2736
  }
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
- };
2737
+ changed.add(table.tableId);
2738
+ return null;
2739
+ }
2740
+ function addColumn(state, [tableId, colId, info]) {
2741
+ const table = getTable(state, requireString(tableId, "AddColumn.tableId"));
2742
+ const col = requireString(colId, "AddColumn.colId");
2743
+ if (table.columns[col]) throw new EmulatorActionError(["AddColumn", tableId, colId], `Column ${col} already exists`);
2744
+ table.columns[col] = info ?? {};
2745
+ for (const row of table.rows) {
2746
+ if (!(col in row)) row[col] = null;
2942
2747
  }
2943
2748
  return null;
2944
2749
  }
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);
2750
+ function removeColumn(state, [tableId, colId], changed) {
2751
+ const table = getTable(state, requireString(tableId, "RemoveColumn.tableId"));
2752
+ const col = requireString(colId, "RemoveColumn.colId");
2753
+ delete table.columns[col];
2754
+ for (const row of table.rows) delete row[col];
2755
+ changed.add(table.tableId);
2756
+ return null;
2757
+ }
2758
+ function renameColumn(state, [tableId, colId, newColId], changed) {
2759
+ const table = getTable(state, requireString(tableId, "RenameColumn.tableId"));
2760
+ const from = requireString(colId, "RenameColumn.colId");
2761
+ const to = requireString(newColId, "RenameColumn.newColId");
2762
+ if (!table.columns[from]) throw new EmulatorActionError(["RenameColumn", tableId, from, to], `Column ${from} not found`);
2763
+ if (from === to) return null;
2764
+ table.columns[to] = table.columns[from];
2765
+ delete table.columns[from];
2766
+ for (const row of table.rows) {
2767
+ if (from in row) {
2768
+ row[to] = row[from];
2769
+ delete row[from];
2958
2770
  }
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"
2771
+ }
2772
+ changed.add(table.tableId);
2773
+ return null;
2774
+ }
2775
+ function modifyColumn(state, [tableId, colId, info], changed) {
2776
+ const table = getTable(state, requireString(tableId, "ModifyColumn.tableId"));
2777
+ const col = requireString(colId, "ModifyColumn.colId");
2778
+ if (!table.columns[col]) throw new EmulatorActionError(["ModifyColumn", tableId, colId], `Column ${col} not found`);
2779
+ table.columns[col] = { ...table.columns[col], ...info };
2780
+ changed.add(table.tableId);
2781
+ return null;
2782
+ }
2783
+ function addTable(state, [tableId, columns]) {
2784
+ const id = requireString(tableId, "AddTable.tableId");
2785
+ if (state.tables.has(id)) throw new EmulatorActionError(["AddTable", tableId], `Table ${id} already exists`);
2786
+ const cols = {};
2787
+ for (const c of columns ?? []) {
2788
+ cols[c.id] = { ...c };
2789
+ }
2790
+ state.tables.set(id, { tableId: id, columns: cols, rows: [] });
2791
+ return id;
2792
+ }
2793
+ function removeTable(state, [tableId]) {
2794
+ const id = requireString(tableId, "RemoveTable.tableId");
2795
+ state.tables.delete(id);
2796
+ if (state.selection.tableId === id) {
2797
+ const fallback = state.tables.keys().next().value;
2798
+ if (fallback) {
2799
+ state.selection.tableId = fallback;
2800
+ state.selection.rowId = null;
2972
2801
  }
2973
- );
2802
+ }
2803
+ return null;
2974
2804
  }
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";
2805
+ function renameTable(state, [from, to]) {
2806
+ const fromId = requireString(from, "RenameTable.from");
2807
+ const toId = requireString(to, "RenameTable.to");
2808
+ const table = state.tables.get(fromId);
2809
+ if (!table) throw new EmulatorActionError(["RenameTable", fromId], `Table ${fromId} not found`);
2810
+ state.tables.delete(fromId);
2811
+ table.tableId = toId;
2812
+ state.tables.set(toId, table);
2813
+ if (state.selection.tableId === fromId) state.selection.tableId = toId;
2814
+ return null;
3052
2815
  }
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
- ] });
2816
+ function ensureColumns(table, values, bulk = false) {
2817
+ for (const col of Object.keys(values)) {
2818
+ if (!table.columns[col]) {
2819
+ const sample = bulk ? values[col]?.[0] : values[col];
2820
+ table.columns[col] = inferType(sample);
2821
+ }
2822
+ }
3088
2823
  }
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] };
2824
+ function inferType(sample) {
2825
+ switch (typeof sample) {
2826
+ case "number":
2827
+ return { type: "Numeric" };
2828
+ case "boolean":
2829
+ return { type: "Bool" };
2830
+ case "string":
2831
+ return { type: "Text" };
2832
+ default:
2833
+ return { type: "Any" };
3104
2834
  }
3105
- if (!channel) return { channel: { kind: "latest" }, basePath: withTrailingSlash(pathname) };
3106
- return { channel, basePath: withTrailingSlash(segments.slice(0, -1).join("/")) };
3107
2835
  }
3108
- function deployChannelLabel(channel) {
3109
- return channel.kind === "version" ? `v${channel.version}` : channel.kind;
2836
+ function lengthOf(cols) {
2837
+ for (const arr of Object.values(cols)) if (Array.isArray(arr)) return arr.length;
2838
+ return 0;
3110
2839
  }
3111
- function withTrailingSlash(path) {
3112
- return path.endsWith("/") ? path : `${path}/`;
2840
+ function requireString(value, name) {
2841
+ if (typeof value !== "string" || !value) throw new Error(`Expected non-empty string for ${name}`);
2842
+ return value;
3113
2843
  }
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;
2844
+ function requireNumber(value, name) {
2845
+ if (typeof value !== "number" || Number.isNaN(value)) throw new Error(`Expected number for ${name}`);
2846
+ return value;
3134
2847
  }
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
- ] });
2848
+ function normalizeValuesForTable(table, values) {
2849
+ const out = {};
2850
+ for (const [colId, value] of Object.entries(values)) {
2851
+ out[colId] = normalizeCellValueForType(value, table.columns[colId]?.type);
2852
+ }
2853
+ return out;
3155
2854
  }
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
- ] });
2855
+ function normalizeColumnarForTable(table, columns) {
2856
+ const out = {};
2857
+ for (const [colId, values] of Object.entries(columns)) {
2858
+ const type = table.columns[colId]?.type;
2859
+ out[colId] = Array.isArray(values) ? values.map((value) => normalizeCellValueForType(value, type)) : [];
2860
+ }
2861
+ return out;
3174
2862
  }
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 });
2863
+ function normalizeCellValueForType(value, type) {
2864
+ if (value === null || value === void 0 || typeof type !== "string") return value;
2865
+ if (isListColumnType(type) && Array.isArray(value) && value[0] !== "L") return ["E", "KeyError"];
2866
+ if (type.startsWith("Ref:")) return normalizeRefValue(value);
2867
+ if (type.startsWith("RefList:")) return normalizeRefListValue(value);
2868
+ return value;
3192
2869
  }
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
- });
2870
+ function extractRefRowId(value) {
2871
+ if (value === null || value === void 0) return null;
2872
+ if (value && typeof value === "object" && "__ref" in value) {
2873
+ const inner = value.rowId;
2874
+ return extractRefRowId(inner);
3201
2875
  }
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
- ] });
2876
+ if (value && typeof value === "object" && "rowId" in value) {
2877
+ return extractRefRowId(value.rowId);
2878
+ }
2879
+ const fromTuple = coerceGristDocApiRefRowId(value);
2880
+ if (fromTuple !== void 0 && fromTuple > 0) return Math.trunc(fromTuple);
2881
+ return toPositiveInt(value);
3247
2882
  }
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" });
2883
+ function isListColumnType(type) {
2884
+ return type === "ChoiceList" || type === "Attachments" || type.startsWith("RefList:");
3251
2885
  }
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;
2886
+ function normalizeRefValue(value) {
2887
+ return extractRefRowId(value);
3285
2888
  }
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(" ")));
2889
+ function normalizeRefListValue(value) {
2890
+ const toIdList = (items) => Array.from(
2891
+ new Set(items.map((item) => extractRefRowId(item)).filter((item) => item !== null))
2892
+ );
2893
+ if (Array.isArray(value) && value[0] === "L") {
2894
+ return ["L", ...toIdList(value.slice(1))];
3296
2895
  }
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"));
2896
+ if (Array.isArray(value)) {
2897
+ return ["L", ...toIdList(value)];
3308
2898
  }
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}.`));
2899
+ if (typeof value === "string") {
2900
+ const tokens2 = value.split(",").map((t) => t.trim()).filter(Boolean);
2901
+ return ["L", ...toIdList(tokens2)];
3312
2902
  }
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.`));
2903
+ if (value && typeof value === "object" && Array.isArray(value.rowIds)) {
2904
+ return ["L", ...toIdList(value.rowIds)];
3319
2905
  }
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
- );
2906
+ return ["L"];
2907
+ }
2908
+ function toPositiveInt(value) {
2909
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return Math.trunc(value);
2910
+ if (typeof value === "string") {
2911
+ const n = Number.parseInt(value.trim(), 10);
2912
+ if (Number.isFinite(n) && n > 0) return n;
2913
+ }
2914
+ return null;
2915
+ }
2916
+
2917
+ // src/emulator/core/columnar.ts
2918
+ function rowsToColumnar(table, rows, options = {}) {
2919
+ const cols = Object.keys(table.columns);
2920
+ const out = { id: rows.map((r) => r.id) };
2921
+ for (const col of cols) {
2922
+ out[col] = rows.map((r) => encodeForWire(r[col], table.columns[col]?.type, options.keepEncoded));
2923
+ }
2924
+ return out;
2925
+ }
2926
+ function rowToRecord(table, row, options = {}) {
2927
+ const out = { id: row.id };
2928
+ for (const col of Object.keys(table.columns)) {
2929
+ out[col] = encodeForWire(row[col], table.columns[col]?.type, options.keepEncoded);
2930
+ }
2931
+ return out;
2932
+ }
2933
+ function encodeForWire(value, type, keepEncoded) {
2934
+ if (value instanceof Date) {
2935
+ const epoch = Math.floor(value.getTime() / 1e3);
2936
+ if (keepEncoded) {
2937
+ return type && type.startsWith("DateTime") ? ["D", epoch, "UTC"] : ["d", epoch];
2938
+ }
2939
+ return epoch;
2940
+ }
2941
+ return value;
2942
+ }
2943
+
2944
+ // src/emulator/api/grist-doc-api.ts
2945
+ function createGristDocApiImpl(state, bus) {
2946
+ return {
2947
+ async getDocName() {
2948
+ return state.docName;
2949
+ },
2950
+ async listTables() {
2951
+ return [...state.tables.keys()];
2952
+ },
2953
+ async fetchTable(tableId) {
2954
+ if (tableId === "_grist_Tables") {
2955
+ return synthesizeTablesMeta(state);
2956
+ }
2957
+ if (tableId === "_grist_Tables_column") {
2958
+ return synthesizeTablesColumnMeta(state);
2959
+ }
2960
+ const table = getTable(state, tableId);
2961
+ return rowsToColumnar(table, table.rows);
2962
+ },
2963
+ async applyUserActions(actions) {
2964
+ const result = applyUserActions(state, bus, actions);
2965
+ return { retValues: result.retValues };
2966
+ },
2967
+ async getAccessToken() {
2968
+ if (!state.accessToken) {
2969
+ const err = new Error("getAccessToken not available (no token configured on emulator)");
2970
+ throw err;
2971
+ }
2972
+ return { ...state.accessToken };
2973
+ }
2974
+ };
2975
+ }
2976
+
2977
+ // src/emulator/api/grist-view.ts
2978
+ function createGristViewImpl(state, bus) {
2979
+ return {
2980
+ async fetchSelectedTable(options) {
2981
+ const table = getSelectedTable(state);
2982
+ return rowsToColumnar(table, table.rows, { keepEncoded: options?.keepEncoded });
2983
+ },
2984
+ async fetchSelectedRecord(rowId, options) {
2985
+ const table = getSelectedTable(state);
2986
+ const row = findRow(table, rowId);
2987
+ if (!row) throw new Error(`No row ${rowId} in ${table.tableId}`);
2988
+ return rowToRecord(table, row, { keepEncoded: options?.keepEncoded });
2989
+ },
2990
+ async allowSelectBy() {
2991
+ return state.configuration?.allowSelectBy ?? false;
2992
+ },
2993
+ async setSelectedRows(rowIds) {
2994
+ if (!rowIds || rowIds.length === 0) {
2995
+ state.selection.rowId = null;
2996
+ } else {
2997
+ state.selection.rowId = rowIds[0];
2998
+ }
2999
+ bus.emit({
3000
+ type: "cursor-change",
3001
+ tableId: state.selection.tableId,
3002
+ rowId: state.selection.rowId
3003
+ });
3004
+ },
3005
+ async setCursorPos(cursor) {
3006
+ const tableId = state.selection.tableId;
3007
+ if (typeof cursor.rowId === "number") state.selection.rowId = cursor.rowId;
3008
+ state.selection.cursor = { ...state.selection.cursor, ...cursor };
3009
+ bus.emit({
3010
+ type: "cursor-change",
3011
+ tableId,
3012
+ rowId: state.selection.rowId,
3013
+ cursor: state.selection.cursor
3014
+ });
3015
+ }
3016
+ };
3017
+ }
3018
+
3019
+ // src/emulator/api/widget-api.ts
3020
+ function createWidgetApiImpl(state, bus) {
3021
+ function emit() {
3022
+ bus.emit({
3023
+ type: "options-change",
3024
+ options: { ...state.widgetOptions },
3025
+ interaction: { ...state.widgetInteraction }
3026
+ });
3027
+ }
3028
+ return {
3029
+ async getOption(key) {
3030
+ return state.widgetOptions[key];
3031
+ },
3032
+ async setOption(key, value) {
3033
+ state.widgetOptions[key] = value;
3034
+ emit();
3035
+ },
3036
+ async getOptions() {
3037
+ return Object.keys(state.widgetOptions).length > 0 ? { ...state.widgetOptions } : null;
3038
+ },
3039
+ // Replaces every option, as Grist's WidgetAPI.setOptions does.
3040
+ async setOptions(options) {
3041
+ state.widgetOptions = { ...options };
3042
+ emit();
3043
+ },
3044
+ async clearOptions() {
3045
+ state.widgetOptions = {};
3046
+ emit();
3047
+ }
3048
+ };
3049
+ }
3050
+
3051
+ // src/emulator/api/index.ts
3052
+ function createApiBundle(state, bus) {
3053
+ return {
3054
+ GristView: createGristViewImpl(state, bus),
3055
+ GristDocAPI: createGristDocApiImpl(state, bus),
3056
+ WidgetAPI: createWidgetApiImpl(state, bus),
3057
+ CustomSectionAPI: createCustomSectionApiImpl(state, bus),
3058
+ GristAPI: createGristApiImpl(),
3059
+ CommandAPI: createCommandApiImpl(bus)
3060
+ };
3061
+ }
3062
+
3063
+ // src/emulator/core/event-bus.ts
3064
+ function createEventBus(options = {}) {
3065
+ const limit = options.historyLimit ?? 200;
3066
+ const listeners = /* @__PURE__ */ new Set();
3067
+ const history = [];
3068
+ return {
3069
+ emit(event) {
3070
+ history.push(event);
3071
+ if (history.length > limit) history.shift();
3072
+ for (const fn of [...listeners]) {
3073
+ try {
3074
+ fn(event);
3075
+ } catch (err) {
3076
+ console.error("[grist-emulator] event listener threw:", err);
3077
+ }
3078
+ }
3079
+ },
3080
+ on(listener) {
3081
+ listeners.add(listener);
3082
+ return () => {
3083
+ listeners.delete(listener);
3084
+ };
3085
+ },
3086
+ history() {
3087
+ return history;
3088
+ },
3089
+ clear() {
3090
+ history.length = 0;
3091
+ }
3092
+ };
3093
+ }
3094
+
3095
+ // src/emulator/theme-message.ts
3096
+ function buildGristThemeHostMessage(appearance, options) {
3097
+ return {
3098
+ theme: {
3099
+ appearance,
3100
+ name: appearance,
3101
+ colors: {}
3102
+ },
3103
+ ...options?.fromReady ? { fromReady: true } : {}
3104
+ };
3105
+ }
3106
+
3107
+ // src/emulator/transports/inline.ts
3108
+ function createInlineTransport(options) {
3109
+ const { state, bus, api } = options;
3110
+ const target = options.target ?? globalThis;
3111
+ installFakeIframeContext(target);
3112
+ const recordCbs = /* @__PURE__ */ new Map();
3113
+ const recordsCbs = /* @__PURE__ */ new Map();
3114
+ const optionsCbs = /* @__PURE__ */ new Set();
3115
+ const newRecordCbs = /* @__PURE__ */ new Set();
3116
+ const generic = /* @__PURE__ */ new Map();
3117
+ let lastRecord = null;
3118
+ let lastRecords = null;
3119
+ let lastOptions = null;
3120
+ let lastNewRecord = void 0;
3121
+ const buildInteraction = options.buildInteraction ?? (() => ({
3122
+ access_level: state.configuration?.requiredAccess ?? "read table",
3123
+ ...state.widgetInteraction
3124
+ }));
3125
+ async function pushRecord(rowId) {
3126
+ const mappings = resolveMappings(state);
3127
+ if (rowId === "new") {
3128
+ lastNewRecord = mappings;
3129
+ for (const cb of newRecordCbs) {
3130
+ try {
3131
+ cb(mappings);
3132
+ } catch (err) {
3133
+ logErr(err);
3134
+ }
3135
+ }
3136
+ return;
3137
+ }
3138
+ if (rowId === null) {
3139
+ lastRecord = { record: null, mappings };
3140
+ for (const [cb] of recordCbs) {
3141
+ try {
3142
+ cb(null, mappings);
3143
+ } catch (err) {
3144
+ logErr(err);
3145
+ }
3146
+ }
3147
+ return;
3148
+ }
3149
+ let record = null;
3150
+ try {
3151
+ record = await api.GristView.fetchSelectedRecord(rowId);
3152
+ } catch (err) {
3153
+ logErr(err);
3154
+ }
3155
+ lastRecord = { record, mappings };
3156
+ for (const [cb, opts] of recordCbs) {
3157
+ try {
3158
+ const r = opts?.keepEncoded ? await api.GristView.fetchSelectedRecord(rowId, { keepEncoded: true }) : record;
3159
+ cb(r, mappings);
3160
+ } catch (err) {
3161
+ logErr(err);
3162
+ }
3163
+ }
3164
+ }
3165
+ async function pushRecords() {
3166
+ const mappings = resolveMappings(state);
3167
+ let baseRows = [];
3168
+ try {
3169
+ const columnar = await api.GristView.fetchSelectedTable({});
3170
+ baseRows = columnsToRows(columnar);
3171
+ } catch (err) {
3172
+ logErr(err);
3173
+ }
3174
+ lastRecords = { records: baseRows, mappings };
3175
+ for (const [cb, opts] of recordsCbs) {
3176
+ try {
3177
+ const fetchOptions = coerceFetchOptions(opts);
3178
+ const rows = fetchOptions?.keepEncoded ? columnsToRows(await api.GristView.fetchSelectedTable({ keepEncoded: true })) : baseRows;
3179
+ cb(rows, mappings);
3180
+ } catch (err) {
3181
+ logErr(err);
3182
+ }
3183
+ }
3184
+ }
3185
+ async function fetchSelectedTableShaped(opts) {
3186
+ const fetchOptions = coerceFetchOptions(opts);
3187
+ const columnar = await api.GristView.fetchSelectedTable(
3188
+ fetchOptions ? { keepEncoded: fetchOptions.keepEncoded } : {}
3189
+ );
3190
+ if (fetchOptions?.format === "rows") return columnsToRows(columnar);
3191
+ return columnar;
3192
+ }
3193
+ function pushOptions() {
3194
+ const opts = Object.keys(state.widgetOptions).length > 0 ? { ...state.widgetOptions } : null;
3195
+ const interaction = buildInteraction();
3196
+ lastOptions = { options: opts, interaction };
3197
+ for (const cb of optionsCbs) {
3198
+ try {
3199
+ cb(opts, interaction);
3200
+ } catch (err) {
3201
+ logErr(err);
3202
+ }
3203
+ }
3204
+ }
3205
+ const offBus = bus.on(async (event) => {
3206
+ switch (event.type) {
3207
+ case "data-change":
3208
+ await pushRecords();
3209
+ await pushRecord(state.selection.rowId);
3210
+ break;
3211
+ case "cursor-change":
3212
+ await pushRecord(event.rowId);
3213
+ break;
3214
+ case "options-change":
3215
+ pushOptions();
3216
+ break;
3217
+ case "ready":
3218
+ pushOptions();
3219
+ if (options.initialTheme) {
3220
+ notify("message", buildGristThemeHostMessage(options.initialTheme, { fromReady: true }));
3221
+ }
3222
+ await pushRecords();
3223
+ await pushRecord(state.selection.rowId);
3224
+ break;
3225
+ case "mappings-change":
3226
+ await pushRecords();
3227
+ await pushRecord(state.selection.rowId);
3228
+ break;
3229
+ case "theme-change":
3230
+ notify("message", event.message);
3231
+ break;
3232
+ }
3233
+ });
3234
+ function makeTableOps(tableId) {
3235
+ return {
3236
+ async getTableId() {
3237
+ return tableId;
3238
+ },
3239
+ async create(records, options2) {
3240
+ const list = Array.isArray(records) ? records : [records];
3241
+ const ids = [];
3242
+ for (const r of list) {
3243
+ const result = applyUserActions(state, bus, [
3244
+ gristAddRecordAction(tableId, parseFields(r.fields, options2))
3245
+ ]);
3246
+ ids.push({ id: result.retValues[0] });
3247
+ }
3248
+ return Array.isArray(records) ? ids : ids[0];
3249
+ },
3250
+ async update(records, options2) {
3251
+ const list = Array.isArray(records) ? records : [records];
3252
+ const actions = list.map((r) => gristUpdateRecordAction(tableId, r.id, parseFields(r.fields, options2)));
3253
+ applyUserActions(state, bus, actions);
3254
+ },
3255
+ async upsert(records, options2) {
3256
+ const list = Array.isArray(records) ? records : [records];
3257
+ const allowAdd = options2?.add !== false;
3258
+ const allowUpdate = options2?.update !== false;
3259
+ const table = getTable(state, tableId);
3260
+ const actions = [];
3261
+ for (const r of list) {
3262
+ const matches = table.rows.filter(
3263
+ (row) => Object.entries(r.require).every(([k, v]) => row[k] === v)
3264
+ );
3265
+ if (matches.length > 0) {
3266
+ if (!allowUpdate) continue;
3267
+ const fields = parseFields(r.fields ?? {}, options2);
3268
+ for (const m of matches) actions.push(gristUpdateRecordAction(tableId, m.id, fields));
3269
+ } else if (allowAdd) {
3270
+ actions.push(gristAddRecordAction(tableId, { ...r.require, ...parseFields(r.fields ?? {}, options2) }));
3271
+ }
3272
+ }
3273
+ if (actions.length > 0) applyUserActions(state, bus, actions);
3274
+ },
3275
+ async destroy(recordIds) {
3276
+ if (Array.isArray(recordIds)) {
3277
+ applyUserActions(state, bus, [gristBulkRemoveRecordAction(tableId, recordIds)]);
3278
+ } else {
3279
+ applyUserActions(state, bus, [gristRemoveRecordAction(tableId, recordIds)]);
3280
+ }
3281
+ void gristBulkAddRecordAction;
3282
+ void gristBulkUpdateRecordAction;
3283
+ }
3284
+ };
3285
+ }
3286
+ function notify(event, ...args) {
3287
+ const set = generic.get(event);
3288
+ if (!set) return;
3289
+ for (const fn of set) {
3290
+ try {
3291
+ fn(...args);
3292
+ } catch (err) {
3293
+ logErr(err);
3294
+ }
3295
+ }
3296
+ }
3297
+ const grist2 = {
3298
+ async ready(opts) {
3299
+ await api.CustomSectionAPI.configure(opts ? { ...opts, onEditOptions: void 0 } : {});
3300
+ },
3301
+ onRecord(cb, opts) {
3302
+ recordCbs.set(cb, opts);
3303
+ if (lastRecord) {
3304
+ try {
3305
+ cb(lastRecord.record, lastRecord.mappings);
3306
+ } catch (err) {
3307
+ logErr(err);
3308
+ }
3309
+ }
3310
+ },
3311
+ onRecords(cb, opts) {
3312
+ recordsCbs.set(cb, opts);
3313
+ if (lastRecords) {
3314
+ try {
3315
+ cb(lastRecords.records, lastRecords.mappings);
3316
+ } catch (err) {
3317
+ logErr(err);
3318
+ }
3319
+ }
3320
+ },
3321
+ onOptions(cb) {
3322
+ optionsCbs.add(cb);
3323
+ if (lastOptions) {
3324
+ try {
3325
+ cb(lastOptions.options, lastOptions.interaction);
3326
+ } catch (err) {
3327
+ logErr(err);
3328
+ }
3329
+ }
3330
+ },
3331
+ onNewRecord(cb) {
3332
+ newRecordCbs.add(cb);
3333
+ if (lastNewRecord !== void 0) {
3334
+ try {
3335
+ cb(lastNewRecord);
3336
+ } catch (err) {
3337
+ logErr(err);
3338
+ }
3339
+ }
3340
+ },
3341
+ mapColumnNames(data) {
3342
+ const mappings = resolveMappings(state);
3343
+ if (!mappings) return null;
3344
+ const out = {};
3345
+ let allMapped = true;
3346
+ for (const [logical, real] of Object.entries(mappings)) {
3347
+ if (Array.isArray(real)) {
3348
+ out[logical] = real.map((r) => data[r]);
3349
+ } else if (typeof real === "string") {
3350
+ out[logical] = data[real];
3351
+ } else {
3352
+ allMapped = false;
3353
+ }
3354
+ }
3355
+ return allMapped ? out : null;
3356
+ },
3357
+ mapColumnNamesBack(data) {
3358
+ const mappings = resolveMappings(state);
3359
+ if (!mappings) return data;
3360
+ const out = {};
3361
+ for (const [logical, real] of Object.entries(mappings)) {
3362
+ if (typeof real === "string" && logical in data) {
3363
+ out[real] = data[logical];
3364
+ }
3365
+ }
3366
+ return out;
3367
+ },
3368
+ getOption: (k) => api.WidgetAPI.getOption(k),
3369
+ setOption: (k, v) => api.WidgetAPI.setOption(k, v),
3370
+ getOptions: () => api.WidgetAPI.getOptions(),
3371
+ setOptions: (o) => api.WidgetAPI.setOptions(o),
3372
+ clearOptions: () => api.WidgetAPI.clearOptions(),
3373
+ setCursorPos: (pos) => api.GristView.setCursorPos(pos),
3374
+ setSelectedRows: (rowIds) => api.GristView.setSelectedRows(rowIds),
3375
+ fetchSelectedTable: (opts) => fetchSelectedTableShaped(opts),
3376
+ fetchSelectedRecord: (rowId, opts) => api.GristView.fetchSelectedRecord(rowId, coerceFetchOptions(opts)),
3377
+ docApi: {
3378
+ getDocName: () => api.GristDocAPI.getDocName(),
3379
+ listTables: () => api.GristDocAPI.listTables(),
3380
+ fetchTable: (id) => api.GristDocAPI.fetchTable(id),
3381
+ applyUserActions: (actions, options2) => api.GristDocAPI.applyUserActions(actions, options2),
3382
+ getAccessToken: (opts) => api.GristDocAPI.getAccessToken(opts),
3383
+ fetchSelectedTable: (opts) => fetchSelectedTableShaped(opts),
3384
+ fetchSelectedRecord: (rowId, opts) => api.GristView.fetchSelectedRecord(rowId, coerceFetchOptions(opts))
3385
+ },
3386
+ get selectedTable() {
3387
+ return makeTableOps(state.selection.tableId);
3388
+ },
3389
+ getTable(tableId) {
3390
+ return makeTableOps(tableId ?? state.selection.tableId);
3391
+ },
3392
+ sectionApi: {
3393
+ configure: (settings) => api.CustomSectionAPI.configure(settings),
3394
+ mappings: () => api.CustomSectionAPI.mappings()
3395
+ },
3396
+ on(eventName, listener) {
3397
+ let set = generic.get(eventName);
3398
+ if (!set) generic.set(eventName, set = /* @__PURE__ */ new Set());
3399
+ set.add(listener);
3400
+ return () => set.delete(listener);
3401
+ }
3402
+ };
3403
+ target.grist = grist2;
3404
+ return {
3405
+ grist: grist2,
3406
+ dispose() {
3407
+ offBus();
3408
+ const t = target;
3409
+ if (t.grist === grist2) delete t.grist;
3410
+ recordCbs.clear();
3411
+ recordsCbs.clear();
3412
+ optionsCbs.clear();
3413
+ newRecordCbs.clear();
3414
+ generic.clear();
3415
+ lastRecord = null;
3416
+ lastRecords = null;
3417
+ lastOptions = null;
3418
+ lastNewRecord = void 0;
3419
+ },
3420
+ fireDataChange(tableId) {
3421
+ bus.emit({ type: "data-change", tableId: tableId ?? state.selection.tableId });
3422
+ },
3423
+ fireCursor(rowId) {
3424
+ const id = rowId === void 0 ? state.selection.rowId : rowId;
3425
+ bus.emit({ type: "cursor-change", tableId: state.selection.tableId, rowId: id });
3426
+ },
3427
+ fireOptions() {
3428
+ pushOptions();
3429
+ },
3430
+ fireTheme(message2) {
3431
+ bus.emit({ type: "theme-change", message: message2 });
3432
+ },
3433
+ emitInitialState() {
3434
+ pushOptions();
3435
+ void pushRecords().then(() => pushRecord(state.selection.rowId));
3329
3436
  }
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));
3437
+ };
3341
3438
  }
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);
3439
+ function columnsToRows(columnar) {
3440
+ const cols = Object.keys(columnar);
3441
+ if (cols.length === 0) return [];
3442
+ const length = (columnar[cols[0]] ?? []).length;
3443
+ const rows = [];
3444
+ for (let i = 0; i < length; i++) {
3445
+ const row = {};
3446
+ for (const c of cols) row[c] = columnar[c][i];
3447
+ rows.push(row);
3448
+ }
3449
+ return rows;
3347
3450
  }
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
- ] });
3451
+ function parseFields(fields, _options) {
3452
+ return { ...fields };
3361
3453
  }
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
- ] });
3454
+ function coerceFetchOptions(opts) {
3455
+ if (!opts) return void 0;
3456
+ const format = opts.format === "rows" || opts.format === "columns" ? opts.format : void 0;
3457
+ return { keepEncoded: opts.keepEncoded, format };
3371
3458
  }
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
- ] });
3459
+ function logErr(err) {
3460
+ console.error("[grist-emulator/inline]", err);
3404
3461
  }
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
- ] });
3462
+ function installFakeIframeContext(target) {
3463
+ try {
3464
+ if (target.self !== target.top) return;
3465
+ Object.defineProperty(target, "top", {
3466
+ configurable: true,
3467
+ get: () => ({})
3468
+ });
3469
+ } catch {
3470
+ }
3411
3471
  }
3412
3472
 
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";
3473
+ // src/emulator/validators.ts
3474
+ var import_zod = require("zod");
3475
+ var emulatorRowSchema = import_zod.z.object({ id: import_zod.z.number() }).catchall(import_zod.z.unknown());
3476
+ var formulaKindSchema = import_zod.z.enum(["computed", "trigger", "none", "empty_slot"]);
3477
+ var replicaColumnSchema = import_zod.z.object({
3478
+ colRef: import_zod.z.number().optional(),
3479
+ type: import_zod.z.string().optional(),
3480
+ label: import_zod.z.string().optional(),
3481
+ parentPos: import_zod.z.number().optional(),
3482
+ isFormula: import_zod.z.boolean().optional(),
3483
+ formulaKind: formulaKindSchema.optional(),
3484
+ formula: import_zod.z.string().optional(),
3485
+ description: import_zod.z.string().optional(),
3486
+ widgetOptions: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
3487
+ displayColRef: import_zod.z.number().optional(),
3488
+ visibleColRef: import_zod.z.number().optional()
3489
+ }).strict();
3490
+ var replicaTableSchema = import_zod.z.object({
3491
+ label: import_zod.z.string().optional(),
3492
+ columns: import_zod.z.record(import_zod.z.string(), replicaColumnSchema).optional(),
3493
+ rows: import_zod.z.array(emulatorRowSchema).optional(),
3494
+ rowCount: import_zod.z.number().optional(),
3495
+ dataOmitted: import_zod.z.boolean().optional(),
3496
+ dataOmittedReason: import_zod.z.string().optional(),
3497
+ error: import_zod.z.string().optional()
3498
+ }).strict();
3499
+ var replicaSelectionSchema = import_zod.z.object({
3500
+ tableId: import_zod.z.string().optional(),
3501
+ rowId: import_zod.z.union([import_zod.z.number(), import_zod.z.literal("new"), import_zod.z.null()]).optional(),
3502
+ mappings: import_zod.z.unknown().nullable().optional(),
3503
+ cursor: import_zod.z.unknown().optional()
3504
+ }).strict();
3505
+ var replicaWidgetConfigurationSchema = import_zod.z.object({
3506
+ requiredAccess: import_zod.z.string().optional(),
3507
+ columns: import_zod.z.unknown().optional(),
3508
+ allowSelectBy: import_zod.z.boolean().optional(),
3509
+ hasCustomOptions: import_zod.z.boolean().optional()
3510
+ }).strict();
3511
+ var replicaAccessTokenSchema = import_zod.z.object({
3512
+ token: import_zod.z.string(),
3513
+ baseUrl: import_zod.z.string(),
3514
+ ttlMsecs: import_zod.z.number()
3515
+ }).strict();
3516
+ var replicaDocumentSchema = import_zod.z.object({
3517
+ generatedAt: import_zod.z.string(),
3518
+ source: import_zod.z.string().optional(),
3519
+ docName: import_zod.z.string().optional(),
3520
+ mode: import_zod.z.enum(["schema-only", "schema+samples", "schema+data"]).optional(),
3521
+ tables: import_zod.z.record(import_zod.z.string(), replicaTableSchema),
3522
+ selection: replicaSelectionSchema.optional(),
3523
+ widgetOptions: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
3524
+ widgetInteraction: import_zod.z.unknown().optional(),
3525
+ configuration: replicaWidgetConfigurationSchema.optional(),
3526
+ accessToken: replicaAccessTokenSchema.nullable().optional()
3527
+ }).strict().refine((value) => Object.keys(value.tables).length > 0, {
3528
+ message: "at least one table is required",
3529
+ path: ["tables"]
3530
+ });
3531
+ function formatZodError(prefix, error) {
3532
+ const details = error.issues.map((issue) => {
3533
+ const where = issue.path.length > 0 ? issue.path.join(".") : "<root>";
3534
+ return `${where}: ${issue.message}`;
3535
+ }).join("; ");
3536
+ return new Error(`${prefix}: ${details}`);
3493
3537
  }
3494
- function other(theme) {
3495
- return theme === "dark" ? "light" : "dark";
3538
+ function safeParseDocument(input) {
3539
+ return replicaDocumentSchema.safeParse(input);
3496
3540
  }
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
- };
3541
+ function parseDocument(input) {
3542
+ const result = safeParseDocument(input);
3543
+ if (!result.success) {
3544
+ throw formatZodError("parseDocument", result.error);
3545
+ }
3546
+ return result.data;
3507
3547
  }
3508
- function isEditableTarget(target) {
3509
- if (!(target instanceof HTMLElement)) return false;
3510
- return target.isContentEditable || target.closest("input, textarea, select, [contenteditable='true']") !== null;
3548
+
3549
+ // src/emulator/create-grist-emulator.ts
3550
+ function createGristEmulator(options) {
3551
+ const validatedDocument = parseDocument(options.document);
3552
+ const state = createDocument({
3553
+ ...validatedDocument,
3554
+ widgetOptions: options.widgetOptions ?? validatedDocument.widgetOptions,
3555
+ // No access_level by default: the emulator then grants the access the
3556
+ // widget asks for in grist.ready(), as a user accepting it in Grist would.
3557
+ widgetInteraction: options.widgetInteraction ?? validatedDocument.widgetInteraction ?? {}
3558
+ });
3559
+ const bus = createEventBus();
3560
+ const api = createApiBundle(state, bus);
3561
+ const inline = createInlineTransport({
3562
+ state,
3563
+ bus,
3564
+ api,
3565
+ initialTheme: options.initialTheme,
3566
+ target: options.transport?.target
3567
+ });
3568
+ const transport = inline;
3569
+ const grist2 = inline.grist;
3570
+ function apply(actions) {
3571
+ return applyUserActions(state, bus, actions);
3572
+ }
3573
+ return {
3574
+ state,
3575
+ bus,
3576
+ api,
3577
+ transport,
3578
+ grist: grist2,
3579
+ apply,
3580
+ mutate: {
3581
+ addRow: (tableId, fields) => apply([["AddRecord", tableId, null, fields]]).retValues[0],
3582
+ updateRow: (tableId, rowId, fields) => {
3583
+ apply([["UpdateRecord", tableId, rowId, fields]]);
3584
+ },
3585
+ removeRow: (tableId, rowId) => {
3586
+ apply([["RemoveRecord", tableId, rowId]]);
3587
+ }
3588
+ },
3589
+ selectRow(rowId) {
3590
+ state.selection.rowId = rowId;
3591
+ bus.emit({ type: "cursor-change", tableId: state.selection.tableId, rowId });
3592
+ },
3593
+ selectTable(tableId) {
3594
+ if (!state.tables.has(tableId)) throw new Error(`Unknown table: ${tableId}`);
3595
+ state.selection.tableId = tableId;
3596
+ const first = state.tables.get(tableId).rows[0]?.id ?? null;
3597
+ state.selection.rowId = first;
3598
+ bus.emit({ type: "data-change", tableId });
3599
+ bus.emit({ type: "cursor-change", tableId, rowId: first });
3600
+ },
3601
+ setColumnMappings(mappings) {
3602
+ if (mappings === void 0) {
3603
+ delete state.selection.mappings;
3604
+ } else {
3605
+ state.selection.mappings = mappings;
3606
+ }
3607
+ bus.emit({ type: "mappings-change", mappings: resolveMappings(state) });
3608
+ },
3609
+ async setOptions(opts) {
3610
+ await api.WidgetAPI.setOptions(opts);
3611
+ },
3612
+ setInteraction(interaction) {
3613
+ state.widgetInteraction = { ...interaction };
3614
+ bus.emit({ type: "options-change", options: { ...state.widgetOptions }, interaction: { ...interaction } });
3615
+ },
3616
+ theme: {
3617
+ set(next) {
3618
+ bus.emit({ type: "theme-change", message: buildGristThemeHostMessage(next) });
3619
+ }
3620
+ },
3621
+ dispose() {
3622
+ transport.dispose();
3623
+ bus.clear();
3624
+ }
3625
+ };
3511
3626
  }
3512
- function isTheme(value) {
3513
- return value !== null && THEMES.includes(value);
3627
+
3628
+ // src/emulator/templates.ts
3629
+ var PRESET_GENERATED_AT = "1970-01-01T00:00:00.000Z";
3630
+ function replicaBase(overrides) {
3631
+ return {
3632
+ generatedAt: PRESET_GENERATED_AT,
3633
+ ...overrides
3634
+ };
3514
3635
  }
3515
- function readStoredTheme() {
3516
- try {
3517
- const value = localStorage.getItem(STORAGE_KEY);
3518
- return isTheme(value) ? value : null;
3519
- } catch {
3520
- return null;
3636
+ var presets = {
3637
+ /**
3638
+ * Default starter fixture for demos / sandboxes that need a neutral document
3639
+ * without choosing a domain-specific preset.
3640
+ */
3641
+ defaultTemplate() {
3642
+ return replicaBase({
3643
+ docName: "Default",
3644
+ tables: {
3645
+ Table1: {
3646
+ label: "Table1",
3647
+ rows: [
3648
+ { id: 1, Name: "Alice" },
3649
+ { id: 2, Name: "Bob" }
3650
+ ]
3651
+ }
3652
+ }
3653
+ });
3654
+ },
3655
+ blank() {
3656
+ return replicaBase({
3657
+ docName: "Blank",
3658
+ tables: {
3659
+ Table1: { rows: [{ id: 1 }] }
3660
+ }
3661
+ });
3662
+ },
3663
+ todoList() {
3664
+ return replicaBase({
3665
+ docName: "Todo list",
3666
+ mode: "schema+data",
3667
+ tables: {
3668
+ Tasks: {
3669
+ label: "Tasks",
3670
+ columns: {
3671
+ title: { type: "Text", label: "Title" },
3672
+ done: { type: "Bool", label: "Done" },
3673
+ priority: { type: "Choice", label: "Priority" }
3674
+ },
3675
+ rows: [
3676
+ { id: 1, title: "Buy milk", done: false, priority: "high" },
3677
+ { id: 2, title: "Write the SOTA emulator", done: true, priority: "high" },
3678
+ { id: 3, title: "Ship it", done: false, priority: "medium" }
3679
+ ]
3680
+ }
3681
+ }
3682
+ });
3683
+ },
3684
+ contacts() {
3685
+ return replicaBase({
3686
+ docName: "Contacts",
3687
+ mode: "schema+data",
3688
+ tables: {
3689
+ Contacts: {
3690
+ label: "Contacts",
3691
+ columns: {
3692
+ firstName: { type: "Text" },
3693
+ lastName: { type: "Text" },
3694
+ email: { type: "Text" },
3695
+ company: { type: "Text" }
3696
+ },
3697
+ rows: [
3698
+ { id: 1, firstName: "Ada", lastName: "Lovelace", email: "ada@analytical.engine", company: "British Crown" },
3699
+ { id: 2, firstName: "Linus", lastName: "Torvalds", email: "linus@kernel.org", company: "Linux Foundation" },
3700
+ { id: 3, firstName: "Margaret", lastName: "Hamilton", email: "margaret@apollo.nasa", company: "NASA" }
3701
+ ]
3702
+ }
3703
+ }
3704
+ });
3521
3705
  }
3706
+ };
3707
+
3708
+ // src/emulator/testing/render-with-grist.tsx
3709
+ var import_jsx_runtime8 = require("react/jsx-runtime");
3710
+ function renderWithGrist(ui, options = {}) {
3711
+ fakeIframeContext();
3712
+ resetGristForTests();
3713
+ const widgetOptions = options.widget ? isGristWidget(options.widget) ? options.widget.options : options.widget : void 0;
3714
+ const columns = widgetOptions ? declaredColumns(widgetOptions) : [];
3715
+ const emulatorOptions = widgetOptions && !options.emulator?.document ? {
3716
+ ...options.emulator,
3717
+ document: declaredDocument(columns, options.rows),
3718
+ widgetInteraction: options.emulator?.widgetInteraction ?? { access_level: widgetOptions.requiredAccess ?? "read table" }
3719
+ } : options.emulator ?? { document: presets.blank() };
3720
+ const emulator = options.existing ?? createGristEmulator(emulatorOptions);
3721
+ if (widgetOptions && columns.length > 0) emulator.setColumnMappings(mapEveryColumn(columns));
3722
+ const Widget = () => ui;
3723
+ const result = (0, import_react10.render)(widgetOptions ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(GristWidgetApp, { App: Widget, options: widgetOptions }) : ui, options);
3724
+ const originalUnmount = result.unmount;
3725
+ result.unmount = () => {
3726
+ originalUnmount();
3727
+ if (!options.existing) emulator.dispose();
3728
+ resetGristForTests();
3729
+ };
3730
+ return Object.assign(result, { emulator });
3522
3731
  }
3523
- function writeStoredTheme(theme) {
3732
+ function fakeIframeContext() {
3733
+ if (typeof window === "undefined") return;
3734
+ if (window.self !== window.top) return;
3524
3735
  try {
3525
- localStorage.setItem(STORAGE_KEY, theme);
3736
+ Object.defineProperty(window, "top", {
3737
+ configurable: true,
3738
+ get: () => ({})
3739
+ });
3526
3740
  } catch {
3527
3741
  }
3528
3742
  }
3529
- function noop() {
3530
- }
3531
3743
 
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
- }
3744
+ // src/emulator/testing/smoke-test-widget.tsx
3745
+ var import_react11 = require("react");
3746
+ var import_react12 = require("@testing-library/react");
3556
3747
 
3557
3748
  // src/emulator/testing/jsdom-stubs.ts
3558
3749
  function installJsdomStubs() {
@@ -3582,21 +3773,19 @@ function installJsdomStubs() {
3582
3773
  }
3583
3774
 
3584
3775
  // 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
- }) {
3776
+ var import_jsx_runtime9 = require("react/jsx-runtime");
3777
+ async function smokeTestWidget(input, extra = {}) {
3778
+ const { App, options: given, widget, document: document2, timeout = 1e4 } = isGristWidgetEntry(input) ? { ...entryProps(input), ...extra } : input;
3592
3779
  installJsdomStubs();
3780
+ const options = given ?? widget?.options;
3781
+ if (!options) throw new Error("smokeTestWidget: pass the widget's `widget` (defineGristWidget) or its `options` (GRIST_OPTIONS).");
3593
3782
  const crash = { error: void 0 };
3594
3783
  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, {}) }) });
3784
+ 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
3785
  }
3597
3786
  const columns = declaredColumns(options);
3598
- const gristDocument = document2 ?? smokeTestDocument(columns);
3599
- const result = renderWithGrist(/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(GristWidgetApp, { App: Probe, options }), {
3787
+ const gristDocument = document2 ?? declaredDocument(columns);
3788
+ const result = renderWithGrist(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(GristWidgetApp, { App: Probe, options }), {
3600
3789
  emulator: {
3601
3790
  document: gristDocument,
3602
3791
  // Granted as the user would in Grist's widget panel: the access the
@@ -3607,7 +3796,7 @@ async function smokeTestWidget({
3607
3796
  try {
3608
3797
  if (columns.length > 0) result.emulator.setColumnMappings(mapEveryColumn(columns));
3609
3798
  try {
3610
- await (0, import_react10.waitFor)(
3799
+ await (0, import_react12.waitFor)(
3611
3800
  () => {
3612
3801
  if (crash.error !== void 0) return;
3613
3802
  const probe = result.container.querySelector("[data-grist-smoke-test]");
@@ -3626,7 +3815,7 @@ async function smokeTestWidget({
3626
3815
  result.unmount();
3627
3816
  }
3628
3817
  }
3629
- var CatchRenderError = class extends import_react9.Component {
3818
+ var CatchRenderError = class extends import_react11.Component {
3630
3819
  constructor() {
3631
3820
  super(...arguments);
3632
3821
  this.state = { failed: false };
@@ -3641,45 +3830,6 @@ var CatchRenderError = class extends import_react9.Component {
3641
3830
  return this.state.failed ? null : this.props.children;
3642
3831
  }
3643
3832
  };
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
3833
 
3684
3834
  // src/emulator/testing/matchers.ts
3685
3835
  function actionsOf(emulator) {
@@ -3735,7 +3885,7 @@ function waitForEvent(emulator, kindOrPredicate, timeoutOrOptions = 1e3) {
3735
3885
  }
3736
3886
 
3737
3887
  // src/emulator/testing/index.ts
3738
- var import_react11 = require("@testing-library/react");
3888
+ var import_react13 = require("@testing-library/react");
3739
3889
  // Annotate the CommonJS export names for ESM import in node:
3740
3890
  0 && (module.exports = {
3741
3891
  act,