BPMN-Vorschau im Browser, offline, auf Basis von bpmn-js
- werkzeuge/vorschau/vorschau.html: Anzeige per Drag & Drop oder Dateiauswahl, Zoom, Einpassen, Fehlermeldungen; funktioniert ohne Python direkt aus dem Dateisystem. - scripts/vorschau.py: erzeugt aus .bpmn oder Arbeitsschritt-.md eine in sich geschlossene Seite und öffnet sie; --stdin als Filter, etwa für einen Gitea-Renderer. Diagramm wird mit maskiertem '<' eingebettet, die Bibliothek unverändert und nur auf <script/</script geprüft. - bpmn-js 18.29.1 (navigated viewer + CSS + LICENSE) lokal eingebettet, npm-Integrität geprüft; lädt nichts aus dem Netz. Wasserzeichen bleibt laut Lizenzauflage sichtbar. - tests/test_vorschau.py: 11 Tests; Skill sn-bpmn, Anleitung, AGENTS.md, README, Leitfäden, Fallstricke und STATUS ergänzt. - Tests: python -m pytest -q -> 55 bestanden. Darstellung mit Edge headless geprüft (beide Beispiele, leere Seite, kaputte Datei). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# BPMN-Vorschau
|
||||
|
||||
Zeigt eine `.bpmn`-Datei als Diagramm im Browser — offline, ohne PICTURE, ohne dass die Datei den Rechner verlässt.
|
||||
Grundlage ist [bpmn-js](https://github.com/bpmn-io/bpmn-js), die Bibliothek hinter bpmn.io, lokal eingebettet.
|
||||
|
||||
## Benutzen
|
||||
|
||||
**Mit Python** (aus dem Workspace-Root) — öffnet die Vorschau direkt im Browser, auch für einen Entwurf:
|
||||
|
||||
```bash
|
||||
python scripts/vorschau.py prozesse/antrag-pruefen/antrag-pruefen.bpmn
|
||||
python scripts/vorschau.py entwuerfe/antrag-pruefen.md # .md wird vorher umgewandelt
|
||||
```
|
||||
|
||||
**Ohne Python:** `werkzeuge/vorschau/vorschau.html` im Browser öffnen (Doppelklick) und eine `.bpmn` hineinziehen
|
||||
oder über „Datei öffnen …“ wählen. Das funktioniert auch mit einer aus Gitea heruntergeladenen Datei.
|
||||
|
||||
Mausrad zoomt, Ziehen verschiebt, „Einpassen“ zeigt das ganze Diagramm.
|
||||
|
||||
## Was die Vorschau zeigt — und was nicht
|
||||
|
||||
Sie zeigt, was **in der Datei steht**: Elemente, Lanes, Kanten mit genau den Anschlusspunkten, die `md_to_bpmn.py`
|
||||
berechnet hat. Damit eignet sie sich, um Entwürfe vor der Freigabe und Modelländerungen im Review anzusehen.
|
||||
PICTURE kann dieselbe Datei anders darstellen (eigene Schriften, eigene Behandlung von Anschlusspunkten). Der Import
|
||||
in PICTURE bleibt deshalb der letzte Nachweis.
|
||||
|
||||
## Eingebettete Bibliothek
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Paket | `bpmn-js` 18.29.1 (npm), Datei `dist/bpmn-navigated-viewer.production.min.js` und `dist/assets/*.css` |
|
||||
| Quelle | `https://registry.npmjs.org/bpmn-js/-/bpmn-js-18.29.1.tgz` |
|
||||
| Integrität (npm) | `sha512-7M3O9LeHbV2ZW9xLXap8CMmIjR+iXWqiXrz7Rt1hE+DqLGhULuwOIgTsQGtXPGVUCrnrk4L38NVlc5R8thOHJw==` |
|
||||
| Lizenz | MIT mit Auflage, siehe `bpmn-js/LICENSE` |
|
||||
|
||||
**Lizenzauflage:** Das bpmn.io-Wasserzeichen unten rechts im Diagramm darf nicht entfernt, verändert oder von
|
||||
anderen Elementen überdeckt werden. Bedienelemente deshalb nie in die untere rechte Ecke der Zeichenfläche legen.
|
||||
|
||||
Die Bibliothek lädt nichts aus dem Netz nach; die Adressen darin sind XML-Namensräume und der Link des Wasserzeichens.
|
||||
Das Font-Paket `bpmn-font` wird nur vom Editor gebraucht und fehlt bewusst.
|
||||
|
||||
## Aktualisieren
|
||||
|
||||
1. Neue Version bei npm nachsehen, Tarball laden und die `integrity` aus `https://registry.npmjs.org/bpmn-js/<version>`
|
||||
gegen die Prüfsumme des geladenen Archivs prüfen.
|
||||
2. Die vier Dateien in `bpmn-js/` ersetzen (`LICENSE` auf geänderte Auflagen lesen).
|
||||
3. `python -m pytest -q` — `scripts/vorschau.py` lehnt eine Bibliothek ab, die `<script`/`</script` enthält und damit
|
||||
nicht sicher eingebettet werden kann.
|
||||
4. Eine Vorschau erzeugen und im Browser ansehen; Tabelle oben nachziehen. Eigener Pull Request.
|
||||
@@ -0,0 +1,23 @@
|
||||
Copyright (c) 2014-present Camunda Services GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without limitation the rights to use, copy,
|
||||
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
The source code responsible for displaying the bpmn.io project watermark that
|
||||
links back to https://bpmn.io as part of rendered diagrams MUST NOT be
|
||||
removed or changed. When this software is being used in a website or application,
|
||||
the watermark must stay fully visible and not visually overlapped by other elements.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||
OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,160 @@
|
||||
.bjs-container {
|
||||
--bjs-font-family: Arial, sans-serif;
|
||||
|
||||
--color-grey-225-10-15: hsl(225, 10%, 15%);
|
||||
--color-grey-225-10-35: hsl(225, 10%, 35%);
|
||||
--color-grey-225-10-55: hsl(225, 10%, 55%);
|
||||
--color-grey-225-10-75: hsl(225, 10%, 75%);
|
||||
--color-grey-225-10-80: hsl(225, 10%, 80%);
|
||||
--color-grey-225-10-85: hsl(225, 10%, 85%);
|
||||
--color-grey-225-10-90: hsl(225, 10%, 90%);
|
||||
--color-grey-225-10-95: hsl(225, 10%, 95%);
|
||||
--color-grey-225-10-97: hsl(225, 10%, 97%);
|
||||
|
||||
--color-blue-205-100-40: hsl(205, 100%, 40%);
|
||||
--color-blue-205-100-45: hsl(205, 100%, 45%);
|
||||
--color-blue-205-100-45-opacity-30: hsla(205, 100%, 45%, 30%);
|
||||
--color-blue-205-100-50: hsl(205, 100%, 50%);
|
||||
--color-blue-205-100-95: hsl(205, 100%, 95%);
|
||||
|
||||
--color-green-150-86-44: hsl(150, 86%, 44%);
|
||||
|
||||
--color-red-360-100-40: hsl(360, 100%, 40%);
|
||||
--color-red-360-100-45: hsl(360, 100%, 45%);
|
||||
--color-red-360-100-92: hsl(360, 100%, 92%);
|
||||
--color-red-360-100-97: hsl(360, 100%, 97%);
|
||||
|
||||
--color-white: hsl(0, 0%, 100%);
|
||||
--color-black: hsl(0, 0%, 0%);
|
||||
--color-black-opacity-05: hsla(0, 0%, 0%, 5%);
|
||||
--color-black-opacity-10: hsla(0, 0%, 0%, 10%);
|
||||
|
||||
--accent-color: var(--color-blue-205-100-40);
|
||||
|
||||
--breadcrumbs-font-family: var(--bjs-font-family);
|
||||
--breadcrumbs-item-color: var(--accent-color);
|
||||
--breadcrumbs-arrow-color: var(--color-black);
|
||||
--breadcrumbs-background-color: var(--color-grey-225-10-97);
|
||||
--breadcrumbs-border-color: var(--color-grey-225-10-75);
|
||||
--breadcrumbs-focus-outline-color: var(--accent-color);
|
||||
--drilldown-fill-color: var(--color-white);
|
||||
--drilldown-background-color: var(--accent-color);
|
||||
--drilldown-focus-outline-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.bjs-breadcrumbs {
|
||||
position: absolute;
|
||||
display: none;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
padding: 5px 10px;
|
||||
margin: 0px;
|
||||
font-family: var(--breadcrumbs-font-family);
|
||||
font-size: 16px;
|
||||
line-height: normal;
|
||||
background-color: var(--breadcrumbs-background-color);
|
||||
border: solid 1px var(--breadcrumbs-border-color);
|
||||
border-radius: 2px;
|
||||
row-gap: 5px;
|
||||
}
|
||||
|
||||
.bjs-breadcrumbs-shown .bjs-breadcrumbs {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.djs-palette-shown .bjs-breadcrumbs {
|
||||
left: 90px;
|
||||
}
|
||||
|
||||
.djs-palette-shown.djs-palette-two-column .bjs-breadcrumbs {
|
||||
left: 140px;
|
||||
}
|
||||
|
||||
.bjs-breadcrumbs li {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bjs-breadcrumbs a.bjs-crumb {
|
||||
cursor: pointer;
|
||||
color: var(--breadcrumbs-item-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.bjs-breadcrumbs a.bjs-crumb:focus-visible {
|
||||
outline: solid 1px var(--breadcrumbs-focus-outline-color);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.bjs-breadcrumbs li:not(:first-child)::before {
|
||||
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" /><path d="M0 0h24v24H0z" fill="none" /></svg>');
|
||||
padding: 0 8px;
|
||||
color: var(--breadcrumbs-arrow-color);
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.bjs-breadcrumbs .bjs-crumb {
|
||||
display: inline-block;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bjs-drilldown {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
padding: 0px;
|
||||
margin-left: -20px;
|
||||
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
|
||||
fill: var(--drilldown-fill-color);
|
||||
background-color: var(--drilldown-background-color);
|
||||
}
|
||||
|
||||
.bjs-drilldown:focus-visible {
|
||||
outline: solid 1px var(--drilldown-focus-outline-color);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.bjs-drilldown-empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selected .bjs-drilldown-empty {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
[data-popup="align-elements"] .djs-popup-results {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
[data-popup="align-elements"] .djs-popup-body [data-group] + [data-group] {
|
||||
border-left: 1px solid var(--popup-border-color);
|
||||
}
|
||||
|
||||
[data-popup="align-elements"] [data-group="align"] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
[data-popup="align-elements"] .djs-popup-body .entry {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
[data-popup="align-elements"] .djs-popup-body .entry:not(:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
[data-popup="align-elements"] .djs-popup-entry-icon {
|
||||
display: block;
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,129 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BPMN-Vorschau</title>
|
||||
<!-- bpmn-js (bpmn.io), lokal eingebettet – siehe README.md. Nichts wird aus dem Netz geladen. -->
|
||||
<link rel="stylesheet" href="bpmn-js/diagram-js.css">
|
||||
<link rel="stylesheet" href="bpmn-js/bpmn-js.css">
|
||||
<style>
|
||||
:root {
|
||||
--hintergrund: #ffffff;
|
||||
--leiste: #f3f5f7;
|
||||
--rand: #d0d7de;
|
||||
--text: #1f2328;
|
||||
--gedaempft: #59636e;
|
||||
--akzent: #0b6bcb;
|
||||
--fehler: #b42318;
|
||||
--warnung: #9a6700;
|
||||
}
|
||||
html, body { height: 100%; margin: 0; }
|
||||
body {
|
||||
display: flex; flex-direction: column;
|
||||
font-family: "Segoe UI", system-ui, sans-serif; font-size: 14px;
|
||||
color: var(--text); background: var(--hintergrund);
|
||||
}
|
||||
header {
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
|
||||
padding: 8px 16px; background: var(--leiste); border-bottom: 1px solid var(--rand);
|
||||
}
|
||||
header h1 { font-size: 15px; font-weight: 600; margin: 0 12px 0 0; }
|
||||
button, label.knopf {
|
||||
font: inherit; padding: 4px 10px; border: 1px solid var(--rand); border-radius: 6px;
|
||||
background: #fff; color: var(--text); cursor: pointer;
|
||||
}
|
||||
button:hover, label.knopf:hover { border-color: var(--akzent); }
|
||||
button:disabled { opacity: .5; cursor: default; }
|
||||
#datei { display: none; }
|
||||
#dateiname { color: var(--gedaempft); overflow-wrap: anywhere; }
|
||||
#meldung { flex-basis: 100%; margin: 0; }
|
||||
#meldung:empty { display: none; }
|
||||
#meldung.fehler { color: var(--fehler); }
|
||||
#meldung.warnung { color: var(--warnung); }
|
||||
/* Zeichenfläche: das bpmn.io-Wasserzeichen unten rechts muss laut Lizenz sichtbar und unverdeckt bleiben. */
|
||||
main { position: relative; flex: 1; min-height: 0; }
|
||||
#flaeche { position: absolute; inset: 0; }
|
||||
#hinweis {
|
||||
position: absolute; inset: 24px 24px 64px 24px; display: flex; align-items: center; justify-content: center;
|
||||
text-align: center; color: var(--gedaempft); border: 2px dashed var(--rand); border-radius: 12px;
|
||||
padding: 16px; pointer-events: none;
|
||||
}
|
||||
main.ziehen #hinweis { border-color: var(--akzent); color: var(--akzent); }
|
||||
main.geladen #hinweis { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>BPMN-Vorschau</h1>
|
||||
<label class="knopf" for="datei">Datei öffnen …</label>
|
||||
<input type="file" id="datei" accept=".bpmn,.xml">
|
||||
<button type="button" id="einpassen" disabled>Einpassen</button>
|
||||
<button type="button" id="groesser" disabled title="Vergrößern">+</button>
|
||||
<button type="button" id="kleiner" disabled title="Verkleinern">−</button>
|
||||
<span id="dateiname"></span>
|
||||
<p id="meldung" role="status"></p>
|
||||
</header>
|
||||
<main id="bereich">
|
||||
<div id="flaeche"></div>
|
||||
<div id="hinweis">.bpmn-Datei hierher ziehen oder über „Datei öffnen …“ wählen.<br>
|
||||
Die Datei wird nur in diesem Browser angezeigt und nirgendwohin übertragen.</div>
|
||||
</main>
|
||||
<script src="bpmn-js/bpmn-navigated-viewer.production.min.js"></script>
|
||||
<script>
|
||||
// Wird von scripts/vorschau.py durch den Inhalt einer .bpmn und ihren Namen ersetzt.
|
||||
const EINGEBETTET = null;
|
||||
const DATEINAME = null;
|
||||
|
||||
const bereich = document.getElementById('bereich');
|
||||
const meldung = document.getElementById('meldung');
|
||||
const knoepfe = ['einpassen', 'groesser', 'kleiner'].map(id => document.getElementById(id));
|
||||
const viewer = new BpmnJS({ container: '#flaeche' });
|
||||
const canvas = () => viewer.get('canvas');
|
||||
|
||||
function melde(text, art) {
|
||||
meldung.textContent = text || '';
|
||||
meldung.className = art || '';
|
||||
}
|
||||
|
||||
async function zeige(xml, name) {
|
||||
melde('');
|
||||
try {
|
||||
const { warnings } = await viewer.importXML(xml);
|
||||
canvas().zoom('fit-viewport', 'auto');
|
||||
bereich.classList.add('geladen');
|
||||
knoepfe.forEach(k => { k.disabled = false; });
|
||||
document.getElementById('dateiname').textContent = name || '';
|
||||
document.title = name ? `${name} – BPMN-Vorschau` : 'BPMN-Vorschau';
|
||||
if (warnings.length) {
|
||||
melde(`${warnings.length} Warnung(en) beim Einlesen: ` + warnings.map(w => w.message).join(' | '), 'warnung');
|
||||
}
|
||||
} catch (fehler) {
|
||||
bereich.classList.remove('geladen');
|
||||
knoepfe.forEach(k => { k.disabled = true; });
|
||||
melde(`Die Datei ${name || ''} lässt sich nicht als BPMN lesen: ${fehler.message}`, 'fehler');
|
||||
}
|
||||
}
|
||||
|
||||
function lies(datei) {
|
||||
if (!datei) return;
|
||||
datei.text().then(xml => zeige(xml, datei.name));
|
||||
}
|
||||
|
||||
document.getElementById('datei').addEventListener('change', e => lies(e.target.files[0]));
|
||||
document.getElementById('einpassen').addEventListener('click', () => canvas().zoom('fit-viewport', 'auto'));
|
||||
document.getElementById('groesser').addEventListener('click', () => canvas().zoom(canvas().zoom() * 1.25));
|
||||
document.getElementById('kleiner').addEventListener('click', () => canvas().zoom(canvas().zoom() / 1.25));
|
||||
|
||||
['dragenter', 'dragover'].forEach(art => document.addEventListener(art, e => {
|
||||
e.preventDefault(); bereich.classList.add('ziehen');
|
||||
}));
|
||||
['dragleave', 'drop'].forEach(art => document.addEventListener(art, e => {
|
||||
e.preventDefault(); bereich.classList.remove('ziehen');
|
||||
}));
|
||||
document.addEventListener('drop', e => lies(e.dataTransfer.files[0]));
|
||||
|
||||
if (EINGEBETTET) zeige(EINGEBETTET, DATEINAME);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user