@altertable/data-app 0.64.0 → 0.66.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 (157) hide show
  1. package/AGENTS.md +40 -0
  2. package/CONTRIBUTING.md +37 -3
  3. package/README.md +2 -2
  4. package/dist/chunks/{contract-mwe7gnmh.js → contract-13j5zb3c.js} +110 -216
  5. package/dist/chunks/contract-13j5zb3c.js.map +13 -0
  6. package/dist/chunks/contract-19ckn3n7.js +78 -0
  7. package/dist/chunks/contract-19ckn3n7.js.map +10 -0
  8. package/dist/chunks/contract-awa2d5b1.js +272 -0
  9. package/dist/chunks/contract-awa2d5b1.js.map +12 -0
  10. package/dist/chunks/{contract-xtxza1fy.js → contract-e11m4y7f.js} +35 -71
  11. package/dist/chunks/contract-e11m4y7f.js.map +12 -0
  12. package/dist/chunks/{contract-zr3jd7mr.js → contract-h8a6f559.js} +100 -75
  13. package/dist/chunks/contract-h8a6f559.js.map +12 -0
  14. package/dist/chunks/contract-havnjmdr.js +13011 -0
  15. package/dist/chunks/contract-havnjmdr.js.map +99 -0
  16. package/dist/chunks/{contract-txjy0en2.js → contract-m6n8ctfc.js} +64 -72
  17. package/dist/chunks/contract-m6n8ctfc.js.map +10 -0
  18. package/dist/chunks/contract-pk603qj7.js +146 -0
  19. package/dist/chunks/contract-pk603qj7.js.map +10 -0
  20. package/dist/chunks/{contract-yxbjea23.js → contract-rfbakka4.js} +179 -2
  21. package/dist/chunks/contract-rfbakka4.js.map +13 -0
  22. package/dist/client/index.js +11 -6
  23. package/dist/client/index.js.map +1 -1
  24. package/dist/core/appearance.js +1 -1
  25. package/dist/core/contract.js +13 -3
  26. package/dist/core/contract.js.map +1 -1
  27. package/dist/embed/index.js +25 -13
  28. package/dist/embed/index.js.map +3 -3
  29. package/dist/local.js +1 -1
  30. package/dist/local.js.map +2 -2
  31. package/dist/react/embed/index.js +5 -2
  32. package/dist/react/embed/index.js.map +3 -3
  33. package/dist/react/index.js +2532 -11273
  34. package/dist/react/index.js.map +16 -88
  35. package/dist/react/ui/index.js +1614 -0
  36. package/dist/react/ui/index.js.map +22 -0
  37. package/dist/server.js +1 -1
  38. package/dist/server.js.map +2 -2
  39. package/dist/types/client/annotations.d.ts +23 -0
  40. package/dist/types/client/iframe.d.ts +2 -0
  41. package/dist/types/client/index.d.ts +2 -0
  42. package/dist/types/client/logger.d.ts +6 -0
  43. package/dist/types/core/annotations.d.ts +92 -0
  44. package/dist/types/core/appearance.d.ts +2 -2
  45. package/dist/types/core/bridge-endpoint.d.ts +21 -0
  46. package/dist/types/core/bridge.d.ts +137 -16
  47. package/dist/types/core/contract.d.ts +2 -0
  48. package/dist/types/core/logger.d.ts +12 -0
  49. package/dist/types/core/presentation.d.ts +2 -0
  50. package/dist/types/core/variables.d.ts +2 -5
  51. package/dist/types/embed/host.d.ts +6 -2
  52. package/dist/types/embed/index.d.ts +1 -0
  53. package/dist/types/embed/source.d.ts +1 -1
  54. package/dist/types/react/annotations/AnnotationBar.d.ts +24 -0
  55. package/dist/types/react/annotations/AnnotationControls.d.ts +10 -0
  56. package/dist/types/react/annotations/AnnotationEditor.d.ts +15 -0
  57. package/dist/types/react/annotations/AnnotationMarkers.d.ts +19 -0
  58. package/dist/types/react/annotations/AnnotationSelectionLayer.d.ts +12 -0
  59. package/dist/types/react/annotations/AnnotationTarget.d.ts +9 -0
  60. package/dist/types/react/annotations/AnnotationTooltip.d.ts +9 -0
  61. package/dist/types/react/annotations/AnnotationTrigger.d.ts +10 -0
  62. package/dist/types/react/annotations/annotation-editor-state.d.ts +56 -0
  63. package/dist/types/react/annotations/annotation-screenshot.d.ts +4 -0
  64. package/dist/types/react/annotations/annotation-targets.d.ts +33 -0
  65. package/dist/types/react/annotations/getAnnotationProps.d.ts +15 -0
  66. package/dist/types/react/annotations/styles.d.ts +3 -0
  67. package/dist/types/react/annotations/useAnnotationGeometry.d.ts +22 -0
  68. package/dist/types/react/annotations/useDataAppAnnotations.d.ts +19 -0
  69. package/dist/types/react/bindings.d.ts +110 -0
  70. package/dist/types/react/content.d.ts +24 -11
  71. package/dist/types/react/embed/bridge.d.ts +1 -1
  72. package/dist/types/react/hooks.d.ts +29 -787
  73. package/dist/types/react/index.d.ts +30 -104
  74. package/dist/types/react/source-owner.d.ts +2 -0
  75. package/dist/types/react/style-contract.d.ts +60 -0
  76. package/dist/types/react/style-validation.d.ts +2 -0
  77. package/dist/types/react/ui/AboutData.d.ts +9 -1
  78. package/dist/types/react/ui/AppHeader.d.ts +1 -2
  79. package/dist/types/react/ui/AppLayout.d.ts +3 -9
  80. package/dist/types/react/ui/AppToolbar.d.ts +6 -7
  81. package/dist/types/react/ui/AreaChart.d.ts +7 -0
  82. package/dist/types/react/ui/BarChart.d.ts +6 -0
  83. package/dist/types/react/ui/{ComparisonVisual.d.ts → Comparison.d.ts} +3 -3
  84. package/dist/types/react/ui/ContentSkeleton.d.ts +2 -0
  85. package/dist/types/react/ui/DataApp.d.ts +17 -53
  86. package/dist/types/react/ui/DataAppFrame.d.ts +42 -0
  87. package/dist/types/react/ui/DataBoundary.d.ts +4 -5
  88. package/dist/types/react/ui/DataSection.d.ts +6 -18
  89. package/dist/types/react/ui/DataSectionBoundary.d.ts +30 -0
  90. package/dist/types/react/ui/DataValue.d.ts +9 -0
  91. package/dist/types/react/ui/DataWidget.d.ts +2 -1
  92. package/dist/types/react/ui/DateTimeTooltip.d.ts +1 -1
  93. package/dist/types/react/ui/ExportControl.d.ts +4 -0
  94. package/dist/types/react/ui/HelpPopover.d.ts +3 -4
  95. package/dist/types/react/ui/InspectionContext.d.ts +2 -0
  96. package/dist/types/react/ui/InspectionProvider.d.ts +29 -0
  97. package/dist/types/react/ui/LineChart.d.ts +7 -0
  98. package/dist/types/react/ui/MetricWidget.d.ts +4 -3
  99. package/dist/types/react/ui/PieChart.d.ts +8 -0
  100. package/dist/types/react/ui/PresentStory.d.ts +2 -5
  101. package/dist/types/react/ui/RefreshControl.d.ts +1 -2
  102. package/dist/types/react/ui/ScatterChart.d.ts +17 -0
  103. package/dist/types/react/ui/SearchField.d.ts +1 -2
  104. package/dist/types/react/ui/SearchInput.d.ts +3 -1
  105. package/dist/types/react/ui/Sheet.d.ts +2 -1
  106. package/dist/types/react/ui/Skeleton.d.ts +5 -2
  107. package/dist/types/react/ui/StaticDataApp.d.ts +4 -0
  108. package/dist/types/react/ui/TableWidget.d.ts +19 -16
  109. package/dist/types/react/ui/Tabs.d.ts +6 -2
  110. package/dist/types/react/ui/TextWidget.d.ts +2 -1
  111. package/dist/types/react/ui/Toast.d.ts +8 -0
  112. package/dist/types/react/ui/Tooltip.d.ts +4 -3
  113. package/dist/types/react/ui/TrendChart.d.ts +5 -0
  114. package/dist/types/react/ui/UpdatedAt.d.ts +1 -1
  115. package/dist/types/react/ui/VisualizationWidget.d.ts +12 -13
  116. package/dist/types/react/ui/WidgetViewTabs.d.ts +1 -1
  117. package/dist/types/react/ui/chart-data.d.ts +25 -0
  118. package/dist/types/react/ui/csv-export.d.ts +19 -0
  119. package/dist/types/react/ui/data-context.d.ts +18 -7
  120. package/dist/types/react/ui/icons.d.ts +2 -0
  121. package/dist/types/react/ui/index.d.ts +74 -0
  122. package/dist/types/react/ui/metric.d.ts +1 -1
  123. package/dist/types/react/ui/presentation.d.ts +5 -1
  124. package/dist/types/react/ui/shortcuts.d.ts +7 -1
  125. package/dist/types/react/view-runtime.d.ts +29 -0
  126. package/dist/types/react/view.d.ts +21 -4
  127. package/dist/types/react/widgets.d.ts +79 -0
  128. package/dist/worker.js +381 -79
  129. package/docs/app-authoring.md +64 -30
  130. package/docs/data-context.md +89 -0
  131. package/docs/embed.md +1 -1
  132. package/docs/formatting-and-appearance.md +52 -0
  133. package/docs/hosted-apps.md +2 -15
  134. package/docs/layout.md +33 -0
  135. package/docs/local-data-apps.md +1 -1
  136. package/docs/react-embed.md +2 -2
  137. package/docs/react.md +20 -254
  138. package/docs/stories-and-export.md +52 -0
  139. package/docs/styling.md +55 -0
  140. package/docs/ui-quality.md +20 -0
  141. package/docs/ui.md +41 -0
  142. package/docs/variables.md +98 -0
  143. package/docs/views.md +127 -0
  144. package/docs/widgets.md +142 -0
  145. package/examples/starter-data-app/index.tsx +89 -44
  146. package/package.json +13 -4
  147. package/dist/chunks/contract-cxr9t12b.js +0 -18
  148. package/dist/chunks/contract-cxr9t12b.js.map +0 -10
  149. package/dist/chunks/contract-mwe7gnmh.js.map +0 -13
  150. package/dist/chunks/contract-txjy0en2.js.map +0 -10
  151. package/dist/chunks/contract-xtxza1fy.js.map +0 -12
  152. package/dist/chunks/contract-yxbjea23.js.map +0 -12
  153. package/dist/chunks/contract-zr3jd7mr.js.map +0 -12
  154. package/dist/types/react/ui/RefreshRegion.d.ts +0 -9
  155. package/dist/types/react/ui/SelectableBarChart.d.ts +0 -15
  156. package/docs/releasing.md +0 -79
  157. /package/dist/types/react/ui/{comparison.d.ts → metric-comparison.d.ts} +0 -0
package/dist/worker.js CHANGED
@@ -48,19 +48,273 @@ function trustedParent(config, searchParams) {
48
48
  var TOKEN_RE = /^(?=.{1,63}$)[a-z0-9]+(?:-[a-z0-9]+)+-app-[1-9][0-9]*$/;
49
49
  var PAGE_CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; connect-src 'none'; form-action 'none'; base-uri 'none'";
50
50
  var inlineBootstrap = `(() => {
51
+ // src/core/logger.ts
52
+ function isLogEntry(value) {
53
+ if (!value || typeof value !== "object")
54
+ return false;
55
+ const entry = value;
56
+ return (entry.method === "log" || entry.method === "info" || entry.method === "warn" || entry.method === "error") && Array.isArray(entry.args) && entry.args.length <= 128;
57
+ }
58
+
51
59
  // src/core/bridge.ts
52
60
  var BRIDGE = "altertable:data-app";
53
61
  var MAX_PENDING = 128;
54
62
  var REQUEST_TIMEOUT_MS = 60000;
55
- function isBridgeMessage(value) {
56
- if (!value || typeof value !== "object")
57
- return false;
58
- const message = value;
59
- return message.channel === BRIDGE && message.version === 1 && typeof message.type === "string" && typeof message.documentId === "string" && message.documentId.length > 0 && message.documentId.length <= 128;
60
- }
61
63
  function validId(value) {
62
64
  return typeof value === "string" && /^[a-zA-Z0-9_-]{1,128}$/.test(value);
63
65
  }
66
+ function event({
67
+ wire,
68
+ from,
69
+ session,
70
+ parse
71
+ }) {
72
+ return { wire, from, session, parse };
73
+ }
74
+ function id(fields) {
75
+ if (!validId(fields.id))
76
+ throw new Error("Invalid request ID.");
77
+ return fields.id;
78
+ }
79
+ function string(fields, key) {
80
+ const value = fields[key];
81
+ if (typeof value !== "string")
82
+ throw new Error(\`Invalid \${key}.\`);
83
+ return value;
84
+ }
85
+ function empty() {
86
+ return {};
87
+ }
88
+ var bridgeProtocol = {
89
+ channel: BRIDGE,
90
+ version: 1,
91
+ events: {
92
+ connect: event({
93
+ wire: "bridge:connect",
94
+ from: "host",
95
+ session: "none",
96
+ parse: empty
97
+ }),
98
+ ready: event({
99
+ wire: "bridge:ready",
100
+ from: "app",
101
+ session: "none",
102
+ parse: empty
103
+ }),
104
+ initialize: event({
105
+ wire: "bridge:initialize",
106
+ from: "host",
107
+ session: "establish",
108
+ parse: (fields) => ({ state: fields.state })
109
+ }),
110
+ request: event({
111
+ wire: "bridge:request",
112
+ from: "app",
113
+ session: "current",
114
+ parse: (fields) => {
115
+ const route = string(fields, "route");
116
+ if (!route || route.length > 256)
117
+ throw new Error("Invalid message route.");
118
+ return { id: id(fields), route, payload: fields.payload };
119
+ }
120
+ }),
121
+ result: event({
122
+ wire: "bridge:result",
123
+ from: "host",
124
+ session: "current",
125
+ parse: (fields) => {
126
+ if (!Object.hasOwn(fields, "response"))
127
+ throw new Error("Missing response.");
128
+ return { id: id(fields), response: fields.response };
129
+ }
130
+ }),
131
+ error: event({
132
+ wire: "bridge:error",
133
+ from: "host",
134
+ session: "current",
135
+ parse: (fields) => {
136
+ if (fields.requestId !== undefined && typeof fields.requestId !== "string")
137
+ throw new Error("Invalid requestId.");
138
+ return {
139
+ id: id(fields),
140
+ code: string(fields, "code"),
141
+ message: string(fields, "message"),
142
+ ...fields.requestId === undefined ? {} : { requestId: fields.requestId }
143
+ };
144
+ }
145
+ }),
146
+ cancel: event({
147
+ wire: "bridge:cancel",
148
+ from: "app",
149
+ session: "current",
150
+ parse: (fields) => ({ id: id(fields) })
151
+ }),
152
+ disconnect: event({
153
+ wire: "bridge:disconnect",
154
+ from: "app",
155
+ session: "current",
156
+ parse: empty
157
+ }),
158
+ scriptLoad: event({
159
+ wire: "script:load",
160
+ from: "host",
161
+ session: "current",
162
+ parse: (fields) => ({ javascript: string(fields, "javascript") })
163
+ }),
164
+ stateUpdate: event({
165
+ wire: "state:update",
166
+ from: "host",
167
+ session: "current",
168
+ parse: (fields) => ({ state: fields.state })
169
+ }),
170
+ runtimeLog: event({
171
+ wire: "runtime:log",
172
+ from: "app",
173
+ session: "current",
174
+ parse: (fields) => {
175
+ if (!isLogEntry(fields.payload))
176
+ throw new Error("Invalid log entry.");
177
+ return { payload: fields.payload };
178
+ }
179
+ }),
180
+ runtimeReady: event({
181
+ wire: "runtime:ready",
182
+ from: "app",
183
+ session: "current",
184
+ parse: empty
185
+ }),
186
+ runtimeError: event({
187
+ wire: "runtime:error",
188
+ from: "app",
189
+ session: "current",
190
+ parse: empty
191
+ })
192
+ }
193
+ };
194
+ var byWire = new Map;
195
+ for (const name of Object.keys(bridgeProtocol.events)) {
196
+ const wire = bridgeProtocol.events[name].wire;
197
+ if (byWire.has(wire))
198
+ throw new Error(\`Duplicate bridge event: \${wire}\`);
199
+ byWire.set(wire, name);
200
+ }
201
+ function bridgeEventName(wire) {
202
+ return byWire.get(wire);
203
+ }
204
+ function parseBridgeEnvelope(value) {
205
+ if (!value || typeof value !== "object")
206
+ return;
207
+ const fields = value;
208
+ if (fields.channel !== BRIDGE || fields.version !== bridgeProtocol.version || typeof fields.type !== "string" || typeof fields.documentId !== "string" || !fields.documentId || fields.documentId.length > 128 || fields.sessionId !== undefined && !validId(fields.sessionId) || fields.token !== undefined && typeof fields.token !== "string")
209
+ return;
210
+ return fields;
211
+ }
212
+ function parseBridgePayload(envelope, definition) {
213
+ try {
214
+ return {
215
+ channel: BRIDGE,
216
+ version: bridgeProtocol.version,
217
+ documentId: envelope.documentId,
218
+ ...envelope.sessionId === undefined ? {} : { sessionId: envelope.sessionId },
219
+ ...envelope.token === undefined ? {} : { token: envelope.token },
220
+ type: definition.wire,
221
+ ...definition.parse(envelope)
222
+ };
223
+ } catch {
224
+ return;
225
+ }
226
+ }
227
+ function parseBridgeMessage(value) {
228
+ const envelope = parseBridgeEnvelope(value);
229
+ if (!envelope)
230
+ return;
231
+ const name = bridgeEventName(envelope.type);
232
+ if (!name)
233
+ return;
234
+ const definition = bridgeProtocol.events[name];
235
+ if (definition.session !== "none" && envelope.sessionId === undefined)
236
+ return;
237
+ return parseBridgePayload(envelope, definition);
238
+ }
239
+
240
+ // src/core/bridge-endpoint.ts
241
+ function createBridgeEndpoint({
242
+ role,
243
+ origin,
244
+ source,
245
+ opaque,
246
+ context,
247
+ post,
248
+ diagnostic,
249
+ invalidRequest,
250
+ handlers
251
+ }) {
252
+ let disposed = false;
253
+ function send(name, payload) {
254
+ if (disposed)
255
+ throw new Error("The bridge endpoint has closed.");
256
+ const definition = bridgeProtocol.events[name];
257
+ const current = context();
258
+ const message = parseBridgeMessage({
259
+ ...payload,
260
+ channel: bridgeProtocol.channel,
261
+ version: bridgeProtocol.version,
262
+ type: definition.wire,
263
+ documentId: name === "connect" ? "host" : current.documentId,
264
+ sessionId: current.sessionId,
265
+ token: opaque ? current.token : undefined
266
+ });
267
+ if (!message || opaque && !validId(current.token))
268
+ throw new Error("The bridge is not ready to send this event.");
269
+ diagnostic?.("send", definition.wire);
270
+ if (disposed)
271
+ return;
272
+ post(message);
273
+ }
274
+ function receive(event) {
275
+ const peer = source();
276
+ if (disposed || !peer || event.origin !== origin || event.source !== peer)
277
+ return;
278
+ const envelope = parseBridgeEnvelope(event.data);
279
+ if (!envelope)
280
+ return;
281
+ const current = context();
282
+ const connecting = role === "app" && envelope.type === bridgeProtocol.events.connect.wire;
283
+ if (opaque && (connecting ? !validId(envelope.token) : !current.token || envelope.token !== current.token))
284
+ return;
285
+ const name = bridgeEventName(envelope.type);
286
+ if (!name)
287
+ return;
288
+ const definition = bridgeProtocol.events[name];
289
+ if (definition.from === role)
290
+ return;
291
+ if (definition.session === "establish") {
292
+ if (envelope.documentId !== current.documentId || envelope.sessionId === undefined)
293
+ return;
294
+ } else if (definition.session === "current") {
295
+ if (!current.sessionId || envelope.sessionId !== current.sessionId || envelope.documentId !== current.documentId)
296
+ return;
297
+ }
298
+ const message = parseBridgePayload(envelope, definition);
299
+ if (!message) {
300
+ if (role === "host" && name === "request" && validId(envelope.id))
301
+ invalidRequest?.(envelope.id);
302
+ return;
303
+ }
304
+ diagnostic?.("receive", definition.wire);
305
+ if (disposed)
306
+ return;
307
+ const handler = handlers[name];
308
+ handler(message);
309
+ }
310
+ return {
311
+ send,
312
+ receive,
313
+ dispose() {
314
+ disposed = true;
315
+ }
316
+ };
317
+ }
64
318
 
65
319
  // src/core/messages.ts
66
320
  var errorBrand = Symbol.for("@altertable/data-app/MessageRoutingError");
@@ -193,6 +447,46 @@ var inlineBootstrap = `(() => {
193
447
  }
194
448
  }
195
449
 
450
+ // src/client/logger.ts
451
+ function createBridgeLogger(enabled, send) {
452
+ function resolve(args) {
453
+ if (args.length === 1 && typeof args[0] === "function") {
454
+ const value = args[0]();
455
+ return Array.isArray(value) ? value : [value];
456
+ }
457
+ return args.map((arg) => typeof arg === "function" ? arg() : arg);
458
+ }
459
+ function write(method, args) {
460
+ if (!enabled())
461
+ return;
462
+ try {
463
+ send({ method, args: resolve(args) });
464
+ } catch {}
465
+ }
466
+ return {
467
+ log: (...args) => write("log", args),
468
+ info: (...args) => write("info", args),
469
+ warn: (...args) => write("warn", args),
470
+ error: (...args) => write("error", args)
471
+ };
472
+ }
473
+ function logBridgeMessage(logger, message) {
474
+ if (message.type === "runtime:log")
475
+ return;
476
+ logger.log(() => {
477
+ const metadata = { type: message.type };
478
+ if ("id" in message)
479
+ metadata.id = message.id;
480
+ if (message.type === "bridge:request") {
481
+ metadata.route = message.route;
482
+ const payload = message.payload;
483
+ if (message.route === "data:query" && payload && typeof payload === "object" && "operation" in payload && typeof payload.operation === "string")
484
+ metadata.operation = payload.operation;
485
+ }
486
+ return ["Sending message to parent", metadata];
487
+ });
488
+ }
489
+
196
490
  // src/client/iframe.ts
197
491
  function rethrowDataMessageError(error) {
198
492
  if (error instanceof MessageRoutingError)
@@ -215,67 +509,61 @@ var inlineBootstrap = `(() => {
215
509
  const pending = new Map;
216
510
  let hostState;
217
511
  const stateListeners = new Set;
218
- function send(message) {
219
- frame.parent.postMessage({ channel: BRIDGE, version: 1, documentId, sessionId, token, ...message }, parentOrigin);
220
- }
512
+ const logger = createBridgeLogger(() => !disposed && !!sessionId && !!hostState && typeof hostState === "object" && ("logging" in hostState) && hostState.logging === true, (entry) => send("runtimeLog", { payload: entry }));
513
+ const endpoint = createBridgeEndpoint({
514
+ role: "app",
515
+ origin: parentOrigin,
516
+ source: () => frame.parent,
517
+ opaque: mode === "bundle",
518
+ context: () => ({ documentId, sessionId, token }),
519
+ post(message) {
520
+ frame.parent.postMessage(message, parentOrigin);
521
+ logBridgeMessage(logger, message);
522
+ },
523
+ handlers: {
524
+ connect(message) {
525
+ if (mode === "bundle")
526
+ token = message.token;
527
+ send("ready", {});
528
+ },
529
+ initialize(message) {
530
+ if (sessionId && sessionId !== message.sessionId) {
531
+ for (const entry of pending.values())
532
+ entry.reject(new DataAppError("The preview reconnected. Retry the request.", "bridge_reset"));
533
+ }
534
+ const first = !sessionId;
535
+ sessionId = message.sessionId;
536
+ receiveState(message.state);
537
+ if (first)
538
+ for (const entry of pending.values())
539
+ entry.start();
540
+ if (disposed)
541
+ return;
542
+ if (mode === "url")
543
+ send("runtimeReady", {});
544
+ },
545
+ stateUpdate(message) {
546
+ receiveState(message.state);
547
+ },
548
+ scriptLoad(message) {
549
+ if (mode === "bundle")
550
+ loadScript?.(message.javascript);
551
+ },
552
+ result(message) {
553
+ pending.get(message.id)?.resolve(message.response);
554
+ },
555
+ error(message) {
556
+ pending.get(message.id)?.reject(new MessageRoutingError(message.code, message.message, message.requestId));
557
+ }
558
+ }
559
+ });
560
+ const send = endpoint.send;
561
+ const receive = endpoint.receive;
221
562
  function receiveState(state) {
222
563
  hostState = state;
223
564
  for (const listener of stateListeners)
224
565
  listener(state);
225
566
  }
226
- function receive(event) {
227
- if (event.origin !== parentOrigin || event.source !== frame.parent || !isBridgeMessage(event.data))
228
- return;
229
- const message = event.data;
230
- if (message.type === "bridge:connect") {
231
- if (mode === "bundle") {
232
- if (!validId(message.token))
233
- return;
234
- token = message.token;
235
- }
236
- send({ type: "bridge:ready" });
237
- return;
238
- }
239
- if (mode === "bundle" && (!token || message.token !== token))
240
- return;
241
- if (message.documentId !== documentId)
242
- return;
243
- if (message.type === "bridge:initialize" && validId(message.sessionId)) {
244
- if (sessionId && sessionId !== message.sessionId) {
245
- for (const entry of pending.values())
246
- entry.reject(new DataAppError("The preview reconnected. Retry the request.", "bridge_reset"));
247
- }
248
- const first = !sessionId;
249
- sessionId = message.sessionId;
250
- if (first)
251
- for (const entry of pending.values())
252
- entry.start();
253
- receiveState(message.state);
254
- if (mode === "url")
255
- send({ type: "runtime:ready" });
256
- return;
257
- }
258
- if (!sessionId || message.sessionId !== sessionId)
259
- return;
260
- if (message.type === "state:update") {
261
- receiveState(message.state);
262
- return;
263
- }
264
- if (message.type === "script:load" && mode === "bundle" && typeof message.javascript === "string") {
265
- loadScript?.(message.javascript);
266
- return;
267
- }
268
- if (typeof message.id !== "string")
269
- return;
270
- const entry = pending.get(message.id);
271
- if (!entry)
272
- return;
273
- if (message.type === "bridge:result" && "response" in message) {
274
- entry.resolve(message.response);
275
- } else if (message.type === "bridge:error" && typeof message.code === "string" && typeof message.message === "string") {
276
- entry.reject(new MessageRoutingError(message.code, message.message, typeof message.requestId === "string" ? message.requestId : undefined));
277
- }
278
- }
279
567
  frame.addEventListener("message", receive);
280
568
  function requestMessage(message, signal) {
281
569
  return new Promise((resolve, reject) => {
@@ -294,13 +582,13 @@ var inlineBootstrap = `(() => {
294
582
  }
295
583
  function abort() {
296
584
  if (sent)
297
- send({ type: "bridge:cancel", id });
585
+ send("cancel", { id });
298
586
  cleanup();
299
587
  reject(signal?.reason);
300
588
  }
301
589
  const timer = setTimeout(() => {
302
590
  if (sent)
303
- send({ type: "bridge:cancel", id });
591
+ send("cancel", { id });
304
592
  cleanup();
305
593
  reject(new DataAppError(sessionId ? "The data request timed out." : "The preview shell is unavailable. Reload the preview.", sessionId ? "timeout" : "bridge_unavailable"));
306
594
  }, timeoutMs);
@@ -309,8 +597,7 @@ var inlineBootstrap = `(() => {
309
597
  if (sent)
310
598
  return;
311
599
  try {
312
- send({
313
- type: "bridge:request",
600
+ send("request", {
314
601
  id,
315
602
  route: message.route,
316
603
  payload: message.payload
@@ -334,8 +621,8 @@ var inlineBootstrap = `(() => {
334
621
  signal?.addEventListener("abort", abort, { once: true });
335
622
  if (sessionId)
336
623
  entry.start();
337
- else
338
- send({ type: "bridge:ready" });
624
+ else if (mode === "url" || token)
625
+ send("ready", {});
339
626
  });
340
627
  }
341
628
  const messages = createMessageClient({ "data:query": defineDataQueryRoute(), "data:sql": sqlQueryRoute }, requestMessage);
@@ -343,20 +630,22 @@ var inlineBootstrap = `(() => {
343
630
  return messages.request("data:query", { operation, input }, { signal }).catch(rethrowDataMessageError);
344
631
  }
345
632
  function disconnect() {
346
- send({ type: "bridge:disconnect" });
633
+ if (sessionId)
634
+ send("disconnect", {});
347
635
  sessionId = undefined;
348
636
  for (const entry of pending.values())
349
637
  entry.reject(new DataAppError("The preview has closed.", "bridge_closed"));
350
638
  }
351
639
  function resume(event) {
352
- if (event.persisted)
353
- send({ type: "bridge:ready" });
640
+ if (event.persisted && (mode === "url" || token))
641
+ send("ready", {});
354
642
  }
355
643
  function dispose() {
356
644
  if (disposed)
357
645
  return;
358
646
  disconnect();
359
647
  disposed = true;
648
+ endpoint.dispose();
360
649
  frame.removeEventListener("message", receive);
361
650
  frame.removeEventListener("pagehide", disconnect);
362
651
  frame.removeEventListener("pageshow", resume);
@@ -365,8 +654,9 @@ var inlineBootstrap = `(() => {
365
654
  frame.addEventListener("pagehide", disconnect);
366
655
  frame.addEventListener("pageshow", resume);
367
656
  if (mode === "url")
368
- send({ type: "bridge:ready" });
657
+ send("ready", {});
369
658
  return {
659
+ logger,
370
660
  request: requestMessage,
371
661
  transport: queryOperation,
372
662
  lakehouse: {
@@ -386,10 +676,12 @@ var inlineBootstrap = `(() => {
386
676
  };
387
677
  },
388
678
  ready() {
389
- send({ type: "runtime:ready" });
679
+ if (!disposed && sessionId)
680
+ send("runtimeReady", {});
390
681
  },
391
682
  fail() {
392
- send({ type: "runtime:error" });
683
+ if (!disposed && sessionId)
684
+ send("runtimeError", {});
393
685
  }
394
686
  };
395
687
  }
@@ -417,6 +709,7 @@ var inlineBootstrap = `(() => {
417
709
  }) {
418
710
  let loaded;
419
711
  let failed = false;
712
+ const bundleSource = \`altertable-data-app-\${crypto.randomUUID()}.js\`;
420
713
  const bridge = createIframeTransport({
421
714
  parentOrigin,
422
715
  window: frame,
@@ -435,7 +728,8 @@ var inlineBootstrap = `(() => {
435
728
  loaded = javascript;
436
729
  try {
437
730
  const script = frame.document.createElement("script");
438
- script.textContent = javascript;
731
+ script.textContent = \`\${javascript}
732
+ //# sourceURL=\${bundleSource}\`;
439
733
  frame.document.body.append(script);
440
734
  if (!failed)
441
735
  bridge.ready();
@@ -448,12 +742,20 @@ var inlineBootstrap = `(() => {
448
742
  failed = true;
449
743
  bridge.fail();
450
744
  }
451
- frame.addEventListener("error", reportError);
452
- frame.addEventListener("unhandledrejection", reportError);
745
+ function reportBundleError(event) {
746
+ if (event.filename === bundleSource)
747
+ reportError();
748
+ }
749
+ function reportBundleRejection(event) {
750
+ if (event.reason instanceof Error && event.reason.stack?.includes(bundleSource))
751
+ reportError();
752
+ }
753
+ frame.addEventListener("error", reportBundleError);
754
+ frame.addEventListener("unhandledrejection", reportBundleRejection);
453
755
  const uninstall = installDataAppTransport(bridge, frame);
454
756
  return () => {
455
- frame.removeEventListener("error", reportError);
456
- frame.removeEventListener("unhandledrejection", reportError);
757
+ frame.removeEventListener("error", reportBundleError);
758
+ frame.removeEventListener("unhandledrejection", reportBundleRejection);
457
759
  uninstall();
458
760
  };
459
761
  }
@@ -1,7 +1,9 @@
1
1
  # Author a data app
2
2
 
3
3
  Build an exploration that answers the user's question and a story that presents
4
- its strongest findings. Both use the same queries, definitions, and evidence.
4
+ its strongest findings. Include CSV export so readers can take the displayed
5
+ results into their own tools. Exploration, export, and story use the same
6
+ queries, definitions, and evidence.
5
7
 
6
8
  ## Inspect the data
7
9
 
@@ -25,39 +27,71 @@ readers can inspect the source of each claim.
25
27
 
26
28
  Connect visualizations with introductions and explanations. Use `<TextWidget>`
27
29
  for a narrative panel with the standard widget frame, or `<TextContent>` for
28
- borderless prose. Bind claims to `result.select((data, input) => ...)` so their
29
- values and scope follow the displayed results through filter changes, refresh,
30
- and failure.
31
-
32
- | Task | Documentation |
33
- | ------------------------------------------ | ---------------------------------------------------------- |
34
- | Define queries, inputs, and result parsing | [Operations](contract.md) |
35
- | Build views, filters, and request states | [React](react.md) |
36
- | Introduce and explain visualizations | [Narrative text](react.md#narrative-text) |
37
- | Find formatters and presentation helpers | [App helpers](react.md#reuse-app-helpers) |
38
- | Register source names | [Source identifiers](react.md#register-source-identifiers) |
39
- | Choose date and field filters | [Filter variables](react.md#time-views-and-field-filters) |
40
- | Handle refresh and stale results | [Displayed results](react.md#preserve-displayed-results) |
41
- | Bind definitions and source evidence | [Data context](react.md#bind-evidence) |
42
-
43
- Use the exported types for configuration, appearance, formatting, and component
44
- options. Declare configuration with `satisfies DataAppConfig` so appearance
45
- fields and values are checked before bundling.
46
-
47
- ## Present the findings
48
-
49
- Compose a [story](react.md#present-data-with-stories) from the exploration's
50
- findings. Lead with the answer, then show the evidence and comparisons that
51
- explain it. Select the findings that matter to the audience; do not turn every
52
- row or chart into a step.
30
+ borderless prose. Render static titles, descriptions, and instructions immediately. Use metric and dataset
31
+ bindings for dynamic values and `<DataValue>` for values within static prose.
32
+ Skeletonize only the content that needs data. Reuse bindings and the displayed
33
+ source in narrative so values, formatting, and evidence follow filter changes,
34
+ refresh, and failure.
35
+
36
+ | Task | Documentation |
37
+ | ------------------------------------------------ | ----------------------------------------------------------------- |
38
+ | Choose components, CSS tokens, and styling hooks | [Styling](styling.md) |
39
+ | Define queries, inputs, and result parsing | [Operations](contract.md) |
40
+ | Build views, filters, and request states | [Views](views.md) |
41
+ | Introduce and explain visualizations | [Narrative text](widgets.md#narrative-text) |
42
+ | Find formatters and presentation helpers | [App helpers](formatting-and-appearance.md) |
43
+ | Register source names | [Source identifiers](data-context.md#register-source-identifiers) |
44
+ | Choose date and field filters | [Filter variables](variables.md) |
45
+ | Handle refresh and stale results | [Displayed results](views.md#preserve-displayed-results) |
46
+ | Export displayed data as CSV | [CSV export](stories-and-export.md#export-displayed-data-as-csv) |
47
+ | Render widgets and custom visuals | [Widgets](widgets.md) |
48
+ | Bind definitions and source evidence | [Data context](data-context.md#bind-evidence) |
49
+
50
+ Declare reusable [datasets and metrics](widgets.md#declare-datasets-and-metrics)
51
+ on the view so tables, exports, and stories share values and evidence.
52
+
53
+ Every data app supplies a [story and CSV export](stories-and-export.md)
54
+ from the displayed result. Export all distinct datasets at their displayed grain;
55
+ reuse a dataset when several visuals derive from the same rows. Present the
56
+ findings that answer the reader's question, rather than every row or chart.
57
+ Use the [standard layout](layout.md) and built-in toolbar actions.
58
+
59
+ ### Organize the exploration
60
+
61
+ Use sections for a focused question and for findings readers should compare
62
+ side by side. Add app-level `<Tabs>` from `/react/ui` when the exploration has
63
+ distinct analytical questions, such as Overview, Retention, and Segments, each
64
+ with its own context and group of widgets. Lead with the most useful overview
65
+ and label tabs by the question or subject they explore.
66
+
67
+ Use `<VisualizationWidget>`'s `views` for alternate representations of the same
68
+ dataset, such as a chart and its rows. Keep these choices within the widget;
69
+ use filter variables when the reader is changing the data scope.
70
+
71
+ Navigation tabs, widget views, and declared data views have different roles.
72
+ A declared view owns inputs, requests, and displayed results; a tab does not
73
+ require a separate data view. Keep related datasets in one view for a coherent
74
+ snapshot. Use independent views and `<DataSection>` boundaries when content
75
+ needs separate requests. Keep filter placement consistent across tabs and make
76
+ each filter's scope clear. Derive findings, story, and export from the displayed
77
+ results and their inputs.
78
+
79
+ Choose each visual for the question it answers; see
80
+ [visualization selection](widgets.md#choose-a-visualization).
53
81
 
54
82
  ## Verify the app
55
83
 
84
+ Follow the [UI quality contract](ui-quality.md) for hierarchy, responsive
85
+ composition, typography, and control states.
86
+
56
87
  Verify findings against the source and the user's question. Distinguish measured
57
88
  zero, unavailable values, and empty results. Check filters, refresh, loading,
58
- empty, error, and stale states, then present the story. Inspect both experiences
89
+ empty, error, and stale states, then present the story. Download CSV from the
90
+ standalone and embedded toolbar and verify its filename, columns, raw values,
91
+ and filter scope against the displayed result. Export and Present story must be
92
+ available once results are shown; initial loading, empty, and initial
93
+ errors keep both actions visible and disabled. Inspect both experiences
59
94
  at phone and desktop widths in light and dark themes.
60
95
 
61
- The app owns its queries, result parsing, business definitions, configuration,
62
- and presentation. Credentials, authorization, and enforced access/query limits
63
- stay backend-owned. Edit app-owned files; installed package files are dependencies.
96
+ Keep credentials, authorization, and enforced query limits in the host/backend.
97
+ Edit app-owned files; installed package files are dependencies.