@brianmsm/quarto-bento 0.0.0-stage → 0.1.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 (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -2
  3. package/bin/qbento-npm.mjs +62 -0
  4. package/distribution/LICENSE +21 -0
  5. package/distribution/PACKAGING.md +59 -0
  6. package/distribution/bin/qbento +5 -0
  7. package/distribution/bin/qbento.cmd +4 -0
  8. package/distribution/manifest.json +720 -0
  9. package/distribution/runtime/LICENSE +21 -0
  10. package/distribution/runtime/_extensions/qbento/_extension.yml +24 -0
  11. package/distribution/runtime/_extensions/qbento/capture.lua +21 -0
  12. package/distribution/runtime/_extensions/qbento/guard.lua +7 -0
  13. package/distribution/runtime/_extensions/qbento/post.ts +2 -0
  14. package/distribution/runtime/_extensions/qbento/pre.ts +2 -0
  15. package/distribution/runtime/_extensions/qbento/producer-activation.js +129 -0
  16. package/distribution/runtime/src/bridge/browser.js +168 -0
  17. package/distribution/runtime/src/bridge/isolation.ts +55 -0
  18. package/distribution/runtime/src/browser/chromium.ts +218 -0
  19. package/distribution/runtime/src/cli.ts +219 -0
  20. package/distribution/runtime/src/core/agent.ts +565 -0
  21. package/distribution/runtime/src/core/annotations.ts +384 -0
  22. package/distribution/runtime/src/core/capture.ts +203 -0
  23. package/distribution/runtime/src/core/component-actions.ts +550 -0
  24. package/distribution/runtime/src/core/component-frames.ts +59 -0
  25. package/distribution/runtime/src/core/component-sync.ts +290 -0
  26. package/distribution/runtime/src/core/design-diagnostics.ts +78 -0
  27. package/distribution/runtime/src/core/design-registry.ts +287 -0
  28. package/distribution/runtime/src/core/graph-relations.ts +354 -0
  29. package/distribution/runtime/src/core/graph-scope.ts +63 -0
  30. package/distribution/runtime/src/core/graph-sync.ts +228 -0
  31. package/distribution/runtime/src/core/io.ts +27 -0
  32. package/distribution/runtime/src/core/kpi-appearance.ts +205 -0
  33. package/distribution/runtime/src/core/layouts.ts +245 -0
  34. package/distribution/runtime/src/core/project.ts +759 -0
  35. package/distribution/runtime/src/core/reconcile.ts +384 -0
  36. package/distribution/runtime/src/core/recovery.ts +1255 -0
  37. package/distribution/runtime/src/core/slide-actions.ts +158 -0
  38. package/distribution/runtime/src/core/source-scope.ts +50 -0
  39. package/distribution/runtime/src/core/sync.ts +2423 -0
  40. package/distribution/runtime/src/core/widgets.ts +300 -0
  41. package/distribution/runtime/src/integration/install.ts +98 -0
  42. package/distribution/runtime/src/integration/invocation.ts +100 -0
  43. package/distribution/runtime/src/integration/quarto.ts +457 -0
  44. package/distribution/runtime/src/render/assets.ts +66 -0
  45. package/distribution/runtime/src/render/capture.lua +8 -0
  46. package/distribution/runtime/src/render/chart.ts +165 -0
  47. package/distribution/runtime/src/render/components.ts +199 -0
  48. package/distribution/runtime/src/render/design.ts +820 -0
  49. package/distribution/runtime/src/render/diagnostics.ts +36 -0
  50. package/distribution/runtime/src/render/graphs.ts +747 -0
  51. package/distribution/runtime/src/render/html.ts +82 -0
  52. package/distribution/runtime/src/render/images.ts +268 -0
  53. package/distribution/runtime/src/render/inline.ts +259 -0
  54. package/distribution/runtime/src/render/kpi.ts +385 -0
  55. package/distribution/runtime/src/render/lists.ts +183 -0
  56. package/distribution/runtime/src/render/math.ts +413 -0
  57. package/distribution/runtime/src/render/measure.ts +185 -0
  58. package/distribution/runtime/src/render/native-layout.ts +174 -0
  59. package/distribution/runtime/src/render/producer-capture.ts +319 -0
  60. package/distribution/runtime/src/render/producer-environment.R +49 -0
  61. package/distribution/runtime/src/render/producer-environment.ts +96 -0
  62. package/distribution/runtime/src/render/producer-profile.R +91 -0
  63. package/distribution/runtime/src/render/producer-site-profile.R +18 -0
  64. package/distribution/runtime/src/render/project.ts +1806 -0
  65. package/distribution/runtime/src/render/quarto.ts +127 -0
  66. package/distribution/runtime/src/render/registry.ts +233 -0
  67. package/distribution/runtime/src/render/resources.ts +292 -0
  68. package/distribution/runtime/src/render/shell.ts +110 -0
  69. package/distribution/runtime/src/render/source-inline.ts +169 -0
  70. package/distribution/runtime/src/render/table.ts +220 -0
  71. package/distribution/runtime/src/render/text-overflow.ts +208 -0
  72. package/distribution/runtime/src/render/types.ts +71 -0
  73. package/distribution/runtime/src/render/validate.ts +572 -0
  74. package/distribution/runtime/src/rich/project.ts +117 -0
  75. package/distribution/runtime/src/rich/table.ts +374 -0
  76. package/distribution/runtime/src/source/annotations.ts +490 -0
  77. package/distribution/runtime/src/source/attributes.ts +415 -0
  78. package/distribution/runtime/src/source/chart.ts +165 -0
  79. package/distribution/runtime/src/source/code-language.ts +277 -0
  80. package/distribution/runtime/src/source/component-insertion.ts +160 -0
  81. package/distribution/runtime/src/source/deletion.ts +278 -0
  82. package/distribution/runtime/src/source/graphs.ts +517 -0
  83. package/distribution/runtime/src/source/identities.ts +256 -0
  84. package/distribution/runtime/src/source/identity-id.ts +6 -0
  85. package/distribution/runtime/src/source/image.ts +50 -0
  86. package/distribution/runtime/src/source/kpi.ts +196 -0
  87. package/distribution/runtime/src/source/mod.ts +876 -0
  88. package/distribution/runtime/src/source/notes.ts +308 -0
  89. package/distribution/runtime/src/source/structure.ts +647 -0
  90. package/distribution/runtime/src/source/table.ts +290 -0
  91. package/distribution/runtime/src/source/text-conversion.ts +146 -0
  92. package/distribution/runtime/src/state/components.ts +281 -0
  93. package/distribution/runtime/src/state/graphs.ts +200 -0
  94. package/distribution/runtime/src/state/json.ts +162 -0
  95. package/distribution/runtime/src/state/merge.ts +1034 -0
  96. package/distribution/runtime/src/state/mod.ts +5 -0
  97. package/distribution/runtime/src/state/store.ts +226 -0
  98. package/distribution/runtime/src/state/types.ts +151 -0
  99. package/distribution/runtime/src/studio/client.js +790 -0
  100. package/distribution/runtime/src/studio/components.js +188 -0
  101. package/distribution/runtime/src/studio/index.html +278 -0
  102. package/distribution/runtime/src/studio/layouts.js +170 -0
  103. package/distribution/runtime/src/studio/registry.js +283 -0
  104. package/distribution/runtime/src/studio/relations.js +169 -0
  105. package/distribution/runtime/src/studio/server.ts +433 -0
  106. package/distribution/runtime/src/widgets/deck.ts +39 -0
  107. package/distribution/runtime/src/widgets/plotly.ts +354 -0
  108. package/distribution/runtime/src/widgets/presenter.ts +367 -0
  109. package/distribution/runtime/src/widgets/snapshot.ts +116 -0
  110. package/distribution/runtime/vendor/bento/FONT_NOTICES.md +49 -0
  111. package/distribution/runtime/vendor/bento/LICENSE +21 -0
  112. package/distribution/runtime/vendor/bento/THIRD_PARTY_NOTICES.md +51 -0
  113. package/distribution/runtime/vendor/bento/font-licenses/fraunces-OFL.txt +93 -0
  114. package/distribution/runtime/vendor/bento/font-licenses/instrument-sans-OFL.txt +93 -0
  115. package/distribution/runtime/vendor/bento/pin.json +69 -0
  116. package/distribution/runtime/vendor/bento/provenance/slides.package-lock.json +1928 -0
  117. package/distribution/runtime/vendor/bento/provenance/slides.package.json +28 -0
  118. package/distribution/runtime/vendor/bento/runtime-dependencies.json +244 -0
  119. package/distribution/runtime/vendor/bento/runtime-licenses.json +4038 -0
  120. package/distribution/runtime/vendor/bento/shell.html +269 -0
  121. package/distribution/runtime/vendor/bento/slides.schema.json +2379 -0
  122. package/package.json +40 -4
@@ -0,0 +1,790 @@
1
+ import { attachLayoutEditor } from "/layouts.js";
2
+ import { attachComponentEditor } from "/components.js";
3
+ import { attachGraphRelations } from "/relations.js";
4
+ import { attachRegistryEditor } from "/registry.js";
5
+ import { createBentoBridge } from "/bridge.js";
6
+ const $ = (id) => document.getElementById(id);
7
+ let state, bridge, syncPlan, reconcilePlan, busy = false, previewGeneration = 0;
8
+ let diagnosticGeneration = 0, projectDiagnostics = [], renderDiagnostics = [];
9
+ let renderDiagnosticSnapshot;
10
+ let connectedApi, connectedSnapshot;
11
+ async function api(path, body) {
12
+ const response = await fetch("/api/" + path, {
13
+ method: body === undefined ? "GET" : "POST",
14
+ headers: {
15
+ "x-qbento-token": globalThis.qbentoToken,
16
+ "Content-Type": "application/json",
17
+ },
18
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
19
+ });
20
+ const result = await response.json();
21
+ if (!response.ok) throw new Error(result.message || result.error);
22
+ if (
23
+ body !== undefined &&
24
+ [
25
+ "save",
26
+ "design-registry",
27
+ "sync/apply",
28
+ "reconcile/apply",
29
+ "slides/action",
30
+ "graphs/relations",
31
+ "components/action",
32
+ "layouts/apply",
33
+ ].includes(path)
34
+ ) {
35
+ void refreshDiagnostics();
36
+ }
37
+ return result;
38
+ }
39
+ function showDiagnostics(diagnostics) {
40
+ // Bounds are freshly checked against saved state so corrected boxes clear warnings.
41
+ renderDiagnostics = renderDiagnostics.filter((d) =>
42
+ ![
43
+ "OVERFLOW",
44
+ "TEXT_OVERFLOW",
45
+ "TEXT_MEASUREMENT_UNAVAILABLE",
46
+ "NATIVE_CONTENT_OVERFLOW",
47
+ "NATIVE_CONTENT_UNMEASURED",
48
+ ].includes(d.code)
49
+ );
50
+ projectDiagnostics = diagnostics;
51
+ $("diagnostic-list").replaceChildren();
52
+ const visible = [...renderDiagnostics, ...diagnostics];
53
+ for (const diagnostic of visible) {
54
+ const item = document.createElement("li");
55
+ item.textContent = `${diagnostic.code}${
56
+ diagnostic.slideId ? ` · ${diagnostic.slideId}` : ""
57
+ }${diagnostic.elementId ? ` · ${diagnostic.elementId}` : ""}${
58
+ diagnostic.reference ? ` · ${diagnostic.reference}` : ""
59
+ }: ${diagnostic.message}`;
60
+ $("diagnostic-list").append(item);
61
+ }
62
+ $("project-diagnostics").hidden = visible.length === 0;
63
+ }
64
+ function showRenderDiagnostics(receipt) {
65
+ // Design and content bounds can change after Save without source execution.
66
+ // Only projection-specific warnings remain until a new render/snapshot.
67
+ const isFresh = (diagnostic) =>
68
+ [
69
+ "MISSING_DESIGN",
70
+ "UNVERIFIABLE_DESIGN_ANNOTATION",
71
+ "OVERFLOW",
72
+ "TEXT_OVERFLOW",
73
+ "TEXT_MEASUREMENT_UNAVAILABLE",
74
+ "NATIVE_CONTENT_OVERFLOW",
75
+ "NATIVE_CONTENT_UNMEASURED",
76
+ ].includes(
77
+ diagnostic.code,
78
+ );
79
+ const diagnostics = receipt.diagnostics ?? [];
80
+ renderDiagnostics = diagnostics.filter((diagnostic) => !isFresh(diagnostic));
81
+ renderDiagnosticSnapshot = { docId: state.docId, revision: receipt.revision };
82
+ ++diagnosticGeneration;
83
+ showDiagnostics(diagnostics.filter(isFresh));
84
+ }
85
+ async function refreshDiagnostics() {
86
+ const generation = ++diagnosticGeneration;
87
+ try {
88
+ const diagnostics = await api("diagnostics");
89
+ if (generation === diagnosticGeneration) showDiagnostics(diagnostics);
90
+ } catch (error) {
91
+ if (generation === diagnosticGeneration) {
92
+ showDiagnostics([
93
+ ...projectDiagnostics.filter((d) =>
94
+ d.code !== "DIAGNOSTICS_UNAVAILABLE"
95
+ ),
96
+ {
97
+ code: "DIAGNOSTICS_UNAVAILABLE",
98
+ message:
99
+ `No se pudo comprobar el diseño del proyecto: ${error.message}`,
100
+ },
101
+ ]);
102
+ }
103
+ }
104
+ }
105
+ function status(text) {
106
+ $("status").textContent = text;
107
+ }
108
+ function showError(error) {
109
+ status(error.message);
110
+ }
111
+ function setBusy(value) {
112
+ busy = value;
113
+ $("busy").hidden = !value;
114
+ $("editor").inert = value;
115
+ for (
116
+ const id of [
117
+ "save",
118
+ "slides",
119
+ "relations",
120
+ "components",
121
+ "designs",
122
+ "layouts",
123
+ "review",
124
+ "reconcile",
125
+ "render",
126
+ "presenter",
127
+ ]
128
+ ) {
129
+ $(id).disabled = value;
130
+ }
131
+ if (value) {
132
+ $("editor").contentDocument?.activeElement?.blur();
133
+ $("render").focus();
134
+ }
135
+ }
136
+ function connect() {
137
+ try {
138
+ const editor = $("editor").contentWindow;
139
+ if (!editor.bento) {
140
+ setTimeout(connect, 150);
141
+ return;
142
+ }
143
+ // The iframe and its save revision must describe the same immutable snapshot.
144
+ // A later /api/state fetch can already belong to another editor's save.
145
+ const snapshot = editor.__qbentoSnapshot;
146
+ if (!snapshot || !Number.isSafeInteger(snapshot.revision)) {
147
+ throw new Error(
148
+ "El editor no tiene una revisión verificable. Recarga el proyecto.",
149
+ );
150
+ }
151
+ // The initial iframe load may finish before this module's imports load.
152
+ // Poll/load duplicates must not reset a revision already advanced by Save.
153
+ if (connectedApi === editor.bento && connectedSnapshot === snapshot) return;
154
+ if (
155
+ renderDiagnosticSnapshot &&
156
+ (renderDiagnosticSnapshot.docId !== snapshot.docId ||
157
+ renderDiagnosticSnapshot.revision !== snapshot.revision)
158
+ ) {
159
+ renderDiagnostics = [];
160
+ renderDiagnosticSnapshot = undefined;
161
+ showDiagnostics(projectDiagnostics);
162
+ }
163
+ state = {
164
+ docId: snapshot.docId,
165
+ revision: snapshot.revision,
166
+ pending: snapshot.pending,
167
+ };
168
+ const oldBridge = bridge;
169
+ bridge = createBentoBridge(editor.bento, {
170
+ docId: state.docId,
171
+ persist: async ({ doc }) => {
172
+ const receipt = await api("save", { doc, revision: state.revision });
173
+ state.revision = receipt.revision;
174
+ return receipt;
175
+ },
176
+ onEvent: (event) => {
177
+ if (event.type === "document" && !busy) status("Cambios sin guardar");
178
+ if (event.type === "saved" && !busy) {
179
+ status(
180
+ event.dirty
181
+ ? "Hay cambios nuevos sin guardar"
182
+ : "Proyecto guardado",
183
+ );
184
+ }
185
+ if (event.type === "error" || event.type === "save-error") {
186
+ showError(event.error);
187
+ }
188
+ },
189
+ });
190
+ oldBridge?.dispose();
191
+ connectedApi = editor.bento;
192
+ connectedSnapshot = snapshot;
193
+ void refreshDiagnostics();
194
+ setBusy(false);
195
+ status(
196
+ state.pending
197
+ ? `${state.pending} cambios pendientes de sincronización`
198
+ : "Proyecto abierto",
199
+ );
200
+ } catch (error) {
201
+ setBusy(false);
202
+ showError(error);
203
+ }
204
+ }
205
+ $("editor").addEventListener("load", connect);
206
+ $("save").onclick = async () => {
207
+ try {
208
+ if (!busy) await bridge.save();
209
+ } catch (error) {
210
+ showError(error);
211
+ }
212
+ };
213
+ function showPatches(plan) {
214
+ $("patches").replaceChildren();
215
+ function showPatch(patch, phase) {
216
+ const section = document.createElement("section"),
217
+ title = document.createElement("h3"),
218
+ before = document.createElement("pre"),
219
+ after = document.createElement("pre");
220
+ title.textContent = `${phase} · ${patch.blockId}`;
221
+ before.textContent = patch.expected;
222
+ before.className = "before";
223
+ after.textContent = patch.replacement;
224
+ after.className = "after";
225
+ section.append(title, before, after);
226
+ $("patches").append(section);
227
+ }
228
+ if (plan.identities?.patches.length) {
229
+ const explanation = document.createElement("p");
230
+ explanation.textContent =
231
+ "Primero se fijan las identidades de los bloques siguientes, incluidos los que no has editado, para conservar su vínculo con QMD al cambiar el archivo. Revisa cada fragmento antes y después; a continuación se aplican los cambios de contenido.";
232
+ $("patches").append(explanation);
233
+ for (const patch of plan.identities.patches) {
234
+ showPatch(patch, "1. Identidad de fuente");
235
+ }
236
+ }
237
+ for (const patch of plan.patch.patches) showPatch(patch, "2. Contenido");
238
+ for (const patch of plan.notes?.patches ?? []) {
239
+ showPatch(patch, "3. Notas de orador");
240
+ }
241
+ for (
242
+ const change of plan.changes.filter((change) =>
243
+ change.kind === "visual-attribute"
244
+ )
245
+ ) {
246
+ showPatch({
247
+ blockId: change.blockId,
248
+ expected: change.before,
249
+ replacement: change.after,
250
+ }, "4. Atributo visual");
251
+ }
252
+ for (const patch of plan.languages?.patches ?? []) {
253
+ showPatch(patch, "Lenguaje del código");
254
+ }
255
+ if (plan.structure) {
256
+ for (const step of plan.structure.steps) {
257
+ for (const edit of step.edits) {
258
+ const section = document.createElement("section"),
259
+ title = document.createElement("h3"),
260
+ before = document.createElement("pre"),
261
+ after = document.createElement("pre");
262
+ title.textContent = step.operation.type === "delete-slide"
263
+ ? `Eliminar ${step.operation.id} del QMD`
264
+ : step.operation.type === "delete-block"
265
+ ? `Eliminar elemento ${step.operation.blockId} del QMD`
266
+ : `5. Estructura · ${step.operation.type}`;
267
+ before.textContent = edit.expected;
268
+ before.className = "before";
269
+ after.textContent = edit.replacement || "Se elimina este fragmento";
270
+ after.className = "after";
271
+ section.append(title, before, after);
272
+ $("patches").append(section);
273
+ }
274
+ }
275
+ }
276
+ for (const [index, patch] of (plan.graphs?.patches ?? []).entries()) {
277
+ const request = plan.graphs.requests[index];
278
+ showPatch({
279
+ ...patch,
280
+ blockId:
281
+ `${request.componentId} · ${patch.blockId} · ${request.endpoint}`,
282
+ }, "6. Relación del diagrama");
283
+ }
284
+ for (const patch of plan.annotations?.patches ?? []) {
285
+ showPatch(patch, "7. Anotación visual opcional");
286
+ }
287
+ for (const diagnostic of plan.annotations?.diagnostics ?? []) {
288
+ const section = document.createElement("section"),
289
+ title = document.createElement("h3"),
290
+ message = document.createElement("p");
291
+ title.textContent = diagnostic.code;
292
+ message.textContent = diagnostic.message;
293
+ section.append(title, message);
294
+ $("patches").append(section);
295
+ }
296
+ if (plan.structure || plan.identities || plan.graphs || plan.languages) {
297
+ const section = document.createElement("section"),
298
+ title = document.createElement("h3"),
299
+ result = document.createElement("pre");
300
+ title.textContent = "QMD completo propuesto";
301
+ result.textContent = plan.result;
302
+ section.append(title, result);
303
+ $("patches").append(section);
304
+ }
305
+ }
306
+ async function prepareSyncPreview() {
307
+ if (busy) return;
308
+ const generation = ++previewGeneration;
309
+ syncPlan = undefined;
310
+ $("apply").disabled = true;
311
+ $("patches").replaceChildren();
312
+ const boxes = [...$("delete-confirmations").querySelectorAll("input")];
313
+ if (boxes.some((box) => !box.checked)) {
314
+ $("sync-note").textContent =
315
+ "Confirma cada eliminación para preparar la propuesta. Puedes restaurar elementos desde Comparar con QMD y diapositivas desde Diapositivas.";
316
+ return;
317
+ }
318
+ $("sync-note").textContent = "Preparando los cambios propuestos…";
319
+ try {
320
+ const plan = await api("sync/preview", {
321
+ confirmedDeleteIds: boxes.map((box) => box.dataset.change),
322
+ });
323
+ if (generation !== previewGeneration) return;
324
+ if (plan.stateRevision !== state.revision) {
325
+ throw new Error(
326
+ "El proyecto cambió mientras se preparaba la revisión. Recarga antes de sincronizar.",
327
+ );
328
+ }
329
+ syncPlan = plan;
330
+ showPatches(syncPlan);
331
+ const hasChanges = syncPlan.patch.patches.length ||
332
+ syncPlan.identities?.patches.length ||
333
+ syncPlan.attributes?.patches.length || syncPlan.notes?.patches.length ||
334
+ syncPlan.graphs?.patches.length ||
335
+ syncPlan.annotations?.patches.length ||
336
+ syncPlan.structure;
337
+ $("sync-note").textContent = hasChanges
338
+ ? "Revisa los cambios propuestos. Aplicar modifica los fragmentos y la estructura indicados del QMD."
339
+ : syncPlan.annotations?.diagnostics.length
340
+ ? "Las anotaciones opcionales se han omitido. Revisa los diagnósticos mostrados; el QMD se conserva."
341
+ : "No hay cambios compatibles pendientes de sincronización.";
342
+ $("apply").disabled = !hasChanges;
343
+ } catch (error) {
344
+ if (generation === previewGeneration) {
345
+ $("sync-note").textContent = error.message;
346
+ }
347
+ }
348
+ }
349
+ $("review").onclick = async () => {
350
+ if (busy) return;
351
+ try {
352
+ await bridge.save();
353
+ const saved = await api("state");
354
+ if (saved.revision !== state.revision) {
355
+ throw new Error(
356
+ "Otra pestaña ha cambiado el proyecto. Conserva tus cambios y recarga antes de sincronizar.",
357
+ );
358
+ }
359
+ syncPlan = undefined;
360
+ $("apply").disabled = true;
361
+ $("patches").replaceChildren();
362
+ $("delete-confirmations").replaceChildren();
363
+ for (
364
+ const change of saved.pending.filter((item) =>
365
+ item.kind === "delete-slide" || item.kind === "delete-element"
366
+ )
367
+ ) {
368
+ const label = document.createElement("label"),
369
+ checkbox = document.createElement("input");
370
+ checkbox.type = "checkbox";
371
+ checkbox.dataset.change = change.id;
372
+ checkbox.onchange = prepareSyncPreview;
373
+ label.append(
374
+ checkbox,
375
+ document.createTextNode(
376
+ change.kind === "delete-element"
377
+ ? ` Confirmo eliminar el elemento ${change.target.elementId} de la diapositiva ${change.target.slideId} y su bloque del QMD`
378
+ : ` Confirmo eliminar la diapositiva ${change.target.slideId} y su contenido del QMD`,
379
+ ),
380
+ );
381
+ $("delete-confirmations").append(label);
382
+ }
383
+ showChanges($("changes"), saved.pending.map((change) => ({ change })));
384
+ $("review-dialog").showModal();
385
+ await prepareSyncPreview();
386
+ } catch (error) {
387
+ showError(error);
388
+ }
389
+ };
390
+ $("apply").onclick = async () => {
391
+ if (!syncPlan || busy) return;
392
+ if (
393
+ [...$("delete-confirmations").querySelectorAll("input")].some((box) =>
394
+ !box.checked
395
+ )
396
+ ) return;
397
+ setBusy(true);
398
+ $("busy").textContent = "Aplicando los cambios revisados al QMD…";
399
+ $("sync-note").textContent = $("busy").textContent;
400
+ $("apply").disabled = true;
401
+ $("close").disabled = true;
402
+ $("delete-confirmations").inert = true;
403
+ previewGeneration++;
404
+ try {
405
+ const receipt = await api("sync/apply", syncPlan);
406
+ state.revision = receipt.revision;
407
+ syncPlan = undefined;
408
+ $("sync-note").textContent = receipt.annotations?.diagnostics.length
409
+ ? "Cambios semánticos aplicados al QMD; las anotaciones opcionales se han omitido por los diagnósticos revisados. Recalcula para actualizar la presentación."
410
+ : "Cambios aplicados al QMD. Recalcula para actualizar la presentación.";
411
+ status("QMD sincronizado; listo para recalcular");
412
+ } catch (error) {
413
+ $("sync-note").textContent = error.message;
414
+ } finally {
415
+ setBusy(false);
416
+ $("close").disabled = false;
417
+ $("delete-confirmations").inert = false;
418
+ $("apply").disabled = !syncPlan;
419
+ }
420
+ };
421
+ $("close").onclick = () => {
422
+ if (!busy) $("review-dialog").close();
423
+ };
424
+ $("review-dialog").addEventListener("cancel", (event) => {
425
+ if (busy) event.preventDefault();
426
+ });
427
+ $("review-dialog").addEventListener("close", () => {
428
+ previewGeneration++;
429
+ syncPlan = undefined;
430
+ });
431
+ function reloadEditor() {
432
+ $("editor").contentWindow.__qbentoManagedReload = true;
433
+ $("editor").src = "/deck?revision=" + Date.now();
434
+ }
435
+ async function runSlideAction(action, slideId) {
436
+ if (busy) return;
437
+ setBusy(true);
438
+ $("busy").textContent = "Guardando cambios de diapositivas…";
439
+ try {
440
+ await bridge.save();
441
+ const receipt = await api("slides/action", {
442
+ action,
443
+ slideId,
444
+ revision: state.revision,
445
+ expectedDocId: state.docId,
446
+ });
447
+ state.revision = receipt.revision;
448
+ $("slides-dialog").close();
449
+ reloadEditor();
450
+ } catch (error) {
451
+ $("slides-note").textContent = error.message;
452
+ setBusy(false);
453
+ }
454
+ }
455
+ function slideLabel(slide) {
456
+ const title = slide.name || slide.title ||
457
+ slide.elements.find((element) => element.type === "text")?.html || "";
458
+ return `${slide.id}${
459
+ title ? " · " + String(title).replace(/<[^>]*>/g, " ") : ""
460
+ }`;
461
+ }
462
+ $("slides").onclick = async () => {
463
+ if (busy) return;
464
+ setBusy(true);
465
+ $("busy").textContent = "Guardando antes de revisar las diapositivas…";
466
+ try {
467
+ await bridge.save();
468
+ const saved = await api("state");
469
+ if (saved.revision !== state.revision) {
470
+ throw new Error(
471
+ "Otra pestaña cambió el proyecto. Recarga antes de cambiar diapositivas.",
472
+ );
473
+ }
474
+ $("slides-note").textContent = "";
475
+ $("slide-list").replaceChildren();
476
+ const row = (slide, description, actions) => {
477
+ const section = document.createElement("section"),
478
+ title = document.createElement("h3"),
479
+ detail = document.createElement("p");
480
+ title.textContent = slideLabel(slide);
481
+ detail.textContent = description;
482
+ section.append(title, detail);
483
+ for (const [action, text, disabled] of actions) {
484
+ const button = document.createElement("button");
485
+ button.textContent = text;
486
+ button.disabled = !!disabled;
487
+ button.onclick = () => runSlideAction(action, slide.id);
488
+ section.append(button);
489
+ }
490
+ $("slide-list").append(section);
491
+ };
492
+ for (const slide of saved.emitted.slides) {
493
+ const sourced = saved.generatedBaseline.slides.some((item) =>
494
+ item.id === slide.id
495
+ );
496
+ row(
497
+ slide,
498
+ (slide.hidden ? "Oculta" : "Visible") +
499
+ (!sourced ? " · Nueva: sincroniza antes de eliminarla de QMD." : ""),
500
+ [
501
+ [
502
+ slide.hidden ? "show" : "hide",
503
+ slide.hidden ? "Mostrar diapositiva" : "Ocultar diapositiva",
504
+ ],
505
+ [
506
+ "delete",
507
+ "Marcar para eliminar",
508
+ !sourced || saved.emitted.slides.length === 1,
509
+ ],
510
+ ],
511
+ );
512
+ }
513
+ for (
514
+ const change of saved.pending.filter((item) =>
515
+ item.kind === "delete-slide"
516
+ )
517
+ ) {
518
+ if (change.before.present && change.before.value?.elements) {
519
+ row(
520
+ change.before.value,
521
+ "Eliminación pendiente · QMD conservado hasta sincronizar.",
522
+ [
523
+ ["restore", "Restaurar diapositiva"],
524
+ ],
525
+ );
526
+ }
527
+ }
528
+ $("slides-dialog").showModal();
529
+ } catch (error) {
530
+ showError(error);
531
+ } finally {
532
+ setBusy(false);
533
+ }
534
+ };
535
+ $("slides-close").onclick = () => $("slides-dialog").close();
536
+ function describeValue(value) {
537
+ if (!value?.present) return "No existe";
538
+ return typeof value.value === "string"
539
+ ? value.value
540
+ : JSON.stringify(value.value, null, 2);
541
+ }
542
+ function showChanges(container, items, selectable = false) {
543
+ container.replaceChildren();
544
+ const origins = {
545
+ authored: "Contenido escrito",
546
+ computed: "Resultado calculado",
547
+ "bento-only": "Objeto añadido en Bento",
548
+ detached: "Desvinculado del cálculo",
549
+ };
550
+ const actions = {
551
+ "keep-source": "Usar QMD y descartar esta edición de Bento",
552
+ "keep-bento": "Conservar Bento para sincronizar después",
553
+ detach: "Desvincular este valor del cálculo",
554
+ };
555
+ for (const { change, actions: choices } of items) {
556
+ const section = document.createElement("section"),
557
+ title = document.createElement("h3"),
558
+ origin = document.createElement("p");
559
+ title.textContent = [
560
+ change.target.slideId || "Presentación",
561
+ change.target.elementId,
562
+ change.path.join(".") || change.kind,
563
+ ].filter(Boolean).join(" · ");
564
+ origin.textContent = `${origins[change.origin] || change.origin} · ${
565
+ change.status === "conflict" ? "Conflicto" : "Pendiente"
566
+ }`;
567
+ section.append(title, origin);
568
+ for (
569
+ const [label, value] of [["Base compartida", change.before], [
570
+ "QMD actual",
571
+ change.source ?? change.before,
572
+ ], ["Edición Bento", change.after]]
573
+ ) {
574
+ const heading = document.createElement("strong"),
575
+ content = document.createElement("pre");
576
+ heading.textContent = label;
577
+ content.textContent = describeValue(value);
578
+ section.append(heading, content);
579
+ }
580
+ if (change.reason) {
581
+ const note = document.createElement("p");
582
+ note.textContent = change.reason;
583
+ section.append(note);
584
+ }
585
+ if (selectable) {
586
+ const label = document.createElement("label"),
587
+ select = document.createElement("select");
588
+ label.textContent = "Decisión: ";
589
+ select.dataset.change = change.id;
590
+ for (const action of ["", ...choices]) {
591
+ const option = document.createElement("option");
592
+ option.value = action;
593
+ option.textContent =
594
+ change.kind === "delete-element" && action === "keep-source"
595
+ ? change.source?.present === false
596
+ ? "Descartar esta eliminación pendiente; recuperar su fuente desde historial"
597
+ : "Restaurar elemento con su texto y diseño pendientes"
598
+ : actions[action] || "Mantener pendiente";
599
+ select.append(option);
600
+ }
601
+ select.onchange = () => {
602
+ $("resolve").disabled = ![...container.querySelectorAll("select")].some(
603
+ (s) => s.value,
604
+ );
605
+ };
606
+ label.append(select);
607
+ section.append(label);
608
+ }
609
+ container.append(section);
610
+ }
611
+ }
612
+ $("reconcile").onclick = async () => {
613
+ if (busy) return;
614
+ setBusy(true);
615
+ $("busy").textContent = "Ejecutando QMD para comparar sus resultados…";
616
+ try {
617
+ await bridge.save();
618
+ reconcilePlan = await api("reconcile/preview", {});
619
+ if (reconcilePlan.stateRevision !== state.revision) {
620
+ reconcilePlan = undefined;
621
+ throw new Error(
622
+ "Otra pestaña cambió el proyecto. Recarga antes de elegir valores.",
623
+ );
624
+ }
625
+ showChanges($("reconcile-changes"), reconcilePlan.changes, true);
626
+ $("reconcile-note").textContent = reconcilePlan.changes.length
627
+ ? "No se ha aplicado ninguna decisión."
628
+ : "No hay divergencias pendientes.";
629
+ $("resolve").disabled = true;
630
+ $("reconcile-dialog").showModal();
631
+ } catch (error) {
632
+ showError(error);
633
+ } finally {
634
+ setBusy(false);
635
+ }
636
+ };
637
+ $("resolve-close").onclick = () => $("reconcile-dialog").close();
638
+ $("resolve").onclick = async () => {
639
+ if (!reconcilePlan || busy) return;
640
+ const choices = [...$("reconcile-changes").querySelectorAll("select")].filter(
641
+ (s) => s.value,
642
+ ).map((s) => ({ changeId: s.dataset.change, action: s.value }));
643
+ setBusy(true);
644
+ $("resolve").disabled = true;
645
+ $("resolve-close").disabled = true;
646
+ $("reconcile-note").textContent =
647
+ "Verificando de nuevo los resultados antes de guardar…";
648
+ try {
649
+ const result = await api("reconcile/apply", {
650
+ plan: reconcilePlan,
651
+ choices,
652
+ });
653
+ state.revision = result.revision;
654
+ reconcilePlan = undefined;
655
+ $("reconcile-dialog").close();
656
+ $("editor").contentWindow.__qbentoManagedReload = true;
657
+ $("editor").src = "/deck?revision=" + Date.now();
658
+ } catch (error) {
659
+ $("reconcile-note").textContent = error.message;
660
+ setBusy(false);
661
+ } finally {
662
+ $("resolve-close").disabled = false;
663
+ }
664
+ };
665
+ $("render").onclick = async () => {
666
+ if (busy) return;
667
+ setBusy(true);
668
+ $("busy").textContent = "Recalculando. Tus cambios están guardados.";
669
+ try {
670
+ await bridge.save();
671
+ status("Recalculando…");
672
+ const receipt = await api("render", {});
673
+ showRenderDiagnostics(receipt);
674
+ state.revision = receipt.revision;
675
+ if (bridge.dirty) {
676
+ throw new Error(
677
+ "Aparecieron cambios durante el recálculo. Se conservan en el editor; guárdalos antes de recargar.",
678
+ );
679
+ }
680
+ // Only this confirmed saved-and-recomputed navigation bypasses upstream's stale dirty flag.
681
+ $("editor").contentWindow.__qbentoManagedReload = true;
682
+ $("editor").src = "/deck?revision=" + Date.now();
683
+ // connect disposes the old bridge only after the replacement editor exists.
684
+ } catch (error) {
685
+ setBusy(false);
686
+ showError(error);
687
+ }
688
+ };
689
+ globalThis.addEventListener("beforeunload", (event) => {
690
+ if (bridge?.dirty) {
691
+ event.preventDefault();
692
+ event.returnValue = "";
693
+ }
694
+ });
695
+
696
+ $("presenter").onclick = async () => {
697
+ if (busy) return;
698
+ try {
699
+ setBusy(true);
700
+ $("busy").textContent =
701
+ "Preparando presentación con tus cambios guardados.";
702
+ await bridge.save();
703
+ if (bridge.dirty) {
704
+ throw new Error(
705
+ "Hay cambios nuevos sin guardar. Vuelve a preparar la presentación.",
706
+ );
707
+ }
708
+ const result = await api("presenter", { revision: state.revision });
709
+ $("presenter-frame").src = result.url;
710
+ $("presenter-dialog").showModal();
711
+ status("Presentación preparada con tus cambios guardados");
712
+ } catch (error) {
713
+ showError(error);
714
+ } finally {
715
+ setBusy(false);
716
+ }
717
+ };
718
+ $("presenter-close").onclick = () => $("presenter-dialog").close();
719
+ $("presenter-dialog").onclose = () => {
720
+ $("presenter-frame").src = "about:blank";
721
+ };
722
+
723
+ const layoutEditor = attachLayoutEditor({
724
+ api,
725
+ getState: () => state,
726
+ setBusy,
727
+ showError,
728
+ reloadEditor,
729
+ saveBefore: async () => {
730
+ if (!bridge) throw new Error("Espera a que abra el editor.");
731
+ await bridge.save();
732
+ if (bridge.dirty) {
733
+ throw new Error(
734
+ "Hay cambios nuevos sin guardar; vuelve a abrir Aplicar diseño.",
735
+ );
736
+ }
737
+ },
738
+ });
739
+ // WindowProxy alone survives navigation. Bind the request to the iframe's
740
+ // immutable loaded snapshot as well, then save live edits before any changes.
741
+ globalThis.addEventListener("message", (event) => {
742
+ const frame = $("editor").contentWindow, snapshot = frame?.__qbentoSnapshot;
743
+ if (
744
+ event.source !== frame || event.origin !== location.origin || busy ||
745
+ event.data?.type !== "qbento:apply-layout" ||
746
+ event.data.docId !== snapshot?.docId ||
747
+ event.data.revision !== snapshot?.revision ||
748
+ snapshot?.docId !== state?.docId
749
+ ) return;
750
+ void layoutEditor.open();
751
+ });
752
+ attachRegistryEditor({ api, getState: () => state, showError });
753
+ attachGraphRelations({
754
+ api,
755
+ getState: () => state,
756
+ setBusy,
757
+ showError,
758
+ saveBefore: async () => {
759
+ await bridge.save();
760
+ if (bridge.dirty) {
761
+ throw new Error(
762
+ "Hay cambios nuevos sin guardar; vuelve a abrir Relaciones.",
763
+ );
764
+ }
765
+ },
766
+ reloadEditor,
767
+ });
768
+
769
+ attachComponentEditor({
770
+ api,
771
+ getState: () => state,
772
+ setBusy,
773
+ showError,
774
+ saveBefore: async () => {
775
+ await bridge.save();
776
+ if (bridge.dirty) {
777
+ throw new Error(
778
+ "Hay cambios nuevos sin guardar; vuelve a abrir Añadir componente.",
779
+ );
780
+ }
781
+ },
782
+ reloadEditor: () => {
783
+ $("editor").contentWindow.__qbentoManagedReload = true;
784
+ $("editor").src = "/deck?revision=" + Date.now();
785
+ },
786
+ });
787
+
788
+ void refreshDiagnostics();
789
+
790
+ connect();