@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.
- package/LICENSE +21 -0
- package/README.md +86 -2
- package/bin/qbento-npm.mjs +62 -0
- package/distribution/LICENSE +21 -0
- package/distribution/PACKAGING.md +59 -0
- package/distribution/bin/qbento +5 -0
- package/distribution/bin/qbento.cmd +4 -0
- package/distribution/manifest.json +720 -0
- package/distribution/runtime/LICENSE +21 -0
- package/distribution/runtime/_extensions/qbento/_extension.yml +24 -0
- package/distribution/runtime/_extensions/qbento/capture.lua +21 -0
- package/distribution/runtime/_extensions/qbento/guard.lua +7 -0
- package/distribution/runtime/_extensions/qbento/post.ts +2 -0
- package/distribution/runtime/_extensions/qbento/pre.ts +2 -0
- package/distribution/runtime/_extensions/qbento/producer-activation.js +129 -0
- package/distribution/runtime/src/bridge/browser.js +168 -0
- package/distribution/runtime/src/bridge/isolation.ts +55 -0
- package/distribution/runtime/src/browser/chromium.ts +218 -0
- package/distribution/runtime/src/cli.ts +219 -0
- package/distribution/runtime/src/core/agent.ts +565 -0
- package/distribution/runtime/src/core/annotations.ts +384 -0
- package/distribution/runtime/src/core/capture.ts +203 -0
- package/distribution/runtime/src/core/component-actions.ts +550 -0
- package/distribution/runtime/src/core/component-frames.ts +59 -0
- package/distribution/runtime/src/core/component-sync.ts +290 -0
- package/distribution/runtime/src/core/design-diagnostics.ts +78 -0
- package/distribution/runtime/src/core/design-registry.ts +287 -0
- package/distribution/runtime/src/core/graph-relations.ts +354 -0
- package/distribution/runtime/src/core/graph-scope.ts +63 -0
- package/distribution/runtime/src/core/graph-sync.ts +228 -0
- package/distribution/runtime/src/core/io.ts +27 -0
- package/distribution/runtime/src/core/kpi-appearance.ts +205 -0
- package/distribution/runtime/src/core/layouts.ts +245 -0
- package/distribution/runtime/src/core/project.ts +759 -0
- package/distribution/runtime/src/core/reconcile.ts +384 -0
- package/distribution/runtime/src/core/recovery.ts +1255 -0
- package/distribution/runtime/src/core/slide-actions.ts +158 -0
- package/distribution/runtime/src/core/source-scope.ts +50 -0
- package/distribution/runtime/src/core/sync.ts +2423 -0
- package/distribution/runtime/src/core/widgets.ts +300 -0
- package/distribution/runtime/src/integration/install.ts +98 -0
- package/distribution/runtime/src/integration/invocation.ts +100 -0
- package/distribution/runtime/src/integration/quarto.ts +457 -0
- package/distribution/runtime/src/render/assets.ts +66 -0
- package/distribution/runtime/src/render/capture.lua +8 -0
- package/distribution/runtime/src/render/chart.ts +165 -0
- package/distribution/runtime/src/render/components.ts +199 -0
- package/distribution/runtime/src/render/design.ts +820 -0
- package/distribution/runtime/src/render/diagnostics.ts +36 -0
- package/distribution/runtime/src/render/graphs.ts +747 -0
- package/distribution/runtime/src/render/html.ts +82 -0
- package/distribution/runtime/src/render/images.ts +268 -0
- package/distribution/runtime/src/render/inline.ts +259 -0
- package/distribution/runtime/src/render/kpi.ts +385 -0
- package/distribution/runtime/src/render/lists.ts +183 -0
- package/distribution/runtime/src/render/math.ts +413 -0
- package/distribution/runtime/src/render/measure.ts +185 -0
- package/distribution/runtime/src/render/native-layout.ts +174 -0
- package/distribution/runtime/src/render/producer-capture.ts +319 -0
- package/distribution/runtime/src/render/producer-environment.R +49 -0
- package/distribution/runtime/src/render/producer-environment.ts +96 -0
- package/distribution/runtime/src/render/producer-profile.R +91 -0
- package/distribution/runtime/src/render/producer-site-profile.R +18 -0
- package/distribution/runtime/src/render/project.ts +1806 -0
- package/distribution/runtime/src/render/quarto.ts +127 -0
- package/distribution/runtime/src/render/registry.ts +233 -0
- package/distribution/runtime/src/render/resources.ts +292 -0
- package/distribution/runtime/src/render/shell.ts +110 -0
- package/distribution/runtime/src/render/source-inline.ts +169 -0
- package/distribution/runtime/src/render/table.ts +220 -0
- package/distribution/runtime/src/render/text-overflow.ts +208 -0
- package/distribution/runtime/src/render/types.ts +71 -0
- package/distribution/runtime/src/render/validate.ts +572 -0
- package/distribution/runtime/src/rich/project.ts +117 -0
- package/distribution/runtime/src/rich/table.ts +374 -0
- package/distribution/runtime/src/source/annotations.ts +490 -0
- package/distribution/runtime/src/source/attributes.ts +415 -0
- package/distribution/runtime/src/source/chart.ts +165 -0
- package/distribution/runtime/src/source/code-language.ts +277 -0
- package/distribution/runtime/src/source/component-insertion.ts +160 -0
- package/distribution/runtime/src/source/deletion.ts +278 -0
- package/distribution/runtime/src/source/graphs.ts +517 -0
- package/distribution/runtime/src/source/identities.ts +256 -0
- package/distribution/runtime/src/source/identity-id.ts +6 -0
- package/distribution/runtime/src/source/image.ts +50 -0
- package/distribution/runtime/src/source/kpi.ts +196 -0
- package/distribution/runtime/src/source/mod.ts +876 -0
- package/distribution/runtime/src/source/notes.ts +308 -0
- package/distribution/runtime/src/source/structure.ts +647 -0
- package/distribution/runtime/src/source/table.ts +290 -0
- package/distribution/runtime/src/source/text-conversion.ts +146 -0
- package/distribution/runtime/src/state/components.ts +281 -0
- package/distribution/runtime/src/state/graphs.ts +200 -0
- package/distribution/runtime/src/state/json.ts +162 -0
- package/distribution/runtime/src/state/merge.ts +1034 -0
- package/distribution/runtime/src/state/mod.ts +5 -0
- package/distribution/runtime/src/state/store.ts +226 -0
- package/distribution/runtime/src/state/types.ts +151 -0
- package/distribution/runtime/src/studio/client.js +790 -0
- package/distribution/runtime/src/studio/components.js +188 -0
- package/distribution/runtime/src/studio/index.html +278 -0
- package/distribution/runtime/src/studio/layouts.js +170 -0
- package/distribution/runtime/src/studio/registry.js +283 -0
- package/distribution/runtime/src/studio/relations.js +169 -0
- package/distribution/runtime/src/studio/server.ts +433 -0
- package/distribution/runtime/src/widgets/deck.ts +39 -0
- package/distribution/runtime/src/widgets/plotly.ts +354 -0
- package/distribution/runtime/src/widgets/presenter.ts +367 -0
- package/distribution/runtime/src/widgets/snapshot.ts +116 -0
- package/distribution/runtime/vendor/bento/FONT_NOTICES.md +49 -0
- package/distribution/runtime/vendor/bento/LICENSE +21 -0
- package/distribution/runtime/vendor/bento/THIRD_PARTY_NOTICES.md +51 -0
- package/distribution/runtime/vendor/bento/font-licenses/fraunces-OFL.txt +93 -0
- package/distribution/runtime/vendor/bento/font-licenses/instrument-sans-OFL.txt +93 -0
- package/distribution/runtime/vendor/bento/pin.json +69 -0
- package/distribution/runtime/vendor/bento/provenance/slides.package-lock.json +1928 -0
- package/distribution/runtime/vendor/bento/provenance/slides.package.json +28 -0
- package/distribution/runtime/vendor/bento/runtime-dependencies.json +244 -0
- package/distribution/runtime/vendor/bento/runtime-licenses.json +4038 -0
- package/distribution/runtime/vendor/bento/shell.html +269 -0
- package/distribution/runtime/vendor/bento/slides.schema.json +2379 -0
- package/package.json +40 -4
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/** Studio form editor: explicit registry Save, independent of native deck edits. */
|
|
2
|
+
export function attachRegistryEditor({ api, getState, showError }) {
|
|
3
|
+
const $ = (id) => document.getElementById(id);
|
|
4
|
+
let snapshot, saving = false, loading = false, dirty = false, serial = 0;
|
|
5
|
+
const note = (text) => {
|
|
6
|
+
$("registry-note").textContent = text;
|
|
7
|
+
};
|
|
8
|
+
function changed() {
|
|
9
|
+
dirty = true;
|
|
10
|
+
note(
|
|
11
|
+
"Cambios de diseño sin guardar. Cerrar conserva este borrador en la pestaña.",
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
function field(label, value, options = {}) {
|
|
15
|
+
const node = document.createElement("label"),
|
|
16
|
+
input = document.createElement("input");
|
|
17
|
+
input.id = "registry-field-" + (++serial);
|
|
18
|
+
node.htmlFor = input.id;
|
|
19
|
+
node.append(document.createTextNode(label));
|
|
20
|
+
input.value = String(value);
|
|
21
|
+
input.required = true;
|
|
22
|
+
Object.assign(input, options);
|
|
23
|
+
input.oninput = changed;
|
|
24
|
+
node.append(input);
|
|
25
|
+
return { node, input };
|
|
26
|
+
}
|
|
27
|
+
function activeOptions(rename) {
|
|
28
|
+
const select = $("registry-active"), current = rename ?? select.value;
|
|
29
|
+
select.replaceChildren(
|
|
30
|
+
new Option("Selecciona un tema (o ninguno si no hay temas)", ""),
|
|
31
|
+
);
|
|
32
|
+
for (const row of $("registry-themes").children) {
|
|
33
|
+
select.add(new Option(row.fields.name.value, row.fields.name.value));
|
|
34
|
+
}
|
|
35
|
+
select.value = current;
|
|
36
|
+
}
|
|
37
|
+
function addRow(kind, name, definition) {
|
|
38
|
+
const root = $("registry-" + kind),
|
|
39
|
+
row = document.createElement("fieldset");
|
|
40
|
+
row.className = "registry-row";
|
|
41
|
+
const legend = document.createElement("legend");
|
|
42
|
+
legend.textContent = kind === "layouts"
|
|
43
|
+
? "Layout con nombre"
|
|
44
|
+
: "Tema con nombre";
|
|
45
|
+
row.append(legend);
|
|
46
|
+
row.fields = {};
|
|
47
|
+
const add = (key, label, value, options) => {
|
|
48
|
+
const item = field(label, value, options);
|
|
49
|
+
row.fields[key] = item.input;
|
|
50
|
+
row.append(item.node);
|
|
51
|
+
};
|
|
52
|
+
add("name", "Nombre", name, {
|
|
53
|
+
maxLength: 64,
|
|
54
|
+
pattern: "[a-z][a-z0-9\\-]{0,63}",
|
|
55
|
+
});
|
|
56
|
+
if (kind === "layouts") {
|
|
57
|
+
add("columns", "Columnas", definition.columns, {
|
|
58
|
+
type: "number",
|
|
59
|
+
min: "1",
|
|
60
|
+
max: "6",
|
|
61
|
+
step: "1",
|
|
62
|
+
});
|
|
63
|
+
add("gap", "Separación (px)", definition.gap, {
|
|
64
|
+
type: "number",
|
|
65
|
+
min: "0",
|
|
66
|
+
max: "1000",
|
|
67
|
+
step: "any",
|
|
68
|
+
});
|
|
69
|
+
add("padding", "Margen interior (px)", definition.padding, {
|
|
70
|
+
type: "number",
|
|
71
|
+
min: "0",
|
|
72
|
+
max: "1000",
|
|
73
|
+
step: "any",
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
for (
|
|
77
|
+
const [key, label] of [["background", "Fondo"], ["color", "Texto"], [
|
|
78
|
+
"accent",
|
|
79
|
+
"Acento",
|
|
80
|
+
], ["fontFamily", "Familia tipográfica"]]
|
|
81
|
+
) {
|
|
82
|
+
add(key, label, definition[key], {
|
|
83
|
+
maxLength: key === "fontFamily" ? 200 : 100,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
let previous = name;
|
|
87
|
+
row.fields.name.addEventListener("input", () => {
|
|
88
|
+
const active = $("registry-active").value === previous
|
|
89
|
+
? row.fields.name.value
|
|
90
|
+
: $("registry-active").value;
|
|
91
|
+
previous = row.fields.name.value;
|
|
92
|
+
activeOptions(active);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const remove = document.createElement("button");
|
|
96
|
+
remove.type = "button";
|
|
97
|
+
remove.textContent = kind === "layouts"
|
|
98
|
+
? "Eliminar layout"
|
|
99
|
+
: "Eliminar tema";
|
|
100
|
+
remove.onclick = () => {
|
|
101
|
+
row.remove();
|
|
102
|
+
activeOptions();
|
|
103
|
+
changed();
|
|
104
|
+
};
|
|
105
|
+
row.append(remove);
|
|
106
|
+
root.append(row);
|
|
107
|
+
}
|
|
108
|
+
function display(value) {
|
|
109
|
+
$("registry-layouts").replaceChildren();
|
|
110
|
+
$("registry-themes").replaceChildren();
|
|
111
|
+
for (const [name, definition] of Object.entries(value.layouts ?? {})) {
|
|
112
|
+
addRow("layouts", name, definition);
|
|
113
|
+
}
|
|
114
|
+
for (const [name, definition] of Object.entries(value.themes ?? {})) {
|
|
115
|
+
addRow("themes", name, definition);
|
|
116
|
+
}
|
|
117
|
+
activeOptions(value.activeTheme ?? "");
|
|
118
|
+
dirty = false;
|
|
119
|
+
}
|
|
120
|
+
function controlsDisabled(value) {
|
|
121
|
+
$("designs").disabled = value;
|
|
122
|
+
for (
|
|
123
|
+
const node of $("registry-form").querySelectorAll("input, select, button")
|
|
124
|
+
) node.disabled = value;
|
|
125
|
+
}
|
|
126
|
+
async function load() {
|
|
127
|
+
if (loading || saving) return;
|
|
128
|
+
loading = true;
|
|
129
|
+
controlsDisabled(true);
|
|
130
|
+
note("Cargando el registro…");
|
|
131
|
+
try {
|
|
132
|
+
const next = await api("design-registry"), state = getState();
|
|
133
|
+
if (next.docId !== state.docId || next.revision !== state.revision) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"Otra pestaña cambió el proyecto. Recarga el proyecto antes de editar sus diseños.",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
snapshot = next;
|
|
139
|
+
display(next.registry);
|
|
140
|
+
note(
|
|
141
|
+
"Registro cargado. Guardar diseños no modifica QMD ni ejecuta cálculos.",
|
|
142
|
+
);
|
|
143
|
+
} finally {
|
|
144
|
+
loading = false;
|
|
145
|
+
controlsDisabled(false);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function serialize() {
|
|
149
|
+
const registry = { schemaVersion: 1 };
|
|
150
|
+
for (const kind of ["layouts", "themes"]) {
|
|
151
|
+
const result = Object.create(null);
|
|
152
|
+
for (const row of $("registry-" + kind).children) {
|
|
153
|
+
const { name, ...fields } = row.fields;
|
|
154
|
+
if (Object.hasOwn(result, name.value)) {
|
|
155
|
+
throw new Error(`El nombre ${name.value} está repetido.`);
|
|
156
|
+
}
|
|
157
|
+
result[name.value] = Object.fromEntries(
|
|
158
|
+
Object.entries(fields).map((
|
|
159
|
+
[key, input],
|
|
160
|
+
) => [
|
|
161
|
+
key,
|
|
162
|
+
input.type === "number" ? Number(input.value) : input.value,
|
|
163
|
+
]),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (Object.keys(result).length) registry[kind] = result;
|
|
167
|
+
}
|
|
168
|
+
if ($("registry-active").value) {
|
|
169
|
+
registry.activeTheme = $("registry-active").value;
|
|
170
|
+
}
|
|
171
|
+
if (registry.themes && !registry.activeTheme) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
"Selecciona explícitamente el tema activo antes de guardar.",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return registry;
|
|
177
|
+
}
|
|
178
|
+
$("designs").onclick = async () => {
|
|
179
|
+
try {
|
|
180
|
+
if (loading || saving) return;
|
|
181
|
+
if (!snapshot || !dirty) await load();
|
|
182
|
+
$("registry-dialog").showModal();
|
|
183
|
+
if (dirty) {
|
|
184
|
+
note(
|
|
185
|
+
"Borrador conservado. Guardar comprobará si el proyecto o el registro cambiaron.",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
showError(error);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
$("registry-add-layout").onclick = () => {
|
|
193
|
+
if ($("registry-layouts").children.length >= 64) {
|
|
194
|
+
note("Se admiten como máximo 64 layouts.");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
let index = 1;
|
|
198
|
+
while (
|
|
199
|
+
[...$("registry-layouts").children].some((row) =>
|
|
200
|
+
row.fields.name.value === "layout-" + index
|
|
201
|
+
)
|
|
202
|
+
) index++;
|
|
203
|
+
addRow("layouts", "layout-" + index, { columns: 2, gap: 32, padding: 0 });
|
|
204
|
+
changed();
|
|
205
|
+
};
|
|
206
|
+
$("registry-add-theme").onclick = () => {
|
|
207
|
+
if ($("registry-themes").children.length >= 32) {
|
|
208
|
+
note("Se admiten como máximo 32 temas.");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
let index = 1;
|
|
212
|
+
while (
|
|
213
|
+
[...$("registry-themes").children].some((row) =>
|
|
214
|
+
row.fields.name.value === "tema-" + index
|
|
215
|
+
)
|
|
216
|
+
) index++;
|
|
217
|
+
addRow("themes", "tema-" + index, {
|
|
218
|
+
background: "#ffffff",
|
|
219
|
+
color: "#172a35",
|
|
220
|
+
accent: "#156f65",
|
|
221
|
+
fontFamily: "Arial, sans-serif",
|
|
222
|
+
});
|
|
223
|
+
activeOptions();
|
|
224
|
+
changed();
|
|
225
|
+
};
|
|
226
|
+
$("registry-active").onchange = changed;
|
|
227
|
+
$("registry-reload").onclick = async () => {
|
|
228
|
+
try {
|
|
229
|
+
await load();
|
|
230
|
+
} catch (error) {
|
|
231
|
+
note(error.message);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
$("registry-form").onsubmit = async (event) => {
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
if (saving || loading || !snapshot) return;
|
|
237
|
+
try {
|
|
238
|
+
const registry = serialize();
|
|
239
|
+
saving = true;
|
|
240
|
+
const controls = [
|
|
241
|
+
...$("registry-form").querySelectorAll("input, select, button"),
|
|
242
|
+
];
|
|
243
|
+
controls.forEach((node) => {
|
|
244
|
+
node.disabled = true;
|
|
245
|
+
});
|
|
246
|
+
try {
|
|
247
|
+
const next = await api("design-registry", {
|
|
248
|
+
registry,
|
|
249
|
+
expectedDocId: snapshot.docId,
|
|
250
|
+
expectedRevision: snapshot.revision,
|
|
251
|
+
expectedHash: snapshot.hash,
|
|
252
|
+
});
|
|
253
|
+
snapshot = next;
|
|
254
|
+
dirty = false;
|
|
255
|
+
note(
|
|
256
|
+
"Diseños guardados. Recalcular aplicará los valores seleccionados conservando tu diseño guardado y los cambios pendientes.",
|
|
257
|
+
);
|
|
258
|
+
} finally {
|
|
259
|
+
controls.forEach((node) => {
|
|
260
|
+
node.disabled = false;
|
|
261
|
+
});
|
|
262
|
+
saving = false;
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
note(
|
|
266
|
+
error.message +
|
|
267
|
+
" El borrador se conserva.",
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
$("registry-close").onclick = () => {
|
|
272
|
+
if (!saving && !loading) $("registry-dialog").close();
|
|
273
|
+
};
|
|
274
|
+
$("registry-dialog").addEventListener("cancel", (event) => {
|
|
275
|
+
if (saving || loading) event.preventDefault();
|
|
276
|
+
});
|
|
277
|
+
addEventListener("beforeunload", (event) => {
|
|
278
|
+
if (dirty || saving) {
|
|
279
|
+
event.preventDefault();
|
|
280
|
+
event.returnValue = "";
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/** Explicit diagram relationships, with saved source/native identities. */
|
|
2
|
+
export function attachGraphRelations(
|
|
3
|
+
{ api, getState, saveBefore, setBusy, reloadEditor, showError },
|
|
4
|
+
) {
|
|
5
|
+
const $ = (id) => document.getElementById(id);
|
|
6
|
+
let snapshot, working = false;
|
|
7
|
+
const graph = () =>
|
|
8
|
+
snapshot?.graphs.find((graph) =>
|
|
9
|
+
graph.slideId === $("relation-slide").value &&
|
|
10
|
+
graph.componentId === $("relation-graph").value
|
|
11
|
+
);
|
|
12
|
+
const edge = () =>
|
|
13
|
+
graph()?.edges.find((edge) => edge.id === $("relation-edge").value);
|
|
14
|
+
function options(id, items, label = (item) => item.label) {
|
|
15
|
+
$(id).replaceChildren();
|
|
16
|
+
for (const item of items) {
|
|
17
|
+
const option = document.createElement("option");
|
|
18
|
+
option.value = item.id;
|
|
19
|
+
option.textContent = label(item);
|
|
20
|
+
$(id).append(option);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function controls(disabled) {
|
|
24
|
+
working = disabled;
|
|
25
|
+
for (
|
|
26
|
+
const control of $("relations-dialog").querySelectorAll("button, select")
|
|
27
|
+
) control.disabled = disabled;
|
|
28
|
+
if (!disabled) {
|
|
29
|
+
const editable = graph()?.kind === "diagram" && !!edge();
|
|
30
|
+
for (const id of ["relation-edge", "relation-from", "relation-to"]) {
|
|
31
|
+
$(id).disabled = !editable;
|
|
32
|
+
}
|
|
33
|
+
$("relation-save").disabled = !editable ||
|
|
34
|
+
$("relation-from").value === $("relation-to").value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function showEdge() {
|
|
38
|
+
const current = graph(), relation = edge();
|
|
39
|
+
options(
|
|
40
|
+
"relation-from",
|
|
41
|
+
current?.nodes ?? [],
|
|
42
|
+
(node) => `${node.label} · #${node.id}`,
|
|
43
|
+
);
|
|
44
|
+
options(
|
|
45
|
+
"relation-to",
|
|
46
|
+
current?.nodes ?? [],
|
|
47
|
+
(node) => `${node.label} · #${node.id}`,
|
|
48
|
+
);
|
|
49
|
+
if (relation) {
|
|
50
|
+
$("relation-from").value = relation.fromNodeId;
|
|
51
|
+
$("relation-to").value = relation.toNodeId;
|
|
52
|
+
}
|
|
53
|
+
$("relation-sequence").textContent = current?.kind === "timeline"
|
|
54
|
+
? current.nodes.map((node) => `${node.label} (#${node.id})`).join(" → ")
|
|
55
|
+
: "";
|
|
56
|
+
$("relation-note").textContent = !current
|
|
57
|
+
? "Este proyecto no tiene diagramas ni secuencias QMD existentes."
|
|
58
|
+
: current.kind === "timeline"
|
|
59
|
+
? "Secuencia de solo lectura: sus relaciones derivan del orden de los hitos en QMD. Reordena los bloques y recalcula para cambiarla."
|
|
60
|
+
: relation?.detached
|
|
61
|
+
? "El conector está desvinculado. Selecciona ambos nodos para reconectarlo explícitamente. Guardar conserva el cambio pendiente; Revisar cambios permite aplicarlo a QMD."
|
|
62
|
+
: "Guardar conserva la relación pendiente sin recalcular. Después usa Revisar cambios para comprobar el diagrama y aplicar el parche a QMD.";
|
|
63
|
+
controls(false);
|
|
64
|
+
}
|
|
65
|
+
function showGraph() {
|
|
66
|
+
const current = graph();
|
|
67
|
+
options(
|
|
68
|
+
"relation-edge",
|
|
69
|
+
current?.kind === "diagram" ? current.edges : [],
|
|
70
|
+
(edge) => `${edge.label || "Conexión"} · #${edge.id}`,
|
|
71
|
+
);
|
|
72
|
+
showEdge();
|
|
73
|
+
}
|
|
74
|
+
function showSlide() {
|
|
75
|
+
options(
|
|
76
|
+
"relation-graph",
|
|
77
|
+
snapshot?.graphs.filter((graph) =>
|
|
78
|
+
graph.slideId === $("relation-slide").value
|
|
79
|
+
).map((graph) => ({
|
|
80
|
+
id: graph.componentId,
|
|
81
|
+
label: `${
|
|
82
|
+
graph.kind === "diagram" ? "Diagrama" : "Secuencia · solo lectura"
|
|
83
|
+
} · #${graph.componentId}`,
|
|
84
|
+
})) ?? [],
|
|
85
|
+
);
|
|
86
|
+
showGraph();
|
|
87
|
+
}
|
|
88
|
+
function checkedSnapshot() {
|
|
89
|
+
const current = getState();
|
|
90
|
+
if (
|
|
91
|
+
!snapshot || snapshot.docId !== current?.docId ||
|
|
92
|
+
snapshot.revision !== current?.revision
|
|
93
|
+
) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
"El proyecto cambió desde que abriste este diálogo. Ciérralo y vuelve a abrir Relaciones; tus cambios siguen guardados.",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
$("relations").onclick = async () => {
|
|
100
|
+
if (working) return;
|
|
101
|
+
controls(true);
|
|
102
|
+
setBusy(true);
|
|
103
|
+
$("busy").textContent = "Guardando cambios antes de revisar relaciones…";
|
|
104
|
+
try {
|
|
105
|
+
await saveBefore();
|
|
106
|
+
snapshot = await api("graphs/relations");
|
|
107
|
+
checkedSnapshot();
|
|
108
|
+
const slides = [
|
|
109
|
+
...new Map(
|
|
110
|
+
snapshot.graphs.map((
|
|
111
|
+
graph,
|
|
112
|
+
) => [graph.slideId, {
|
|
113
|
+
id: graph.slideId,
|
|
114
|
+
label: `${graph.slideLabel} · #${graph.slideId}`,
|
|
115
|
+
}]),
|
|
116
|
+
).values(),
|
|
117
|
+
];
|
|
118
|
+
options("relation-slide", slides);
|
|
119
|
+
showSlide();
|
|
120
|
+
$("relations-dialog").showModal();
|
|
121
|
+
} catch (error) {
|
|
122
|
+
showError(error);
|
|
123
|
+
} finally {
|
|
124
|
+
controls(false);
|
|
125
|
+
setBusy(false);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
$("relation-slide").onchange = showSlide;
|
|
129
|
+
$("relation-graph").onchange = showGraph;
|
|
130
|
+
$("relation-edge").onchange = showEdge;
|
|
131
|
+
for (const id of ["relation-from", "relation-to"]) {
|
|
132
|
+
$(id).onchange = () => controls(false);
|
|
133
|
+
}
|
|
134
|
+
$("relation-form").onsubmit = async (event) => {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
if (working || graph()?.kind !== "diagram" || !edge()) return;
|
|
137
|
+
try {
|
|
138
|
+
checkedSnapshot();
|
|
139
|
+
const current = graph();
|
|
140
|
+
controls(true);
|
|
141
|
+
setBusy(true);
|
|
142
|
+
$("busy").textContent = "Guardando relación pendiente en el proyecto…";
|
|
143
|
+
await api("graphs/relations", {
|
|
144
|
+
expectedDocId: snapshot.docId,
|
|
145
|
+
expectedRevision: snapshot.revision,
|
|
146
|
+
expectedSourceHash: snapshot.sourceHash,
|
|
147
|
+
slideId: current.slideId,
|
|
148
|
+
componentId: current.componentId,
|
|
149
|
+
edgeId: edge().id,
|
|
150
|
+
fromNodeId: $("relation-from").value,
|
|
151
|
+
toNodeId: $("relation-to").value,
|
|
152
|
+
});
|
|
153
|
+
$("relations-dialog").close();
|
|
154
|
+
reloadEditor();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
$("relation-note").textContent = error.message;
|
|
157
|
+
setBusy(false);
|
|
158
|
+
showError(error);
|
|
159
|
+
} finally {
|
|
160
|
+
controls(false);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
$("relation-close").onclick = () => {
|
|
164
|
+
if (!working) $("relations-dialog").close();
|
|
165
|
+
};
|
|
166
|
+
$("relations-dialog").addEventListener("cancel", (event) => {
|
|
167
|
+
if (working) event.preventDefault();
|
|
168
|
+
});
|
|
169
|
+
}
|