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:
Vendored
+8
-1
@@ -2,12 +2,14 @@
|
||||
"editor.formatOnSave": true,
|
||||
"cSpell.blockCheckingWhenLineLengthGreaterThan": 150,
|
||||
"cSpell.words": [
|
||||
"appinstalled",
|
||||
"asyncable",
|
||||
"Browserslist",
|
||||
"ckppp",
|
||||
"corg",
|
||||
"cssnano",
|
||||
"esserializer",
|
||||
"fsegurai",
|
||||
"gantt",
|
||||
"gitgraph",
|
||||
"KROKI",
|
||||
@@ -16,14 +18,19 @@
|
||||
"mindmap",
|
||||
"Pageview",
|
||||
"pako",
|
||||
"panmove",
|
||||
"panstart",
|
||||
"panzoom",
|
||||
"pinchmove",
|
||||
"pinchstart",
|
||||
"pzoom",
|
||||
"roughjs",
|
||||
"Serde",
|
||||
"serdes",
|
||||
"Stackable",
|
||||
"tailwindcss",
|
||||
"uparrow"
|
||||
"uparrow",
|
||||
"zenuml"
|
||||
],
|
||||
"vitest.commandLine": "pnpm test:unit",
|
||||
"vitest.enable": true,
|
||||
|
||||
@@ -14,7 +14,7 @@ Edit, preview and share mermaid charts/diagrams.
|
||||
|
||||
## 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!
|
||||
|
||||
|
||||
+23
-12
@@ -24,14 +24,15 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@sveltejs/adapter-static": "3.0.8",
|
||||
"@sveltejs/kit": "2.20.3",
|
||||
"@sveltejs/kit": "2.20.6",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
||||
"@types/hammerjs": "^2.0.46",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.13.17",
|
||||
"@types/node": "^22.14.1",
|
||||
"@types/pako": "2.0.3",
|
||||
"@types/uuid": "9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
@@ -39,7 +40,7 @@
|
||||
"@vitest/coverage-v8": "2.1.9",
|
||||
"@vitest/ui": "^2.1.9",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"bits-ui": "^1.3.16",
|
||||
"bits-ui": "^1.3.19",
|
||||
"c8": "7.14.0",
|
||||
"chai": "^4.5.0",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -57,35 +58,45 @@
|
||||
"esserializer": "^1.3.11",
|
||||
"husky": "^8.0.3",
|
||||
"jsdom": "^25.0.1",
|
||||
"lint-staged": "^15.5.0",
|
||||
"lucide-svelte": "^0.487.0",
|
||||
"lint-staged": "^15.5.1",
|
||||
"lucide-svelte": "^0.488.0",
|
||||
"node-html-parser": "^6.1.13",
|
||||
"paneforge": "^1.0.0-next.4",
|
||||
"paneforge": "^1.0.0-next.5",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-load-config": "5.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"svelte": "^5.23.2",
|
||||
"svelte": "^5.27.0",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"svelte-sonner": "^0.3.28",
|
||||
"tailwind-merge": "^3.1.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwind-variants": "^0.3.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript": "^5.8.3",
|
||||
"unplugin-icons": "^22.1.0",
|
||||
"vite": "^5.4.14",
|
||||
"vitest": "^2.1.9",
|
||||
"vitest-dom": "^0.1.1"
|
||||
},
|
||||
"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",
|
||||
"@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/layout-elk": "^0.1.7",
|
||||
"@mermaid-js/mermaid-zenuml": "^0.2.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"hammerjs": "^2.0.8",
|
||||
"js-base64": "3.7.7",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mermaid": "^11.6.0",
|
||||
@@ -107,7 +118,7 @@
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.7.1+sha512.2d92c86b7928dc8284f53494fb4201f983da65f0fb4f0d40baafa5cf628fa31dae3e5968f12466f17df7e97310e30f343a648baea1b9b350685dafafffdf5808",
|
||||
"packageManager": "pnpm@10.8.0+sha512.0e82714d1b5b43c74610193cb20734897c1d00de89d0e18420aebc5977fa13d780a9cb05734624e81ebd81cc876cd464794850641c48b9544326b5622ca29971",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"deasync",
|
||||
|
||||
Generated
+596
-282
File diff suppressed because it is too large
Load Diff
+2
-4
@@ -4,16 +4,14 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Online FlowChart & Diagrams Editor - Mermaid Live Editor</title>
|
||||
<meta
|
||||
name="og:image"
|
||||
content="https://github.com/mermaid-js/mermaid/raw/develop/img/header.png" />
|
||||
<meta name="og:image" content="%sveltekit.assets%/favicon.svg" />
|
||||
<link rel="canonical" href="https://mermaid.live" />
|
||||
<meta
|
||||
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." />
|
||||
<link rel="icon" type="image/png" href="%sveltekit.assets%/favicon.svg" />
|
||||
<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" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||
@import '@fontsource-variable/recursive/crsv.css';
|
||||
|
||||
@tailwind base;
|
||||
|
||||
@@ -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>
|
||||
@@ -1,117 +1,30 @@
|
||||
<script lang="ts">
|
||||
import DesktopEditor from '$/components/DesktopEditor.svelte';
|
||||
import McWrapper from '$/components/McWrapper.svelte';
|
||||
import MermaidChartIcon from '$/components/MermaidChartIcon.svelte';
|
||||
import MobileEditor from '$/components/MobileEditor.svelte';
|
||||
import { Button } from '$/components/ui/button';
|
||||
import { TID } from '$/constants';
|
||||
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 { 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';
|
||||
|
||||
let divElement: HTMLDivElement | undefined = $state();
|
||||
let editor: monaco.editor.IStandaloneCodeEditor | undefined;
|
||||
let editorOptions: monaco.editor.IStandaloneEditorConstructionOptions = {
|
||||
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') {
|
||||
let { isMobile }: { isMobile: boolean } = $props();
|
||||
const onUpdate = (text: string) => {
|
||||
if ($stateStore.editorMode === 'code') {
|
||||
updateCode(text);
|
||||
} else {
|
||||
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>
|
||||
|
||||
<div class="flex h-full flex-col pt-1">
|
||||
<div bind:this={divElement} id="editor" class="h-full flex-grow overflow-hidden"></div>
|
||||
<div class="flex h-full flex-col">
|
||||
{#if isMobile}
|
||||
<MobileEditor {onUpdate} />
|
||||
{:else}
|
||||
<DesktopEditor {onUpdate} />
|
||||
{/if}
|
||||
{#if $stateStore.error instanceof Error}
|
||||
<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">
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
let { children }: Props = $props();
|
||||
</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()}
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -22,10 +22,11 @@
|
||||
import DropdownNavMenu from './DropdownNavMenu.svelte';
|
||||
|
||||
interface Props {
|
||||
mobileToggle?: Snippet;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
let { children, mobileToggle }: Props = $props();
|
||||
|
||||
const isReferral = document.referrer.includes(MCBaseURL);
|
||||
|
||||
@@ -81,7 +82,7 @@
|
||||
</div>
|
||||
{/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">
|
||||
<MainMenu />
|
||||
<div
|
||||
@@ -89,7 +90,7 @@
|
||||
class="flex items-center justify-center gap-4 font-medium"
|
||||
class:flex-row-reverse={isReferral}>
|
||||
<a href="/" class="whitespace-nowrap text-accent">
|
||||
{#if !isReferral}
|
||||
{#if !isReferral && !mobileToggle}
|
||||
Mermaid
|
||||
{/if}
|
||||
Live Editor
|
||||
@@ -132,4 +133,5 @@
|
||||
<Separator orientation="vertical" />
|
||||
{@render children()}
|
||||
</div>
|
||||
{@render mobileToggle?.()}
|
||||
</nav>
|
||||
|
||||
@@ -17,13 +17,17 @@
|
||||
<ArrowsToCircleIcon />
|
||||
</Button>
|
||||
<Separator orientation="vertical" />
|
||||
<Button variant="ghost" size="icon" onclick={() => panZoomState.zoomOut()}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="hidden sm:block"
|
||||
onclick={() => panZoomState.zoomOut()}>
|
||||
<MagnifyingGlassMinusIcon />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onclick={() => panZoomState.zoomIn()}>
|
||||
<Button variant="ghost" size="icon" class="hidden sm:block" onclick={() => panZoomState.zoomIn()}>
|
||||
<MagnifyingGlassPlusIcon />
|
||||
</Button>
|
||||
<Separator orientation="vertical" />
|
||||
<Separator orientation="vertical" class="hidden sm:block" />
|
||||
<Button variant="ghost" size="icon" title="Full Screen" href={$urlsStore.view} target="_blank">
|
||||
<ExpandIcon />
|
||||
</Button>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<Dialog.Trigger>
|
||||
<ShieldIcon />
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content>
|
||||
<Dialog.Content class="max-h-full overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="flex items-center gap-2 text-xl">
|
||||
<ShieldIcon class="size-8 text-green-700" />
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { PanZoomState } from '$/util/panZoom';
|
||||
import { inputStateStore, stateStore, updateCodeStore } from '$/util/state';
|
||||
import { logEvent, saveStatistics } from '$/util/stats';
|
||||
import FontAwesome, { mayContainFontAwesome } from '$lib/components/FontAwesome.svelte';
|
||||
import uniqueID from 'lodash-es/uniqueId';
|
||||
import type { MermaidConfig } from 'mermaid';
|
||||
import { mode } from 'mode-watcher';
|
||||
@@ -23,6 +24,7 @@
|
||||
let error = $state(false);
|
||||
let panZoom = true;
|
||||
let manualUpdate = true;
|
||||
let waitForFontAwesomeToLoad: FontAwesome['waitForFontAwesomeToLoad'] | undefined = $state();
|
||||
|
||||
// Set up panZoom state observer to update the store when pan/zoom changes
|
||||
const setupPanZoomObserver = () => {
|
||||
@@ -65,6 +67,11 @@
|
||||
config = state.mermaid;
|
||||
rough = state.rough;
|
||||
panZoom = state.panZoom ?? true;
|
||||
|
||||
if (mayContainFontAwesome(code)) {
|
||||
await waitForFontAwesomeToLoad?.();
|
||||
}
|
||||
|
||||
const scroll = view?.parentElement?.scrollTop;
|
||||
delete container.dataset.processed;
|
||||
const viewID = uniqueID('graph-');
|
||||
@@ -136,6 +143,8 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<FontAwesome bind:waitForFontAwesomeToLoad />
|
||||
|
||||
<div
|
||||
id="view"
|
||||
bind:this={view}
|
||||
|
||||
Vendored
+4
@@ -101,3 +101,7 @@ export interface ErrorHash {
|
||||
}
|
||||
|
||||
export type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
|
||||
|
||||
export interface EditorProps {
|
||||
onUpdate: (text: string) => void;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { State } from '$/types';
|
||||
import Hammer from 'hammerjs';
|
||||
import type { Point } from 'mermaid/dist/types.js';
|
||||
import panzoom from 'svg-pan-zoom';
|
||||
type PanZoom = typeof panzoom;
|
||||
@@ -24,9 +25,56 @@ export class PanZoomState {
|
||||
}
|
||||
|
||||
public updateElement(diagramView: SVGElement, { pan, zoom }: Pick<State, 'pan' | 'zoom'>) {
|
||||
this.pzoom?.destroy();
|
||||
let hammer: HammerManager | undefined;
|
||||
this.pzoom = panzoom(diagramView, {
|
||||
center: true,
|
||||
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,
|
||||
maxZoom: 12,
|
||||
minZoom: 0.2,
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex w-full bg-[#E0095F] p-1.5"
|
||||
class="flex w-full items-center bg-[#E0095F] p-1.5"
|
||||
role="banner"
|
||||
onmouseenter={() => (shouldAnimate = false)}
|
||||
onmouseleave={() => (shouldAnimate = true)}>
|
||||
|
||||
@@ -152,7 +152,7 @@ export const urlsStore = derived([stateStore], ([{ code, serialized }]) => {
|
||||
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,
|
||||
svg: `${rendererUrl}/svg/${serialized}`,
|
||||
view: `/view#${serialized}`
|
||||
|
||||
@@ -85,8 +85,10 @@ const delaysPerEvent = {
|
||||
loadGist: defaultDelay,
|
||||
loadSampleDiagram: defaultDelay,
|
||||
migration: defaultDelay,
|
||||
mobileViewToggle: defaultDelay,
|
||||
panZoom: minutesToMilliSeconds(10),
|
||||
playgroundToggle: 0,
|
||||
pwaInstalled: defaultDelay,
|
||||
render: minutesToMilliSeconds(5),
|
||||
renderDiagram: defaultDelay,
|
||||
themeChange: defaultDelay,
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<ModeWatcher />
|
||||
<Toaster />
|
||||
|
||||
<main class="h-screen">
|
||||
<main class="h-[100dvh]">
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
|
||||
@@ -13,12 +13,14 @@
|
||||
import SyncRoughToolbar from '$/components/SyncRoughToolbar.svelte';
|
||||
import { Button } from '$/components/ui/button';
|
||||
import * as Resizable from '$/components/ui/resizable';
|
||||
import { Switch } from '$/components/ui/switch';
|
||||
import { Toggle } from '$/components/ui/toggle';
|
||||
import VersionSecurityToolbar from '$/components/VersionSecurityToolbar.svelte';
|
||||
import View from '$/components/View.svelte';
|
||||
import type { EditorMode, Tab } from '$/types';
|
||||
import { PanZoomState } from '$/util/panZoom';
|
||||
import { stateStore, updateCodeStore, urlsStore } from '$/util/state';
|
||||
import { logEvent } from '$/util/stats';
|
||||
import { initHandler } from '$/util/util';
|
||||
import { onMount } from 'svelte';
|
||||
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 () => {
|
||||
await initHandler();
|
||||
window.addEventListener('appinstalled', () => {
|
||||
logEvent('pwaInstalled', { isMobile });
|
||||
});
|
||||
});
|
||||
|
||||
let isHistoryOpen = $state(false);
|
||||
|
||||
let editorPane: Resizable.Pane | undefined;
|
||||
$effect(() => {
|
||||
if (isMobile) {
|
||||
editorPane?.resize(50);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<HistoryIcon />
|
||||
</Toggle>
|
||||
@@ -70,47 +98,53 @@
|
||||
</McWrapper>
|
||||
</Navbar>
|
||||
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<Resizable.PaneGroup direction="horizontal" autoSaveId="liveEditor" class="p-6 pt-0">
|
||||
<Resizable.Pane defaultSize={30} minSize={15} class="hidden md:block">
|
||||
<div class="flex h-full flex-col gap-6" id="editorPane">
|
||||
<Card
|
||||
onselect={tabSelectHandler}
|
||||
isOpen
|
||||
tabs={editorTabs}
|
||||
activeTabID={$stateStore.editorMode}
|
||||
isClosable={false}>
|
||||
{#snippet actions()}
|
||||
<DiagramDocButton />
|
||||
{/snippet}
|
||||
<Editor />
|
||||
</Card>
|
||||
<div class="flex flex-1 flex-col overflow-hidden" bind:clientWidth={width}>
|
||||
<div
|
||||
class={[
|
||||
'size-full',
|
||||
isMobile && ['w-[200%] duration-300', isViewMode && '-translate-x-1/2']
|
||||
]}>
|
||||
<Resizable.PaneGroup
|
||||
direction="horizontal"
|
||||
autoSaveId="liveEditor"
|
||||
class="gap-4 p-2 pt-0 sm:gap-0 sm:p-6 sm:pt-0">
|
||||
<Resizable.Pane bind:this={editorPane} defaultSize={30} minSize={15}>
|
||||
<div class="flex h-full flex-col gap-4 sm:gap-6">
|
||||
<Card
|
||||
onselect={tabSelectHandler}
|
||||
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">
|
||||
<Preset />
|
||||
<Actions />
|
||||
<div class="group flex flex-wrap justify-between gap-4 sm:gap-6">
|
||||
<Preset />
|
||||
<Actions />
|
||||
</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>
|
||||
{/if}
|
||||
</Resizable.PaneGroup>
|
||||
<Resizable.Handle class="mr-1 hidden opacity-0 sm:block" />
|
||||
<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>
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
<?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">
|
||||
<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;">
|
||||
<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);"/>
|
||||
<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:#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;"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -9,10 +9,10 @@
|
||||
}
|
||||
],
|
||||
"start_url": "/edit",
|
||||
"background_color": "#6366F1",
|
||||
"background_color": "#ff3670",
|
||||
"display": "standalone",
|
||||
"scope": "/edit",
|
||||
"theme_color": "#6366F1",
|
||||
"theme_color": "#ff3670",
|
||||
"description": "FlowChart & Diagrams Editor.",
|
||||
"orientation": "landscape"
|
||||
"orientation": "natural"
|
||||
}
|
||||
|
||||
+2
-1
@@ -15,7 +15,8 @@ const config = {
|
||||
'$/*': './src/lib/*'
|
||||
},
|
||||
adapter: adapter({
|
||||
pages: 'docs'
|
||||
pages: 'docs',
|
||||
fallback: '404.html'
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user