grist-widget-sdk 0.3.27 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +79 -208
  3. package/dist/app.cjs +1336 -4415
  4. package/dist/app.cjs.map +1 -1
  5. package/dist/app.d.cts +6 -103
  6. package/dist/app.d.ts +6 -103
  7. package/dist/app.js +5 -7
  8. package/dist/chunk-4ZGVKGDF.js +204 -0
  9. package/dist/chunk-4ZGVKGDF.js.map +1 -0
  10. package/dist/{chunk-6Y2IONGJ.js → chunk-LSALXIGM.js} +191 -20
  11. package/dist/chunk-LSALXIGM.js.map +1 -0
  12. package/dist/chunk-MAPAWVQC.js +1898 -0
  13. package/dist/chunk-MAPAWVQC.js.map +1 -0
  14. package/dist/{chunk-YQNMIL33.js → chunk-OXA2VITC.js} +532 -181
  15. package/dist/chunk-OXA2VITC.js.map +1 -0
  16. package/dist/define-widget-B1FIvI-h.d.cts +433 -0
  17. package/dist/define-widget-B9gEtJP0.d.ts +433 -0
  18. package/dist/emulator/testing.cjs +3341 -6512
  19. package/dist/emulator/testing.cjs.map +1 -1
  20. package/dist/emulator/testing.d.cts +29 -14
  21. package/dist/emulator/testing.d.ts +29 -14
  22. package/dist/emulator/testing.js +78 -72
  23. package/dist/emulator/testing.js.map +1 -1
  24. package/dist/emulator.cjs +63 -249
  25. package/dist/emulator.cjs.map +1 -1
  26. package/dist/emulator.d.cts +6 -8
  27. package/dist/emulator.d.ts +6 -8
  28. package/dist/emulator.js +5 -11
  29. package/dist/index.cjs +1442 -4862
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +81 -260
  32. package/dist/index.d.ts +81 -260
  33. package/dist/index.js +12 -158
  34. package/dist/mount-grist-widget-CkB-3iG_.d.ts +137 -0
  35. package/dist/mount-grist-widget-Dtelqao_.d.cts +137 -0
  36. package/dist/{inline-DpcT8tV9.d.ts → templates-CGmRIQBV.d.cts} +89 -35
  37. package/dist/{inline-C7DkvuET.d.cts → templates-Cq94qOcf.d.ts} +89 -35
  38. package/dist/{types-D3JxUCfL.d.cts → types-DBStn0Dk.d.cts} +2 -26
  39. package/dist/{types-D3JxUCfL.d.ts → types-DBStn0Dk.d.ts} +2 -26
  40. package/package.json +8 -37
  41. package/dist/advanced.cjs +0 -2799
  42. package/dist/advanced.cjs.map +0 -1
  43. package/dist/advanced.d.cts +0 -822
  44. package/dist/advanced.d.ts +0 -822
  45. package/dist/advanced.js +0 -872
  46. package/dist/advanced.js.map +0 -1
  47. package/dist/chunk-22U7WAY3.js +0 -463
  48. package/dist/chunk-22U7WAY3.js.map +0 -1
  49. package/dist/chunk-64SVRWW7.js +0 -758
  50. package/dist/chunk-64SVRWW7.js.map +0 -1
  51. package/dist/chunk-6Y2IONGJ.js.map +0 -1
  52. package/dist/chunk-B4AKVSWV.js +0 -2362
  53. package/dist/chunk-B4AKVSWV.js.map +0 -1
  54. package/dist/chunk-ELGVOHFD.js +0 -32
  55. package/dist/chunk-ELGVOHFD.js.map +0 -1
  56. package/dist/chunk-GTTD5HPO.js +0 -36
  57. package/dist/chunk-GTTD5HPO.js.map +0 -1
  58. package/dist/chunk-NO6FYX33.js +0 -2084
  59. package/dist/chunk-NO6FYX33.js.map +0 -1
  60. package/dist/chunk-OPTOYD2Y.js +0 -277
  61. package/dist/chunk-OPTOYD2Y.js.map +0 -1
  62. package/dist/chunk-PM3B4WYV.js +0 -12
  63. package/dist/chunk-PM3B4WYV.js.map +0 -1
  64. package/dist/chunk-REV5AUXD.js +0 -25
  65. package/dist/chunk-REV5AUXD.js.map +0 -1
  66. package/dist/chunk-YQNMIL33.js.map +0 -1
  67. package/dist/create-grist-emulator-BCqp1BF9.d.ts +0 -81
  68. package/dist/create-grist-emulator-DBM8OSIR.d.cts +0 -81
  69. package/dist/emulator/devtools.cjs +0 -1404
  70. package/dist/emulator/devtools.cjs.map +0 -1
  71. package/dist/emulator/devtools.d.cts +0 -48
  72. package/dist/emulator/devtools.d.ts +0 -48
  73. package/dist/emulator/devtools.js +0 -506
  74. package/dist/emulator/devtools.js.map +0 -1
  75. package/dist/emulator/transports.cjs +0 -1132
  76. package/dist/emulator/transports.cjs.map +0 -1
  77. package/dist/emulator/transports.d.cts +0 -38
  78. package/dist/emulator/transports.d.ts +0 -38
  79. package/dist/emulator/transports.js +0 -12
  80. package/dist/emulator/transports.js.map +0 -1
  81. package/dist/templates-8k2LKoO-.d.cts +0 -24
  82. package/dist/templates-DIwi_mvI.d.ts +0 -24
  83. package/dist/use-grist-reads-DUMvJgV7.d.ts +0 -480
  84. package/dist/use-grist-reads-DpjVuI0H.d.cts +0 -480
  85. package/dist/use-grist-types-DdOLEJXN.d.cts +0 -499
  86. package/dist/use-grist-types-kwxoFeub.d.ts +0 -499
  87. package/dist/use-grist-widget-options-from-context-CGOlQ5Vc.d.cts +0 -43
  88. package/dist/use-grist-widget-options-from-context-CGOlQ5Vc.d.ts +0 -43
@@ -1,9 +1,3 @@
1
- import {
2
- normalizeReplicaDocumentInput
3
- } from "./chunk-GTTD5HPO.js";
4
- import {
5
- resolveMappings
6
- } from "./chunk-PM3B4WYV.js";
7
1
  import {
8
2
  coerceGristDocApiRefRowId,
9
3
  gristAddRecordAction,
@@ -12,172 +6,75 @@ import {
12
6
  gristBulkUpdateRecordAction,
13
7
  gristRemoveRecordAction,
14
8
  gristUpdateRecordAction,
15
- inferDisplayHelpersByRef
16
- } from "./chunk-64SVRWW7.js";
17
-
18
- // src/emulator/transports/iframe.ts
19
- import { MsgType, Rpc } from "grain-rpc";
9
+ normalizeReplicaDocumentInput
10
+ } from "./chunk-4ZGVKGDF.js";
20
11
 
21
- // src/emulator/theme-message.ts
22
- function buildGristThemeHostMessage(appearance, options) {
23
- return {
24
- theme: {
25
- appearance,
26
- name: appearance,
27
- colors: {}
28
- },
29
- ...options?.fromReady ? { fromReady: true } : {}
30
- };
12
+ // src/emulator/core/mappings.ts
13
+ function resolveMappings(state) {
14
+ if (state.selection.mappings !== void 0 && state.selection.mappings !== null) {
15
+ return state.selection.mappings;
16
+ }
17
+ return null;
31
18
  }
32
19
 
33
- // src/emulator/transports/iframe.ts
34
- function createIframeTransport(options) {
35
- const { iframe, state, bus, api, targetOrigin = "*" } = options;
36
- const timers = /* @__PURE__ */ new Set();
37
- const rpc = new Rpc({
38
- sendMessage: (msg) => {
39
- const target = iframe.contentWindow;
40
- if (!target) return;
41
- target.postMessage(msg, targetOrigin);
42
- },
43
- logger: { info: () => {
44
- }, warn: () => {
45
- } }
46
- });
47
- rpc.queueOutgoingUntilReadyMessage();
48
- const receiveFromWidget = rpc.receiveMessage.bind(rpc);
49
- rpc.receiveMessage = (msg) => {
50
- if (msg && typeof msg === "object" && msg.mtype === MsgType.RpcCall && msg.mdest === "grist") {
51
- const copy = { ...msg };
52
- delete copy.mdest;
53
- receiveFromWidget(copy);
54
- return;
20
+ // src/emulator/core/display-helpers.ts
21
+ function inferDisplayHelpersByRef(columns) {
22
+ const out = {};
23
+ const helperColumns = Object.entries(columns).filter(
24
+ ([colId]) => colId.startsWith("gristHelper_")
25
+ );
26
+ const usedHelpers = /* @__PURE__ */ new Set();
27
+ const colRefToColId = /* @__PURE__ */ new Map();
28
+ for (const [colId, col] of Object.entries(columns)) {
29
+ if (typeof col?.colRef === "number" && Number.isFinite(col.colRef)) {
30
+ colRefToColId.set(col.colRef, colId);
55
31
  }
56
- receiveFromWidget(msg);
57
- };
58
- const onMessage = (event) => {
59
- if (event.source !== iframe.contentWindow) return;
60
- if (!isRpcEnvelope(event.data)) return;
61
- rpc.receiveMessage(event.data);
62
- };
63
- window.addEventListener("message", onMessage);
64
- rpc.registerImpl("GristView", api.GristView);
65
- rpc.registerImpl("GristDocAPI", api.GristDocAPI);
66
- rpc.registerImpl("WidgetAPI", api.WidgetAPI);
67
- rpc.registerImpl("CustomSectionAPI", api.CustomSectionAPI);
68
- rpc.registerImpl("_grist_api", api.GristAPI);
69
- rpc.registerImpl("CommandAPI", api.CommandAPI);
70
- const buildInteraction = options.buildInteraction ?? defaultInteraction(state);
71
- const cursorOf = options.cursor ?? (() => state.selection.cursor);
72
- const initialCursorOf = options.initialCursor ?? (() => state.selection.rowId);
73
- function fireDataChange(tableId, opts) {
74
- safePostMessage({
75
- tableId: tableId ?? state.selection.tableId,
76
- dataChange: true,
77
- ...opts?.mappingsChange ? { mappingsChange: true } : {}
78
- });
79
32
  }
80
- function fireCursor(rowId, opts) {
81
- const id = rowId === void 0 ? initialCursorOf() : rowId;
82
- if (id === null) return;
83
- safePostMessage({
84
- tableId: state.selection.tableId,
85
- rowId: id,
86
- cursor: cursorOf(),
87
- ...opts?.mappingsChange ? { mappingsChange: true } : {}
88
- });
89
- }
90
- function fireOptions() {
91
- safePostMessage({
92
- options: hasOptions(state.widgetOptions) ? { ...state.widgetOptions } : null,
93
- settings: { ...buildInteraction() }
94
- });
95
- }
96
- function fireTheme(message) {
97
- safePostMessage(message);
98
- }
99
- function emitInitialState(opts) {
100
- fireOptions();
101
- if (options.initialTheme) {
102
- fireTheme(buildGristThemeHostMessage(options.initialTheme, { fromReady: true }));
33
+ for (const [colId, col] of Object.entries(columns)) {
34
+ const type = col?.type;
35
+ if (typeof type !== "string" || !type.startsWith("Ref:") && !type.startsWith("RefList:")) {
36
+ continue;
103
37
  }
104
- fireDataChange(void 0, opts);
105
- fireCursor(void 0, opts);
106
- }
107
- function emitInitialStateWithReplay(opts) {
108
- emitInitialState(opts);
109
- queueMicrotask(() => emitInitialState(opts));
110
- const timer = setTimeout(() => {
111
- timers.delete(timer);
112
- emitInitialState(opts);
113
- }, 0);
114
- timers.add(timer);
115
- }
116
- const offBus = bus.on((event) => {
117
- switch (event.type) {
118
- case "ready":
119
- emitInitialStateWithReplay();
120
- break;
121
- case "data-change":
122
- fireDataChange(event.tableId);
123
- break;
124
- case "cursor-change": {
125
- const rowId = event.rowId === null ? state.selection.rowId : event.rowId;
126
- if (rowId !== null) {
127
- safePostMessage({
128
- tableId: event.tableId,
129
- rowId,
130
- cursor: cursorOf()
131
- });
132
- }
133
- fireDataChange(event.tableId);
134
- break;
38
+ if (typeof col.displayColRef === "number" && Number.isFinite(col.displayColRef)) {
39
+ const helperColId = colRefToColId.get(col.displayColRef);
40
+ if (helperColId) {
41
+ out[colId] = helperColId;
42
+ usedHelpers.add(helperColId);
43
+ continue;
135
44
  }
136
- case "options-change":
137
- fireOptions();
138
- break;
139
- case "mappings-change":
140
- emitInitialStateWithReplay({ mappingsChange: true });
141
- break;
142
- case "theme-change":
143
- fireTheme(event.message);
144
- break;
145
45
  }
146
- });
147
- rpc.processIncoming();
148
- function safePostMessage(payload) {
149
- try {
150
- rpc.postMessage(payload);
151
- } catch (err) {
152
- console.warn("[grist-emulator/iframe] failed to deliver postMessage", err);
46
+ const match = helperColumns.find(([, helper]) => {
47
+ const formula = helper?.formula;
48
+ return typeof formula === "string" && (formula.includes(`$${colId}.`) || formula.includes(`$${colId}`));
49
+ });
50
+ if (match) {
51
+ out[colId] = match[0];
52
+ usedHelpers.add(match[0]);
153
53
  }
154
54
  }
155
- return {
156
- dispose() {
157
- window.removeEventListener("message", onMessage);
158
- offBus();
159
- for (const timer of timers) clearTimeout(timer);
160
- timers.clear();
161
- },
162
- fireDataChange,
163
- fireCursor,
164
- fireOptions,
165
- fireTheme,
166
- emitInitialState
167
- };
168
- }
169
- function hasOptions(options) {
170
- return Object.keys(options).length > 0;
171
- }
172
- function isRpcEnvelope(data) {
173
- if (!data || typeof data !== "object") return false;
174
- return typeof data.mtype === "number";
175
- }
176
- function defaultInteraction(state) {
177
- return () => ({
178
- access_level: state.configuration?.requiredAccess ?? "read table",
179
- ...state.widgetInteraction
180
- });
55
+ const unresolvedRefs = Object.entries(columns).filter(([colId, col]) => {
56
+ const type = col?.type;
57
+ return !out[colId] && typeof type === "string" && (type.startsWith("Ref:") || type.startsWith("RefList:"));
58
+ }).sort((a, b) => (a[1]?.parentPos ?? Number.MAX_SAFE_INTEGER) - (b[1]?.parentPos ?? Number.MAX_SAFE_INTEGER));
59
+ 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));
60
+ for (const [refId, refCol] of unresolvedRefs) {
61
+ const refPos = refCol?.parentPos ?? Number.MAX_SAFE_INTEGER;
62
+ const candidate = helperByPos.find(([, helperCol]) => {
63
+ const helperPos = helperCol?.parentPos ?? Number.MAX_SAFE_INTEGER;
64
+ return helperPos > refPos;
65
+ });
66
+ if (!candidate) continue;
67
+ out[refId] = candidate[0];
68
+ usedHelpers.add(candidate[0]);
69
+ }
70
+ for (const [refId] of unresolvedRefs) {
71
+ if (out[refId]) continue;
72
+ const candidate = helperByPos.find(([helperId]) => !usedHelpers.has(helperId));
73
+ if (!candidate) continue;
74
+ out[refId] = candidate[0];
75
+ usedHelpers.add(candidate[0]);
76
+ }
77
+ return out;
181
78
  }
182
79
 
183
80
  // src/emulator/core/document.ts
@@ -750,6 +647,7 @@ function normalizeColumnarForTable(table, columns) {
750
647
  }
751
648
  function normalizeCellValueForType(value, type) {
752
649
  if (value === null || value === void 0 || typeof type !== "string") return value;
650
+ if (isListColumnType(type) && Array.isArray(value) && value[0] !== "L") return ["E", "KeyError"];
753
651
  if (type.startsWith("Ref:")) return normalizeRefValue(value);
754
652
  if (type.startsWith("RefList:")) return normalizeRefListValue(value);
755
653
  return value;
@@ -767,6 +665,9 @@ function extractRefRowId(value) {
767
665
  if (fromTuple !== void 0 && fromTuple > 0) return Math.trunc(fromTuple);
768
666
  return toPositiveInt(value);
769
667
  }
668
+ function isListColumnType(type) {
669
+ return type === "ChoiceList" || type === "Attachments" || type.startsWith("RefList:");
670
+ }
770
671
  function normalizeRefValue(value) {
771
672
  return extractRefRowId(value);
772
673
  }
@@ -798,6 +699,310 @@ function toPositiveInt(value) {
798
699
  return null;
799
700
  }
800
701
 
702
+ // src/emulator/validators.ts
703
+ import { z } from "zod";
704
+ var emulatorRowSchema = z.object({ id: z.number() }).catchall(z.unknown());
705
+ var formulaKindSchema = z.enum(["computed", "trigger", "none", "empty_slot"]);
706
+ var replicaColumnSchema = z.object({
707
+ colRef: z.number().optional(),
708
+ type: z.string().optional(),
709
+ label: z.string().optional(),
710
+ parentPos: z.number().optional(),
711
+ isFormula: z.boolean().optional(),
712
+ formulaKind: formulaKindSchema.optional(),
713
+ formula: z.string().optional(),
714
+ description: z.string().optional(),
715
+ widgetOptions: z.record(z.string(), z.unknown()).optional(),
716
+ displayColRef: z.number().optional(),
717
+ visibleColRef: z.number().optional()
718
+ }).strict();
719
+ var replicaTableSchema = z.object({
720
+ label: z.string().optional(),
721
+ columns: z.record(z.string(), replicaColumnSchema).optional(),
722
+ rows: z.array(emulatorRowSchema).optional(),
723
+ rowCount: z.number().optional(),
724
+ dataOmitted: z.boolean().optional(),
725
+ dataOmittedReason: z.string().optional(),
726
+ error: z.string().optional()
727
+ }).strict();
728
+ var replicaSelectionSchema = z.object({
729
+ tableId: z.string().optional(),
730
+ rowId: z.union([z.number(), z.literal("new"), z.null()]).optional(),
731
+ mappings: z.unknown().nullable().optional(),
732
+ cursor: z.unknown().optional()
733
+ }).strict();
734
+ var replicaWidgetConfigurationSchema = z.object({
735
+ requiredAccess: z.string().optional(),
736
+ columns: z.unknown().optional(),
737
+ allowSelectBy: z.boolean().optional(),
738
+ hasCustomOptions: z.boolean().optional()
739
+ }).strict();
740
+ var replicaAccessTokenSchema = z.object({
741
+ token: z.string(),
742
+ baseUrl: z.string(),
743
+ ttlMsecs: z.number()
744
+ }).strict();
745
+ var replicaDocumentSchema = z.object({
746
+ generatedAt: z.string(),
747
+ source: z.string().optional(),
748
+ docName: z.string().optional(),
749
+ mode: z.enum(["schema-only", "schema+samples", "schema+data"]).optional(),
750
+ tables: z.record(z.string(), replicaTableSchema),
751
+ selection: replicaSelectionSchema.optional(),
752
+ widgetOptions: z.record(z.string(), z.unknown()).optional(),
753
+ widgetInteraction: z.unknown().optional(),
754
+ configuration: replicaWidgetConfigurationSchema.optional(),
755
+ accessToken: replicaAccessTokenSchema.nullable().optional()
756
+ }).strict().refine((value) => Object.keys(value.tables).length > 0, {
757
+ message: "at least one table is required",
758
+ path: ["tables"]
759
+ });
760
+ function formatZodError(prefix, error) {
761
+ const details = error.issues.map((issue) => {
762
+ const where = issue.path.length > 0 ? issue.path.join(".") : "<root>";
763
+ return `${where}: ${issue.message}`;
764
+ }).join("; ");
765
+ return new Error(`${prefix}: ${details}`);
766
+ }
767
+ function safeParseDocument(input) {
768
+ return replicaDocumentSchema.safeParse(input);
769
+ }
770
+ function parseDocument(input) {
771
+ const result = safeParseDocument(input);
772
+ if (!result.success) {
773
+ throw formatZodError("parseDocument", result.error);
774
+ }
775
+ return result.data;
776
+ }
777
+
778
+ // src/emulator/api/command-api.ts
779
+ function createCommandApiImpl(bus) {
780
+ return {
781
+ async run(name) {
782
+ bus.emit({ type: "user-action", action: ["RunCommand", name] });
783
+ }
784
+ };
785
+ }
786
+
787
+ // src/emulator/api/custom-section-api.ts
788
+ function createCustomSectionApiImpl(state, bus) {
789
+ return {
790
+ async configure(settings) {
791
+ const config = {
792
+ requiredAccess: settings.requiredAccess,
793
+ columns: settings.columns,
794
+ allowSelectBy: settings.allowSelectBy,
795
+ hasCustomOptions: settings.hasCustomOptions
796
+ };
797
+ state.configuration = config;
798
+ bus.emit({ type: "ready", configuration: config });
799
+ bus.emit({ type: "mappings-change", mappings: resolveMappings(state) });
800
+ },
801
+ async mappings() {
802
+ return resolveMappings(state);
803
+ }
804
+ };
805
+ }
806
+
807
+ // src/emulator/api/grist-api.ts
808
+ function createGristApiImpl() {
809
+ return {
810
+ async ping() {
811
+ return "pong";
812
+ }
813
+ };
814
+ }
815
+
816
+ // src/emulator/core/columnar.ts
817
+ function rowsToColumnar(table, rows, options = {}) {
818
+ const cols = Object.keys(table.columns);
819
+ const out = { id: rows.map((r) => r.id) };
820
+ for (const col of cols) {
821
+ out[col] = rows.map((r) => encodeForWire(r[col], table.columns[col]?.type, options.keepEncoded));
822
+ }
823
+ return out;
824
+ }
825
+ function rowToRecord(table, row, options = {}) {
826
+ const out = { id: row.id };
827
+ for (const col of Object.keys(table.columns)) {
828
+ out[col] = encodeForWire(row[col], table.columns[col]?.type, options.keepEncoded);
829
+ }
830
+ return out;
831
+ }
832
+ function encodeForWire(value, type, keepEncoded) {
833
+ if (value instanceof Date) {
834
+ const epoch = Math.floor(value.getTime() / 1e3);
835
+ if (keepEncoded) {
836
+ return type && type.startsWith("DateTime") ? ["D", epoch, "UTC"] : ["d", epoch];
837
+ }
838
+ return epoch;
839
+ }
840
+ return value;
841
+ }
842
+
843
+ // src/emulator/api/grist-doc-api.ts
844
+ function createGristDocApiImpl(state, bus) {
845
+ return {
846
+ async getDocName() {
847
+ return state.docName;
848
+ },
849
+ async listTables() {
850
+ return [...state.tables.keys()];
851
+ },
852
+ async fetchTable(tableId) {
853
+ if (tableId === "_grist_Tables") {
854
+ return synthesizeTablesMeta(state);
855
+ }
856
+ if (tableId === "_grist_Tables_column") {
857
+ return synthesizeTablesColumnMeta(state);
858
+ }
859
+ const table = getTable(state, tableId);
860
+ return rowsToColumnar(table, table.rows);
861
+ },
862
+ async applyUserActions(actions) {
863
+ const result = applyUserActions(state, bus, actions);
864
+ return { retValues: result.retValues };
865
+ },
866
+ async getAccessToken() {
867
+ if (!state.accessToken) {
868
+ const err = new Error("getAccessToken not available (no token configured on emulator)");
869
+ throw err;
870
+ }
871
+ return { ...state.accessToken };
872
+ }
873
+ };
874
+ }
875
+
876
+ // src/emulator/api/grist-view.ts
877
+ function createGristViewImpl(state, bus) {
878
+ return {
879
+ async fetchSelectedTable(options) {
880
+ const table = getSelectedTable(state);
881
+ return rowsToColumnar(table, table.rows, { keepEncoded: options?.keepEncoded });
882
+ },
883
+ async fetchSelectedRecord(rowId, options) {
884
+ const table = getSelectedTable(state);
885
+ const row = findRow(table, rowId);
886
+ if (!row) throw new Error(`No row ${rowId} in ${table.tableId}`);
887
+ return rowToRecord(table, row, { keepEncoded: options?.keepEncoded });
888
+ },
889
+ async allowSelectBy() {
890
+ return state.configuration?.allowSelectBy ?? false;
891
+ },
892
+ async setSelectedRows(rowIds) {
893
+ if (!rowIds || rowIds.length === 0) {
894
+ state.selection.rowId = null;
895
+ } else {
896
+ state.selection.rowId = rowIds[0];
897
+ }
898
+ bus.emit({
899
+ type: "cursor-change",
900
+ tableId: state.selection.tableId,
901
+ rowId: state.selection.rowId
902
+ });
903
+ },
904
+ async setCursorPos(cursor) {
905
+ const tableId = state.selection.tableId;
906
+ if (typeof cursor.rowId === "number") state.selection.rowId = cursor.rowId;
907
+ state.selection.cursor = { ...state.selection.cursor, ...cursor };
908
+ bus.emit({
909
+ type: "cursor-change",
910
+ tableId,
911
+ rowId: state.selection.rowId,
912
+ cursor: state.selection.cursor
913
+ });
914
+ }
915
+ };
916
+ }
917
+
918
+ // src/emulator/api/widget-api.ts
919
+ function createWidgetApiImpl(state, bus) {
920
+ function emit() {
921
+ bus.emit({
922
+ type: "options-change",
923
+ options: { ...state.widgetOptions },
924
+ interaction: { ...state.widgetInteraction }
925
+ });
926
+ }
927
+ return {
928
+ async getOption(key) {
929
+ return state.widgetOptions[key];
930
+ },
931
+ async setOption(key, value) {
932
+ state.widgetOptions[key] = value;
933
+ emit();
934
+ },
935
+ async getOptions() {
936
+ return Object.keys(state.widgetOptions).length > 0 ? { ...state.widgetOptions } : null;
937
+ },
938
+ // Replaces every option, as Grist's WidgetAPI.setOptions does.
939
+ async setOptions(options) {
940
+ state.widgetOptions = { ...options };
941
+ emit();
942
+ },
943
+ async clearOptions() {
944
+ state.widgetOptions = {};
945
+ emit();
946
+ }
947
+ };
948
+ }
949
+
950
+ // src/emulator/api/index.ts
951
+ function createApiBundle(state, bus) {
952
+ return {
953
+ GristView: createGristViewImpl(state, bus),
954
+ GristDocAPI: createGristDocApiImpl(state, bus),
955
+ WidgetAPI: createWidgetApiImpl(state, bus),
956
+ CustomSectionAPI: createCustomSectionApiImpl(state, bus),
957
+ GristAPI: createGristApiImpl(),
958
+ CommandAPI: createCommandApiImpl(bus)
959
+ };
960
+ }
961
+
962
+ // src/emulator/core/event-bus.ts
963
+ function createEventBus(options = {}) {
964
+ const limit = options.historyLimit ?? 200;
965
+ const listeners = /* @__PURE__ */ new Set();
966
+ const history = [];
967
+ return {
968
+ emit(event) {
969
+ history.push(event);
970
+ if (history.length > limit) history.shift();
971
+ for (const fn of [...listeners]) {
972
+ try {
973
+ fn(event);
974
+ } catch (err) {
975
+ console.error("[grist-emulator] event listener threw:", err);
976
+ }
977
+ }
978
+ },
979
+ on(listener) {
980
+ listeners.add(listener);
981
+ return () => {
982
+ listeners.delete(listener);
983
+ };
984
+ },
985
+ history() {
986
+ return history;
987
+ },
988
+ clear() {
989
+ history.length = 0;
990
+ }
991
+ };
992
+ }
993
+
994
+ // src/emulator/theme-message.ts
995
+ function buildGristThemeHostMessage(appearance, options) {
996
+ return {
997
+ theme: {
998
+ appearance,
999
+ name: appearance,
1000
+ colors: {}
1001
+ },
1002
+ ...options?.fromReady ? { fromReady: true } : {}
1003
+ };
1004
+ }
1005
+
801
1006
  // src/emulator/transports/inline.ts
802
1007
  function createInlineTransport(options) {
803
1008
  const { state, bus, api } = options;
@@ -827,14 +1032,6 @@ function createInlineTransport(options) {
827
1032
  logErr(err);
828
1033
  }
829
1034
  }
830
- lastRecord = { record: null, mappings };
831
- for (const [cb] of recordCbs) {
832
- try {
833
- cb(null, mappings);
834
- } catch (err) {
835
- logErr(err);
836
- }
837
- }
838
1035
  return;
839
1036
  }
840
1037
  if (rowId === null) {
@@ -1172,18 +1369,172 @@ function installFakeIframeContext(target) {
1172
1369
  }
1173
1370
  }
1174
1371
 
1372
+ // src/emulator/create-grist-emulator.ts
1373
+ function createGristEmulator(options) {
1374
+ const validatedDocument = parseDocument(options.document);
1375
+ const state = createDocument({
1376
+ ...validatedDocument,
1377
+ widgetOptions: options.widgetOptions ?? validatedDocument.widgetOptions,
1378
+ // No access_level by default: the emulator then grants the access the
1379
+ // widget asks for in grist.ready(), as a user accepting it in Grist would.
1380
+ widgetInteraction: options.widgetInteraction ?? validatedDocument.widgetInteraction ?? {}
1381
+ });
1382
+ const bus = createEventBus();
1383
+ const api = createApiBundle(state, bus);
1384
+ const inline = createInlineTransport({
1385
+ state,
1386
+ bus,
1387
+ api,
1388
+ initialTheme: options.initialTheme,
1389
+ target: options.transport?.target
1390
+ });
1391
+ const transport = inline;
1392
+ const grist = inline.grist;
1393
+ function apply(actions) {
1394
+ return applyUserActions(state, bus, actions);
1395
+ }
1396
+ return {
1397
+ state,
1398
+ bus,
1399
+ api,
1400
+ transport,
1401
+ grist,
1402
+ apply,
1403
+ mutate: {
1404
+ addRow: (tableId, fields) => apply([["AddRecord", tableId, null, fields]]).retValues[0],
1405
+ updateRow: (tableId, rowId, fields) => {
1406
+ apply([["UpdateRecord", tableId, rowId, fields]]);
1407
+ },
1408
+ removeRow: (tableId, rowId) => {
1409
+ apply([["RemoveRecord", tableId, rowId]]);
1410
+ }
1411
+ },
1412
+ selectRow(rowId) {
1413
+ state.selection.rowId = rowId;
1414
+ bus.emit({ type: "cursor-change", tableId: state.selection.tableId, rowId });
1415
+ },
1416
+ selectTable(tableId) {
1417
+ if (!state.tables.has(tableId)) throw new Error(`Unknown table: ${tableId}`);
1418
+ state.selection.tableId = tableId;
1419
+ const first = state.tables.get(tableId).rows[0]?.id ?? null;
1420
+ state.selection.rowId = first;
1421
+ bus.emit({ type: "data-change", tableId });
1422
+ bus.emit({ type: "cursor-change", tableId, rowId: first });
1423
+ },
1424
+ setColumnMappings(mappings) {
1425
+ if (mappings === void 0) {
1426
+ delete state.selection.mappings;
1427
+ } else {
1428
+ state.selection.mappings = mappings;
1429
+ }
1430
+ bus.emit({ type: "mappings-change", mappings: resolveMappings(state) });
1431
+ },
1432
+ async setOptions(opts) {
1433
+ await api.WidgetAPI.setOptions(opts);
1434
+ },
1435
+ setInteraction(interaction) {
1436
+ state.widgetInteraction = { ...interaction };
1437
+ bus.emit({ type: "options-change", options: { ...state.widgetOptions }, interaction: { ...interaction } });
1438
+ },
1439
+ theme: {
1440
+ set(next) {
1441
+ bus.emit({ type: "theme-change", message: buildGristThemeHostMessage(next) });
1442
+ }
1443
+ },
1444
+ dispose() {
1445
+ transport.dispose();
1446
+ bus.clear();
1447
+ }
1448
+ };
1449
+ }
1450
+
1451
+ // src/emulator/templates.ts
1452
+ var PRESET_GENERATED_AT = "1970-01-01T00:00:00.000Z";
1453
+ function replicaBase(overrides) {
1454
+ return {
1455
+ generatedAt: PRESET_GENERATED_AT,
1456
+ ...overrides
1457
+ };
1458
+ }
1459
+ var presets = {
1460
+ /**
1461
+ * Default starter fixture for demos / sandboxes that need a neutral document
1462
+ * without choosing a domain-specific preset.
1463
+ */
1464
+ defaultTemplate() {
1465
+ return replicaBase({
1466
+ docName: "Default",
1467
+ tables: {
1468
+ Table1: {
1469
+ label: "Table1",
1470
+ rows: [
1471
+ { id: 1, Name: "Alice" },
1472
+ { id: 2, Name: "Bob" }
1473
+ ]
1474
+ }
1475
+ }
1476
+ });
1477
+ },
1478
+ blank() {
1479
+ return replicaBase({
1480
+ docName: "Blank",
1481
+ tables: {
1482
+ Table1: { rows: [{ id: 1 }] }
1483
+ }
1484
+ });
1485
+ },
1486
+ todoList() {
1487
+ return replicaBase({
1488
+ docName: "Todo list",
1489
+ mode: "schema+data",
1490
+ tables: {
1491
+ Tasks: {
1492
+ label: "Tasks",
1493
+ columns: {
1494
+ title: { type: "Text", label: "Title" },
1495
+ done: { type: "Bool", label: "Done" },
1496
+ priority: { type: "Choice", label: "Priority" }
1497
+ },
1498
+ rows: [
1499
+ { id: 1, title: "Buy milk", done: false, priority: "high" },
1500
+ { id: 2, title: "Write the SOTA emulator", done: true, priority: "high" },
1501
+ { id: 3, title: "Ship it", done: false, priority: "medium" }
1502
+ ]
1503
+ }
1504
+ }
1505
+ });
1506
+ },
1507
+ contacts() {
1508
+ return replicaBase({
1509
+ docName: "Contacts",
1510
+ mode: "schema+data",
1511
+ tables: {
1512
+ Contacts: {
1513
+ label: "Contacts",
1514
+ columns: {
1515
+ firstName: { type: "Text" },
1516
+ lastName: { type: "Text" },
1517
+ email: { type: "Text" },
1518
+ company: { type: "Text" }
1519
+ },
1520
+ rows: [
1521
+ { id: 1, firstName: "Ada", lastName: "Lovelace", email: "ada@analytical.engine", company: "British Crown" },
1522
+ { id: 2, firstName: "Linus", lastName: "Torvalds", email: "linus@kernel.org", company: "Linux Foundation" },
1523
+ { id: 3, firstName: "Margaret", lastName: "Hamilton", email: "margaret@apollo.nasa", company: "NASA" }
1524
+ ]
1525
+ }
1526
+ }
1527
+ });
1528
+ }
1529
+ };
1530
+
1175
1531
  export {
1532
+ resolveMappings,
1176
1533
  enrichEmulatorColumnMetadata,
1177
- createDocument,
1178
- getTable,
1179
- getSelectedTable,
1180
- findRow,
1181
- synthesizeTablesMeta,
1182
- synthesizeTablesColumnMeta,
1183
1534
  EmulatorActionError,
1184
- applyUserActions,
1185
- buildGristThemeHostMessage,
1186
- createIframeTransport,
1187
- createInlineTransport
1535
+ safeParseDocument,
1536
+ parseDocument,
1537
+ createGristEmulator,
1538
+ presets
1188
1539
  };
1189
- //# sourceMappingURL=chunk-YQNMIL33.js.map
1540
+ //# sourceMappingURL=chunk-OXA2VITC.js.map