diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index bc6f10b9..5e9b065b 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -77,6 +77,7 @@ module.exports = {
'error',
{
allowList: {
+ args: true,
ctx: true,
db: true,
doc: true,
diff --git a/src/lib/components/FloatingToolbar.svelte b/src/lib/components/FloatingToolbar.svelte
new file mode 100644
index 00000000..5e7c63f4
--- /dev/null
+++ b/src/lib/components/FloatingToolbar.svelte
@@ -0,0 +1,13 @@
+
+
+
+ {@render children()}
+
diff --git a/src/lib/components/SyncRoughToolbar.svelte b/src/lib/components/SyncRoughToolbar.svelte
new file mode 100644
index 00000000..14839407
--- /dev/null
+++ b/src/lib/components/SyncRoughToolbar.svelte
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ {#if !$stateStore.autoSync}
+
+ {/if}
+
diff --git a/src/lib/components/View.svelte b/src/lib/components/View.svelte
index 6b3d108a..d6d37339 100644
--- a/src/lib/components/View.svelte
+++ b/src/lib/components/View.svelte
@@ -102,7 +102,6 @@
);
if (svg.length > 0) {
- handlePanZoom(state);
container.innerHTML = svg;
const graphDiv = document.querySelector('#graph-div');
if (!graphDiv) {
@@ -119,6 +118,7 @@
}
const height = sketch.getAttribute('height');
const width = sketch.getAttribute('width');
+ sketch.setAttribute('id', 'graph-div');
sketch.setAttribute('height', '100%');
sketch.setAttribute('width', '100%');
sketch.setAttribute('viewBox', `0 0 ${width} ${height}`);
@@ -130,6 +130,7 @@
bindFunctions(graphDiv);
}
}
+ handlePanZoom(state);
}
if (view?.parentElement && scroll) {
view.parentElement.scrollTop = scroll;
@@ -165,7 +166,7 @@
{#if outOfSync}
Diagram out of sync.
{#if $stateStore.autoSync}
diff --git a/src/lib/components/ui/switch/switch.svelte b/src/lib/components/ui/switch/switch.svelte
index 03f5ef31..fe111cbd 100644
--- a/src/lib/components/ui/switch/switch.svelte
+++ b/src/lib/components/ui/switch/switch.svelte
@@ -1,27 +1,25 @@
-
+ bind:ref
+ bind:checked
+ class={cn(
+ 'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-secondary data-[state=unchecked]:bg-input',
+ className
+ )}
+ {...restProps}>
+
diff --git a/src/lib/util/panZoom.ts b/src/lib/util/panZoom.ts
new file mode 100644
index 00000000..694eeab4
--- /dev/null
+++ b/src/lib/util/panZoom.ts
@@ -0,0 +1,94 @@
+import type { State } from '$/types';
+import type { Point } from 'mermaid/dist/types.js';
+import panzoom from 'svg-pan-zoom';
+type PanZoom = typeof panzoom;
+
+export class PanZoomState {
+ private pan?: Point;
+ private zoom?: number;
+ private pzoom: PanZoom | undefined;
+ private isDirty = false;
+ private resizeObserver: ResizeObserver;
+
+ public isPanEnabled: boolean;
+
+ constructor() {
+ this.isPanEnabled = true;
+ this.resizeObserver = new ResizeObserver(() => {
+ this.resize();
+ if (!this.isDirty) {
+ this.reset();
+ }
+ });
+ }
+
+ public updateElement(diagramView: SVGElement, { pan, zoom }: Pick) {
+ this.pzoom = panzoom(diagramView, {
+ onPan: (pan) => {
+ this.pan = pan;
+ this.zoom = this.pzoom?.getZoom();
+ this.isDirty = true;
+ },
+ onZoom: (zoom) => {
+ this.zoom = zoom;
+ this.pan = this.pzoom?.getPan();
+ this.isDirty = true;
+ },
+ controlIconsEnabled: false,
+ panEnabled: true,
+ zoomEnabled: true,
+ fit: true,
+ center: true,
+ maxZoom: 12,
+ minZoom: 0.2
+ });
+
+ this.pzoom.disableDblClickZoom();
+
+ this.resizeObserver.disconnect();
+ this.resizeObserver.observe(diagramView);
+
+ // TODO: Investigate why this is necessary
+ if (pan !== undefined && zoom !== undefined && Number.isFinite(zoom)) {
+ this.restorePanZoom(pan, zoom);
+ } else if (this.pan && this.zoom) {
+ this.restorePanZoom(this.pan, this.zoom);
+ }
+
+ // we start out with both pan and zoom enabled so that the tool can auto position view refreshed
+ // then set enable/disable pan based on state
+ if (this.isPanEnabled) {
+ this.pzoom.enablePan();
+ this.pzoom.enableZoom();
+ } else {
+ this.pzoom.disableZoom();
+ this.pzoom.disablePan();
+ }
+ }
+
+ public restorePanZoom(pan: Point, zoom: number) {
+ if (!this.pzoom) {
+ console.error('PanZoomState.restorePanZoom: pzoom is not initialized');
+ return;
+ }
+ this.pzoom.zoom(zoom);
+ this.pzoom.pan(pan);
+ }
+
+ public resize() {
+ this.pzoom?.resize();
+ }
+
+ public zoomIn() {
+ this.pzoom?.zoomIn();
+ }
+
+ public zoomOut() {
+ this.pzoom?.zoomOut();
+ }
+
+ public reset() {
+ this.pzoom?.reset();
+ this.isDirty = false;
+ }
+}
diff --git a/src/routes/edit/+page.svelte b/src/routes/edit/+page.svelte
index 7868fee0..db581875 100644
--- a/src/routes/edit/+page.svelte
+++ b/src/routes/edit/+page.svelte
@@ -1,4 +1,5 @@