grist-widget-sdk 0.3.27 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +1 -1
  3. package/dist/app.cjs +1139 -4336
  4. package/dist/app.cjs.map +1 -1
  5. package/dist/app.d.cts +1 -3
  6. package/dist/app.d.ts +1 -3
  7. package/dist/app.js +3 -7
  8. package/dist/chunk-4ZGVKGDF.js +204 -0
  9. package/dist/chunk-4ZGVKGDF.js.map +1 -0
  10. package/dist/{chunk-YQNMIL33.js → chunk-B5ZZNLVS.js} +528 -181
  11. package/dist/chunk-B5ZZNLVS.js.map +1 -0
  12. package/dist/chunk-O7NY46QY.js +1830 -0
  13. package/dist/chunk-O7NY46QY.js.map +1 -0
  14. package/dist/{chunk-6Y2IONGJ.js → chunk-P6VTEKWO.js} +113 -14
  15. package/dist/chunk-P6VTEKWO.js.map +1 -0
  16. package/dist/emulator/testing.cjs +1504 -4822
  17. package/dist/emulator/testing.cjs.map +1 -1
  18. package/dist/emulator/testing.d.cts +3 -6
  19. package/dist/emulator/testing.d.ts +3 -6
  20. package/dist/emulator/testing.js +7 -20
  21. package/dist/emulator/testing.js.map +1 -1
  22. package/dist/emulator.cjs +59 -249
  23. package/dist/emulator.cjs.map +1 -1
  24. package/dist/emulator.d.cts +3 -5
  25. package/dist/emulator.d.ts +3 -5
  26. package/dist/emulator.js +5 -11
  27. package/dist/index.cjs +1373 -4859
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +81 -260
  30. package/dist/index.d.ts +81 -260
  31. package/dist/index.js +8 -158
  32. package/dist/{inline-DpcT8tV9.d.ts → templates-CGmRIQBV.d.cts} +89 -35
  33. package/dist/{inline-C7DkvuET.d.cts → templates-Cq94qOcf.d.ts} +89 -35
  34. package/dist/{types-D3JxUCfL.d.cts → types-DBStn0Dk.d.cts} +2 -26
  35. package/dist/{types-D3JxUCfL.d.ts → types-DBStn0Dk.d.ts} +2 -26
  36. package/dist/use-grist-BK0eke9K.d.cts +358 -0
  37. package/dist/use-grist-C5DMZ3wU.d.ts +358 -0
  38. package/package.json +8 -37
  39. package/dist/advanced.cjs +0 -2799
  40. package/dist/advanced.cjs.map +0 -1
  41. package/dist/advanced.d.cts +0 -822
  42. package/dist/advanced.d.ts +0 -822
  43. package/dist/advanced.js +0 -872
  44. package/dist/advanced.js.map +0 -1
  45. package/dist/chunk-22U7WAY3.js +0 -463
  46. package/dist/chunk-22U7WAY3.js.map +0 -1
  47. package/dist/chunk-64SVRWW7.js +0 -758
  48. package/dist/chunk-64SVRWW7.js.map +0 -1
  49. package/dist/chunk-6Y2IONGJ.js.map +0 -1
  50. package/dist/chunk-B4AKVSWV.js +0 -2362
  51. package/dist/chunk-B4AKVSWV.js.map +0 -1
  52. package/dist/chunk-ELGVOHFD.js +0 -32
  53. package/dist/chunk-ELGVOHFD.js.map +0 -1
  54. package/dist/chunk-GTTD5HPO.js +0 -36
  55. package/dist/chunk-GTTD5HPO.js.map +0 -1
  56. package/dist/chunk-NO6FYX33.js +0 -2084
  57. package/dist/chunk-NO6FYX33.js.map +0 -1
  58. package/dist/chunk-OPTOYD2Y.js +0 -277
  59. package/dist/chunk-OPTOYD2Y.js.map +0 -1
  60. package/dist/chunk-PM3B4WYV.js +0 -12
  61. package/dist/chunk-PM3B4WYV.js.map +0 -1
  62. package/dist/chunk-REV5AUXD.js +0 -25
  63. package/dist/chunk-REV5AUXD.js.map +0 -1
  64. package/dist/chunk-YQNMIL33.js.map +0 -1
  65. package/dist/create-grist-emulator-BCqp1BF9.d.ts +0 -81
  66. package/dist/create-grist-emulator-DBM8OSIR.d.cts +0 -81
  67. package/dist/emulator/devtools.cjs +0 -1404
  68. package/dist/emulator/devtools.cjs.map +0 -1
  69. package/dist/emulator/devtools.d.cts +0 -48
  70. package/dist/emulator/devtools.d.ts +0 -48
  71. package/dist/emulator/devtools.js +0 -506
  72. package/dist/emulator/devtools.js.map +0 -1
  73. package/dist/emulator/transports.cjs +0 -1132
  74. package/dist/emulator/transports.cjs.map +0 -1
  75. package/dist/emulator/transports.d.cts +0 -38
  76. package/dist/emulator/transports.d.ts +0 -38
  77. package/dist/emulator/transports.js +0 -12
  78. package/dist/emulator/transports.js.map +0 -1
  79. package/dist/templates-8k2LKoO-.d.cts +0 -24
  80. package/dist/templates-DIwi_mvI.d.ts +0 -24
  81. package/dist/use-grist-reads-DUMvJgV7.d.ts +0 -480
  82. package/dist/use-grist-reads-DpjVuI0H.d.cts +0 -480
  83. package/dist/use-grist-types-DdOLEJXN.d.cts +0 -499
  84. package/dist/use-grist-types-kwxoFeub.d.ts +0 -499
  85. package/dist/use-grist-widget-options-from-context-CGOlQ5Vc.d.cts +0 -43
  86. 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
@@ -798,6 +695,310 @@ function toPositiveInt(value) {
798
695
  return null;
799
696
  }
800
697
 
698
+ // src/emulator/validators.ts
699
+ import { z } from "zod";
700
+ var emulatorRowSchema = z.object({ id: z.number() }).catchall(z.unknown());
701
+ var formulaKindSchema = z.enum(["computed", "trigger", "none", "empty_slot"]);
702
+ var replicaColumnSchema = z.object({
703
+ colRef: z.number().optional(),
704
+ type: z.string().optional(),
705
+ label: z.string().optional(),
706
+ parentPos: z.number().optional(),
707
+ isFormula: z.boolean().optional(),
708
+ formulaKind: formulaKindSchema.optional(),
709
+ formula: z.string().optional(),
710
+ description: z.string().optional(),
711
+ widgetOptions: z.record(z.string(), z.unknown()).optional(),
712
+ displayColRef: z.number().optional(),
713
+ visibleColRef: z.number().optional()
714
+ }).strict();
715
+ var replicaTableSchema = z.object({
716
+ label: z.string().optional(),
717
+ columns: z.record(z.string(), replicaColumnSchema).optional(),
718
+ rows: z.array(emulatorRowSchema).optional(),
719
+ rowCount: z.number().optional(),
720
+ dataOmitted: z.boolean().optional(),
721
+ dataOmittedReason: z.string().optional(),
722
+ error: z.string().optional()
723
+ }).strict();
724
+ var replicaSelectionSchema = z.object({
725
+ tableId: z.string().optional(),
726
+ rowId: z.union([z.number(), z.literal("new"), z.null()]).optional(),
727
+ mappings: z.unknown().nullable().optional(),
728
+ cursor: z.unknown().optional()
729
+ }).strict();
730
+ var replicaWidgetConfigurationSchema = z.object({
731
+ requiredAccess: z.string().optional(),
732
+ columns: z.unknown().optional(),
733
+ allowSelectBy: z.boolean().optional(),
734
+ hasCustomOptions: z.boolean().optional()
735
+ }).strict();
736
+ var replicaAccessTokenSchema = z.object({
737
+ token: z.string(),
738
+ baseUrl: z.string(),
739
+ ttlMsecs: z.number()
740
+ }).strict();
741
+ var replicaDocumentSchema = z.object({
742
+ generatedAt: z.string(),
743
+ source: z.string().optional(),
744
+ docName: z.string().optional(),
745
+ mode: z.enum(["schema-only", "schema+samples", "schema+data"]).optional(),
746
+ tables: z.record(z.string(), replicaTableSchema),
747
+ selection: replicaSelectionSchema.optional(),
748
+ widgetOptions: z.record(z.string(), z.unknown()).optional(),
749
+ widgetInteraction: z.unknown().optional(),
750
+ configuration: replicaWidgetConfigurationSchema.optional(),
751
+ accessToken: replicaAccessTokenSchema.nullable().optional()
752
+ }).strict().refine((value) => Object.keys(value.tables).length > 0, {
753
+ message: "at least one table is required",
754
+ path: ["tables"]
755
+ });
756
+ function formatZodError(prefix, error) {
757
+ const details = error.issues.map((issue) => {
758
+ const where = issue.path.length > 0 ? issue.path.join(".") : "<root>";
759
+ return `${where}: ${issue.message}`;
760
+ }).join("; ");
761
+ return new Error(`${prefix}: ${details}`);
762
+ }
763
+ function safeParseDocument(input) {
764
+ return replicaDocumentSchema.safeParse(input);
765
+ }
766
+ function parseDocument(input) {
767
+ const result = safeParseDocument(input);
768
+ if (!result.success) {
769
+ throw formatZodError("parseDocument", result.error);
770
+ }
771
+ return result.data;
772
+ }
773
+
774
+ // src/emulator/api/command-api.ts
775
+ function createCommandApiImpl(bus) {
776
+ return {
777
+ async run(name) {
778
+ bus.emit({ type: "user-action", action: ["RunCommand", name] });
779
+ }
780
+ };
781
+ }
782
+
783
+ // src/emulator/api/custom-section-api.ts
784
+ function createCustomSectionApiImpl(state, bus) {
785
+ return {
786
+ async configure(settings) {
787
+ const config = {
788
+ requiredAccess: settings.requiredAccess,
789
+ columns: settings.columns,
790
+ allowSelectBy: settings.allowSelectBy,
791
+ hasCustomOptions: settings.hasCustomOptions
792
+ };
793
+ state.configuration = config;
794
+ bus.emit({ type: "ready", configuration: config });
795
+ bus.emit({ type: "mappings-change", mappings: resolveMappings(state) });
796
+ },
797
+ async mappings() {
798
+ return resolveMappings(state);
799
+ }
800
+ };
801
+ }
802
+
803
+ // src/emulator/api/grist-api.ts
804
+ function createGristApiImpl() {
805
+ return {
806
+ async ping() {
807
+ return "pong";
808
+ }
809
+ };
810
+ }
811
+
812
+ // src/emulator/core/columnar.ts
813
+ function rowsToColumnar(table, rows, options = {}) {
814
+ const cols = Object.keys(table.columns);
815
+ const out = { id: rows.map((r) => r.id) };
816
+ for (const col of cols) {
817
+ out[col] = rows.map((r) => encodeForWire(r[col], table.columns[col]?.type, options.keepEncoded));
818
+ }
819
+ return out;
820
+ }
821
+ function rowToRecord(table, row, options = {}) {
822
+ const out = { id: row.id };
823
+ for (const col of Object.keys(table.columns)) {
824
+ out[col] = encodeForWire(row[col], table.columns[col]?.type, options.keepEncoded);
825
+ }
826
+ return out;
827
+ }
828
+ function encodeForWire(value, type, keepEncoded) {
829
+ if (value instanceof Date) {
830
+ const epoch = Math.floor(value.getTime() / 1e3);
831
+ if (keepEncoded) {
832
+ return type && type.startsWith("DateTime") ? ["D", epoch, "UTC"] : ["d", epoch];
833
+ }
834
+ return epoch;
835
+ }
836
+ return value;
837
+ }
838
+
839
+ // src/emulator/api/grist-doc-api.ts
840
+ function createGristDocApiImpl(state, bus) {
841
+ return {
842
+ async getDocName() {
843
+ return state.docName;
844
+ },
845
+ async listTables() {
846
+ return [...state.tables.keys()];
847
+ },
848
+ async fetchTable(tableId) {
849
+ if (tableId === "_grist_Tables") {
850
+ return synthesizeTablesMeta(state);
851
+ }
852
+ if (tableId === "_grist_Tables_column") {
853
+ return synthesizeTablesColumnMeta(state);
854
+ }
855
+ const table = getTable(state, tableId);
856
+ return rowsToColumnar(table, table.rows);
857
+ },
858
+ async applyUserActions(actions) {
859
+ const result = applyUserActions(state, bus, actions);
860
+ return { retValues: result.retValues };
861
+ },
862
+ async getAccessToken() {
863
+ if (!state.accessToken) {
864
+ const err = new Error("getAccessToken not available (no token configured on emulator)");
865
+ throw err;
866
+ }
867
+ return { ...state.accessToken };
868
+ }
869
+ };
870
+ }
871
+
872
+ // src/emulator/api/grist-view.ts
873
+ function createGristViewImpl(state, bus) {
874
+ return {
875
+ async fetchSelectedTable(options) {
876
+ const table = getSelectedTable(state);
877
+ return rowsToColumnar(table, table.rows, { keepEncoded: options?.keepEncoded });
878
+ },
879
+ async fetchSelectedRecord(rowId, options) {
880
+ const table = getSelectedTable(state);
881
+ const row = findRow(table, rowId);
882
+ if (!row) throw new Error(`No row ${rowId} in ${table.tableId}`);
883
+ return rowToRecord(table, row, { keepEncoded: options?.keepEncoded });
884
+ },
885
+ async allowSelectBy() {
886
+ return state.configuration?.allowSelectBy ?? false;
887
+ },
888
+ async setSelectedRows(rowIds) {
889
+ if (!rowIds || rowIds.length === 0) {
890
+ state.selection.rowId = null;
891
+ } else {
892
+ state.selection.rowId = rowIds[0];
893
+ }
894
+ bus.emit({
895
+ type: "cursor-change",
896
+ tableId: state.selection.tableId,
897
+ rowId: state.selection.rowId
898
+ });
899
+ },
900
+ async setCursorPos(cursor) {
901
+ const tableId = state.selection.tableId;
902
+ if (typeof cursor.rowId === "number") state.selection.rowId = cursor.rowId;
903
+ state.selection.cursor = { ...state.selection.cursor, ...cursor };
904
+ bus.emit({
905
+ type: "cursor-change",
906
+ tableId,
907
+ rowId: state.selection.rowId,
908
+ cursor: state.selection.cursor
909
+ });
910
+ }
911
+ };
912
+ }
913
+
914
+ // src/emulator/api/widget-api.ts
915
+ function createWidgetApiImpl(state, bus) {
916
+ function emit() {
917
+ bus.emit({
918
+ type: "options-change",
919
+ options: { ...state.widgetOptions },
920
+ interaction: { ...state.widgetInteraction }
921
+ });
922
+ }
923
+ return {
924
+ async getOption(key) {
925
+ return state.widgetOptions[key];
926
+ },
927
+ async setOption(key, value) {
928
+ state.widgetOptions[key] = value;
929
+ emit();
930
+ },
931
+ async getOptions() {
932
+ return Object.keys(state.widgetOptions).length > 0 ? { ...state.widgetOptions } : null;
933
+ },
934
+ // Replaces every option, as Grist's WidgetAPI.setOptions does.
935
+ async setOptions(options) {
936
+ state.widgetOptions = { ...options };
937
+ emit();
938
+ },
939
+ async clearOptions() {
940
+ state.widgetOptions = {};
941
+ emit();
942
+ }
943
+ };
944
+ }
945
+
946
+ // src/emulator/api/index.ts
947
+ function createApiBundle(state, bus) {
948
+ return {
949
+ GristView: createGristViewImpl(state, bus),
950
+ GristDocAPI: createGristDocApiImpl(state, bus),
951
+ WidgetAPI: createWidgetApiImpl(state, bus),
952
+ CustomSectionAPI: createCustomSectionApiImpl(state, bus),
953
+ GristAPI: createGristApiImpl(),
954
+ CommandAPI: createCommandApiImpl(bus)
955
+ };
956
+ }
957
+
958
+ // src/emulator/core/event-bus.ts
959
+ function createEventBus(options = {}) {
960
+ const limit = options.historyLimit ?? 200;
961
+ const listeners = /* @__PURE__ */ new Set();
962
+ const history = [];
963
+ return {
964
+ emit(event) {
965
+ history.push(event);
966
+ if (history.length > limit) history.shift();
967
+ for (const fn of [...listeners]) {
968
+ try {
969
+ fn(event);
970
+ } catch (err) {
971
+ console.error("[grist-emulator] event listener threw:", err);
972
+ }
973
+ }
974
+ },
975
+ on(listener) {
976
+ listeners.add(listener);
977
+ return () => {
978
+ listeners.delete(listener);
979
+ };
980
+ },
981
+ history() {
982
+ return history;
983
+ },
984
+ clear() {
985
+ history.length = 0;
986
+ }
987
+ };
988
+ }
989
+
990
+ // src/emulator/theme-message.ts
991
+ function buildGristThemeHostMessage(appearance, options) {
992
+ return {
993
+ theme: {
994
+ appearance,
995
+ name: appearance,
996
+ colors: {}
997
+ },
998
+ ...options?.fromReady ? { fromReady: true } : {}
999
+ };
1000
+ }
1001
+
801
1002
  // src/emulator/transports/inline.ts
802
1003
  function createInlineTransport(options) {
803
1004
  const { state, bus, api } = options;
@@ -827,14 +1028,6 @@ function createInlineTransport(options) {
827
1028
  logErr(err);
828
1029
  }
829
1030
  }
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
1031
  return;
839
1032
  }
840
1033
  if (rowId === null) {
@@ -1172,18 +1365,172 @@ function installFakeIframeContext(target) {
1172
1365
  }
1173
1366
  }
1174
1367
 
1368
+ // src/emulator/create-grist-emulator.ts
1369
+ function createGristEmulator(options) {
1370
+ const validatedDocument = parseDocument(options.document);
1371
+ const state = createDocument({
1372
+ ...validatedDocument,
1373
+ widgetOptions: options.widgetOptions ?? validatedDocument.widgetOptions,
1374
+ // No access_level by default: the emulator then grants the access the
1375
+ // widget asks for in grist.ready(), as a user accepting it in Grist would.
1376
+ widgetInteraction: options.widgetInteraction ?? validatedDocument.widgetInteraction ?? {}
1377
+ });
1378
+ const bus = createEventBus();
1379
+ const api = createApiBundle(state, bus);
1380
+ const inline = createInlineTransport({
1381
+ state,
1382
+ bus,
1383
+ api,
1384
+ initialTheme: options.initialTheme,
1385
+ target: options.transport?.target
1386
+ });
1387
+ const transport = inline;
1388
+ const grist = inline.grist;
1389
+ function apply(actions) {
1390
+ return applyUserActions(state, bus, actions);
1391
+ }
1392
+ return {
1393
+ state,
1394
+ bus,
1395
+ api,
1396
+ transport,
1397
+ grist,
1398
+ apply,
1399
+ mutate: {
1400
+ addRow: (tableId, fields) => apply([["AddRecord", tableId, null, fields]]).retValues[0],
1401
+ updateRow: (tableId, rowId, fields) => {
1402
+ apply([["UpdateRecord", tableId, rowId, fields]]);
1403
+ },
1404
+ removeRow: (tableId, rowId) => {
1405
+ apply([["RemoveRecord", tableId, rowId]]);
1406
+ }
1407
+ },
1408
+ selectRow(rowId) {
1409
+ state.selection.rowId = rowId;
1410
+ bus.emit({ type: "cursor-change", tableId: state.selection.tableId, rowId });
1411
+ },
1412
+ selectTable(tableId) {
1413
+ if (!state.tables.has(tableId)) throw new Error(`Unknown table: ${tableId}`);
1414
+ state.selection.tableId = tableId;
1415
+ const first = state.tables.get(tableId).rows[0]?.id ?? null;
1416
+ state.selection.rowId = first;
1417
+ bus.emit({ type: "data-change", tableId });
1418
+ bus.emit({ type: "cursor-change", tableId, rowId: first });
1419
+ },
1420
+ setColumnMappings(mappings) {
1421
+ if (mappings === void 0) {
1422
+ delete state.selection.mappings;
1423
+ } else {
1424
+ state.selection.mappings = mappings;
1425
+ }
1426
+ bus.emit({ type: "mappings-change", mappings: resolveMappings(state) });
1427
+ },
1428
+ async setOptions(opts) {
1429
+ await api.WidgetAPI.setOptions(opts);
1430
+ },
1431
+ setInteraction(interaction) {
1432
+ state.widgetInteraction = { ...interaction };
1433
+ bus.emit({ type: "options-change", options: { ...state.widgetOptions }, interaction: { ...interaction } });
1434
+ },
1435
+ theme: {
1436
+ set(next) {
1437
+ bus.emit({ type: "theme-change", message: buildGristThemeHostMessage(next) });
1438
+ }
1439
+ },
1440
+ dispose() {
1441
+ transport.dispose();
1442
+ bus.clear();
1443
+ }
1444
+ };
1445
+ }
1446
+
1447
+ // src/emulator/templates.ts
1448
+ var PRESET_GENERATED_AT = "1970-01-01T00:00:00.000Z";
1449
+ function replicaBase(overrides) {
1450
+ return {
1451
+ generatedAt: PRESET_GENERATED_AT,
1452
+ ...overrides
1453
+ };
1454
+ }
1455
+ var presets = {
1456
+ /**
1457
+ * Default starter fixture for demos / sandboxes that need a neutral document
1458
+ * without choosing a domain-specific preset.
1459
+ */
1460
+ defaultTemplate() {
1461
+ return replicaBase({
1462
+ docName: "Default",
1463
+ tables: {
1464
+ Table1: {
1465
+ label: "Table1",
1466
+ rows: [
1467
+ { id: 1, Name: "Alice" },
1468
+ { id: 2, Name: "Bob" }
1469
+ ]
1470
+ }
1471
+ }
1472
+ });
1473
+ },
1474
+ blank() {
1475
+ return replicaBase({
1476
+ docName: "Blank",
1477
+ tables: {
1478
+ Table1: { rows: [{ id: 1 }] }
1479
+ }
1480
+ });
1481
+ },
1482
+ todoList() {
1483
+ return replicaBase({
1484
+ docName: "Todo list",
1485
+ mode: "schema+data",
1486
+ tables: {
1487
+ Tasks: {
1488
+ label: "Tasks",
1489
+ columns: {
1490
+ title: { type: "Text", label: "Title" },
1491
+ done: { type: "Bool", label: "Done" },
1492
+ priority: { type: "Choice", label: "Priority" }
1493
+ },
1494
+ rows: [
1495
+ { id: 1, title: "Buy milk", done: false, priority: "high" },
1496
+ { id: 2, title: "Write the SOTA emulator", done: true, priority: "high" },
1497
+ { id: 3, title: "Ship it", done: false, priority: "medium" }
1498
+ ]
1499
+ }
1500
+ }
1501
+ });
1502
+ },
1503
+ contacts() {
1504
+ return replicaBase({
1505
+ docName: "Contacts",
1506
+ mode: "schema+data",
1507
+ tables: {
1508
+ Contacts: {
1509
+ label: "Contacts",
1510
+ columns: {
1511
+ firstName: { type: "Text" },
1512
+ lastName: { type: "Text" },
1513
+ email: { type: "Text" },
1514
+ company: { type: "Text" }
1515
+ },
1516
+ rows: [
1517
+ { id: 1, firstName: "Ada", lastName: "Lovelace", email: "ada@analytical.engine", company: "British Crown" },
1518
+ { id: 2, firstName: "Linus", lastName: "Torvalds", email: "linus@kernel.org", company: "Linux Foundation" },
1519
+ { id: 3, firstName: "Margaret", lastName: "Hamilton", email: "margaret@apollo.nasa", company: "NASA" }
1520
+ ]
1521
+ }
1522
+ }
1523
+ });
1524
+ }
1525
+ };
1526
+
1175
1527
  export {
1528
+ resolveMappings,
1176
1529
  enrichEmulatorColumnMetadata,
1177
- createDocument,
1178
- getTable,
1179
- getSelectedTable,
1180
- findRow,
1181
- synthesizeTablesMeta,
1182
- synthesizeTablesColumnMeta,
1183
1530
  EmulatorActionError,
1184
- applyUserActions,
1185
- buildGristThemeHostMessage,
1186
- createIframeTransport,
1187
- createInlineTransport
1531
+ safeParseDocument,
1532
+ parseDocument,
1533
+ createGristEmulator,
1534
+ presets
1188
1535
  };
1189
- //# sourceMappingURL=chunk-YQNMIL33.js.map
1536
+ //# sourceMappingURL=chunk-B5ZZNLVS.js.map