Merge branch 'develop' into sidv/examples

* develop: (32 commits)
  feat: Support jsonSchema to validate config
  link: live version
  fix: Fallback page
  chore(deps): update dependency vite to v5.4.18 [security]
  fix: Fallback to sveltekit on 404 in GH pages
  chore(deps): update all non-major dependencies
  fix: New diagram link
  chore(deps): update dependency @sveltejs/kit to v2.20.6 [security]
  feat: Wait for fontawesome to load
  chore(deps): update all non-major dependencies
  feat: Support pan while zooming
  Update src/lib/util/panZoom.ts
  chore: Track PWA install device type
  chore: Track PWA installs
  chore: Add analytics for mobileViewToggle
  fix: Reduce gap in mobile
  fix: SVG size to avoid
  chore: Unify code update logic
  chore: Cleanup deps
  feat: Add syntax highlighting
  ...
This commit is contained in:
Sidharth Vinod
2025-04-20 13:42:04 +05:30
26 changed files with 1087 additions and 456 deletions
+8 -1
View File
@@ -2,12 +2,14 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"cSpell.blockCheckingWhenLineLengthGreaterThan": 150, "cSpell.blockCheckingWhenLineLengthGreaterThan": 150,
"cSpell.words": [ "cSpell.words": [
"appinstalled",
"asyncable", "asyncable",
"Browserslist", "Browserslist",
"ckppp", "ckppp",
"corg", "corg",
"cssnano", "cssnano",
"esserializer", "esserializer",
"fsegurai",
"gantt", "gantt",
"gitgraph", "gitgraph",
"KROKI", "KROKI",
@@ -16,14 +18,19 @@
"mindmap", "mindmap",
"Pageview", "Pageview",
"pako", "pako",
"panmove",
"panstart",
"panzoom", "panzoom",
"pinchmove",
"pinchstart",
"pzoom", "pzoom",
"roughjs", "roughjs",
"Serde", "Serde",
"serdes", "serdes",
"Stackable", "Stackable",
"tailwindcss", "tailwindcss",
"uparrow" "uparrow",
"zenuml"
], ],
"vitest.commandLine": "pnpm test:unit", "vitest.commandLine": "pnpm test:unit",
"vitest.enable": true, "vitest.enable": true,
+1 -1
View File
@@ -14,7 +14,7 @@ Edit, preview and share mermaid charts/diagrams.
## Live demo ## Live demo
You can try out a live version [here](https://mermaid.live/). You can try out a [live version](https://mermaid.live/).
# Contributors are welcome! # Contributors are welcome!
+23 -12
View File
@@ -24,14 +24,15 @@
}, },
"devDependencies": { "devDependencies": {
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.7.2",
"@iconify-json/material-symbols": "^1.2.17", "@iconify-json/material-symbols": "^1.2.19",
"@iconify-json/mdi": "^1.2.3", "@iconify-json/mdi": "^1.2.3",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.51.1",
"@sveltejs/adapter-static": "3.0.8", "@sveltejs/adapter-static": "3.0.8",
"@sveltejs/kit": "2.20.3", "@sveltejs/kit": "2.20.6",
"@sveltejs/vite-plugin-svelte": "^4.0.4", "@sveltejs/vite-plugin-svelte": "^4.0.4",
"@types/hammerjs": "^2.0.46",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^22.13.17", "@types/node": "^22.14.1",
"@types/pako": "2.0.3", "@types/pako": "2.0.3",
"@types/uuid": "9.0.8", "@types/uuid": "9.0.8",
"@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/eslint-plugin": "6.21.0",
@@ -39,7 +40,7 @@
"@vitest/coverage-v8": "2.1.9", "@vitest/coverage-v8": "2.1.9",
"@vitest/ui": "^2.1.9", "@vitest/ui": "^2.1.9",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"bits-ui": "^1.3.16", "bits-ui": "^1.3.19",
"c8": "7.14.0", "c8": "7.14.0",
"chai": "^4.5.0", "chai": "^4.5.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -57,35 +58,45 @@
"esserializer": "^1.3.11", "esserializer": "^1.3.11",
"husky": "^8.0.3", "husky": "^8.0.3",
"jsdom": "^25.0.1", "jsdom": "^25.0.1",
"lint-staged": "^15.5.0", "lint-staged": "^15.5.1",
"lucide-svelte": "^0.487.0", "lucide-svelte": "^0.488.0",
"node-html-parser": "^6.1.13", "node-html-parser": "^6.1.13",
"paneforge": "^1.0.0-next.4", "paneforge": "^1.0.0-next.5",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"postcss-load-config": "5.1.0", "postcss-load-config": "5.1.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.23.2", "svelte": "^5.27.0",
"svelte-preprocess": "^6.0.3", "svelte-preprocess": "^6.0.3",
"svelte-sonner": "^0.3.28", "svelte-sonner": "^0.3.28",
"tailwind-merge": "^3.1.0", "tailwind-merge": "^3.2.0",
"tailwind-variants": "^0.3.1", "tailwind-variants": "^0.3.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.8.2", "typescript": "^5.8.3",
"unplugin-icons": "^22.1.0", "unplugin-icons": "^22.1.0",
"vite": "^5.4.14", "vite": "^5.4.14",
"vitest": "^2.1.9", "vitest": "^2.1.9",
"vitest-dom": "^0.1.1" "vitest-dom": "^0.1.1"
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.3.2",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.11.0",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.36.5",
"@fontsource-variable/recursive": "^5.2.5", "@fontsource-variable/recursive": "^5.2.5",
"@fsegurai/codemirror-theme-vscode-dark": "^6.1.2",
"@fsegurai/codemirror-theme-vscode-light": "^6.1.2",
"@mermaid-js/examples": "0.0.1-beta.1", "@mermaid-js/examples": "0.0.1-beta.1",
"@mermaid-js/layout-elk": "^0.1.7", "@mermaid-js/layout-elk": "^0.1.7",
"@mermaid-js/mermaid-zenuml": "^0.2.0", "@mermaid-js/mermaid-zenuml": "^0.2.0",
"codemirror": "^6.0.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"hammerjs": "^2.0.8",
"js-base64": "3.7.7", "js-base64": "3.7.7",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mermaid": "^11.6.0", "mermaid": "^11.6.0",
@@ -107,7 +118,7 @@
"engines": { "engines": {
"node": ">=20.19.0" "node": ">=20.19.0"
}, },
"packageManager": "pnpm@10.7.1+sha512.2d92c86b7928dc8284f53494fb4201f983da65f0fb4f0d40baafa5cf628fa31dae3e5968f12466f17df7e97310e30f343a648baea1b9b350685dafafffdf5808", "packageManager": "pnpm@10.8.0+sha512.0e82714d1b5b43c74610193cb20734897c1d00de89d0e18420aebc5977fa13d780a9cb05734624e81ebd81cc876cd464794850641c48b9544326b5622ca29971",
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"deasync", "deasync",
+596 -282
View File
File diff suppressed because it is too large Load Diff
+2 -4
View File
@@ -4,16 +4,14 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Online FlowChart &amp; Diagrams Editor - Mermaid Live Editor</title> <title>Online FlowChart &amp; Diagrams Editor - Mermaid Live Editor</title>
<meta <meta name="og:image" content="%sveltekit.assets%/favicon.svg" />
name="og:image"
content="https://github.com/mermaid-js/mermaid/raw/develop/img/header.png" />
<link rel="canonical" href="https://mermaid.live" /> <link rel="canonical" href="https://mermaid.live" />
<meta <meta
name="description" name="description"
content="Simplify documentation and avoid heavy tools. Open source Visio Alternative. Commonly used for explaining your code! Mermaid is a simple markdown-like script language for generating charts from text via javascript." /> content="Simplify documentation and avoid heavy tools. Open source Visio Alternative. Commonly used for explaining your code! Mermaid is a simple markdown-like script language for generating charts from text via javascript." />
<link rel="icon" type="image/png" href="%sveltekit.assets%/favicon.svg" /> <link rel="icon" type="image/png" href="%sveltekit.assets%/favicon.svg" />
<link rel="mask-icon" href="%sveltekit.assets%/favicon.svg" color="#000000" /> <link rel="mask-icon" href="%sveltekit.assets%/favicon.svg" color="#000000" />
<meta name="theme-color" content="#6366F1" /> <meta name="theme-color" content="#ff3670" />
<link rel="manifest" href="%sveltekit.assets%/manifest.json" /> <link rel="manifest" href="%sveltekit.assets%/manifest.json" />
%sveltekit.head% %sveltekit.head%
</head> </head>
-1
View File
@@ -1,4 +1,3 @@
@import '@fortawesome/fontawesome-free/css/all.min.css';
@import '@fontsource-variable/recursive/crsv.css'; @import '@fontsource-variable/recursive/crsv.css';
@tailwind base; @tailwind base;
+118
View File
@@ -0,0 +1,118 @@
<script lang="ts">
import type { EditorProps } from '$/types';
import { stateStore } from '$/util/state';
import { initEditor } from '$lib/util/monacoExtra';
import { errorDebug } from '$lib/util/util';
import { mode } from 'mode-watcher';
import * as monaco from 'monaco-editor';
import monacoEditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import monacoJsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import { onMount } from 'svelte';
const { onUpdate }: EditorProps = $props();
let divElement: HTMLDivElement | undefined = $state();
let editor: monaco.editor.IStandaloneCodeEditor | undefined;
let editorOptions = {
minimap: {
enabled: false
},
overviewRulerLanes: 0
} satisfies monaco.editor.IStandaloneEditorConstructionOptions;
let currentText = '';
const jsonModel = monaco.editor.createModel(
'',
'json',
monaco.Uri.parse('internal://config.json')
);
const mermaidModel = monaco.editor.createModel(
'',
'mermaid',
monaco.Uri.parse('internal://mermaid.mmd')
);
onMount(() => {
self.MonacoEnvironment = {
getWorker(_, label) {
if (label === 'json') {
return new monacoJsonWorker();
}
return new monacoEditorWorker();
}
};
if (!divElement) {
throw new Error('divEl is undefined');
}
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
enableSchemaRequest: true,
schemas: [
{
fileMatch: ['config.json'],
uri: 'https://mermaid.js.org/schemas/config.schema.json'
}
]
});
initEditor(monaco);
errorDebug();
editor = monaco.editor.create(divElement, editorOptions);
editor.onDidChangeModelContent(({ isFlush }) => {
const newText = editor?.getValue();
if (!newText || currentText === newText || isFlush) {
return;
}
currentText = newText;
onUpdate(currentText);
});
const unsubscribeState = stateStore.subscribe(({ errorMarkers, editorMode, code, mermaid }) => {
if (!editor) {
return;
}
const model = editorMode === 'code' ? mermaidModel : jsonModel;
if (editor.getModel()?.id !== model.id) {
editor.setModel(model);
}
// Update editor text if it's different
const newText = editorMode === 'code' ? code : mermaid;
if (newText !== currentText) {
editor.setScrollTop(0);
editor.setValue(newText);
currentText = newText;
}
// Display/clear errors
monaco.editor.setModelMarkers(model, 'mermaid', errorMarkers);
});
const unsubscribeMode = mode.subscribe((mode) => {
editor && monaco.editor.setTheme(`mermaid${mode === 'dark' ? '-dark' : ''}`);
});
const resizeObserver = new ResizeObserver((entries) => {
editor?.layout({
height: entries[0].contentRect.height,
width: entries[0].contentRect.width
});
});
if (divElement.parentElement) {
resizeObserver.observe(divElement);
}
return () => {
unsubscribeState();
unsubscribeMode();
resizeObserver.disconnect();
editor?.dispose();
};
});
</script>
<div bind:this={divElement} id="editor" class="h-full flex-grow overflow-hidden"></div>
+11 -98
View File
@@ -1,117 +1,30 @@
<script lang="ts"> <script lang="ts">
import DesktopEditor from '$/components/DesktopEditor.svelte';
import McWrapper from '$/components/McWrapper.svelte'; import McWrapper from '$/components/McWrapper.svelte';
import MermaidChartIcon from '$/components/MermaidChartIcon.svelte'; import MermaidChartIcon from '$/components/MermaidChartIcon.svelte';
import MobileEditor from '$/components/MobileEditor.svelte';
import { Button } from '$/components/ui/button'; import { Button } from '$/components/ui/button';
import { TID } from '$/constants'; import { TID } from '$/constants';
import { env } from '$/util/env'; import { env } from '$/util/env';
import type { EditorMode } from '$lib/types';
import { initEditor } from '$lib/util/monacoExtra';
import { stateStore, updateCode, updateConfig, urlsStore } from '$lib/util/state'; import { stateStore, updateCode, updateConfig, urlsStore } from '$lib/util/state';
import { errorDebug } from '$lib/util/util';
import { mode } from 'mode-watcher';
import * as monaco from 'monaco-editor';
import monacoEditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import monacoJsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import { onMount } from 'svelte';
import ExclamationCircleIcon from '~icons/material-symbols/error-outline-rounded'; import ExclamationCircleIcon from '~icons/material-symbols/error-outline-rounded';
let divElement: HTMLDivElement | undefined = $state(); let { isMobile }: { isMobile: boolean } = $props();
let editor: monaco.editor.IStandaloneCodeEditor | undefined; const onUpdate = (text: string) => {
let editorOptions: monaco.editor.IStandaloneEditorConstructionOptions = { if ($stateStore.editorMode === 'code') {
minimap: {
enabled: false
},
theme: 'mermaid',
overviewRulerLanes: 0
};
let currentText = '';
const unsubscribeState = stateStore.subscribe(({ errorMarkers, editorMode, code, mermaid }) => {
if (!editor) {
return;
}
// Update editor text if it's different
const newText = editorMode === 'code' ? code : mermaid;
if (newText !== currentText) {
editor.setScrollTop(0);
editor.setValue(newText);
currentText = newText;
}
// Update editor mode if it's different
const language = editorMode === 'code' ? 'mermaid' : 'json';
const model = editor.getModel();
if (!model) {
console.error("editor model doesn't exist");
return;
}
if (model.getLanguageId() !== language) {
monaco.editor.setModelLanguage(model, language);
}
// Display/clear errors
monaco.editor.setModelMarkers(model, 'mermaid', errorMarkers);
});
const handleUpdate = (text: string, mode: EditorMode) => {
if (mode === 'code') {
updateCode(text); updateCode(text);
} else { } else {
updateConfig(text); updateConfig(text);
} }
}; };
onMount(() => {
self.MonacoEnvironment = {
getWorker(_, label) {
if (label === 'json') {
return new monacoJsonWorker();
}
return new monacoEditorWorker();
}
};
if (!divElement) {
throw new Error('divEl is undefined');
}
initEditor(monaco);
errorDebug();
editor = monaco.editor.create(divElement, editorOptions);
editor.onDidChangeModelContent(({ isFlush }) => {
const newText = editor?.getValue();
if (!newText || currentText === newText || isFlush) {
return;
}
currentText = newText;
handleUpdate(currentText, $stateStore.editorMode);
});
const unsubscribeMode = mode.subscribe((mode) => {
editor && monaco.editor.setTheme(`mermaid${mode === 'dark' ? '-dark' : ''}`);
});
const resizeObserver = new ResizeObserver((entries) => {
editor?.layout({
height: entries[0].contentRect.height,
width: entries[0].contentRect.width
});
});
if (divElement.parentElement) {
resizeObserver.observe(divElement);
}
return () => {
unsubscribeState();
unsubscribeMode();
resizeObserver.disconnect();
editor?.dispose();
};
});
</script> </script>
<div class="flex h-full flex-col pt-1"> <div class="flex h-full flex-col">
<div bind:this={divElement} id="editor" class="h-full flex-grow overflow-hidden"></div> {#if isMobile}
<MobileEditor {onUpdate} />
{:else}
<DesktopEditor {onUpdate} />
{/if}
{#if $stateStore.error instanceof Error} {#if $stateStore.error instanceof Error}
<div class="flex flex-col text-sm" data-testid={TID.errorContainer}> <div class="flex flex-col text-sm" data-testid={TID.errorContainer}>
<div class="flex items-center justify-between gap-2 bg-slate-900 p-2 text-white"> <div class="flex items-center justify-between gap-2 bg-slate-900 p-2 text-white">
+1 -1
View File
@@ -8,6 +8,6 @@
let { children }: Props = $props(); let { children }: Props = $props();
</script> </script>
<div class="hidden h-12 items-center justify-between gap-2 rounded-2xl bg-muted p-3 md:flex"> <div class="flex h-12 items-center justify-between gap-2 rounded-2xl bg-muted p-3">
{@render children()} {@render children()}
</div> </div>
+54
View File
@@ -0,0 +1,54 @@
<!--
@component
Exports a `waitForFontAwesomeToLoad` function that waits for FontAwesome to load.
Usage:
```svelte
<script lang="ts">
import FontAwesome from '$lib/client/components/FontAwesome';
let waitForFontAwesomeToLoad: FontAwesome["waitForFontAwesomeToLoad"];
</script>
<FontAwesome bind:waitForFontAwesomeToLoad />
```
-->
<script context="module" lang="ts">
/**
* Returns `true` if the code may contain FontAwesome icons, and we should
* wait for the fonts to load before rendering.
*/
export function mayContainFontAwesome(code: string) {
// taken from https://github.com/mermaid-js/mermaid/blob/7043892e871d0c413ec63dc1570a8ef738d15568/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js#L63
// Not ideal, since we're looking at unparsed code.
const regex = /fa[blrs]?:fa-[\w-]+/g;
return regex.test(code);
}
</script>
<script lang="ts">
// Vite will automatically take care of adding this to our `<head>`
let lazyLoadFontAwesomeCSS = import('./FontAwesomeCSS.svelte');
// eslint-disable-next-line unicorn/prefer-top-level-await
let fontsLoaded = (async () => {
await lazyLoadFontAwesomeCSS;
// Once the stylesheet has been parsed, the fonts will be in document.fonts
// TODO: Maybe we should scan the CSS style sheet only, and only load FontAwesome fonts
await Promise.allSettled(Array.from(document.fonts, (font) => font.load()));
})();
/**
* Wait for FontAwesome to load.
*
* @returns A promise that resolves when FontAwesome is
* loaded, or there was an error that we ignore.
*/
export async function waitForFontAwesomeToLoad() {
return await fontsLoaded;
}
</script>
{#await lazyLoadFontAwesomeCSS then FontAwesomeCSS}
<FontAwesomeCSS.default />
{/await}
+14
View File
@@ -0,0 +1,14 @@
<!--
@component
Imports the FontAwesome CSS file.
Needed, since the `@sveltejs/adapter-node` plugin doesn't seem to support
lazy-importing CSS, even though `@sveltejs/adapter-vercel` is fine with it!
However, making a `.svelte` file that imports the CSS file, and then
lazy-loading the `.svelte` file works.
-->
<script lang="ts">
import '@fortawesome/fontawesome-free/css/all.css';
</script>
+99
View File
@@ -0,0 +1,99 @@
<script lang="ts">
import type { EditorProps } from '$/types';
import { stateStore } from '$/util/state';
import { json, jsonLanguage } from '@codemirror/lang-json';
import { markdown } from '@codemirror/lang-markdown';
import { yamlFrontmatter } from '@codemirror/lang-yaml';
import { language } from '@codemirror/language';
import { Compartment, EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { vsCodeDark } from '@fsegurai/codemirror-theme-vscode-dark';
import { vsCodeLight } from '@fsegurai/codemirror-theme-vscode-light';
import { basicSetup } from 'codemirror';
import { mode } from 'mode-watcher';
import { onMount } from 'svelte';
let editorView: EditorView | undefined;
let editorContainer: HTMLDivElement;
let currentText = $state('');
const { onUpdate }: EditorProps = $props();
onMount(() => {
const themeCompartment = new Compartment();
const languageCompartment = new Compartment();
editorView = new EditorView({
state: EditorState.create({
doc: currentText,
extensions: [
basicSetup,
languageCompartment.of([]),
themeCompartment.of([]),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
const newText = update.state.doc.toString();
if (currentText === newText) {
return;
}
currentText = newText;
onUpdate(newText);
}
}),
EditorView.theme({
'&.cm-focused': {
outline: 'none'
},
'&.cm-editor': {
height: '100%'
},
'&.cm-scroller': {
overflow: 'auto'
}
})
]
}),
parent: editorContainer
});
const unsubscribeMode = mode.subscribe((mode) => {
editorView?.dispatch({
effects: themeCompartment.reconfigure(mode === 'dark' ? vsCodeDark : vsCodeLight)
});
});
const unsubscribeState = stateStore.subscribe(({ editorMode, code, mermaid }) => {
const text = editorMode === 'code' ? code : mermaid;
if (currentText === text || !editorView) {
return;
}
currentText = text;
editorView.dispatch({
changes: {
from: 0,
to: editorView.state.doc.length,
insert: text
}
});
const stateLanguage = editorView.state.facet(language);
const isStateJson = stateLanguage === jsonLanguage;
const isCodeJson = editorMode === 'config';
if (stateLanguage && isStateJson === isCodeJson) {
return;
}
editorView.dispatch({
effects: languageCompartment.reconfigure(
isCodeJson ? json() : yamlFrontmatter({ content: markdown() })
)
});
});
return () => {
unsubscribeMode();
unsubscribeState();
editorView?.destroy();
};
});
</script>
<div bind:this={editorContainer} class="size-full"></div>
+5 -3
View File
@@ -22,10 +22,11 @@
import DropdownNavMenu from './DropdownNavMenu.svelte'; import DropdownNavMenu from './DropdownNavMenu.svelte';
interface Props { interface Props {
mobileToggle?: Snippet;
children: Snippet; children: Snippet;
} }
let { children }: Props = $props(); let { children, mobileToggle }: Props = $props();
const isReferral = document.referrer.includes(MCBaseURL); const isReferral = document.referrer.includes(MCBaseURL);
@@ -81,7 +82,7 @@
</div> </div>
{/if} {/if}
<nav class="z-50 flex p-6"> <nav class="z-50 flex p-4 sm:p-6">
<div class="flex flex-1 items-center gap-4"> <div class="flex flex-1 items-center gap-4">
<MainMenu /> <MainMenu />
<div <div
@@ -89,7 +90,7 @@
class="flex items-center justify-center gap-4 font-medium" class="flex items-center justify-center gap-4 font-medium"
class:flex-row-reverse={isReferral}> class:flex-row-reverse={isReferral}>
<a href="/" class="whitespace-nowrap text-accent"> <a href="/" class="whitespace-nowrap text-accent">
{#if !isReferral} {#if !isReferral && !mobileToggle}
Mermaid Mermaid
{/if} {/if}
Live Editor Live Editor
@@ -132,4 +133,5 @@
<Separator orientation="vertical" /> <Separator orientation="vertical" />
{@render children()} {@render children()}
</div> </div>
{@render mobileToggle?.()}
</nav> </nav>
+7 -3
View File
@@ -17,13 +17,17 @@
<ArrowsToCircleIcon /> <ArrowsToCircleIcon />
</Button> </Button>
<Separator orientation="vertical" /> <Separator orientation="vertical" />
<Button variant="ghost" size="icon" onclick={() => panZoomState.zoomOut()}> <Button
variant="ghost"
size="icon"
class="hidden sm:block"
onclick={() => panZoomState.zoomOut()}>
<MagnifyingGlassMinusIcon /> <MagnifyingGlassMinusIcon />
</Button> </Button>
<Button variant="ghost" size="icon" onclick={() => panZoomState.zoomIn()}> <Button variant="ghost" size="icon" class="hidden sm:block" onclick={() => panZoomState.zoomIn()}>
<MagnifyingGlassPlusIcon /> <MagnifyingGlassPlusIcon />
</Button> </Button>
<Separator orientation="vertical" /> <Separator orientation="vertical" class="hidden sm:block" />
<Button variant="ghost" size="icon" title="Full Screen" href={$urlsStore.view} target="_blank"> <Button variant="ghost" size="icon" title="Full Screen" href={$urlsStore.view} target="_blank">
<ExpandIcon /> <ExpandIcon />
</Button> </Button>
+1 -1
View File
@@ -7,7 +7,7 @@
<Dialog.Trigger> <Dialog.Trigger>
<ShieldIcon /> <ShieldIcon />
</Dialog.Trigger> </Dialog.Trigger>
<Dialog.Content> <Dialog.Content class="max-h-full overflow-y-auto">
<Dialog.Header> <Dialog.Header>
<Dialog.Title class="flex items-center gap-2 text-xl"> <Dialog.Title class="flex items-center gap-2 text-xl">
<ShieldIcon class="size-8 text-green-700" /> <ShieldIcon class="size-8 text-green-700" />
+9
View File
@@ -5,6 +5,7 @@
import { PanZoomState } from '$/util/panZoom'; import { PanZoomState } from '$/util/panZoom';
import { inputStateStore, stateStore, updateCodeStore } from '$/util/state'; import { inputStateStore, stateStore, updateCodeStore } from '$/util/state';
import { logEvent, saveStatistics } from '$/util/stats'; import { logEvent, saveStatistics } from '$/util/stats';
import FontAwesome, { mayContainFontAwesome } from '$lib/components/FontAwesome.svelte';
import uniqueID from 'lodash-es/uniqueId'; import uniqueID from 'lodash-es/uniqueId';
import type { MermaidConfig } from 'mermaid'; import type { MermaidConfig } from 'mermaid';
import { mode } from 'mode-watcher'; import { mode } from 'mode-watcher';
@@ -23,6 +24,7 @@
let error = $state(false); let error = $state(false);
let panZoom = true; let panZoom = true;
let manualUpdate = true; let manualUpdate = true;
let waitForFontAwesomeToLoad: FontAwesome['waitForFontAwesomeToLoad'] | undefined = $state();
// Set up panZoom state observer to update the store when pan/zoom changes // Set up panZoom state observer to update the store when pan/zoom changes
const setupPanZoomObserver = () => { const setupPanZoomObserver = () => {
@@ -65,6 +67,11 @@
config = state.mermaid; config = state.mermaid;
rough = state.rough; rough = state.rough;
panZoom = state.panZoom ?? true; panZoom = state.panZoom ?? true;
if (mayContainFontAwesome(code)) {
await waitForFontAwesomeToLoad?.();
}
const scroll = view?.parentElement?.scrollTop; const scroll = view?.parentElement?.scrollTop;
delete container.dataset.processed; delete container.dataset.processed;
const viewID = uniqueID('graph-'); const viewID = uniqueID('graph-');
@@ -136,6 +143,8 @@
}); });
</script> </script>
<FontAwesome bind:waitForFontAwesomeToLoad />
<div <div
id="view" id="view"
bind:this={view} bind:this={view}
+4
View File
@@ -101,3 +101,7 @@ export interface ErrorHash {
} }
export type InputType = Exclude<HTMLInputTypeAttribute, 'file'>; export type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
export interface EditorProps {
onUpdate: (text: string) => void;
}
+48
View File
@@ -1,4 +1,5 @@
import type { State } from '$/types'; import type { State } from '$/types';
import Hammer from 'hammerjs';
import type { Point } from 'mermaid/dist/types.js'; import type { Point } from 'mermaid/dist/types.js';
import panzoom from 'svg-pan-zoom'; import panzoom from 'svg-pan-zoom';
type PanZoom = typeof panzoom; type PanZoom = typeof panzoom;
@@ -24,9 +25,56 @@ export class PanZoomState {
} }
public updateElement(diagramView: SVGElement, { pan, zoom }: Pick<State, 'pan' | 'zoom'>) { public updateElement(diagramView: SVGElement, { pan, zoom }: Pick<State, 'pan' | 'zoom'>) {
this.pzoom?.destroy();
let hammer: HammerManager | undefined;
this.pzoom = panzoom(diagramView, { this.pzoom = panzoom(diagramView, {
center: true, center: true,
controlIconsEnabled: false, controlIconsEnabled: false,
customEventsHandler: {
haltEventListeners: ['touchstart', 'touchend', 'touchmove', 'touchleave', 'touchcancel'],
init: function (options) {
const instance = options.instance;
let initialScale = 1;
let pannedX = 0;
let pannedY = 0;
hammer = new Hammer(options.svgElement);
const resetPanned = () => {
pannedX = 0;
pannedY = 0;
};
const handlePan = (event: HammerInput) => {
instance.panBy({ x: event.deltaX - pannedX, y: event.deltaY - pannedY });
pannedX = event.deltaX;
pannedY = event.deltaY;
};
hammer.get('pinch').set({ enable: true });
hammer.on('panstart panmove', function (event) {
if (event.type === 'panstart') {
resetPanned();
}
handlePan(event);
});
hammer.on('pinchstart pinchmove', function (event) {
if (event.type === 'pinchstart') {
initialScale = instance.getZoom();
resetPanned();
}
instance.zoomAtPoint(initialScale * event.scale, {
x: event.center.x,
y: event.center.y
});
handlePan(event);
});
options.svgElement.addEventListener('touchmove', function (event) {
event.preventDefault();
});
},
destroy: function () {
hammer?.destroy();
}
},
fit: true, fit: true,
maxZoom: 12, maxZoom: 12,
minZoom: 0.2, minZoom: 0.2,
+1 -1
View File
@@ -70,7 +70,7 @@
</script> </script>
<div <div
class="flex w-full bg-[#E0095F] p-1.5" class="flex w-full items-center bg-[#E0095F] p-1.5"
role="banner" role="banner"
onmouseenter={() => (shouldAnimate = false)} onmouseenter={() => (shouldAnimate = false)}
onmouseleave={() => (shouldAnimate = true)}> onmouseleave={() => (shouldAnimate = true)}>
+1 -1
View File
@@ -152,7 +152,7 @@ export const urlsStore = derived([stateStore], ([{ code, serialized }]) => {
home: `${MCBaseURL}/?${params}` home: `${MCBaseURL}/?${params}`
}; };
}, },
new: `${window.location.protocol}//${window.location.host}${window.location.pathname}/#${serializeState(defaultState)}`, new: `${window.location.protocol}//${window.location.host}${window.location.pathname}#${serializeState(defaultState)}`,
png, png,
svg: `${rendererUrl}/svg/${serialized}`, svg: `${rendererUrl}/svg/${serialized}`,
view: `/view#${serialized}` view: `/view#${serialized}`
+2
View File
@@ -85,8 +85,10 @@ const delaysPerEvent = {
loadGist: defaultDelay, loadGist: defaultDelay,
loadSampleDiagram: defaultDelay, loadSampleDiagram: defaultDelay,
migration: defaultDelay, migration: defaultDelay,
mobileViewToggle: defaultDelay,
panZoom: minutesToMilliSeconds(10), panZoom: minutesToMilliSeconds(10),
playgroundToggle: 0, playgroundToggle: 0,
pwaInstalled: defaultDelay,
render: minutesToMilliSeconds(5), render: minutesToMilliSeconds(5),
renderDiagram: defaultDelay, renderDiagram: defaultDelay,
themeChange: defaultDelay, themeChange: defaultDelay,
+1 -1
View File
@@ -42,7 +42,7 @@
<ModeWatcher /> <ModeWatcher />
<Toaster /> <Toaster />
<main class="h-screen"> <main class="h-[100dvh]">
{@render children()} {@render children()}
</main> </main>
+74 -40
View File
@@ -13,12 +13,14 @@
import SyncRoughToolbar from '$/components/SyncRoughToolbar.svelte'; import SyncRoughToolbar from '$/components/SyncRoughToolbar.svelte';
import { Button } from '$/components/ui/button'; import { Button } from '$/components/ui/button';
import * as Resizable from '$/components/ui/resizable'; import * as Resizable from '$/components/ui/resizable';
import { Switch } from '$/components/ui/switch';
import { Toggle } from '$/components/ui/toggle'; import { Toggle } from '$/components/ui/toggle';
import VersionSecurityToolbar from '$/components/VersionSecurityToolbar.svelte'; import VersionSecurityToolbar from '$/components/VersionSecurityToolbar.svelte';
import View from '$/components/View.svelte'; import View from '$/components/View.svelte';
import type { EditorMode, Tab } from '$/types'; import type { EditorMode, Tab } from '$/types';
import { PanZoomState } from '$/util/panZoom'; import { PanZoomState } from '$/util/panZoom';
import { stateStore, updateCodeStore, urlsStore } from '$/util/state'; import { stateStore, updateCodeStore, urlsStore } from '$/util/state';
import { logEvent } from '$/util/stats';
import { initHandler } from '$/util/util'; import { initHandler } from '$/util/util';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import CodeIcon from '~icons/custom/code'; import CodeIcon from '~icons/custom/code';
@@ -45,15 +47,41 @@
} }
]; ];
let width = $state(0);
let isMobile = $derived(width < 640);
let isViewMode = $state(true);
onMount(async () => { onMount(async () => {
await initHandler(); await initHandler();
window.addEventListener('appinstalled', () => {
logEvent('pwaInstalled', { isMobile });
});
}); });
let isHistoryOpen = $state(false); let isHistoryOpen = $state(false);
let editorPane: Resizable.Pane | undefined;
$effect(() => {
if (isMobile) {
editorPane?.resize(50);
}
});
</script> </script>
<div class="flex h-full flex-col overflow-hidden"> <div class="flex h-full flex-col overflow-hidden">
<Navbar> {#snippet mobileToggle()}
<div class="flex items-center gap-2">
Edit <Switch
id="editorMode"
class="data-[state=checked]:bg-accent"
bind:checked={isViewMode}
onclick={() => {
logEvent('mobileViewToggle');
}} /> View
</div>
{/snippet}
<Navbar mobileToggle={isMobile ? mobileToggle : undefined}>
<Toggle bind:pressed={isHistoryOpen} size="sm"> <Toggle bind:pressed={isHistoryOpen} size="sm">
<HistoryIcon /> <HistoryIcon />
</Toggle> </Toggle>
@@ -70,47 +98,53 @@
</McWrapper> </McWrapper>
</Navbar> </Navbar>
<div class="flex flex-1 overflow-hidden"> <div class="flex flex-1 flex-col overflow-hidden" bind:clientWidth={width}>
<Resizable.PaneGroup direction="horizontal" autoSaveId="liveEditor" class="p-6 pt-0"> <div
<Resizable.Pane defaultSize={30} minSize={15} class="hidden md:block"> class={[
<div class="flex h-full flex-col gap-6" id="editorPane"> 'size-full',
<Card isMobile && ['w-[200%] duration-300', isViewMode && '-translate-x-1/2']
onselect={tabSelectHandler} ]}>
isOpen <Resizable.PaneGroup
tabs={editorTabs} direction="horizontal"
activeTabID={$stateStore.editorMode} autoSaveId="liveEditor"
isClosable={false}> class="gap-4 p-2 pt-0 sm:gap-0 sm:p-6 sm:pt-0">
{#snippet actions()} <Resizable.Pane bind:this={editorPane} defaultSize={30} minSize={15}>
<DiagramDocButton /> <div class="flex h-full flex-col gap-4 sm:gap-6">
{/snippet} <Card
<Editor /> onselect={tabSelectHandler}
</Card> isOpen
tabs={editorTabs}
activeTabID={$stateStore.editorMode}
isClosable={false}>
{#snippet actions()}
<DiagramDocButton />
{/snippet}
<Editor {isMobile} />
</Card>
<div class="group flex flex-wrap justify-between gap-6"> <div class="group flex flex-wrap justify-between gap-4 sm:gap-6">
<Preset /> <Preset />
<Actions /> <Actions />
</div>
</div> </div>
</div>
</Resizable.Pane>
<Resizable.Handle class="mr-1 opacity-0" />
<Resizable.Pane minSize={15} class="relative flex h-full flex-1 flex-col overflow-hidden">
<View {panZoomState} shouldShowGrid={$stateStore.grid} />
<div class="absolute right-0 top-0"><PanZoomToolbar {panZoomState} /></div>
<div class="absolute bottom-0 right-0"><VersionSecurityToolbar /></div>
<div class="absolute bottom-0 left-5"><SyncRoughToolbar /></div>
<div class="rounded bg-primary p-2 text-center shadow md:hidden">
Code editing not supported on mobile. Please use a desktop browser.
</div>
</Resizable.Pane>
{#if isHistoryOpen}
<Resizable.Handle class="ml-1 hidden opacity-0 md:block" />
<Resizable.Pane
minSize={15}
defaultSize={30}
class="hidden h-full flex-grow flex-col md:flex">
<History />
</Resizable.Pane> </Resizable.Pane>
{/if} <Resizable.Handle class="mr-1 hidden opacity-0 sm:block" />
</Resizable.PaneGroup> <Resizable.Pane minSize={15} class="relative flex h-full flex-1 flex-col overflow-hidden">
<View {panZoomState} shouldShowGrid={$stateStore.grid} />
<div class="absolute right-0 top-0"><PanZoomToolbar {panZoomState} /></div>
<div class="absolute bottom-0 right-0"><VersionSecurityToolbar /></div>
<div class="absolute bottom-0 left-0 sm:left-5"><SyncRoughToolbar /></div>
</Resizable.Pane>
{#if isHistoryOpen}
<Resizable.Handle class="ml-1 hidden opacity-0 sm:block" />
<Resizable.Pane
minSize={15}
defaultSize={30}
class="hidden h-full flex-grow flex-col sm:flex">
<History />
</Resizable.Pane>
{/if}
</Resizable.PaneGroup>
</div>
</div> </div>
</div> </div>
+2 -2
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 491 491" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"> <svg width="100%" height="100%" viewBox="0 0 490 490" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M490.16,84.61C490.16,37.912 452.248,0 405.55,0L84.61,0C37.912,0 0,37.912 0,84.61L0,405.55C0,452.248 37.912,490.16 84.61,490.16L405.55,490.16C452.248,490.16 490.16,452.248 490.16,405.55L490.16,84.61Z" style="fill:rgb(255,54,112);"/> <path d="M490.16,84.61C490.16,37.912 452.248,0 405.55,0L84.61,0C37.912,0 0,37.912 0,84.61L0,405.55C0,452.248 37.912,490.16 84.61,490.16L405.55,490.16C452.248,490.16 490.16,452.248 490.16,405.55L490.16,84.61Z" style="fill:#ff3670;"/>
<path d="M407.48,111.18C335.587,108.103 269.573,152.338 245.08,220C220.587,152.338 154.573,108.103 82.68,111.18C80.285,168.229 107.577,222.632 154.74,254.82C178.908,271.419 193.35,298.951 193.27,328.27L193.27,379.13L296.9,379.13L296.9,328.27C296.816,298.953 311.255,271.42 335.42,254.82C382.596,222.644 409.892,168.233 407.48,111.18Z" style="fill:white;fill-rule:nonzero;"/> <path d="M407.48,111.18C335.587,108.103 269.573,152.338 245.08,220C220.587,152.338 154.573,108.103 82.68,111.18C80.285,168.229 107.577,222.632 154.74,254.82C178.908,271.419 193.35,298.951 193.27,328.27L193.27,379.13L296.9,379.13L296.9,328.27C296.816,298.953 311.255,271.42 335.42,254.82C382.596,222.644 409.892,168.233 407.48,111.18Z" style="fill:white;fill-rule:nonzero;"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

+3 -3
View File
@@ -9,10 +9,10 @@
} }
], ],
"start_url": "/edit", "start_url": "/edit",
"background_color": "#6366F1", "background_color": "#ff3670",
"display": "standalone", "display": "standalone",
"scope": "/edit", "scope": "/edit",
"theme_color": "#6366F1", "theme_color": "#ff3670",
"description": "FlowChart & Diagrams Editor.", "description": "FlowChart & Diagrams Editor.",
"orientation": "landscape" "orientation": "natural"
} }
+2 -1
View File
@@ -15,7 +15,8 @@ const config = {
'$/*': './src/lib/*' '$/*': './src/lib/*'
}, },
adapter: adapter({ adapter: adapter({
pages: 'docs' pages: 'docs',
fallback: '404.html'
}) })
} }
}; };