@markdstage/markdstage 3.4.0 → 3.8.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/README.md +2 -1
- package/package.json +1 -1
- package/shared/README.md +40 -4
- package/shared/architecture-editor/editor.css +9 -5
- package/shared/architecture-editor/editor.js +440 -75
- package/shared/architecture-editor/index.html +2 -2
- package/shared/docs/custom-theme-authoring.md +61 -4
- package/shared/markdown-deck.mjs +9 -5
- package/shared/renderer/architecture-document.mjs +169 -10
- package/shared/renderer/index.html +26 -3
- package/shared/renderer/mermaid-scene.mjs +6725 -197
- package/shared/renderer/renderer.js +421 -102
- package/shared/renderer/scene-graph.mjs +83 -13
- package/shared/renderer/scene-pptx.mjs +154 -1
- package/shared/renderer/scene-svg.mjs +227 -11
- package/shared/renderer/slide-background.mjs +22 -0
- package/shared/renderer/slide-viewport.mjs +48 -0
- package/shared/renderer/slides.css +41 -10
- package/shared/renderer/theme.mjs +328 -12
- package/shared/runtime/browser.mjs +75 -5
- package/shared/runtime/deck-session.mjs +12 -17
- package/shared/runtime/output-paths.mjs +7 -0
- package/shared/runtime/output.mjs +4 -2
- package/shared/runtime/pptx-package.mjs +103 -22
- package/shared/runtime/presentation-server.mjs +44 -2
- package/shared/runtime/slide-backgrounds.mjs +44 -0
- package/shared/schema/theme-metadata-v1.schema.json +25 -0
- package/shared/schema/theme-v1.json +3 -3
- package/src/cli.mjs +9 -2
- package/src/commands/export.mjs +2 -0
|
@@ -67,10 +67,10 @@
|
|
|
67
67
|
<h2>Elements</h2>
|
|
68
68
|
<button class="editor-panel-close" type="button" data-panel-close="elements" aria-label="Close Elements">×</button>
|
|
69
69
|
</div>
|
|
70
|
-
<div id="elementTree" class="element-tree" role="tree"></div>
|
|
70
|
+
<div id="elementTree" class="element-tree" role="tree" aria-label="Diagram elements" aria-multiselectable="true"></div>
|
|
71
71
|
</aside>
|
|
72
72
|
|
|
73
|
-
<section id="viewport" class="editor-viewport" aria-label="Drawing area">
|
|
73
|
+
<section id="viewport" class="editor-viewport" aria-label="Drawing area" tabindex="-1">
|
|
74
74
|
<div id="canvasSurface" class="canvas-surface"></div>
|
|
75
75
|
</section>
|
|
76
76
|
|
|
@@ -6,7 +6,8 @@ MarkdStage canvas and for AI systems that generate themes.
|
|
|
6
6
|
## Minimum setup
|
|
7
7
|
|
|
8
8
|
Themes can be managed by folder. Put only custom properties in the CSS, and
|
|
9
|
-
define optional cover and back-cover assets in `theme.json`
|
|
9
|
+
define optional slide backgrounds, cover and back-cover assets in `theme.json`
|
|
10
|
+
in the same folder.
|
|
10
11
|
|
|
11
12
|
```markdown
|
|
12
13
|
---
|
|
@@ -113,7 +114,8 @@ supporting brand colors.
|
|
|
113
114
|
`--section-bg` and `--print-section-bg` are used as CSS `background` values.
|
|
114
115
|
They support solid colors and multiple comma-separated gradients. Theme-file
|
|
115
116
|
security restrictions prohibit `url()`. Section dividers have no image, logo,
|
|
116
|
-
or icon settings in `theme.json
|
|
117
|
+
or icon settings in `theme.json`; use per-slide `background-image` for a section
|
|
118
|
+
background image.
|
|
117
119
|
|
|
118
120
|
## theme.json
|
|
119
121
|
|
|
@@ -124,6 +126,11 @@ paths relative to `theme.json` using the `assets/...` form.
|
|
|
124
126
|
{
|
|
125
127
|
"$schema": "../../.github/extensions/markdstage/schema/theme-metadata-v1.schema.json",
|
|
126
128
|
"version": 1,
|
|
129
|
+
"background": { "image": "assets/common.png" },
|
|
130
|
+
"layouts": {
|
|
131
|
+
"default": { "background": { "image": "assets/default.webp" } },
|
|
132
|
+
"center": { "background": { "image": "assets/center.jpg" } }
|
|
133
|
+
},
|
|
127
134
|
"cover": {
|
|
128
135
|
"background": { "image": "assets/cover.svg" },
|
|
129
136
|
"logo": { "image": "assets/logo.svg", "alt": "Example" }
|
|
@@ -135,15 +142,65 @@ paths relative to `theme.json` using the `assets/...` form.
|
|
|
135
142
|
}
|
|
136
143
|
```
|
|
137
144
|
|
|
138
|
-
- `
|
|
145
|
+
- `background`, `layouts.default.background`, `layouts.center.background`, and
|
|
146
|
+
`cover.background` are decorative `{ "image": "assets/...", "alt": "..." }`
|
|
147
|
+
entries; `alt` is optional.
|
|
148
|
+
- The common root `background` applies only to standard (`default`) and `center`
|
|
149
|
+
layouts. Each layout's own `background` takes precedence over the common one.
|
|
150
|
+
- `title` continues to use `cover.background`; `section` and `backcover` do not
|
|
151
|
+
inherit the common background. Existing cover and back-cover logos are unchanged.
|
|
139
152
|
- Logo `alt` text is required.
|
|
140
|
-
- Supported
|
|
153
|
+
- Supported extensions are `.svg`, `.png`, `.webp`, `.jpg`, and `.jpeg`, with a
|
|
154
|
+
2 MiB limit per file.
|
|
141
155
|
- Absolute paths, external URLs, `..`, and symbolic links outside the theme
|
|
142
156
|
folder are rejected.
|
|
143
157
|
- If an existing `theme.json` is invalid, loading returns an error rather than
|
|
144
158
|
silently falling back to CSS only.
|
|
145
159
|
- Slide-front-matter `logo` / `copyright` values override back-cover metadata.
|
|
146
160
|
|
|
161
|
+
### Per-slide background images
|
|
162
|
+
|
|
163
|
+
Every theme (`dark`, `light`, `microsoft`, and `custom`) and every layout,
|
|
164
|
+
including `title`, `section`, and `backcover`, accepts this front matter:
|
|
165
|
+
|
|
166
|
+
```markdown
|
|
167
|
+
---
|
|
168
|
+
layout: center
|
|
169
|
+
background-image: /assets/background.png
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## One decision
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The `assets/background.png` form without the leading slash is also accepted.
|
|
176
|
+
This override applies only to that slide, even in the file's initial front
|
|
177
|
+
matter. Paths use the normal image lookup: first `assets/` beside the source
|
|
178
|
+
Markdown, then workspace-root `assets/`. Without a source name, only the
|
|
179
|
+
workspace root is searched. These are deck assets, not theme-folder assets.
|
|
180
|
+
Only `.svg`, `.png`, `.webp`, `.jpg`, and `.jpeg` files of at most 2 MiB are
|
|
181
|
+
accepted. Remote URLs and `data:` URLs are not allowed.
|
|
182
|
+
Write literal filenames, including spaces and percent signs, with `/`
|
|
183
|
+
separators. Source paths are not URL-decoded: `%20` names those three literal
|
|
184
|
+
characters, not a space. Query strings, fragments, `.` / `..` segments, and
|
|
185
|
+
backslashes are rejected. Resolved files must remain inside their assets
|
|
186
|
+
folder and workspace, including when symbolic links are involved.
|
|
187
|
+
|
|
188
|
+
Background selection, from highest to lowest priority:
|
|
189
|
+
|
|
190
|
+
| Layout | Image selection |
|
|
191
|
+
| --- | --- |
|
|
192
|
+
| Standard (`default`) | Slide `background-image` → `layouts.default.background` → root `background` → existing background |
|
|
193
|
+
| `center` | Slide `background-image` → `layouts.center.background` → root `background` → existing background |
|
|
194
|
+
| `title` | Slide `background-image` → `cover.background` → existing cover background |
|
|
195
|
+
| `section` / `backcover` | Slide `background-image` → existing layout background |
|
|
196
|
+
|
|
197
|
+
Images are centered and cropped to fill the slide (`object-fit: cover`) behind
|
|
198
|
+
content, diagrams, and logos, with the existing background color or gradient
|
|
199
|
+
underneath. No extra overlay or per-layout color properties are introduced.
|
|
200
|
+
Only an absent setting triggers fallback: an invalid value, missing file, or
|
|
201
|
+
oversized image is an error, not a reason to silently substitute another image.
|
|
202
|
+
Omitting all image settings preserves CSS-only themes and existing output.
|
|
203
|
+
|
|
147
204
|
### Sizing and spacing
|
|
148
205
|
|
|
149
206
|
You may change `--deck-pad-y`, `--deck-pad-x`, `--slide-h1-size`,
|
package/shared/markdown-deck.mjs
CHANGED
|
@@ -24,7 +24,8 @@ const META_COMMENT = /^[ \t]*#/;
|
|
|
24
24
|
// - layout: Inheritance would make every page a cover or back cover. The leading
|
|
25
25
|
// front matter is also the first slide's front matter, so it still applies there.
|
|
26
26
|
// - page: Sequence numbers are slide-specific; a deck-wide value has no meaning.
|
|
27
|
-
|
|
27
|
+
// - background-image: An explicit image overrides only its own slide's background.
|
|
28
|
+
const NON_INHERITED_KEYS = new Set(["layout", "page", "background-image"]);
|
|
28
29
|
|
|
29
30
|
// Layouts that do not receive automatic page numbers (cover, section divider, and back cover).
|
|
30
31
|
const UNNUMBERED_LAYOUTS = new Set(["title", "section", "backcover"]);
|
|
@@ -177,8 +178,8 @@ export function splitMarkdownDeck(text) {
|
|
|
177
178
|
* Convert a Markdown file to slide fragments the extension can render directly.
|
|
178
179
|
*
|
|
179
180
|
* - Inherit shared deck front matter on each slide; slide-level values take precedence.
|
|
180
|
-
* - Do not inherit `layout`. Leading file front matter also
|
|
181
|
-
* slide, so
|
|
181
|
+
* - Do not inherit `layout` or `background-image`. Leading file front matter also
|
|
182
|
+
* belongs to the first slide, so these values still apply there.
|
|
182
183
|
* - Add `page` / `total` automatically only when neither the deck nor slide
|
|
183
184
|
* specifies them. Do not display numbers on covers, section dividers, or back
|
|
184
185
|
* covers, although they still participate in sequence numbering.
|
|
@@ -190,8 +191,11 @@ export function buildDeckSlides(text) {
|
|
|
190
191
|
const merged = slides.map((slide, i) => {
|
|
191
192
|
const meta = new Map();
|
|
192
193
|
for (const [key, entry] of deckMeta) {
|
|
193
|
-
//
|
|
194
|
-
if (
|
|
194
|
+
// Preserve the first slide's layout and background, but never inherit page numbers.
|
|
195
|
+
if (
|
|
196
|
+
NON_INHERITED_KEYS.has(key) &&
|
|
197
|
+
!(i === 0 && (key === "layout" || key === "background-image"))
|
|
198
|
+
) continue;
|
|
195
199
|
meta.set(key, entry);
|
|
196
200
|
}
|
|
197
201
|
for (const [key, entry] of slide.meta) meta.set(key, entry);
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
resolveRawElement,
|
|
10
10
|
serializeArchitecture,
|
|
11
11
|
} from "./architecture-edit.mjs";
|
|
12
|
+
import { architectureContract } from "./architecture-contract.mjs";
|
|
12
13
|
|
|
13
14
|
const HISTORY_LIMIT = 200;
|
|
14
15
|
const COORDINATE_MIN = -4000;
|
|
@@ -143,8 +144,50 @@ function setNested(target, path, value) {
|
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
function
|
|
147
|
-
|
|
147
|
+
function operationRoots(entries) {
|
|
148
|
+
return entries.filter((entry) => !entries.some(
|
|
149
|
+
(ancestor) => ancestor.element.type === "group" &&
|
|
150
|
+
entry.sourcePath.startsWith(`${ancestor.sourcePath}.children[`),
|
|
151
|
+
));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function updateProperty(element, path, value) {
|
|
155
|
+
setNested(element, path, value);
|
|
156
|
+
if (element.type === "connector" && path === "routing" && value !== "polyline") {
|
|
157
|
+
delete element.points;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function supportsBatchProperty(element, path) {
|
|
162
|
+
if (typeof path !== "string") return false;
|
|
163
|
+
const [field, nested, ...rest] = path.split(".");
|
|
164
|
+
if (["id", "type", "children", "parent", "layout"].includes(field)) return false;
|
|
165
|
+
const properties = architectureContract.elements[element.type]?.properties;
|
|
166
|
+
if (!properties || !Object.hasOwn(properties, field)) return false;
|
|
167
|
+
if (nested === undefined) return true;
|
|
168
|
+
return field === "style" && rest.length === 0 &&
|
|
169
|
+
Object.hasOwn(architectureContract.definitions.style.properties, nested);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function finiteValues(value) {
|
|
173
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
174
|
+
if (value && typeof value === "object") return Object.values(value).every(finiteValues);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function sharedDisplacement(boxes, dx, dy) {
|
|
179
|
+
return {
|
|
180
|
+
dx: round(clamp(dx,
|
|
181
|
+
Math.max(...boxes.map((box) => COORDINATE_MIN - box.x)),
|
|
182
|
+
Math.min(...boxes.map((box) => COORDINATE_MAX - box.x)))),
|
|
183
|
+
dy: round(clamp(dy,
|
|
184
|
+
Math.max(...boxes.map((box) => COORDINATE_MIN - box.y)),
|
|
185
|
+
Math.min(...boxes.map((box) => COORDINATE_MAX - box.y)))),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function remapCloneIds(raw, elements) {
|
|
190
|
+
const copies = clone(elements);
|
|
148
191
|
const mapping = new Map();
|
|
149
192
|
const reserved = new Set(rawEntries(raw).map((entry) => entry.element.id).filter(Boolean));
|
|
150
193
|
const reserveId = (base) => {
|
|
@@ -170,7 +213,7 @@ function remapCloneIds(raw, element) {
|
|
|
170
213
|
}
|
|
171
214
|
if (Array.isArray(item.children)) item.children.forEach(reserve);
|
|
172
215
|
};
|
|
173
|
-
reserve
|
|
216
|
+
copies.forEach(reserve);
|
|
174
217
|
const updateConnectors = (item) => {
|
|
175
218
|
if (!item || typeof item !== "object") return;
|
|
176
219
|
if (item.type === "connector") {
|
|
@@ -179,8 +222,8 @@ function remapCloneIds(raw, element) {
|
|
|
179
222
|
}
|
|
180
223
|
if (Array.isArray(item.children)) item.children.forEach(updateConnectors);
|
|
181
224
|
};
|
|
182
|
-
updateConnectors
|
|
183
|
-
return
|
|
225
|
+
copies.forEach(updateConnectors);
|
|
226
|
+
return copies;
|
|
184
227
|
}
|
|
185
228
|
|
|
186
229
|
export function createArchitectureDocument(source, options = {}) {
|
|
@@ -207,6 +250,9 @@ export function createArchitectureDocument(source, options = {}) {
|
|
|
207
250
|
}
|
|
208
251
|
|
|
209
252
|
function commit(raw, reason, details = {}) {
|
|
253
|
+
if (JSON.stringify(raw) === JSON.stringify(current().raw)) {
|
|
254
|
+
return reject("unchanged", details);
|
|
255
|
+
}
|
|
210
256
|
const sourceText = serializeArchitecture(raw);
|
|
211
257
|
let next;
|
|
212
258
|
try {
|
|
@@ -233,6 +279,30 @@ export function createArchitectureDocument(source, options = {}) {
|
|
|
233
279
|
return commit(raw, reason, details);
|
|
234
280
|
}
|
|
235
281
|
|
|
282
|
+
function selection(raw, refs) {
|
|
283
|
+
if (!Array.isArray(refs)) return reject("invalid-selection", { refs: [] });
|
|
284
|
+
if (!refs.length) return reject("empty-selection", { refs: [] });
|
|
285
|
+
const entries = [];
|
|
286
|
+
const seen = new Set();
|
|
287
|
+
for (const ref of refs) {
|
|
288
|
+
const entry = typeof ref === "string" ? rawEntry(raw, ref) : null;
|
|
289
|
+
if (!entry) return reject("unknown", { refs, invalidRef: ref });
|
|
290
|
+
if (seen.has(entry.element)) continue;
|
|
291
|
+
seen.add(entry.element);
|
|
292
|
+
entries.push(entry);
|
|
293
|
+
}
|
|
294
|
+
return { entries, refs: entries.map((entry) => entry.ref) };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function mutateMany(reason, refs, mutator) {
|
|
298
|
+
const edited = mutate(reason, (raw) => {
|
|
299
|
+
const selected = selection(raw, refs);
|
|
300
|
+
if (selected.ok === false) return selected;
|
|
301
|
+
return mutator(raw, selected);
|
|
302
|
+
});
|
|
303
|
+
return { refs: Array.isArray(refs) ? refs : [], ...edited };
|
|
304
|
+
}
|
|
305
|
+
|
|
236
306
|
function describe(ref) {
|
|
237
307
|
const element = modelElement(current().model, ref);
|
|
238
308
|
if (!element) return { found: false, movable: false, reason: "unknown", ref };
|
|
@@ -266,14 +336,28 @@ export function createArchitectureDocument(source, options = {}) {
|
|
|
266
336
|
return mutate("element-updated", (raw) => {
|
|
267
337
|
const entry = rawEntry(raw, ref);
|
|
268
338
|
if (!entry) return reject("unknown", { ref });
|
|
269
|
-
|
|
270
|
-
if (entry.element.type === "connector" && path === "routing" && value !== "polyline") {
|
|
271
|
-
delete entry.element.points;
|
|
272
|
-
}
|
|
339
|
+
updateProperty(entry.element, path, value);
|
|
273
340
|
return { ref: entry.element.id || entry.sourcePath, path, value };
|
|
274
341
|
});
|
|
275
342
|
}
|
|
276
343
|
|
|
344
|
+
function setElements(refs, path, value) {
|
|
345
|
+
return mutateMany("elements-updated", refs, (_raw, selected) => {
|
|
346
|
+
if (!finiteValues(value)) return reject("invalid-value", { refs: selected.refs, path });
|
|
347
|
+
for (const entry of selected.entries) {
|
|
348
|
+
if (!supportsBatchProperty(entry.element, path)) {
|
|
349
|
+
return reject("unsupported-property", { refs: selected.refs, path });
|
|
350
|
+
}
|
|
351
|
+
if (["x", "y", "width", "height"].includes(path)) {
|
|
352
|
+
const placement = describePlacement(current().model, entry.element.id);
|
|
353
|
+
if (!placement.movable) return { ...placement, ok: false, refs: selected.refs };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
for (const entry of selected.entries) updateProperty(entry.element, path, value);
|
|
357
|
+
return { refs: selected.refs, path, value };
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
277
361
|
function renameElement(ref, id) {
|
|
278
362
|
const next = String(id || "").trim();
|
|
279
363
|
if (!ID_PATTERN.test(next)) return reject("invalid-id", { id: next });
|
|
@@ -311,6 +395,34 @@ export function createArchitectureDocument(source, options = {}) {
|
|
|
311
395
|
});
|
|
312
396
|
}
|
|
313
397
|
|
|
398
|
+
function moveMany(refs, dx, dy) {
|
|
399
|
+
return mutateMany("moved", refs, (_raw, selected) => {
|
|
400
|
+
if (!Number.isFinite(dx) || !Number.isFinite(dy)) {
|
|
401
|
+
return reject("invalid-displacement", { refs: selected.refs });
|
|
402
|
+
}
|
|
403
|
+
const roots = operationRoots(selected.entries)
|
|
404
|
+
.filter((entry) => entry.element.type !== "connector");
|
|
405
|
+
if (!roots.length) return reject("connector-only", { refs: selected.refs });
|
|
406
|
+
const boxes = [];
|
|
407
|
+
for (const entry of roots) {
|
|
408
|
+
const placement = describePlacement(current().model, entry.element.id);
|
|
409
|
+
if (!placement.movable) return { ...placement, ok: false, refs: selected.refs };
|
|
410
|
+
const element = modelElement(current().model, entry.ref);
|
|
411
|
+
boxes.push({
|
|
412
|
+
x: round(element.x - placement.origin.x),
|
|
413
|
+
y: round(element.y - placement.origin.y),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
const delta = sharedDisplacement(boxes, dx, dy);
|
|
417
|
+
if (!delta.dx && !delta.dy) return reject("unchanged", { refs: selected.refs, ...delta });
|
|
418
|
+
roots.forEach((entry, index) => {
|
|
419
|
+
entry.element.x = round(boxes[index].x + delta.dx);
|
|
420
|
+
entry.element.y = round(boxes[index].y + delta.dy);
|
|
421
|
+
});
|
|
422
|
+
return { refs: selected.refs, ...delta };
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
314
426
|
function resize(ref, box) {
|
|
315
427
|
const element = modelElement(current().model, ref);
|
|
316
428
|
if (!element || element.type === "connector") return reject("unknown", { ref });
|
|
@@ -452,6 +564,19 @@ export function createArchitectureDocument(source, options = {}) {
|
|
|
452
564
|
});
|
|
453
565
|
}
|
|
454
566
|
|
|
567
|
+
function removeMany(refs) {
|
|
568
|
+
return mutateMany("elements-deleted", refs, (raw, selected) => {
|
|
569
|
+
const roots = operationRoots(selected.entries);
|
|
570
|
+
const ids = new Set();
|
|
571
|
+
roots.forEach((entry) => collectIds(entry.element, ids));
|
|
572
|
+
for (const entry of roots) {
|
|
573
|
+
entry.items.splice(entry.items.indexOf(entry.element), 1);
|
|
574
|
+
}
|
|
575
|
+
if (ids.size) removeReferencingConnectors(raw.elements, ids);
|
|
576
|
+
return { refs: [], removedIds: [...ids] };
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
455
580
|
function duplicate(ref) {
|
|
456
581
|
return mutate("element-duplicated", (raw) => {
|
|
457
582
|
const entry = rawEntry(raw, ref);
|
|
@@ -459,7 +584,7 @@ export function createArchitectureDocument(source, options = {}) {
|
|
|
459
584
|
const copy =
|
|
460
585
|
entry.element.type === "connector"
|
|
461
586
|
? clone(entry.element)
|
|
462
|
-
: remapCloneIds(raw, entry.element);
|
|
587
|
+
: remapCloneIds(raw, [entry.element])[0];
|
|
463
588
|
if (typeof copy.x === "number") copy.x = clamp(copy.x + 24, COORDINATE_MIN, COORDINATE_MAX);
|
|
464
589
|
if (typeof copy.y === "number") copy.y = clamp(copy.y + 24, COORDINATE_MIN, COORDINATE_MAX);
|
|
465
590
|
entry.items.splice(entry.index + 1, 0, copy);
|
|
@@ -468,6 +593,36 @@ export function createArchitectureDocument(source, options = {}) {
|
|
|
468
593
|
});
|
|
469
594
|
}
|
|
470
595
|
|
|
596
|
+
function duplicateMany(refs) {
|
|
597
|
+
return mutateMany("elements-duplicated", refs, (raw, selected) => {
|
|
598
|
+
const roots = operationRoots(selected.entries);
|
|
599
|
+
const copies = remapCloneIds(raw, roots.map((entry) => entry.element));
|
|
600
|
+
const boxes = roots.map((entry) => {
|
|
601
|
+
if (entry.element.type === "connector") return null;
|
|
602
|
+
const placement = describePlacement(current().model, entry.element.id);
|
|
603
|
+
if (!placement.movable) return null;
|
|
604
|
+
const element = modelElement(current().model, entry.ref);
|
|
605
|
+
return {
|
|
606
|
+
x: round(element.x - placement.origin.x),
|
|
607
|
+
y: round(element.y - placement.origin.y),
|
|
608
|
+
};
|
|
609
|
+
});
|
|
610
|
+
const movableBoxes = boxes.filter(Boolean);
|
|
611
|
+
const delta = movableBoxes.length ? sharedDisplacement(movableBoxes, 24, 24) : { dx: 0, dy: 0 };
|
|
612
|
+
roots.forEach((entry, index) => {
|
|
613
|
+
const copy = copies[index];
|
|
614
|
+
if (boxes[index]) {
|
|
615
|
+
copy.x = round(boxes[index].x + delta.dx);
|
|
616
|
+
copy.y = round(boxes[index].y + delta.dy);
|
|
617
|
+
}
|
|
618
|
+
entry.items.splice(entry.items.indexOf(entry.element) + 1, 0, copy);
|
|
619
|
+
});
|
|
620
|
+
// Source paths belong to the final arrays, not the insertion-time indexes.
|
|
621
|
+
const inserted = new Map(rawEntries(raw).map((entry) => [entry.element, entry.ref]));
|
|
622
|
+
return { refs: copies.map((copy) => inserted.get(copy)), ...delta };
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
471
626
|
function reorder(ref, delta) {
|
|
472
627
|
return mutate("element-reordered", (raw) => {
|
|
473
628
|
const entry = rawEntry(raw, ref);
|
|
@@ -569,15 +724,19 @@ export function createArchitectureDocument(source, options = {}) {
|
|
|
569
724
|
describe,
|
|
570
725
|
setRoot,
|
|
571
726
|
setElement,
|
|
727
|
+
setElements,
|
|
572
728
|
renameElement,
|
|
573
729
|
move,
|
|
730
|
+
moveMany,
|
|
574
731
|
resize,
|
|
575
732
|
addNode,
|
|
576
733
|
addGroup,
|
|
577
734
|
addImage,
|
|
578
735
|
addConnector,
|
|
579
736
|
remove,
|
|
737
|
+
removeMany,
|
|
580
738
|
duplicate,
|
|
739
|
+
duplicateMany,
|
|
581
740
|
reorder,
|
|
582
741
|
reparent,
|
|
583
742
|
releaseLayout,
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
</button>
|
|
68
68
|
<button id="navExportPptx" class="nav-more-item" type="button" title="Save as editable PowerPoint" aria-label="Save as editable PowerPoint" hidden>
|
|
69
69
|
<span class="nav-more-icon" aria-hidden="true">P</span>
|
|
70
|
-
<span class="nav-more-label">Export PowerPoint
|
|
70
|
+
<span class="nav-more-label">Export PowerPoint…</span>
|
|
71
71
|
</button>
|
|
72
72
|
</section>
|
|
73
73
|
</div>
|
|
@@ -85,6 +85,29 @@
|
|
|
85
85
|
</section>
|
|
86
86
|
<div id="layoutWarning" class="layout-warning" role="status" aria-live="polite" hidden></div>
|
|
87
87
|
|
|
88
|
+
<dialog id="pptxExportDialog" class="pptx-export-dialog" aria-labelledby="pptxExportTitle">
|
|
89
|
+
<form id="pptxExportForm">
|
|
90
|
+
<h2 id="pptxExportTitle">Export PowerPoint</h2>
|
|
91
|
+
<fieldset>
|
|
92
|
+
<legend>Mermaid diagrams</legend>
|
|
93
|
+
<label class="pptx-export-choice">
|
|
94
|
+
<input type="radio" name="mermaidOutput" value="shapes" checked autofocus aria-describedby="pptxShapesDescription">
|
|
95
|
+
<span>Editable shapes</span>
|
|
96
|
+
</label>
|
|
97
|
+
<p id="pptxShapesDescription">Edit shapes and text in PowerPoint. Unsupported details become images.</p>
|
|
98
|
+
<label class="pptx-export-choice">
|
|
99
|
+
<input type="radio" name="mermaidOutput" value="images" aria-describedby="pptxImagesDescription">
|
|
100
|
+
<span>Images</span>
|
|
101
|
+
</label>
|
|
102
|
+
<p id="pptxImagesDescription">Keep each diagram as one image. Text and lines within it cannot be edited individually.</p>
|
|
103
|
+
</fieldset>
|
|
104
|
+
<div class="pptx-export-actions">
|
|
105
|
+
<button id="pptxExportCancel" class="presenter-button" type="button">Cancel</button>
|
|
106
|
+
<button class="presenter-button pptx-export-submit" type="submit">Export</button>
|
|
107
|
+
</div>
|
|
108
|
+
</form>
|
|
109
|
+
</dialog>
|
|
110
|
+
|
|
88
111
|
<section id="presenterView" class="presenter-view" hidden aria-label="Presenter view">
|
|
89
112
|
<header class="presenter-view-head">
|
|
90
113
|
<h1>Presenter view</h1>
|
|
@@ -93,12 +116,12 @@
|
|
|
93
116
|
<div class="presenter-previews">
|
|
94
117
|
<section class="presenter-preview presenter-preview-current" aria-labelledby="presenterCurrentLabel">
|
|
95
118
|
<h2 id="presenterCurrentLabel">Current slide</h2>
|
|
96
|
-
<
|
|
119
|
+
<div id="presenterCurrentViewport" class="presenter-frame slide-viewport"></div>
|
|
97
120
|
</section>
|
|
98
121
|
<div class="presenter-sidebar">
|
|
99
122
|
<section class="presenter-preview" aria-labelledby="presenterNextLabel">
|
|
100
123
|
<h2 id="presenterNextLabel">Next slide</h2>
|
|
101
|
-
<
|
|
124
|
+
<div id="presenterNextViewport" class="presenter-frame slide-viewport"></div>
|
|
102
125
|
<p id="presenterNextEmpty" class="presenter-next-empty" hidden>There is no next slide</p>
|
|
103
126
|
</section>
|
|
104
127
|
<section class="presenter-notes" aria-labelledby="presenterNotesLabel">
|