libpetri 1.8.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +47 -0
  2. package/dist/{chunk-B2D5DMTO.js → chunk-4L6JVKH4.js} +165 -8
  3. package/dist/chunk-4L6JVKH4.js.map +1 -0
  4. package/dist/chunk-H62Z76FY.js +1346 -0
  5. package/dist/chunk-H62Z76FY.js.map +1 -0
  6. package/dist/chunk-SXK2Z45Z.js +50 -0
  7. package/dist/chunk-SXK2Z45Z.js.map +1 -0
  8. package/dist/debug/index.d.ts +50 -3
  9. package/dist/debug/index.js +64 -6
  10. package/dist/debug/index.js.map +1 -1
  11. package/dist/doclet/index.d.ts +152 -31
  12. package/dist/doclet/index.js +458 -57
  13. package/dist/doclet/index.js.map +1 -1
  14. package/dist/doclet/resources/petrinet-diagrams.css +384 -7
  15. package/dist/doclet/resources/petrinet-diagrams.js +7214 -106
  16. package/dist/dot-exporter-PMHOQHZ3.js +8 -0
  17. package/dist/{event-store-BnyHh3TF.d.ts → event-store-2zkXeQkd.d.ts} +1 -1
  18. package/dist/export/index.d.ts +16 -2
  19. package/dist/export/index.js +1 -1
  20. package/dist/index.d.ts +41 -5
  21. package/dist/index.js +1221 -35
  22. package/dist/index.js.map +1 -1
  23. package/dist/petri-net-BDrj4XZE.d.ts +1461 -0
  24. package/dist/render-P6GROU7J.js +16 -0
  25. package/dist/render-P6GROU7J.js.map +1 -0
  26. package/dist/verification/index.d.ts +3 -144
  27. package/dist/verification/index.js +30 -1214
  28. package/dist/verification/index.js.map +1 -1
  29. package/dist/viewer/index.d.ts +196 -0
  30. package/dist/viewer/index.js +442 -0
  31. package/dist/viewer/index.js.map +1 -0
  32. package/dist/viewer/viewer-static.iife.js +3 -0
  33. package/dist/viewer/viewer.css +503 -0
  34. package/dist/viewer/viewer.iife.js +7219 -0
  35. package/package.json +17 -8
  36. package/dist/chunk-B2D5DMTO.js.map +0 -1
  37. package/dist/chunk-VQ4XMJTD.js +0 -107
  38. package/dist/chunk-VQ4XMJTD.js.map +0 -1
  39. package/dist/dot-exporter-3CVCH6J4.js +0 -8
  40. package/dist/petri-net-D-GN9g_D.d.ts +0 -570
  41. /package/dist/{dot-exporter-3CVCH6J4.js.map → dot-exporter-PMHOQHZ3.js.map} +0 -0
@@ -0,0 +1,196 @@
1
+ import panzoom from 'panzoom';
2
+
3
+ /**
4
+ * Pan/zoom wrapper for the canonical libpetri viewer.
5
+ *
6
+ * Delegates to the `panzoom` library (no hand-rolled wheel handlers — we
7
+ * intentionally drop the IIFE/javadoc version's bespoke math in favour of
8
+ * a single battle-tested implementation). Defaults match the old debug-ui
9
+ * and dev-preview values so the canonical viewer feels identical to what
10
+ * users had before.
11
+ *
12
+ * @module viewer/pan-zoom
13
+ */
14
+
15
+ type PanzoomInstance = ReturnType<typeof panzoom>;
16
+ type PanzoomOptions = Parameters<typeof panzoom>[1];
17
+ /**
18
+ * Default panzoom configuration shared across all viewer surfaces.
19
+ *
20
+ * - `maxZoom: 1000` — effectively unlimited; lets users dive into ~150-node
21
+ * diagrams without hitting an artificial ceiling.
22
+ * - `minZoom: 0.02` — allows fitting very large nets in a small viewport.
23
+ * - `smoothScroll: false` — sharper interactive feel.
24
+ * - `zoomDoubleClickSpeed: 1` — single-click double-zoom toggle.
25
+ */
26
+ declare const DEFAULT_PANZOOM_OPTS: {
27
+ readonly smoothScroll: false;
28
+ readonly zoomDoubleClickSpeed: 1;
29
+ readonly maxZoom: 1000;
30
+ readonly minZoom: 0.02;
31
+ };
32
+
33
+ /**
34
+ * Cluster overlay for the canonical libpetri viewer.
35
+ *
36
+ * Post-processes a rendered SVG to surface subnet structure visually:
37
+ * 1. Discover `<g class="cluster">` subgraphs, derive each prefix from the
38
+ * contained `<title>` (Graphviz emits `cluster_<sanitizedPrefix>`).
39
+ * 2. Tag every contained `<g class="node">` with `data-instance="<prefix>"`
40
+ * so filtering can target it via attribute selectors.
41
+ * 3. Assign each cluster a deterministic HSL colour (FNV-1a hash + golden
42
+ * ratio hue increment) and paint its border.
43
+ * 4. Provide collapse/expand and "show only <prefix>" filtering.
44
+ *
45
+ * Unlike the previous debug-ui cluster-overlay, this module is instance-
46
+ * based: every `mount()` call gets its own `ClusterOverlay` so multiple
47
+ * viewers on the same page don't share collapse/isolate state.
48
+ *
49
+ * @module viewer/cluster-overlay
50
+ */
51
+ /** A discovered cluster with its DOM group, node count, and palette colour. */
52
+ interface ClusterDescriptor {
53
+ readonly prefix: string;
54
+ readonly group: SVGGElement;
55
+ readonly nodeCount: number;
56
+ readonly color: string;
57
+ }
58
+ /**
59
+ * Walk the SVG, find every `<g class="cluster">`, derive the prefix from
60
+ * each cluster's `<title>` (`cluster_<sanitized>`), and tag every cluster-
61
+ * interior `<g class="node">` with `data-instance="<prefix>"`. Returns one
62
+ * descriptor per cluster.
63
+ *
64
+ * Graphviz emits cluster nodes as SIBLINGS of the cluster `<g>`, not as
65
+ * children — `<g class="cluster">` only carries title, polygon border, and
66
+ * the cluster's text label. To recover cluster membership we match each
67
+ * node's `<title>` (the original graph id) against libpetri's prefixing
68
+ * convention: interior places are emitted as `p_<prefix>_<name>`,
69
+ * transitions as `t_<prefix>_<name>`, and junctions as `j_<prefix>_<name>`.
70
+ *
71
+ * Does not split nested-cluster prefixes (`outer_inner` could mean either
72
+ * `outer/inner` or a flat `outer_inner` prefix — Graphviz sanitisation is
73
+ * lossy). We treat every cluster as its own top-level entry, matching the
74
+ * doclet's pragmatism.
75
+ */
76
+ declare function discoverClusters(svg: SVGSVGElement): Map<string, ClusterDescriptor>;
77
+ /**
78
+ * Deterministic HSL palette. FNV-1a-ish hash of the prefix, multiplied by
79
+ * the golden-ratio fraction (0.61803...) to spread adjacent prefixes
80
+ * visually. Saturation/lightness are fixed (60% / 45%) so the palette
81
+ * stays readable on both the doc background and the dark debug UI.
82
+ */
83
+ declare function colorForPrefix(prefix: string): string;
84
+
85
+ /**
86
+ * CSS custom properties exposed by the canonical viewer stylesheet.
87
+ *
88
+ * Each property has a default in `viewer.css`. Consumers can override any of
89
+ * them at any scope (the viewer container, the document root, etc.) to
90
+ * theme the viewer without touching the canonical CSS.
91
+ *
92
+ * Names are listed here as a reference + so the IIFE bundle can expose them
93
+ * on `window.LibpetriViewer.cssVariables`. The list is informational — the
94
+ * stylesheet is still the source of truth.
95
+ *
96
+ * @module viewer/styles
97
+ */
98
+ /** CSS custom property names that the canonical stylesheet honours. */
99
+ declare const VIEWER_CSS_VARIABLES: readonly ["--lpv-bg", "--lpv-header-bg", "--lpv-border", "--lpv-text", "--lpv-muted", "--lpv-cluster-bg", "--lpv-cluster-bg-collapsed", "--lpv-cluster-stroke-width", "--lpv-cluster-stroke-dash", "--lpv-cluster-fill-tint", "--lpv-dim-opacity", "--lpv-active-filter-outline", "--lpv-legend-bg", "--lpv-chip-bg", "--lpv-chip-active-bg"];
100
+
101
+ /**
102
+ * Canonical libpetri Petri-net diagram viewer.
103
+ *
104
+ * Single source of truth for DOT → SVG rendering + cluster overlay across
105
+ * all four surfaces:
106
+ * - debug-ui (live debugger)
107
+ * - dev-preview (Vite app)
108
+ * - Javadoc taglet (Java doclet)
109
+ * - Rustdoc embed (libpetri-docgen)
110
+ * - TypeDoc / typescript doclet
111
+ *
112
+ * Build outputs:
113
+ * - ESM: `dist/viewer/index.js` — consumed by Vite/Webpack/Node bundlers.
114
+ * - IIFE: `dist/viewer/viewer.iife.js` — self-contained, exposes
115
+ * `window.LibpetriViewer = { mount, ... }`. Used by the doc taglets
116
+ * that ship pre-rendered HTML pages without a bundler.
117
+ * - CSS: `dist/viewer/viewer.css` — canonical stylesheet (copied from
118
+ * `src/viewer/resources/viewer.css`).
119
+ *
120
+ * Edits to viewer behaviour MUST happen here. The three resource directories
121
+ * under `java/`, `rust/`, and `typescript/src/doclet/` are **build outputs**
122
+ * populated by `scripts/build-viewer.sh` and must not be hand-edited.
123
+ *
124
+ * @module viewer
125
+ */
126
+
127
+ /** Handle returned by {@link mount}. */
128
+ interface ViewerHandle {
129
+ /** The rendered root `<svg>` element. */
130
+ readonly svg: SVGSVGElement;
131
+ /** The panzoom instance attached to the SVG. */
132
+ readonly panzoom: PanzoomInstance;
133
+ /** Map of cluster prefix → descriptor for the current SVG. */
134
+ readonly clusters: ReadonlyMap<string, ClusterDescriptor>;
135
+ /** Prefixes currently marked collapsed on this handle. */
136
+ readonly collapsedPrefixes: ReadonlySet<string>;
137
+ /** Currently active "show only <prefix>" filter, or null. */
138
+ readonly activeFilter: string | null;
139
+ /** Collapse a single cluster by prefix. No-op if unknown. */
140
+ collapse(prefix: string): void;
141
+ /** Expand a single cluster by prefix. No-op if unknown. */
142
+ expand(prefix: string): void;
143
+ /** Collapse every discovered cluster. */
144
+ collapseAll(): void;
145
+ /** Expand every collapsed cluster. */
146
+ expandAll(): void;
147
+ /** Apply the "show only <prefix>" filter; pass `null` to clear. */
148
+ filter(prefix: string | null): void;
149
+ /** Reset pan/zoom to the identity transform. */
150
+ resetZoom(): void;
151
+ /** Scale and translate so the full diagram fits the host viewport. */
152
+ fit(): void;
153
+ /** Reports whether `graphId` is inside a currently-collapsed cluster. */
154
+ isInsideCollapsedCluster(graphId: string): boolean;
155
+ /** Tear down: dispose panzoom and detach listeners. */
156
+ dispose(): void;
157
+ }
158
+ /** Options accepted by {@link mount}. */
159
+ interface MountOptions {
160
+ /**
161
+ * Previous handle to dispose before mounting. Pass the handle returned
162
+ * by a prior `mount()` call so callers don't need to track it separately.
163
+ * Cluster collapse / filter state from `previousHandle` is preserved
164
+ * across the re-render so the user's intent survives a live re-draw.
165
+ */
166
+ readonly previousHandle?: ViewerHandle | null;
167
+ /** Per-call panzoom overrides; merged on top of {@link DEFAULT_PANZOOM_OPTS}. */
168
+ readonly panzoom?: PanzoomOptions;
169
+ /** Fired after a cluster's collapsed state changes via the handle API. */
170
+ readonly onClusterCollapse?: (prefix: string, collapsed: boolean) => void;
171
+ /** Fired after `filter()` runs, with the new filter prefix or null. */
172
+ readonly onClusterFilter?: (prefix: string | null) => void;
173
+ /** When true, append a legend sidebar + filter chip strip inside `container`. */
174
+ readonly chrome?: boolean;
175
+ }
176
+ /**
177
+ * Render a DOT source, mount the resulting SVG into `container`, wire pan/zoom,
178
+ * and surface cluster overlay controls.
179
+ *
180
+ * The container's existing children are removed before the new SVG is
181
+ * appended (the same teardown behaviour `renderDotToContainer` used to provide).
182
+ *
183
+ * Pass `dotSource === null` to adopt an SVG already present as a direct child
184
+ * of `container` (pre-rendered SVG path — e.g. the Java javadoc taglet emits
185
+ * `dot -Tsvg` at build time). In that mode the viewer never imports the
186
+ * runtime DOT renderer, which lets the static IIFE bundle drop `@viz-js/viz`
187
+ * entirely.
188
+ *
189
+ * NOTE: when shipping a pre-rendered SVG (`dotSource === null`), do NOT also
190
+ * pass `previousHandle` — the handle's `dispose()` runs before SVG detection
191
+ * and would orphan the host. The Java taglet mounts once per page so this is
192
+ * a non-issue in practice.
193
+ */
194
+ declare function mount(dotSource: string | null, container: HTMLElement, opts?: MountOptions): Promise<ViewerHandle>;
195
+
196
+ export { type ClusterDescriptor, DEFAULT_PANZOOM_OPTS, type MountOptions, type PanzoomInstance, type PanzoomOptions, VIEWER_CSS_VARIABLES, type ViewerHandle, colorForPrefix, discoverClusters, mount };
@@ -0,0 +1,442 @@
1
+ // src/viewer/pan-zoom.ts
2
+ import panzoom from "panzoom";
3
+ var DEFAULT_PANZOOM_OPTS = {
4
+ smoothScroll: false,
5
+ zoomDoubleClickSpeed: 1,
6
+ maxZoom: 1e3,
7
+ minZoom: 0.02
8
+ };
9
+ function attachPanzoom(svg, options, previous) {
10
+ if (previous) {
11
+ try {
12
+ previous.dispose();
13
+ } catch {
14
+ }
15
+ }
16
+ const opts = { ...DEFAULT_PANZOOM_OPTS, ...options };
17
+ return panzoom(svg, opts);
18
+ }
19
+
20
+ // src/viewer/cluster-overlay.ts
21
+ function discoverClusters(svg) {
22
+ const out = /* @__PURE__ */ new Map();
23
+ const clusterGroups = svg.querySelectorAll("g.cluster");
24
+ if (!clusterGroups.length) return out;
25
+ const allNodes = Array.from(svg.querySelectorAll("g.node"));
26
+ for (const g of Array.from(clusterGroups)) {
27
+ const titleEl = directChild(g, "title");
28
+ if (!titleEl) continue;
29
+ const raw = titleEl.textContent ?? "";
30
+ if (!raw.startsWith("cluster_")) continue;
31
+ const prefix = raw.slice("cluster_".length);
32
+ if (out.has(prefix)) continue;
33
+ let nodeCount = 0;
34
+ for (const node of allNodes) {
35
+ if (node.getAttribute("data-instance")) continue;
36
+ const nodeTitle = directChild(node, "title");
37
+ if (!nodeTitle) continue;
38
+ const id = (nodeTitle.textContent ?? "").trim();
39
+ if (nodeBelongsToPrefix(id, prefix)) {
40
+ node.setAttribute("data-instance", prefix);
41
+ nodeCount++;
42
+ }
43
+ }
44
+ out.set(prefix, {
45
+ prefix,
46
+ group: g,
47
+ nodeCount,
48
+ color: colorForPrefix(prefix)
49
+ });
50
+ }
51
+ return out;
52
+ }
53
+ function nodeBelongsToPrefix(nodeId, prefix) {
54
+ return nodeId === prefix || nodeId.startsWith(`${prefix}/`) || nodeId.startsWith(`p_${prefix}_`) || nodeId.startsWith(`t_${prefix}_`) || nodeId.startsWith(`j_${prefix}_`);
55
+ }
56
+ function colorForPrefix(prefix) {
57
+ let h = 2166136261;
58
+ for (let i = 0; i < prefix.length; i++) {
59
+ h ^= prefix.charCodeAt(i);
60
+ h = h * 16777619 >>> 0;
61
+ }
62
+ let hue = Math.floor((h / 4294967296 * 360 + h % 360 * 0.61803) % 360);
63
+ if (hue < 0) hue += 360;
64
+ return `hsl(${hue}, 60%, 45%)`;
65
+ }
66
+ function paintClusterBorders(clusters) {
67
+ for (const c of clusters.values()) {
68
+ const border = directChild(c.group, "polygon") ?? directChild(c.group, "path");
69
+ if (border) {
70
+ border.setAttribute("stroke", c.color);
71
+ border.setAttribute("stroke-width", "2.2");
72
+ }
73
+ }
74
+ }
75
+ function setClusterCollapsed(cluster, collapsed) {
76
+ const g = cluster.group;
77
+ const isCollapsed = g.classList.contains("cluster-collapsed");
78
+ const root = g.ownerSVGElement;
79
+ if (!root) return;
80
+ if (collapsed && !isCollapsed) {
81
+ g.classList.add("cluster-collapsed");
82
+ const hidden = [];
83
+ root.querySelectorAll(`g.node[data-instance="${cssEscape(cluster.prefix)}"]`).forEach((node) => {
84
+ node.classList.add("petri-collapsed-inside");
85
+ hidden.push(node);
86
+ });
87
+ root.querySelectorAll("g.edge").forEach((edge) => {
88
+ const titleEl = directChild(edge, "title");
89
+ if (!titleEl) return;
90
+ const t = (titleEl.textContent ?? "").trim();
91
+ const arrow = t.indexOf("->");
92
+ if (arrow < 0) return;
93
+ const from = t.slice(0, arrow);
94
+ const to = t.slice(arrow + 2);
95
+ if (nodeBelongsToPrefix(from, cluster.prefix) && nodeBelongsToPrefix(to, cluster.prefix)) {
96
+ edge.classList.add("petri-collapsed-inside");
97
+ hidden.push(edge);
98
+ }
99
+ });
100
+ g._petriHiddenSiblings = hidden;
101
+ if (!g._petriCollapsedBadge) {
102
+ const label = directChild(g, "text");
103
+ if (label) {
104
+ const badge = document.createElementNS("http://www.w3.org/2000/svg", "text");
105
+ badge.setAttribute("x", label.getAttribute("x") ?? "0");
106
+ const labelY = parseFloat(label.getAttribute("y") ?? "0");
107
+ badge.setAttribute("y", String(labelY + 16));
108
+ badge.setAttribute("text-anchor", label.getAttribute("text-anchor") ?? "middle");
109
+ badge.setAttribute("font-size", "11");
110
+ badge.setAttribute("fill", "#9ca3af");
111
+ badge.setAttribute("class", "cluster-collapsed-badge");
112
+ badge.textContent = `(${cluster.nodeCount} internal node${cluster.nodeCount === 1 ? "" : "s"})`;
113
+ g.appendChild(badge);
114
+ g._petriCollapsedBadge = badge;
115
+ }
116
+ }
117
+ } else if (!collapsed && isCollapsed) {
118
+ g.classList.remove("cluster-collapsed");
119
+ if (g._petriHiddenSiblings) {
120
+ g._petriHiddenSiblings.forEach((el) => el.classList.remove("petri-collapsed-inside"));
121
+ g._petriHiddenSiblings = void 0;
122
+ }
123
+ if (g._petriCollapsedBadge && g._petriCollapsedBadge.parentNode === g) {
124
+ g.removeChild(g._petriCollapsedBadge);
125
+ g._petriCollapsedBadge = null;
126
+ }
127
+ }
128
+ }
129
+ function applyFilter(svg, prefix) {
130
+ if (prefix == null) {
131
+ svg.removeAttribute("data-active-filter");
132
+ svg.classList.remove("has-active-filter");
133
+ svg.querySelectorAll("g.node, g.edge").forEach((el) => el.classList.remove("petri-dimmed"));
134
+ return;
135
+ }
136
+ svg.setAttribute("data-active-filter", prefix);
137
+ svg.classList.add("has-active-filter");
138
+ svg.querySelectorAll("g.node").forEach((node) => {
139
+ const di = node.getAttribute("data-instance");
140
+ const match = !!di && (di === prefix || di.indexOf(prefix + "_") === 0 || di.indexOf(prefix + "/") === 0);
141
+ if (match) node.classList.remove("petri-dimmed");
142
+ else node.classList.add("petri-dimmed");
143
+ });
144
+ svg.querySelectorAll("g.edge").forEach((edge) => {
145
+ const titleEl = directChild(edge, "title");
146
+ if (!titleEl) return;
147
+ const titleText = titleEl.textContent ?? "";
148
+ const arrow = titleText.indexOf("->");
149
+ if (arrow < 0) {
150
+ edge.classList.remove("petri-dimmed");
151
+ return;
152
+ }
153
+ const from = titleText.slice(0, arrow);
154
+ const to = titleText.slice(arrow + 2);
155
+ const matches = (name) => name.indexOf(prefix + "/") >= 0 || name.indexOf(prefix + "_") >= 0 || name === prefix;
156
+ if (matches(from) || matches(to)) edge.classList.remove("petri-dimmed");
157
+ else edge.classList.add("petri-dimmed");
158
+ });
159
+ }
160
+ function isInsideCollapsedCluster(svg, clusters, collapsedPrefixes, graphId) {
161
+ if (collapsedPrefixes.size === 0) return false;
162
+ if (!svg) return false;
163
+ for (const prefix of collapsedPrefixes) {
164
+ if (!clusters.has(prefix)) continue;
165
+ if (nodeBelongsToPrefix(graphId, prefix)) return true;
166
+ }
167
+ return false;
168
+ }
169
+ function cssEscape(s) {
170
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(s);
171
+ return s.replace(/[^a-zA-Z0-9_-]/g, (c) => `\\${c}`);
172
+ }
173
+ function directChild(parent, tagName) {
174
+ const lc = tagName.toLowerCase();
175
+ for (const child of Array.from(parent.children)) {
176
+ if (child.tagName.toLowerCase() === lc) return child;
177
+ }
178
+ return null;
179
+ }
180
+
181
+ // src/viewer/styles.ts
182
+ var VIEWER_CSS_VARIABLES = [
183
+ "--lpv-bg",
184
+ "--lpv-header-bg",
185
+ "--lpv-border",
186
+ "--lpv-text",
187
+ "--lpv-muted",
188
+ "--lpv-cluster-bg",
189
+ "--lpv-cluster-bg-collapsed",
190
+ "--lpv-cluster-stroke-width",
191
+ "--lpv-cluster-stroke-dash",
192
+ "--lpv-cluster-fill-tint",
193
+ "--lpv-dim-opacity",
194
+ "--lpv-active-filter-outline",
195
+ "--lpv-legend-bg",
196
+ "--lpv-chip-bg",
197
+ "--lpv-chip-active-bg"
198
+ ];
199
+
200
+ // src/viewer/index.ts
201
+ async function mount(dotSource, container, opts = {}) {
202
+ const previousHandle = opts.previousHandle ?? null;
203
+ const preservedCollapsed = previousHandle ? new Set(previousHandle.collapsedPrefixes) : /* @__PURE__ */ new Set();
204
+ const preservedFilter = previousHandle?.activeFilter ?? null;
205
+ if (previousHandle) {
206
+ previousHandle.dispose();
207
+ }
208
+ let svg;
209
+ const existing = container.querySelector(":scope > svg");
210
+ if (dotSource == null) {
211
+ if (!(existing instanceof SVGSVGElement)) {
212
+ throw new Error("mount: no DOT source and no pre-rendered SVG");
213
+ }
214
+ svg = existing;
215
+ } else {
216
+ const { renderDotToSvg } = await import("../render-P6GROU7J.js");
217
+ svg = await renderDotToSvg(dotSource);
218
+ container.innerHTML = "";
219
+ container.appendChild(svg);
220
+ }
221
+ container.classList.add("libpetri-viewer");
222
+ const panzoomInstance = attachPanzoom(svg, opts.panzoom);
223
+ const clusters = discoverClusters(svg);
224
+ paintClusterBorders(clusters);
225
+ const collapsedPrefixes = /* @__PURE__ */ new Set();
226
+ let activeFilter = null;
227
+ let disposed = false;
228
+ let chromeRoot = null;
229
+ function ensureChrome() {
230
+ if (!opts.chrome) return;
231
+ if (chromeRoot && chromeRoot.parentNode === container) return;
232
+ chromeRoot = document.createElement("div");
233
+ chromeRoot.className = "libpetri-viewer-chrome";
234
+ chromeRoot.style.position = "absolute";
235
+ chromeRoot.style.inset = "0";
236
+ chromeRoot.style.pointerEvents = "none";
237
+ const legend = document.createElement("div");
238
+ legend.className = "diagram-legend";
239
+ legend.style.pointerEvents = "auto";
240
+ legend.innerHTML = '<div class="legend-title">Clusters</div>';
241
+ chromeRoot.appendChild(legend);
242
+ const strip = document.createElement("div");
243
+ strip.className = "diagram-filter-strip";
244
+ strip.style.pointerEvents = "auto";
245
+ strip.innerHTML = '<span class="filter-strip-label">Show only:</span>';
246
+ chromeRoot.appendChild(strip);
247
+ const controls = document.createElement("div");
248
+ controls.className = "diagram-controls";
249
+ controls.style.pointerEvents = "auto";
250
+ const resetBtn = document.createElement("button");
251
+ resetBtn.type = "button";
252
+ resetBtn.className = "diagram-btn btn-reset";
253
+ resetBtn.title = "Reset view";
254
+ resetBtn.textContent = "Reset";
255
+ resetBtn.addEventListener("click", () => handle.fit());
256
+ const fsBtn = document.createElement("button");
257
+ fsBtn.type = "button";
258
+ fsBtn.className = "diagram-btn btn-fullscreen";
259
+ fsBtn.title = "Toggle fullscreen";
260
+ fsBtn.textContent = "Fullscreen";
261
+ fsBtn.addEventListener("click", () => toggleFullscreen(container, fsBtn));
262
+ controls.appendChild(resetBtn);
263
+ controls.appendChild(fsBtn);
264
+ chromeRoot.appendChild(controls);
265
+ if (getComputedStyle(container).position === "static") {
266
+ container.style.position = "relative";
267
+ }
268
+ container.appendChild(chromeRoot);
269
+ renderLegend(legend);
270
+ renderFilterStrip(strip);
271
+ }
272
+ function toggleFullscreen(host, btn) {
273
+ const isFs = host.classList.toggle("diagram-fullscreen");
274
+ btn.textContent = isFs ? "Exit fullscreen" : "Fullscreen";
275
+ requestAnimationFrame(() => handle.fit());
276
+ }
277
+ function renderLegend(legend) {
278
+ legend.innerHTML = '<div class="legend-title">Clusters</div>';
279
+ for (const cluster of clusters.values()) {
280
+ const row = document.createElement("div");
281
+ row.className = "legend-row";
282
+ row.innerHTML = '<span class="legend-ribbon" style="background:' + cluster.color + '"></span><span class="legend-label"></span><span class="legend-count"></span>';
283
+ row.querySelector(".legend-label").textContent = cluster.prefix;
284
+ row.querySelector(".legend-count").textContent = String(cluster.nodeCount);
285
+ row.addEventListener("click", () => {
286
+ if (collapsedPrefixes.has(cluster.prefix)) {
287
+ handle.expand(cluster.prefix);
288
+ } else {
289
+ handle.collapse(cluster.prefix);
290
+ }
291
+ });
292
+ legend.appendChild(row);
293
+ }
294
+ }
295
+ function renderFilterStrip(strip) {
296
+ strip.innerHTML = '<span class="filter-strip-label">Show only:</span>';
297
+ const allChip = document.createElement("button");
298
+ allChip.type = "button";
299
+ allChip.className = "filter-chip filter-chip-all filter-chip-active";
300
+ allChip.textContent = "all";
301
+ allChip.addEventListener("click", () => {
302
+ handle.filter(null);
303
+ });
304
+ strip.appendChild(allChip);
305
+ for (const cluster of clusters.values()) {
306
+ const chip = document.createElement("button");
307
+ chip.type = "button";
308
+ chip.className = "filter-chip";
309
+ chip.style.borderColor = cluster.color;
310
+ chip.dataset.prefix = cluster.prefix;
311
+ chip.innerHTML = '<span class="chip-dot" style="background:' + cluster.color + '"></span>';
312
+ chip.append(document.createTextNode(cluster.prefix));
313
+ chip.addEventListener("click", () => {
314
+ if (activeFilter === cluster.prefix) {
315
+ handle.filter(null);
316
+ } else {
317
+ handle.filter(cluster.prefix);
318
+ }
319
+ });
320
+ strip.appendChild(chip);
321
+ }
322
+ }
323
+ function refreshChromeActiveStates() {
324
+ if (!chromeRoot) return;
325
+ const strip = chromeRoot.querySelector(".diagram-filter-strip");
326
+ if (!strip) return;
327
+ strip.querySelectorAll(".filter-chip").forEach((chip) => {
328
+ chip.classList.remove("filter-chip-active");
329
+ });
330
+ if (activeFilter === null) {
331
+ strip.querySelector(".filter-chip-all")?.classList.add("filter-chip-active");
332
+ } else {
333
+ strip.querySelector(`.filter-chip[data-prefix="${activeFilter}"]`)?.classList.add("filter-chip-active");
334
+ }
335
+ }
336
+ const handle = {
337
+ svg,
338
+ panzoom: panzoomInstance,
339
+ clusters,
340
+ get collapsedPrefixes() {
341
+ return collapsedPrefixes;
342
+ },
343
+ get activeFilter() {
344
+ return activeFilter;
345
+ },
346
+ collapse(prefix) {
347
+ if (disposed) return;
348
+ const cluster = clusters.get(prefix);
349
+ if (!cluster) return;
350
+ if (!collapsedPrefixes.has(prefix)) {
351
+ setClusterCollapsed(cluster, true);
352
+ collapsedPrefixes.add(prefix);
353
+ opts.onClusterCollapse?.(prefix, true);
354
+ }
355
+ },
356
+ expand(prefix) {
357
+ if (disposed) return;
358
+ const cluster = clusters.get(prefix);
359
+ if (!cluster) return;
360
+ if (collapsedPrefixes.has(prefix)) {
361
+ setClusterCollapsed(cluster, false);
362
+ collapsedPrefixes.delete(prefix);
363
+ opts.onClusterCollapse?.(prefix, false);
364
+ }
365
+ },
366
+ collapseAll() {
367
+ if (disposed) return;
368
+ for (const cluster of clusters.values()) {
369
+ if (!collapsedPrefixes.has(cluster.prefix)) {
370
+ setClusterCollapsed(cluster, true);
371
+ collapsedPrefixes.add(cluster.prefix);
372
+ opts.onClusterCollapse?.(cluster.prefix, true);
373
+ }
374
+ }
375
+ },
376
+ expandAll() {
377
+ if (disposed) return;
378
+ for (const prefix of Array.from(collapsedPrefixes)) {
379
+ const cluster = clusters.get(prefix);
380
+ if (!cluster) continue;
381
+ setClusterCollapsed(cluster, false);
382
+ collapsedPrefixes.delete(prefix);
383
+ opts.onClusterCollapse?.(prefix, false);
384
+ }
385
+ },
386
+ filter(prefix) {
387
+ if (disposed) return;
388
+ activeFilter = prefix;
389
+ applyFilter(svg, prefix);
390
+ opts.onClusterFilter?.(prefix);
391
+ refreshChromeActiveStates();
392
+ },
393
+ resetZoom() {
394
+ if (disposed) return;
395
+ try {
396
+ panzoomInstance.moveTo(0, 0);
397
+ panzoomInstance.zoomAbs(0, 0, 1);
398
+ } catch {
399
+ }
400
+ },
401
+ fit() {
402
+ if (disposed) return;
403
+ try {
404
+ panzoomInstance.zoomAbs(0, 0, 1);
405
+ panzoomInstance.moveTo(0, 0);
406
+ } catch {
407
+ }
408
+ },
409
+ isInsideCollapsedCluster(graphId) {
410
+ return isInsideCollapsedCluster(svg, clusters, collapsedPrefixes, graphId);
411
+ },
412
+ dispose() {
413
+ if (disposed) return;
414
+ disposed = true;
415
+ try {
416
+ panzoomInstance.dispose();
417
+ } catch {
418
+ }
419
+ if (chromeRoot && chromeRoot.parentNode === container) {
420
+ container.removeChild(chromeRoot);
421
+ }
422
+ chromeRoot = null;
423
+ }
424
+ };
425
+ for (const prefix of preservedCollapsed) {
426
+ handle.collapse(prefix);
427
+ }
428
+ if (preservedFilter !== null) {
429
+ handle.filter(preservedFilter);
430
+ }
431
+ ensureChrome();
432
+ requestAnimationFrame(() => handle.fit());
433
+ return handle;
434
+ }
435
+ export {
436
+ DEFAULT_PANZOOM_OPTS,
437
+ VIEWER_CSS_VARIABLES,
438
+ colorForPrefix,
439
+ discoverClusters,
440
+ mount
441
+ };
442
+ //# sourceMappingURL=index.js.map