enh: svg zoom pan

This commit is contained in:
Timothy J. Baek 2024-10-06 12:51:29 -07:00
parent 913620ff0c
commit babfc97c90
6 changed files with 130 additions and 30 deletions

34
package-lock.json generated
View File

@ -34,6 +34,7 @@
"marked": "^9.1.0", "marked": "^9.1.0",
"mermaid": "^10.9.1", "mermaid": "^10.9.1",
"paneforge": "^0.0.6", "paneforge": "^0.0.6",
"panzoom": "^9.4.3",
"pyodide": "^0.26.1", "pyodide": "^0.26.1",
"socket.io-client": "^4.2.0", "socket.io-client": "^4.2.0",
"sortablejs": "^1.15.2", "sortablejs": "^1.15.2",
@ -2435,6 +2436,14 @@
"url": "https://github.com/sponsors/epoberezkin" "url": "https://github.com/sponsors/epoberezkin"
} }
}, },
"node_modules/amator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/amator/-/amator-1.1.0.tgz",
"integrity": "sha512-V5+aH8pe+Z3u/UG3L3pG3BaFQGXAyXHVQDroRwjPHdh08bcUEchAVsU1MCuJSCaU5o60wTK6KaE6te5memzgYw==",
"dependencies": {
"bezier-easing": "^2.0.3"
}
},
"node_modules/ansi-colors": { "node_modules/ansi-colors": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@ -2725,6 +2734,11 @@
"tweetnacl": "^0.14.3" "tweetnacl": "^0.14.3"
} }
}, },
"node_modules/bezier-easing": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
"integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -7178,6 +7192,11 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true "dev": true
}, },
"node_modules/ngraph.events": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.2.tgz",
"integrity": "sha512-JsUbEOzANskax+WSYiAPETemLWYXmixuPAlmZmhIbIj6FH/WDgEGCGnRwUQBK0GjOnVm8Ui+e5IJ+5VZ4e32eQ=="
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.14", "version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
@ -7375,6 +7394,16 @@
"svelte": "^4.0.0 || ^5.0.0-next.1" "svelte": "^4.0.0 || ^5.0.0-next.1"
} }
}, },
"node_modules/panzoom": {
"version": "9.4.3",
"resolved": "https://registry.npmjs.org/panzoom/-/panzoom-9.4.3.tgz",
"integrity": "sha512-xaxCpElcRbQsUtIdwlrZA90P90+BHip4Vda2BC8MEb4tkI05PmR6cKECdqUCZ85ZvBHjpI9htJrZBxV5Gp/q/w==",
"dependencies": {
"amator": "^1.1.0",
"ngraph.events": "^1.2.2",
"wheel": "^1.0.0"
}
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -10481,6 +10510,11 @@
"resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz",
"integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==" "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA=="
}, },
"node_modules/wheel": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wheel/-/wheel-1.0.0.tgz",
"integrity": "sha512-XiCMHibOiqalCQ+BaNSwRoZ9FDTAvOsXxGHXChBugewDj7HC8VBIER71dEOiRH1fSdLbRCQzngKTSiZ06ZQzeA=="
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -74,6 +74,7 @@
"marked": "^9.1.0", "marked": "^9.1.0",
"mermaid": "^10.9.1", "mermaid": "^10.9.1",
"paneforge": "^0.0.6", "paneforge": "^0.0.6",
"panzoom": "^9.4.3",
"pyodide": "^0.26.1", "pyodide": "^0.26.1",
"socket.io-client": "^4.2.0", "socket.io-client": "^4.2.0",
"sortablejs": "^1.15.2", "sortablejs": "^1.15.2",

View File

@ -9,12 +9,13 @@
import { copyToClipboard, createMessagesList } from '$lib/utils'; import { copyToClipboard, createMessagesList } from '$lib/utils';
import ArrowsPointingOut from '../icons/ArrowsPointingOut.svelte'; import ArrowsPointingOut from '../icons/ArrowsPointingOut.svelte';
import Tooltip from '../common/Tooltip.svelte'; import Tooltip from '../common/Tooltip.svelte';
import SvgPanZoom from '../common/SVGPanZoom.svelte';
export let overlay = false; export let overlay = false;
export let history; export let history;
let messages = []; let messages = [];
let contents: Array<{ content: string }> = []; let contents: Array<{ type: string; content: string }> = [];
let selectedContentIdx = 0; let selectedContentIdx = 0;
let copied = false; let copied = false;
@ -32,24 +33,32 @@
contents = []; contents = [];
messages.forEach((message) => { messages.forEach((message) => {
if (message.content) { if (message.content) {
const codeBlockContents = message.content.match(/```[\s\S]*?```/g);
let codeBlocks = [];
if (codeBlockContents) {
codeBlockContents.forEach((block) => {
const lang = block.split('\n')[0].replace('```', '').trim().toLowerCase();
const code = block.replace(/```[\s\S]*?\n/, '').replace(/```$/, '');
codeBlocks.push({ lang, code });
});
}
let htmlContent = ''; let htmlContent = '';
let cssContent = ''; let cssContent = '';
let jsContent = ''; let jsContent = '';
const codeBlocks = message.content.match(/```[\s\S]*?```/g); codeBlocks.forEach((block) => {
if (codeBlocks) { const { lang, code } = block;
codeBlocks.forEach((block) => {
const lang = block.split('\n')[0].replace('```', '').trim().toLowerCase(); if (lang === 'html') {
const code = block.replace(/```[\s\S]*?\n/, '').replace(/```$/, ''); htmlContent += code + '\n';
if (lang === 'html') { } else if (lang === 'css') {
htmlContent += code + '\n'; cssContent += code + '\n';
} else if (lang === 'css') { } else if (lang === 'javascript' || lang === 'js') {
cssContent += code + '\n'; jsContent += code + '\n';
} else if (lang === 'javascript' || lang === 'js') { }
jsContent += code + '\n'; });
}
});
}
const inlineHtml = message.content.match(/<html>[\s\S]*?<\/html>/gi); const inlineHtml = message.content.match(/<html>[\s\S]*?<\/html>/gi);
const inlineCss = message.content.match(/<style>[\s\S]*?<\/style>/gi); const inlineCss = message.content.match(/<style>[\s\S]*?<\/style>/gi);
@ -98,7 +107,14 @@
</body> </body>
</html> </html>
`; `;
contents = [...contents, { content: renderedContent }]; contents = [...contents, { type: 'iframe', content: renderedContent }];
} else {
// Check for SVG content
for (const block of codeBlocks) {
if (block.lang === 'svg' || (block.lang === 'xml' && block.code.includes('<svg'))) {
contents = [...contents, { type: 'svg', content: block.code }];
}
}
} }
} }
}); });
@ -184,14 +200,21 @@
<div class=" h-full flex flex-col"> <div class=" h-full flex flex-col">
{#if contents.length > 0} {#if contents.length > 0}
<div class="max-w-full w-full h-full"> <div class="max-w-full w-full h-full">
<iframe {#if contents[selectedContentIdx].type === 'iframe'}
bind:this={iframeElement} <iframe
title="Content" bind:this={iframeElement}
srcdoc={contents[selectedContentIdx].content} title="Content"
class="w-full border-0 h-full rounded-none" srcdoc={contents[selectedContentIdx].content}
sandbox="allow-scripts allow-forms allow-same-origin" class="w-full border-0 h-full rounded-none"
on:load={iframeLoadHandler} sandbox="allow-scripts allow-forms allow-same-origin"
></iframe> on:load={iframeLoadHandler}
></iframe>
{:else if contents[selectedContentIdx].type === 'svg'}
<SvgPanZoom
className=" w-full h-full max-h-full overflow-hidden"
svg={contents[selectedContentIdx].content}
/>
{/if}
</div> </div>
{:else} {:else}
<div class="m-auto font-medium text-xs text-gray-900 dark:text-white"> <div class="m-auto font-medium text-xs text-gray-900 dark:text-white">

View File

@ -12,6 +12,7 @@
import PyodideWorker from '$lib/workers/pyodide.worker?worker'; import PyodideWorker from '$lib/workers/pyodide.worker?worker';
import CodeEditor from '$lib/components/common/CodeEditor.svelte'; import CodeEditor from '$lib/components/common/CodeEditor.svelte';
import SvgPanZoom from '$lib/components/common/SVGPanZoom.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -271,14 +272,18 @@ __builtins__.input = input`);
} }
$: if (lang) { $: if (lang) {
dispatch('code', { lang }); dispatchCode();
} }
const dispatchCode = () => {
dispatch('code', { lang, code });
};
onMount(async () => { onMount(async () => {
console.log('codeblock', lang, code); console.log('codeblock', lang, code);
if (lang) { if (lang) {
dispatch('code', { lang }); dispatchCode();
} }
if (document.documentElement.classList.contains('dark')) { if (document.documentElement.classList.contains('dark')) {
mermaid.initialize({ mermaid.initialize({
@ -300,7 +305,10 @@ __builtins__.input = input`);
<div class="relative my-2 flex flex-col rounded-lg" dir="ltr"> <div class="relative my-2 flex flex-col rounded-lg" dir="ltr">
{#if lang === 'mermaid'} {#if lang === 'mermaid'}
{#if mermaidHtml} {#if mermaidHtml}
{@html `${mermaidHtml}`} <SvgPanZoom
className=" border border-gray-50 dark:border-gray-850 rounded-lg max-h-fit overflow-hidden"
svg={mermaidHtml}
/>
{:else} {:else}
<pre class="mermaid">{code}</pre> <pre class="mermaid">{code}</pre>
{/if} {/if}

View File

@ -71,9 +71,14 @@
dispatch('update', e.detail); dispatch('update', e.detail);
}} }}
on:code={(e) => { on:code={(e) => {
const { lang } = e.detail; const { lang, code } = e.detail;
console.log('code', lang); console.log('lang', lang);
if (['html', 'svg'].includes(lang) && !$mobile) { console.log('code', code);
if (
(['html', 'svg'].includes(lang) || (lang === 'xml' && code.includes('svg'))) &&
!$mobile
) {
showArtifacts.set(true); showArtifacts.set(true);
showControls.set(true); showControls.set(true);
} }

View File

@ -0,0 +1,29 @@
<script lang="ts">
import { onMount } from 'svelte';
import panzoom from 'panzoom';
import DOMPurify from 'dompurify';
export let className = '';
export let svg = '';
let instance;
let sceneParentElement: HTMLElement;
let sceneElement: HTMLElement;
$: if (sceneElement) {
instance = panzoom(sceneElement, {
bounds: true,
boundsPadding: 0.1,
zoomSpeed: 0.065
});
}
</script>
<div bind:this={sceneParentElement} class={className}>
<div bind:this={sceneElement} class="flex h-full max-h-full justify-center items-center">
{@html svg}
</div>
</div>