feat: Floating toolbars
This commit is contained in:
@@ -77,6 +77,7 @@ module.exports = {
|
||||
'error',
|
||||
{
|
||||
allowList: {
|
||||
args: true,
|
||||
ctx: true,
|
||||
db: true,
|
||||
doc: true,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-between gap-2 rounded-2xl bg-border p-2">
|
||||
{@render children()}
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { inputStateStore, stateStore } from '$/util/state';
|
||||
import { cmdKey, syncDiagram } from '$/util/util';
|
||||
import { slide } from 'svelte/transition';
|
||||
import FloatingToolbar from './FloatingToolbar.svelte';
|
||||
import { Switch } from './ui/switch';
|
||||
</script>
|
||||
|
||||
<FloatingToolbar>
|
||||
<label class="label flex cursor-pointer items-center gap-2" for="rough">
|
||||
<Switch bind:checked={$inputStateStore.rough} name="rough" />
|
||||
<span>Hand drawn</span>
|
||||
</label>
|
||||
|
||||
<label class="label flex cursor-pointer items-center gap-2" for="autoSync">
|
||||
<Switch bind:checked={$inputStateStore.autoSync} name="autoSync" />
|
||||
<span> Auto sync</span>
|
||||
</label>
|
||||
|
||||
{#if !$stateStore.autoSync}
|
||||
<button
|
||||
transition:slide={{ axis: 'x' }}
|
||||
class="btn btn-secondary btn-xs ml-2"
|
||||
title="Sync Diagram ({cmdKey} + Enter)"
|
||||
aria-label="Sync Diagram"
|
||||
data-cy="sync"
|
||||
onclick={syncDiagram}><i class="fas fa-sync"></i></button>
|
||||
{/if}
|
||||
</FloatingToolbar>
|
||||
@@ -102,7 +102,6 @@
|
||||
);
|
||||
|
||||
if (svg.length > 0) {
|
||||
handlePanZoom(state);
|
||||
container.innerHTML = svg;
|
||||
const graphDiv = document.querySelector<SVGSVGElement>('#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}
|
||||
<div
|
||||
class="font-monotext-yellow-600 absolute z-10 w-full bg-base-100 bg-opacity-80 p-2 text-left"
|
||||
class="font-monotext-yellow-600 bg-base-100 absolute z-10 w-full bg-opacity-80 p-2 text-left"
|
||||
id="errorContainer">
|
||||
Diagram out of sync. <br />
|
||||
{#if $stateStore.autoSync}
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { Switch as SwitchPrimitive, type WithoutChildrenOrChild } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { Switch as SwitchPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
|
||||
</script>
|
||||
|
||||
<SwitchPrimitive.Root
|
||||
bind:ref
|
||||
bind:checked
|
||||
class={cn(
|
||||
"focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input 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-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
class={cn(
|
||||
"bg-background pointer-events-none block size-4 rounded-full shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
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}>
|
||||
<SwitchPrimitive.Thumb
|
||||
class={cn(
|
||||
'pointer-events-none block size-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
|
||||
)} />
|
||||
</SwitchPrimitive.Root>
|
||||
|
||||
@@ -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<State, 'pan' | 'zoom'>) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import SyncRoughToolbar from '$/components/SyncRoughToolbar.svelte';
|
||||
import { Button } from '$/components/ui/button';
|
||||
import * as Resizable from '$/components/ui/resizable';
|
||||
import Actions from '$lib/components/Actions.svelte';
|
||||
@@ -120,35 +121,12 @@
|
||||
<div class="flex h-full flex-col overflow-hidden">
|
||||
<Navbar />
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<Resizable.PaneGroup direction="horizontal" autoSaveId="liveEditor" class="p-2">
|
||||
<Resizable.PaneGroup direction="horizontal" autoSaveId="liveEditor" class="p-6">
|
||||
<Resizable.Pane defaultSize={40} minSize={15}>
|
||||
<div class="hidden h-full flex-col gap-2 md:flex" id="editorPane">
|
||||
<div class="hidden h-full flex-col gap-6 md:flex" id="editorPane">
|
||||
<Card onselect={tabSelectHandler} isOpen {tabs} {activeTabID} isClosable={false}>
|
||||
{#snippet actions()}
|
||||
<div class="flex flex-row items-center">
|
||||
<!-- <div class="form-control flex-row items-center">
|
||||
<label class="label cursor-pointer" for="autoSync">
|
||||
<span> Auto sync</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle {$stateStore.autoSync
|
||||
? 'btn-secondary'
|
||||
: 'toggle-primary'} ml-1"
|
||||
id="autoSync"
|
||||
bind:checked={$inputStateStore.autoSync} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if !$stateStore.autoSync}
|
||||
<button
|
||||
class="btn btn-secondary btn-xs mr-1"
|
||||
title="Sync Diagram ({cmdKey} + Enter)"
|
||||
aria-label="Sync Diagram"
|
||||
data-cy="sync"
|
||||
onclick={syncDiagram}><i class="fas fa-sync"></i></button>
|
||||
{/if}
|
||||
-->
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
href={docURL}
|
||||
@@ -162,16 +140,17 @@
|
||||
<Editor />
|
||||
</Card>
|
||||
|
||||
<div class="group flex flex-wrap justify-between gap-2">
|
||||
<div class="group flex flex-wrap justify-between gap-6">
|
||||
<Preset />
|
||||
<!-- <History /> -->
|
||||
<Actions />
|
||||
</div>
|
||||
</div>
|
||||
</Resizable.Pane>
|
||||
<Resizable.Handle class="opacity-0" />
|
||||
<Resizable.Pane class="p-2">
|
||||
<div class="flex h-full flex-1 flex-col overflow-hidden">
|
||||
<Resizable.Handle class="mr-2 opacity-0" />
|
||||
<Resizable.Pane>
|
||||
<div class="relative flex h-full flex-1 flex-col overflow-hidden">
|
||||
<div class="absolute left-0 top-0"><SyncRoughToolbar /></div>
|
||||
<!-- <Card title="Diagram" isClosable={false}>
|
||||
{#snippet actions()}
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user