@brianmsm/quarto-bento 0.0.0-stage → 0.1.1
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 +93 -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 +232 -0
- package/distribution/runtime/src/core/agent.ts +546 -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 +851 -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 +2557 -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 +892 -0
- package/distribution/runtime/src/studio/components.js +188 -0
- package/distribution/runtime/src/studio/index.html +283 -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 +461 -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,188 @@
|
|
|
1
|
+
/** Explicit roles and durable recovery, using the saved project revision. */
|
|
2
|
+
export function attachComponentEditor(
|
|
3
|
+
{ api, getState, saveBefore, setBusy, reloadEditor, showError },
|
|
4
|
+
) {
|
|
5
|
+
const $ = (id) => document.getElementById(id);
|
|
6
|
+
let snapshot, submitting = false;
|
|
7
|
+
const labels = {
|
|
8
|
+
title: "Título",
|
|
9
|
+
body: "Cuerpo",
|
|
10
|
+
label: "Etiqueta",
|
|
11
|
+
value: "Valor escrito",
|
|
12
|
+
detail: "Detalle (opcional)",
|
|
13
|
+
};
|
|
14
|
+
function showFields() {
|
|
15
|
+
$("component-fields").replaceChildren();
|
|
16
|
+
const roles = $("component-kind").value === "card"
|
|
17
|
+
? ["title", "body"]
|
|
18
|
+
: ["label", "value", "detail"];
|
|
19
|
+
for (const role of roles) {
|
|
20
|
+
const label = document.createElement("label");
|
|
21
|
+
label.textContent = labels[role];
|
|
22
|
+
const input = document.createElement(
|
|
23
|
+
role === "body" || role === "detail" ? "textarea" : "input",
|
|
24
|
+
);
|
|
25
|
+
input.name = role;
|
|
26
|
+
input.dataset.role = role;
|
|
27
|
+
input.maxLength = 6000;
|
|
28
|
+
input.required = role !== "detail";
|
|
29
|
+
label.append(input);
|
|
30
|
+
$("component-fields").append(label);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function checkedSnapshot() {
|
|
34
|
+
const current = getState();
|
|
35
|
+
if (
|
|
36
|
+
!snapshot || snapshot.docId !== current?.docId ||
|
|
37
|
+
snapshot.revision !== current?.revision
|
|
38
|
+
) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"El proyecto cambió desde que abriste este diálogo. Ciérralo y vuelve a abrir Añadir componente.",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return current;
|
|
44
|
+
}
|
|
45
|
+
function actionControls(disabled) {
|
|
46
|
+
submitting = disabled;
|
|
47
|
+
for (
|
|
48
|
+
const input of $("component-dialog").querySelectorAll(
|
|
49
|
+
"button, input, textarea, select",
|
|
50
|
+
)
|
|
51
|
+
) input.disabled = disabled;
|
|
52
|
+
}
|
|
53
|
+
async function run(action) {
|
|
54
|
+
if (submitting) return;
|
|
55
|
+
try {
|
|
56
|
+
const current = checkedSnapshot();
|
|
57
|
+
actionControls(true);
|
|
58
|
+
setBusy(true);
|
|
59
|
+
$("busy").textContent = "Guardando componente en el proyecto…";
|
|
60
|
+
await api("components/action", {
|
|
61
|
+
...action,
|
|
62
|
+
revision: current.revision,
|
|
63
|
+
expectedDocId: current.docId,
|
|
64
|
+
});
|
|
65
|
+
$("component-dialog").close();
|
|
66
|
+
reloadEditor();
|
|
67
|
+
} catch (error) {
|
|
68
|
+
$("component-note").textContent = error.message;
|
|
69
|
+
setBusy(false);
|
|
70
|
+
showError(error);
|
|
71
|
+
} finally {
|
|
72
|
+
actionControls(false);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function showPending() {
|
|
76
|
+
$("component-pending").replaceChildren();
|
|
77
|
+
for (const intent of snapshot.componentCreations ?? []) {
|
|
78
|
+
const section = document.createElement("section"),
|
|
79
|
+
title = document.createElement("h4");
|
|
80
|
+
const first = intent.members[0].snapshot.html.replace(/<[^>]*>/g, "");
|
|
81
|
+
title.textContent = `${
|
|
82
|
+
intent.kind === "card" ? "Tarjeta" : "Indicador"
|
|
83
|
+
}: ${first}`;
|
|
84
|
+
section.append(title);
|
|
85
|
+
const slide = snapshot.emitted.slides.find((slide) =>
|
|
86
|
+
slide.id === intent.slideId
|
|
87
|
+
);
|
|
88
|
+
const ids = [
|
|
89
|
+
intent.backgroundId,
|
|
90
|
+
...intent.members.map((member) => member.nativeId),
|
|
91
|
+
];
|
|
92
|
+
const missing = ids.filter((id) =>
|
|
93
|
+
!slide?.elements.some((element) => element.id === id)
|
|
94
|
+
);
|
|
95
|
+
const background = slide?.elements.find((element) =>
|
|
96
|
+
element.id === intent.backgroundId
|
|
97
|
+
);
|
|
98
|
+
const invalid = !!background &&
|
|
99
|
+
(background.type !== "shape" || background.shape !== "rect") ||
|
|
100
|
+
slide?.elements.some((element) =>
|
|
101
|
+
intent.members.some((member) => member.nativeId === element.id) &&
|
|
102
|
+
element.type !== "text"
|
|
103
|
+
);
|
|
104
|
+
const extra = slide?.elements.some((element) =>
|
|
105
|
+
element.groupId === intent.id && !ids.includes(element.id)
|
|
106
|
+
);
|
|
107
|
+
const note = document.createElement("p");
|
|
108
|
+
note.textContent = !slide
|
|
109
|
+
? "Su diapositiva está pendiente de eliminación. Restáurala en Diapositivas o revisa su eliminación para resolver este componente."
|
|
110
|
+
: missing.length
|
|
111
|
+
? "Componente incompleto. Restaurar recupera los miembros ausentes con su último contenido y diseño guardados; conserva los que siguen presentes."
|
|
112
|
+
: invalid || extra
|
|
113
|
+
? "El tipo de algún miembro cambió o hay objetos adicionales en el grupo. Puedes resolverlos en Bento o descartar esta creación pendiente."
|
|
114
|
+
: "Creación pendiente. Revisar cambios mostrará su inserción completa en QMD.";
|
|
115
|
+
section.append(note);
|
|
116
|
+
if (slide && missing.length) {
|
|
117
|
+
const restore = document.createElement("button");
|
|
118
|
+
restore.type = "button";
|
|
119
|
+
restore.textContent = "Restaurar componente incompleto";
|
|
120
|
+
restore.onclick = () => run({ action: "restore", id: intent.id });
|
|
121
|
+
section.append(restore);
|
|
122
|
+
}
|
|
123
|
+
const discard = document.createElement("button");
|
|
124
|
+
discard.type = "button";
|
|
125
|
+
discard.textContent = "Descartar componente pendiente";
|
|
126
|
+
discard.onclick = () => run({ action: "discard", id: intent.id });
|
|
127
|
+
section.append(discard);
|
|
128
|
+
$("component-pending").append(section);
|
|
129
|
+
}
|
|
130
|
+
$("component-pending-title").hidden = !snapshot.componentCreations?.length;
|
|
131
|
+
}
|
|
132
|
+
$("components").onclick = async () => {
|
|
133
|
+
if (submitting) return;
|
|
134
|
+
setBusy(true);
|
|
135
|
+
$("busy").textContent = "Guardando cambios antes de añadir un componente…";
|
|
136
|
+
try {
|
|
137
|
+
await saveBefore();
|
|
138
|
+
snapshot = await api("state");
|
|
139
|
+
checkedSnapshot();
|
|
140
|
+
$("component-slide").replaceChildren();
|
|
141
|
+
for (
|
|
142
|
+
const slide of snapshot.emitted.slides.filter((slide) =>
|
|
143
|
+
snapshot.generatedBaseline.slides.some((baseline) =>
|
|
144
|
+
baseline.id === slide.id
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
) {
|
|
148
|
+
const option = document.createElement("option");
|
|
149
|
+
option.value = slide.id;
|
|
150
|
+
option.textContent = slide.name || slide.id;
|
|
151
|
+
$("component-slide").append(option);
|
|
152
|
+
}
|
|
153
|
+
$("component-kind").value = "card";
|
|
154
|
+
showFields();
|
|
155
|
+
showPending();
|
|
156
|
+
$("component-note").textContent = "";
|
|
157
|
+
$("component-dialog").showModal();
|
|
158
|
+
} catch (error) {
|
|
159
|
+
showError(error);
|
|
160
|
+
} finally {
|
|
161
|
+
setBusy(false);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
$("component-kind").onchange = showFields;
|
|
165
|
+
$("component-form").onsubmit = async (event) => {
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
const fields = {};
|
|
168
|
+
for (
|
|
169
|
+
const input of $("component-fields").querySelectorAll("input, textarea")
|
|
170
|
+
) {
|
|
171
|
+
if (input.name !== "detail" || input.value.trim()) {
|
|
172
|
+
fields[input.name] = input.value;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
await run({
|
|
176
|
+
action: "create",
|
|
177
|
+
kind: $("component-kind").value,
|
|
178
|
+
slideId: $("component-slide").value,
|
|
179
|
+
fields,
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
$("component-close").onclick = () => {
|
|
183
|
+
if (!submitting) $("component-dialog").close();
|
|
184
|
+
};
|
|
185
|
+
$("component-dialog").addEventListener("cancel", (event) => {
|
|
186
|
+
if (submitting) event.preventDefault();
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="es">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>Quarto-Bento Studio</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
height: 100vh;
|
|
12
|
+
margin: 0;
|
|
13
|
+
font: 15px system-ui;
|
|
14
|
+
color: #e8efed;
|
|
15
|
+
background: #152c32;
|
|
16
|
+
}
|
|
17
|
+
header {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-wrap: wrap;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: 12px;
|
|
22
|
+
padding: 12px 18px;
|
|
23
|
+
min-height: 42px;
|
|
24
|
+
}
|
|
25
|
+
h1 {
|
|
26
|
+
font-size: 17px;
|
|
27
|
+
margin: 0 16px 0 0;
|
|
28
|
+
}
|
|
29
|
+
button {
|
|
30
|
+
font: inherit;
|
|
31
|
+
padding: 8px 12px;
|
|
32
|
+
border: 0;
|
|
33
|
+
border-radius: 6px;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
}
|
|
36
|
+
button:disabled {
|
|
37
|
+
opacity: .5;
|
|
38
|
+
cursor: default;
|
|
39
|
+
}
|
|
40
|
+
#status {
|
|
41
|
+
margin-left: auto;
|
|
42
|
+
max-width: 100%;
|
|
43
|
+
}
|
|
44
|
+
#project-diagnostics {
|
|
45
|
+
flex-shrink: 0;
|
|
46
|
+
max-height: 25vh;
|
|
47
|
+
overflow: auto;
|
|
48
|
+
padding: 10px 18px;
|
|
49
|
+
color: #382710;
|
|
50
|
+
background: #fff1cf;
|
|
51
|
+
border-block: 1px solid #d7a64b;
|
|
52
|
+
overflow-wrap: anywhere;
|
|
53
|
+
}
|
|
54
|
+
#project-diagnostics h2 {
|
|
55
|
+
font-size: 16px;
|
|
56
|
+
margin: 0 0 6px;
|
|
57
|
+
}
|
|
58
|
+
#diagnostic-list {
|
|
59
|
+
margin: 0;
|
|
60
|
+
padding-left: 22px;
|
|
61
|
+
}
|
|
62
|
+
#diagnostic-list li + li {
|
|
63
|
+
margin-top: 8px;
|
|
64
|
+
}
|
|
65
|
+
#editor-region {
|
|
66
|
+
flex: 1;
|
|
67
|
+
min-height: 0;
|
|
68
|
+
position: relative;
|
|
69
|
+
}
|
|
70
|
+
iframe {
|
|
71
|
+
display: block;
|
|
72
|
+
width: 100%;
|
|
73
|
+
height: 100%;
|
|
74
|
+
border: 0;
|
|
75
|
+
background: white;
|
|
76
|
+
}
|
|
77
|
+
dialog {
|
|
78
|
+
width: min(880px,90vw);
|
|
79
|
+
max-height: 78vh;
|
|
80
|
+
overflow: auto;
|
|
81
|
+
border: 0;
|
|
82
|
+
border-radius: 12px;
|
|
83
|
+
padding: 24px;
|
|
84
|
+
color: #172a35;
|
|
85
|
+
}
|
|
86
|
+
dialog::backdrop {
|
|
87
|
+
background: #102a35aa;
|
|
88
|
+
}
|
|
89
|
+
pre {
|
|
90
|
+
white-space: pre-wrap;
|
|
91
|
+
word-break: break-word;
|
|
92
|
+
background: #f3f6f5;
|
|
93
|
+
padding: 12px;
|
|
94
|
+
border-radius: 6px;
|
|
95
|
+
}
|
|
96
|
+
#busy {
|
|
97
|
+
position: absolute;
|
|
98
|
+
inset: 0;
|
|
99
|
+
z-index: 20;
|
|
100
|
+
background: #eff5f0ed;
|
|
101
|
+
color: #172a35;
|
|
102
|
+
align-items: center;
|
|
103
|
+
justify-content: center;
|
|
104
|
+
font-size: 20px;
|
|
105
|
+
}
|
|
106
|
+
#busy:not([hidden]) {
|
|
107
|
+
display: flex;
|
|
108
|
+
}
|
|
109
|
+
.registry-row {
|
|
110
|
+
display: grid;
|
|
111
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
112
|
+
gap: 12px;
|
|
113
|
+
margin: 14px 0;
|
|
114
|
+
border: 1px solid #cbd9d6;
|
|
115
|
+
border-radius: 6px;
|
|
116
|
+
}
|
|
117
|
+
.registry-row label {
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-direction: column;
|
|
120
|
+
gap: 6px;
|
|
121
|
+
}
|
|
122
|
+
.registry-row input {
|
|
123
|
+
font: inherit;
|
|
124
|
+
padding: 6px;
|
|
125
|
+
min-width: 0;
|
|
126
|
+
}
|
|
127
|
+
#component-fields {
|
|
128
|
+
display: grid;
|
|
129
|
+
gap: 14px;
|
|
130
|
+
margin: 18px 0;
|
|
131
|
+
}
|
|
132
|
+
#component-fields label {
|
|
133
|
+
display: flex;
|
|
134
|
+
flex-direction: column;
|
|
135
|
+
gap: 6px;
|
|
136
|
+
}
|
|
137
|
+
#component-fields input,
|
|
138
|
+
#component-fields textarea {
|
|
139
|
+
font: inherit;
|
|
140
|
+
padding: 8px;
|
|
141
|
+
}
|
|
142
|
+
#component-fields textarea {
|
|
143
|
+
min-height: 80px;
|
|
144
|
+
resize: vertical;
|
|
145
|
+
}
|
|
146
|
+
#component-pending section {
|
|
147
|
+
border-top: 1px solid #cbd9d6;
|
|
148
|
+
margin-top: 16px;
|
|
149
|
+
}
|
|
150
|
+
#component-pending button {
|
|
151
|
+
margin: 0 8px 8px 0;
|
|
152
|
+
}
|
|
153
|
+
#registry-note {
|
|
154
|
+
min-height: 2em;
|
|
155
|
+
}
|
|
156
|
+
#registry-form > button {
|
|
157
|
+
margin-top: 12px;
|
|
158
|
+
}
|
|
159
|
+
#patches section {
|
|
160
|
+
margin: 18px 0;
|
|
161
|
+
border-top: 1px solid #cbd9d6;
|
|
162
|
+
}
|
|
163
|
+
.before {
|
|
164
|
+
border-left: 4px solid #ac4646;
|
|
165
|
+
}
|
|
166
|
+
.after {
|
|
167
|
+
border-left: 4px solid #247367;
|
|
168
|
+
}
|
|
169
|
+
#apply {
|
|
170
|
+
background: #156f65;
|
|
171
|
+
color: white;
|
|
172
|
+
}
|
|
173
|
+
#sync-selection label,
|
|
174
|
+
#delete-confirmations label {
|
|
175
|
+
display: block;
|
|
176
|
+
margin: 10px 0;
|
|
177
|
+
}
|
|
178
|
+
#sync-note {
|
|
179
|
+
line-height: 1.5;
|
|
180
|
+
}
|
|
181
|
+
#slide-list section {
|
|
182
|
+
margin: 18px 0;
|
|
183
|
+
border-top: 1px solid #cbd9d6;
|
|
184
|
+
}
|
|
185
|
+
#slide-list button {
|
|
186
|
+
margin-right: 8px;
|
|
187
|
+
}
|
|
188
|
+
#presenter-dialog {
|
|
189
|
+
width: 95vw;
|
|
190
|
+
max-width: none;
|
|
191
|
+
height: 90vh;
|
|
192
|
+
max-height: 92vh;
|
|
193
|
+
padding: 12px;
|
|
194
|
+
}
|
|
195
|
+
#presenter-frame {
|
|
196
|
+
height: calc(90vh - 50px);
|
|
197
|
+
}
|
|
198
|
+
#delete-confirmations label {
|
|
199
|
+
display: block;
|
|
200
|
+
margin: 12px 0;
|
|
201
|
+
}
|
|
202
|
+
</style>
|
|
203
|
+
</head>
|
|
204
|
+
<body>
|
|
205
|
+
<header><h1>Quarto-Bento</h1><button id="save" disabled>Guardar proyecto</button><button id="slides" disabled>Diapositivas</button><button id="components" disabled>Añadir componente</button><button id="relations" disabled>Relaciones</button><button id="layouts" disabled>Aplicar diseño</button><button id="designs" disabled>Diseños y temas</button><button id="review" disabled>Revisar cambios</button><button id="reconcile" disabled>Comparar con QMD</button><button id="render" disabled>Recalcular</button><button id="presenter" disabled>Presentar con gráficos</button><span id="status" role="status">Abriendo…</span></header>
|
|
206
|
+
<section id="project-diagnostics" hidden role="status" aria-live="polite"
|
|
207
|
+
aria-labelledby="diagnostic-title">
|
|
208
|
+
<h2 id="diagnostic-title">Avisos del proyecto</h2>
|
|
209
|
+
<ul id="diagnostic-list"></ul>
|
|
210
|
+
</section>
|
|
211
|
+
<main id="editor-region">
|
|
212
|
+
<iframe id="editor" src="/deck" title="Editor Bento"></iframe>
|
|
213
|
+
<div id="busy" hidden
|
|
214
|
+
role="status">Recalculando. Tus cambios están guardados.</div>
|
|
215
|
+
</main>
|
|
216
|
+
<dialog
|
|
217
|
+
id="review-dialog"><h2>Revisar cambios hacia QMD</h2><p id="sync-note">Guardar conserva el proyecto; sincronizar aplica únicamente los cambios que revisas aquí.</p><p>Selecciona los cambios que quieres sincronizar. Los cambios excluidos se conservan pendientes. Los miembros de un componente deben revisarse juntos.</p><div id="sync-selection"></div><p id="sync-selection-summary" role="status"></p><div id="delete-confirmations"></div><div id="patches"></div><details><summary>Origen y detalle de los cambios</summary><div id="changes"></div></details><button id="apply" disabled>Aplicar estos cambios a QMD</button> <button id="close">Cerrar</button></dialog>
|
|
218
|
+
<dialog
|
|
219
|
+
id="slides-dialog"><h2>Diapositivas</h2><p>Ocultar conserva el contenido y lo omite en la presentación y el PDF. Marcar para eliminar guarda una eliminación pendiente; después debes revisarla y confirmarla para modificar QMD. Puedes restaurarla antes de sincronizar.</p><p id="slides-note" role="status"></p><div id="slide-list"></div><button id="slides-close">Cerrar</button></dialog>
|
|
220
|
+
<dialog id="layouts-dialog" aria-labelledby="layouts-title">
|
|
221
|
+
<h2 id="layouts-title">Aplicar diseño reutilizable</h2>
|
|
222
|
+
<p>Elige los objetos de cada columna. El título y los objetos no seleccionados conservan su posición. Las columnas siguen el orden actual de los objetos; dentro de cada columna reciben espacios iguales. Texto e imágenes usan ese marco y las tablas conservan la altura de sus filas.</p>
|
|
223
|
+
<p>Se aplican los valores actuales del diseño. Si cambias su definición en Diseños y temas, vuelve a aplicarlo a estos objetos. No se modifica QMD; sus atributos explícitos quedan pendientes para Revisar cambios.</p>
|
|
224
|
+
<form id="layouts-form">
|
|
225
|
+
<label for="layout-slide">Diapositiva</label><select id="layout-slide"></select>
|
|
226
|
+
<label for="layout-name">Diseño</label><select id="layout-name"></select>
|
|
227
|
+
<div id="layout-objects"></div>
|
|
228
|
+
<p id="layout-note" role="status"></p><pre id="layout-preview" hidden></pre>
|
|
229
|
+
<button id="layout-preview-button" type="button">Vista previa de posiciones</button>
|
|
230
|
+
<button id="layout-apply" type="submit" disabled>Aplicar a los objetos seleccionados</button>
|
|
231
|
+
<button id="layout-close" type="button">Cerrar</button>
|
|
232
|
+
</form>
|
|
233
|
+
</dialog>
|
|
234
|
+
<dialog id="relations-dialog" aria-labelledby="relations-title">
|
|
235
|
+
<h2 id="relations-title">Relaciones de diagramas QMD</h2>
|
|
236
|
+
<p>Elige los nodos por su etiqueta e identidad fuente. Guardar conserva un cambio pendiente; la revisión posterior es el paso que modifica QMD.</p>
|
|
237
|
+
<form id="relation-form">
|
|
238
|
+
<p><label for="relation-slide">Diapositiva</label> <select id="relation-slide" required></select></p>
|
|
239
|
+
<p><label for="relation-graph">Diagrama o secuencia</label> <select id="relation-graph" required></select></p>
|
|
240
|
+
<p><label for="relation-edge">Conexión</label> <select id="relation-edge" required></select></p>
|
|
241
|
+
<p><label for="relation-from">Desde</label> <select id="relation-from" required></select></p>
|
|
242
|
+
<p><label for="relation-to">Hacia</label> <select id="relation-to" required></select></p>
|
|
243
|
+
<p id="relation-sequence"></p>
|
|
244
|
+
<p id="relation-note" role="status" aria-live="polite"></p>
|
|
245
|
+
<button id="relation-save" type="submit">Guardar relación pendiente</button> <button id="relation-close" type="button">Cerrar</button>
|
|
246
|
+
</form>
|
|
247
|
+
</dialog>
|
|
248
|
+
<dialog
|
|
249
|
+
id="reconcile-dialog"><h2>Comparar QMD y Bento</h2><p>La comparación ejecuta el QMD. Elige cómo resolver cada diferencia; las demás seguirán pendientes. Estas decisiones guardan el proyecto. Escribir el QMD requiere después revisar y aplicar la sincronización.</p><p id="reconcile-note" role="status"></p><div id="reconcile-changes"></div><button id="resolve" disabled>Guardar decisiones</button> <button id="resolve-close">Cerrar</button></dialog>
|
|
250
|
+
<dialog id="component-dialog" aria-labelledby="component-title">
|
|
251
|
+
<h2 id="component-title">Añadir componente</h2>
|
|
252
|
+
<p>Crea una tarjeta o un indicador con campos escritos. Se guarda en el proyecto; Revisar cambios permite insertar después su contenido completo en QMD. Puedes editar y diseñar los objetos en Bento.</p>
|
|
253
|
+
<p>El valor del indicador es texto escrito. Para un valor calculado, escribe su expresión en QMD.</p>
|
|
254
|
+
<form id="component-form">
|
|
255
|
+
<p><label for="component-slide">Diapositiva</label> <select id="component-slide" required></select></p>
|
|
256
|
+
<p><label for="component-kind">Componente</label> <select id="component-kind"><option value="card">Tarjeta</option><option value="kpi">Indicador</option></select></p>
|
|
257
|
+
<div id="component-fields"></div>
|
|
258
|
+
<p id="component-note" role="status" aria-live="polite"></p>
|
|
259
|
+
<button id="component-create" type="submit">Crear componente</button> <button id="component-close" type="button">Cerrar</button>
|
|
260
|
+
</form>
|
|
261
|
+
<h3 id="component-pending-title" hidden>Componentes pendientes</h3>
|
|
262
|
+
<div id="component-pending"></div>
|
|
263
|
+
</dialog>
|
|
264
|
+
<dialog id="registry-dialog" aria-labelledby="registry-title">
|
|
265
|
+
<h2 id="registry-title">Layouts y temas del proyecto</h2>
|
|
266
|
+
<p>Guardar diseños actualiza el registro del proyecto; no cambia QMD ni ejecuta cálculos. Recalcular aplica los valores seleccionados y conserva tu diseño guardado y los cambios pendientes.</p>
|
|
267
|
+
<p>Los layouts se usan desde QMD con <code>bento-layout-name</code>. Renombrar o eliminar un layout no actualiza sus referencias en QMD; revísalas antes de recalcular. <code>two-column</code> sigue disponible como layout integrado.</p>
|
|
268
|
+
<form id="registry-form">
|
|
269
|
+
<h3>Layouts con nombre</h3><div id="registry-layouts"></div><button id="registry-add-layout" type="button">Añadir layout</button>
|
|
270
|
+
<h3>Temas con nombre</h3><div id="registry-themes"></div><button id="registry-add-theme" type="button">Añadir tema</button>
|
|
271
|
+
<p><label for="registry-active">Tema activo</label> <select id="registry-active"></select></p>
|
|
272
|
+
<p id="registry-note" role="status" aria-live="polite"></p>
|
|
273
|
+
<button id="registry-save" type="submit">Guardar diseños</button> <button id="registry-reload" type="button">Descartar borrador y recargar registro</button> <button id="registry-close" type="button">Cerrar</button>
|
|
274
|
+
</form>
|
|
275
|
+
</dialog>
|
|
276
|
+
<dialog
|
|
277
|
+
id="presenter-dialog"><button id="presenter-close">Cerrar presentación</button><iframe id="presenter-frame" title="Presenter con gráficos interactivos"></iframe></dialog>
|
|
278
|
+
<script>
|
|
279
|
+
globalThis.qbentoToken = __QBENTO_TOKEN__;
|
|
280
|
+
</script>
|
|
281
|
+
<script type="module" src="/client.js"></script>
|
|
282
|
+
</body>
|
|
283
|
+
</html>
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/** Explicit object-to-column assignments; preview and save use the same core operation. */
|
|
2
|
+
export function attachLayoutEditor(
|
|
3
|
+
{ api, getState, setBusy, showError, saveBefore, reloadEditor },
|
|
4
|
+
) {
|
|
5
|
+
const $ = (id) => document.getElementById(id);
|
|
6
|
+
let snapshot, prepared, working = false;
|
|
7
|
+
function options(select, items) {
|
|
8
|
+
select.replaceChildren();
|
|
9
|
+
for (const [value, label] of items) {
|
|
10
|
+
const option = document.createElement("option");
|
|
11
|
+
option.value = value;
|
|
12
|
+
option.textContent = label;
|
|
13
|
+
select.append(option);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function controls(value) {
|
|
17
|
+
working = value;
|
|
18
|
+
for (
|
|
19
|
+
const control of $("layouts-dialog").querySelectorAll("button,select")
|
|
20
|
+
) control.disabled = value;
|
|
21
|
+
$("layout-apply").disabled = value || !prepared;
|
|
22
|
+
}
|
|
23
|
+
function invalidate() {
|
|
24
|
+
prepared = undefined;
|
|
25
|
+
$("layout-preview").hidden = true;
|
|
26
|
+
$("layout-note").textContent =
|
|
27
|
+
"Asigna al menos un objeto a cada columna y revisa las posiciones antes de aplicar.";
|
|
28
|
+
controls(false);
|
|
29
|
+
}
|
|
30
|
+
function objects() {
|
|
31
|
+
const slide = snapshot.slides.find((s) => s.id === $("layout-slide").value);
|
|
32
|
+
const layout = snapshot.layouts[$("layout-name").value];
|
|
33
|
+
$("layout-objects").replaceChildren();
|
|
34
|
+
for (const element of slide?.elements ?? []) {
|
|
35
|
+
const row = document.createElement("p"),
|
|
36
|
+
label = document.createElement("label"),
|
|
37
|
+
select = document.createElement("select");
|
|
38
|
+
label.textContent =
|
|
39
|
+
`${element.label} · ${element.type} · #${element.id} `;
|
|
40
|
+
select.dataset.elementId = element.id;
|
|
41
|
+
select.setAttribute("aria-label", `Columna para ${element.id}`);
|
|
42
|
+
options(select, [
|
|
43
|
+
["", "Conservar posición"],
|
|
44
|
+
...Array.from(
|
|
45
|
+
{ length: layout.columns },
|
|
46
|
+
(_, index) => [String(index), `Columna ${index + 1}`],
|
|
47
|
+
),
|
|
48
|
+
]);
|
|
49
|
+
select.onchange = invalidate;
|
|
50
|
+
label.append(select);
|
|
51
|
+
row.append(label);
|
|
52
|
+
$("layout-objects").append(row);
|
|
53
|
+
}
|
|
54
|
+
invalidate();
|
|
55
|
+
}
|
|
56
|
+
function request() {
|
|
57
|
+
const current = getState();
|
|
58
|
+
if (
|
|
59
|
+
snapshot.docId !== current?.docId ||
|
|
60
|
+
snapshot.revision !== current?.revision
|
|
61
|
+
) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"El proyecto cambió. Cierra y vuelve a abrir Aplicar diseño.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const columns = Array.from({
|
|
67
|
+
length: snapshot.layouts[$("layout-name").value].columns,
|
|
68
|
+
}, () => []);
|
|
69
|
+
for (const select of $("layout-objects").querySelectorAll("select")) {
|
|
70
|
+
if (select.value !== "") {
|
|
71
|
+
columns[Number(select.value)].push(select.dataset.elementId);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
expectedDocId: snapshot.docId,
|
|
76
|
+
expectedRevision: snapshot.revision,
|
|
77
|
+
expectedSourceHash: snapshot.sourceHash,
|
|
78
|
+
expectedRegistryHash: snapshot.registryHash,
|
|
79
|
+
slideId: $("layout-slide").value,
|
|
80
|
+
layout: $("layout-name").value,
|
|
81
|
+
columns,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function open() {
|
|
85
|
+
if (working || $("layouts-dialog").open) return;
|
|
86
|
+
controls(true);
|
|
87
|
+
setBusy(true);
|
|
88
|
+
$("busy").textContent = "Guardando cambios antes de aplicar un diseño…";
|
|
89
|
+
try {
|
|
90
|
+
await saveBefore();
|
|
91
|
+
snapshot = await api("layouts");
|
|
92
|
+
if (
|
|
93
|
+
snapshot.docId !== getState()?.docId ||
|
|
94
|
+
snapshot.revision !== getState()?.revision
|
|
95
|
+
) throw new Error("El proyecto cambió; vuelve a abrir Aplicar diseño.");
|
|
96
|
+
options(
|
|
97
|
+
$("layout-slide"),
|
|
98
|
+
snapshot.slides.map((s) => [s.id, `${s.label} · #${s.id}`]),
|
|
99
|
+
);
|
|
100
|
+
options(
|
|
101
|
+
$("layout-name"),
|
|
102
|
+
Object.entries(snapshot.layouts).map((
|
|
103
|
+
[id, layout],
|
|
104
|
+
) => [
|
|
105
|
+
id,
|
|
106
|
+
`${
|
|
107
|
+
id === "two-column" ? "Dos columnas" : id
|
|
108
|
+
} · ${layout.columns} columnas`,
|
|
109
|
+
]),
|
|
110
|
+
);
|
|
111
|
+
objects();
|
|
112
|
+
$("layouts-dialog").showModal();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
showError(error);
|
|
115
|
+
} finally {
|
|
116
|
+
controls(false);
|
|
117
|
+
setBusy(false);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
$("layouts").onclick = open;
|
|
121
|
+
$("layout-slide").onchange = objects;
|
|
122
|
+
$("layout-name").onchange = objects;
|
|
123
|
+
$("layout-close").onclick = () => $("layouts-dialog").close();
|
|
124
|
+
$("layouts-dialog").addEventListener("cancel", (event) => {
|
|
125
|
+
if (working) event.preventDefault();
|
|
126
|
+
});
|
|
127
|
+
$("layout-preview-button").onclick = async () => {
|
|
128
|
+
if (working) return;
|
|
129
|
+
try {
|
|
130
|
+
const candidate = request();
|
|
131
|
+
controls(true);
|
|
132
|
+
const preview = await api("layouts/preview", candidate);
|
|
133
|
+
prepared = candidate;
|
|
134
|
+
$("layout-preview").textContent = preview.placements.map((p) =>
|
|
135
|
+
`#${p.id}: x=${p.x.toFixed(1)}, y=${p.y.toFixed(1)}, ancho=${
|
|
136
|
+
p.w.toFixed(1)
|
|
137
|
+
}, alto=${p.h.toFixed(1)}`
|
|
138
|
+
).join("\n");
|
|
139
|
+
$("layout-preview").hidden = false;
|
|
140
|
+
$("layout-note").textContent =
|
|
141
|
+
`Se moverán ${preview.placements.length} objetos. Pendientes después de aplicar: ${preview.pending}. El contenido y los identificadores se conservan.`;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
prepared = undefined;
|
|
144
|
+
$("layout-note").textContent = error.message;
|
|
145
|
+
} finally {
|
|
146
|
+
controls(false);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
$("layouts-form").onsubmit = async (event) => {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
if (working || !prepared) return;
|
|
152
|
+
try {
|
|
153
|
+
request();
|
|
154
|
+
controls(true);
|
|
155
|
+
setBusy(true);
|
|
156
|
+
$("busy").textContent =
|
|
157
|
+
"Aplicando el diseño a los objetos seleccionados…";
|
|
158
|
+
await api("layouts/apply", prepared);
|
|
159
|
+
$("layouts-dialog").close();
|
|
160
|
+
prepared = undefined;
|
|
161
|
+
controls(false);
|
|
162
|
+
reloadEditor();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
$("layout-note").textContent = error.message;
|
|
165
|
+
controls(false);
|
|
166
|
+
setBusy(false);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
return { open };
|
|
170
|
+
}
|