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,
|
"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,
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
Generated
+596
-282
File diff suppressed because it is too large
Load Diff
+2
-4
@@ -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 & Diagrams Editor - Mermaid Live Editor</title>
|
<title>Online FlowChart & 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,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;
|
||||||
|
|||||||
@@ -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">
|
<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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Vendored
+4
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)}>
|
||||||
|
|||||||
@@ -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}`
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<ModeWatcher />
|
<ModeWatcher />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
|
||||||
<main class="h-screen">
|
<main class="h-[100dvh]">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 |
@@ -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
@@ -15,7 +15,8 @@ const config = {
|
|||||||
'$/*': './src/lib/*'
|
'$/*': './src/lib/*'
|
||||||
},
|
},
|
||||||
adapter: adapter({
|
adapter: adapter({
|
||||||
pages: 'docs'
|
pages: 'docs',
|
||||||
|
fallback: '404.html'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user