Compare commits

...
Author SHA1 Message Date
Sidharth Vinod d1fd4dc244 feat: More edit modes in CTA 2026-04-02 17:33:52 +05:30
Sidharth Vinod 9436f8ddda chore: Remove canonical 2026-04-01 22:43:58 +05:30
Sidharth Vinod 62699769d5 fix: Use {} instead of undefined 2026-04-01 22:19:02 +05:30
Sidharth Vinod 5608dc5661 feat: Add visual editor button 2026-04-01 21:50:18 +05:30
Sidharth VinodandGitHub 29e92a9702 Merge pull request #1917 from mermaid-js/sidv/fixBasePath
fix(#1914): Resolve base path
2026-04-01 15:13:59 +00:00
Sidharth Vinod 0e0da3b1e9 chore: Reduce Error debounce interval 2026-04-01 20:43:15 +05:30
Sidharth Vinod d273e1d722 fix: Asset links 2026-03-30 21:30:13 +05:30
Sidharth Vinod c799dff042 fix(#1914): Resolve redirectURL considering base path 2026-03-30 21:24:33 +05:30
Sidharth Vinod c675dc02e9 fix: Avoid double popups 2026-03-25 17:03:04 +09:00
Sidharth Vinod ab760f35fa feat: Retain search query in URL to get insights into traffic flow 2026-03-18 22:05:07 +09:00
Sidharth VinodandGitHub 8a436791de Merge pull request #1908 from mermaid-js/renovate/mermaid-packages
fix(deps): update dependency mermaid to ^11.13.0
2026-03-16 07:41:08 -07:00
Sidharth VinodandGitHub d16a1776e6 Merge pull request #1909 from mermaid-js/sidv/correctPrivacyPolicy
feat: Configure Privacy Policy
2026-03-16 07:40:41 -07:00
Sidharth Vinod ab720e0267 fix: local test change 2026-03-16 23:33:20 +09:00
Sidharth Vinod d7db71e38a chore: Add new mermaid home button 2026-03-16 23:29:49 +09:00
Sidharth Vinod b466e8430e feat: Show mermaid.live privacy policy only on mermaid.live
As the OSS project only controls the mermaid.live domain, we're adding an env variable to let anyone who is self hosting live editor to set their own privacy policy, or to hide the button if they want to.
2026-03-16 23:29:13 +09:00
renovate[bot]andGitHub ea404df79b fix(deps): update dependency mermaid to ^11.13.0 2026-03-16 01:36:21 +00:00
Sidharth Vinod 15cdb7b64a chore: Show editor picker once to existing users as well 2026-03-12 13:35:42 +09:00
Sidharth Vinod 340911f271 chore: Remove panZoom event 2026-03-05 23:28:34 +05:30
Sidharth VinodandGitHub 44d15c0f5e Merge pull request #1901 from mermaid-js/claude/hide-editor-picker-mobile-2h7p0
Hide editor picker on mobile
2026-03-03 14:09:58 -08:00
Claude c36e51fb9f Hide editor picker dialog on mobile in shouldShowEditorChooser
Return false when viewport width is below 640px (Tailwind sm breakpoint),
matching the isMobile check used elsewhere in the app.

https://claude.ai/code/session_01KoPM7CZKWdwdZPm4MneGVE
2026-03-03 22:02:10 +00:00
26 changed files with 541 additions and 202 deletions
+1
View File
@@ -1,4 +1,5 @@
MERMAID_DOMAIN=''
MERMAID_BASE_PATH=''
MERMAID_DOCS_URL='https://mermaid.js.org'
MERMAID_ANALYTICS_URL=''
MERMAID_RENDERER_URL='https://mermaid.ink'
+3
View File
@@ -19,6 +19,9 @@ ARG MERMAID_KROKI_RENDERER_URL
ARG MERMAID_ANALYTICS_URL
ARG MERMAID_DOMAIN
ARG MERMAID_IS_ENABLED_MERMAID_CHART_LINKS
ARG MERMAID_PRIVACY_POLICY_URL
ARG MERMAID_HIDE_PRIVACY_POLICY
ARG MERMAID_BASE_PATH
COPY . ./
+2
View File
@@ -28,6 +28,8 @@ If you want to speed up the progress for mermaid-live-editor, join the Discord c
docker run --platform linux/amd64 --publish 8000:8080 ghcr.io/mermaid-js/mermaid-live-editor
```
The published docker image is built using our default environment variables. You cannot override them when running the image. If you need to customize them, you will need to build the image yourself.
### To configure renderer URL
When building set the MERMAID_RENDERER_URL build argument to the rendering
+2 -1
View File
@@ -48,6 +48,7 @@
"chai": "^4.5.0",
"clsx": "^2.1.1",
"cssnano": "^6.1.2",
"dotenv": "^17.3.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-es": "^4.1.0",
@@ -102,7 +103,7 @@
"hammerjs": "^2.0.8",
"js-base64": "3.7.7",
"lodash-es": "^4.17.21",
"mermaid": "^11.12.0",
"mermaid": "^11.13.0",
"mode-watcher": "^0.5.1",
"monaco-editor": "0.52.2",
"pako": "2.1.0",
+126 -93
View File
@@ -37,22 +37,22 @@ importers:
version: 6.1.4(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.7)(@lezer/highlight@1.2.1)
'@mermaid-js/examples':
specifier: ^1.0.0
version: 1.0.0(mermaid@11.12.0)
version: 1.0.0(mermaid@11.13.0)
'@mermaid-js/layout-elk':
specifier: ^0.1.9
version: 0.1.9(mermaid@11.12.0)
version: 0.1.9(mermaid@11.13.0)
'@mermaid-js/layout-tidy-tree':
specifier: ^0.2.1
version: 0.2.1(mermaid@11.12.0)
version: 0.2.1(mermaid@11.13.0)
'@mermaid-js/mermaid-zenuml':
specifier: ^0.2.2
version: 0.2.2(mermaid@11.12.0)
version: 0.2.2(mermaid@11.13.0)
codemirror:
specifier: ^6.0.1
version: 6.0.1
dayjs:
specifier: ^1.11.13
version: 1.11.13
version: 1.11.18
hammerjs:
specifier: ^2.0.8
version: 2.0.8
@@ -63,8 +63,8 @@ importers:
specifier: ^4.17.21
version: 4.17.21
mermaid:
specifier: ^11.12.0
version: 11.12.0
specifier: ^11.13.0
version: 11.13.0
mode-watcher:
specifier: ^0.5.1
version: 0.5.1(svelte@5.38.6)
@@ -165,6 +165,9 @@ importers:
cssnano:
specifier: ^6.1.2
version: 6.1.2(postcss@8.5.3)
dotenv:
specifier: ^17.3.1
version: 17.3.1
eslint:
specifier: ^9.39.2
version: 9.39.2(jiti@2.6.1)
@@ -317,20 +320,20 @@ packages:
'@braintree/sanitize-url@7.1.1':
resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==}
'@chevrotain/cst-dts-gen@11.0.3':
resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==}
'@chevrotain/cst-dts-gen@11.1.2':
resolution: {integrity: sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==}
'@chevrotain/gast@11.0.3':
resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==}
'@chevrotain/gast@11.1.2':
resolution: {integrity: sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g==}
'@chevrotain/regexp-to-ast@11.0.3':
resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==}
'@chevrotain/regexp-to-ast@11.1.2':
resolution: {integrity: sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw==}
'@chevrotain/types@11.0.3':
resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==}
'@chevrotain/types@11.1.2':
resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==}
'@chevrotain/utils@11.0.3':
resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==}
'@chevrotain/utils@11.1.2':
resolution: {integrity: sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==}
'@codemirror/autocomplete@6.18.6':
resolution: {integrity: sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==}
@@ -802,8 +805,8 @@ packages:
peerDependencies:
mermaid: ^10 || ^11
'@mermaid-js/parser@0.6.2':
resolution: {integrity: sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==}
'@mermaid-js/parser@1.0.1':
resolution: {integrity: sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@@ -1320,6 +1323,9 @@ packages:
resolution: {integrity: sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@upsetjs/venn.js@2.0.0':
resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==}
'@vitest/coverage-v8@3.2.4':
resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==}
peerDependencies:
@@ -1585,8 +1591,8 @@ packages:
peerDependencies:
chevrotain: ^11.0.0
chevrotain@11.0.3:
resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==}
chevrotain@11.1.2:
resolution: {integrity: sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
@@ -1759,8 +1765,8 @@ packages:
peerDependencies:
cytoscape: ^3.2.0
cytoscape@3.31.1:
resolution: {integrity: sha512-Hx5Mtb1+hnmAKaZZ/7zL1Y5HTFYOjdDswZy/jD+1WINRU8KVi1B7+vlHdsTwY+VCFucTreoyu1RDzQJ9u0d2Hw==}
cytoscape@3.33.1:
resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==}
engines: {node: '>=0.10'}
d3-array@2.12.1:
@@ -1902,19 +1908,19 @@ packages:
resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
engines: {node: '>=12'}
dagre-d3-es@7.0.11:
resolution: {integrity: sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==}
dagre-d3-es@7.0.14:
resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==}
data-urls@5.0.0:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
dayjs@1.11.18:
resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==}
dayjs@1.11.20:
resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
debug@4.4.0:
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
engines: {node: '>=6.0'}
@@ -1987,9 +1993,16 @@ packages:
dompurify@3.2.6:
resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==}
dompurify@3.3.3:
resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==}
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
dotenv@17.3.1:
resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==}
engines: {node: '>=12'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -2310,11 +2323,12 @@ packages:
glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
@@ -2568,8 +2582,8 @@ packages:
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
katex@0.16.22:
resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==}
katex@0.16.38:
resolution: {integrity: sha512-cjHooZUmIAUmDsHBN+1n8LaZdpmbj03LtYeYPyuYB7OuloiaeaV6N4LcfjcnHVzGWjVQmKrxxTrpDcmSzEZQwQ==}
hasBin: true
keyv@4.5.4:
@@ -2588,9 +2602,9 @@ packages:
kolorist@1.8.0:
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
langium@3.3.1:
resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==}
engines: {node: '>=16.0.0'}
langium@4.2.1:
resolution: {integrity: sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==}
engines: {node: '>=20.10.0', npm: '>=10.2.3'}
layout-base@1.0.2:
resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==}
@@ -2706,6 +2720,9 @@ packages:
lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
lodash-es@4.17.23:
resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==}
lodash.memoize@4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
@@ -2776,8 +2793,8 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
mermaid@11.12.0:
resolution: {integrity: sha512-ZudVx73BwrMJfCFmSSJT84y6u5brEoV8DOItdHomNLz32uBjNrelm7mg95X7g+C6UoQH/W6mBLGDEDv73JdxBg==}
mermaid@11.13.0:
resolution: {integrity: sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw==}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
@@ -3011,6 +3028,7 @@ packages:
plausible-tracker@0.3.9:
resolution: {integrity: sha512-hMhneYm3GCPyQon88SZrVJx+LlqhM1kZFQbuAgXPoh/Az2YvO1B6bitT9qlhpiTdJlsT5lsr3gPmzoVjb5CDXA==}
engines: {node: '>=10'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
playwright-core@1.52.0:
resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==}
@@ -4029,8 +4047,8 @@ packages:
resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==}
hasBin: true
vscode-uri@3.0.8:
resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
vscode-uri@3.1.0:
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
@@ -4049,6 +4067,7 @@ packages:
whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
@@ -4179,22 +4198,22 @@ snapshots:
'@braintree/sanitize-url@7.1.1': {}
'@chevrotain/cst-dts-gen@11.0.3':
'@chevrotain/cst-dts-gen@11.1.2':
dependencies:
'@chevrotain/gast': 11.0.3
'@chevrotain/types': 11.0.3
lodash-es: 4.17.21
'@chevrotain/gast': 11.1.2
'@chevrotain/types': 11.1.2
lodash-es: 4.17.23
'@chevrotain/gast@11.0.3':
'@chevrotain/gast@11.1.2':
dependencies:
'@chevrotain/types': 11.0.3
lodash-es: 4.17.21
'@chevrotain/types': 11.1.2
lodash-es: 4.17.23
'@chevrotain/regexp-to-ast@11.0.3': {}
'@chevrotain/regexp-to-ast@11.1.2': {}
'@chevrotain/types@11.0.3': {}
'@chevrotain/types@11.1.2': {}
'@chevrotain/utils@11.0.3': {}
'@chevrotain/utils@11.1.2': {}
'@codemirror/autocomplete@6.18.6':
dependencies:
@@ -4663,32 +4682,32 @@ snapshots:
'@marijn/find-cluster-break@1.0.2': {}
'@mermaid-js/examples@1.0.0(mermaid@11.12.0)':
'@mermaid-js/examples@1.0.0(mermaid@11.13.0)':
dependencies:
mermaid: 11.12.0
mermaid: 11.13.0
'@mermaid-js/layout-elk@0.1.9(mermaid@11.12.0)':
'@mermaid-js/layout-elk@0.1.9(mermaid@11.13.0)':
dependencies:
d3: 7.9.0
elkjs: 0.9.3
mermaid: 11.12.0
mermaid: 11.13.0
'@mermaid-js/layout-tidy-tree@0.2.1(mermaid@11.12.0)':
'@mermaid-js/layout-tidy-tree@0.2.1(mermaid@11.13.0)':
dependencies:
d3: 7.9.0
mermaid: 11.12.0
mermaid: 11.13.0
'@mermaid-js/mermaid-zenuml@0.2.2(mermaid@11.12.0)':
'@mermaid-js/mermaid-zenuml@0.2.2(mermaid@11.13.0)':
dependencies:
'@zenuml/core': 3.36.0
mermaid: 11.12.0
mermaid: 11.13.0
transitivePeerDependencies:
- '@types/react'
- ts-node
'@mermaid-js/parser@0.6.2':
'@mermaid-js/parser@1.0.1':
dependencies:
langium: 3.3.1
langium: 4.2.1
'@nodelib/fs.scandir@2.1.5':
dependencies:
@@ -5212,6 +5231,11 @@ snapshots:
'@typescript-eslint/types': 8.42.0
eslint-visitor-keys: 4.2.1
'@upsetjs/venn.js@2.0.0':
optionalDependencies:
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
'@vitest/coverage-v8@3.2.4(vitest@3.2.4)':
dependencies:
'@ampproject/remapping': 2.3.0
@@ -5547,19 +5571,19 @@ snapshots:
check-error@2.1.1: {}
chevrotain-allstar@0.3.1(chevrotain@11.0.3):
chevrotain-allstar@0.3.1(chevrotain@11.1.2):
dependencies:
chevrotain: 11.0.3
lodash-es: 4.17.21
chevrotain: 11.1.2
lodash-es: 4.17.23
chevrotain@11.0.3:
chevrotain@11.1.2:
dependencies:
'@chevrotain/cst-dts-gen': 11.0.3
'@chevrotain/gast': 11.0.3
'@chevrotain/regexp-to-ast': 11.0.3
'@chevrotain/types': 11.0.3
'@chevrotain/utils': 11.0.3
lodash-es: 4.17.21
'@chevrotain/cst-dts-gen': 11.1.2
'@chevrotain/gast': 11.1.2
'@chevrotain/regexp-to-ast': 11.1.2
'@chevrotain/types': 11.1.2
'@chevrotain/utils': 11.1.2
lodash-es: 4.17.23
chokidar@3.6.0:
dependencies:
@@ -5749,17 +5773,17 @@ snapshots:
'@asamuzakjp/css-color': 3.1.1
rrweb-cssom: 0.8.0
cytoscape-cose-bilkent@4.1.0(cytoscape@3.31.1):
cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1):
dependencies:
cose-base: 1.0.3
cytoscape: 3.31.1
cytoscape: 3.33.1
cytoscape-fcose@2.2.0(cytoscape@3.31.1):
cytoscape-fcose@2.2.0(cytoscape@3.33.1):
dependencies:
cose-base: 2.2.0
cytoscape: 3.31.1
cytoscape: 3.33.1
cytoscape@3.31.1: {}
cytoscape@3.33.1: {}
d3-array@2.12.1:
dependencies:
@@ -5928,20 +5952,20 @@ snapshots:
d3-transition: 3.0.1(d3-selection@3.0.0)
d3-zoom: 3.0.0
dagre-d3-es@7.0.11:
dagre-d3-es@7.0.14:
dependencies:
d3: 7.9.0
lodash-es: 4.17.21
lodash-es: 4.17.23
data-urls@5.0.0:
dependencies:
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
dayjs@1.11.13: {}
dayjs@1.11.18: {}
dayjs@1.11.20: {}
debug@4.4.0:
dependencies:
ms: 2.1.3
@@ -5994,12 +6018,18 @@ snapshots:
optionalDependencies:
'@types/trusted-types': 2.0.7
dompurify@3.3.3:
optionalDependencies:
'@types/trusted-types': 2.0.7
domutils@3.2.2:
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
dotenv@17.3.1: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -6594,7 +6624,7 @@ snapshots:
json-stable-stringify-without-jsonify@1.0.1: {}
katex@0.16.22:
katex@0.16.38:
dependencies:
commander: 8.3.0
@@ -6610,13 +6640,13 @@ snapshots:
kolorist@1.8.0: {}
langium@3.3.1:
langium@4.2.1:
dependencies:
chevrotain: 11.0.3
chevrotain-allstar: 0.3.1(chevrotain@11.0.3)
chevrotain: 11.1.2
chevrotain-allstar: 0.3.1(chevrotain@11.1.2)
vscode-languageserver: 9.0.1
vscode-languageserver-textdocument: 1.0.12
vscode-uri: 3.0.8
vscode-uri: 3.1.0
layout-base@1.0.2: {}
@@ -6720,6 +6750,8 @@ snapshots:
lodash-es@4.17.21: {}
lodash-es@4.17.23: {}
lodash.memoize@4.1.2: {}
lodash.merge@4.6.2: {}
@@ -6780,23 +6812,24 @@ snapshots:
merge2@1.4.1: {}
mermaid@11.12.0:
mermaid@11.13.0:
dependencies:
'@braintree/sanitize-url': 7.1.1
'@iconify/utils': 3.0.2
'@mermaid-js/parser': 0.6.2
'@mermaid-js/parser': 1.0.1
'@types/d3': 7.4.3
cytoscape: 3.31.1
cytoscape-cose-bilkent: 4.1.0(cytoscape@3.31.1)
cytoscape-fcose: 2.2.0(cytoscape@3.31.1)
'@upsetjs/venn.js': 2.0.0
cytoscape: 3.33.1
cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1)
cytoscape-fcose: 2.2.0(cytoscape@3.33.1)
d3: 7.9.0
d3-sankey: 0.12.3
dagre-d3-es: 7.0.11
dayjs: 1.11.18
dompurify: 3.2.6
katex: 0.16.22
dagre-d3-es: 7.0.14
dayjs: 1.11.20
dompurify: 3.3.3
katex: 0.16.38
khroma: 2.1.0
lodash-es: 4.17.21
lodash-es: 4.17.23
marked: 16.3.0
roughjs: 4.6.6
stylis: 4.3.6
@@ -7915,7 +7948,7 @@ snapshots:
dependencies:
vscode-languageserver-protocol: 3.17.5
vscode-uri@3.0.8: {}
vscode-uri@3.1.0: {}
w3c-keyname@2.2.8: {}
-1
View File
@@ -5,7 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Online FlowChart &amp; Diagrams Editor - Mermaid Live Editor</title>
<meta name="og:image" content="%sveltekit.assets%/favicon.svg" />
<link rel="canonical" href="https://mermaid.ai/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." />
+2
View File
@@ -7,6 +7,8 @@ interface ImportMetaEnv {
readonly MERMAID_DOCS_URL?: string;
readonly MERMAID_DOMAIN?: string;
readonly MERMAID_IS_ENABLED_MERMAID_CHART_LINKS?: string;
readonly MERMAID_PRIVACY_POLICY_URL?: string;
readonly MERMAID_HIDE_PRIVACY_POLICY?: string;
// more env variables...
}
+1 -1
View File
@@ -24,7 +24,7 @@
const showErrorDebounced = debounce(() => {
showError = true;
}, 5000);
}, 3000);
$effect(() => {
if ($stateStore.error) {
@@ -0,0 +1,118 @@
<script lang="ts">
import McWrapper from '$/components/McWrapper.svelte';
import MermaidChartIcon from '$/components/MermaidChartIcon.svelte';
import { Button } from '$/components/ui/button';
import { standardizeDiagramType } from '$/util/mermaid';
import { stateStore, urlsStore } from '$/util/state';
import { logMermaidChartClick } from '$/util/stats';
import { quintInOut } from 'svelte/easing';
import { slide } from 'svelte/transition';
const visualEditDiagramTypes = new Set([
'flowchart',
'stateDiagram',
'classDiagram',
'sequenceDiagram',
'er',
'requirement',
'mindmap'
]);
const diagramType = $derived.by(() => {
const dt = $stateStore.diagramType;
return dt ? standardizeDiagramType(dt) : undefined;
});
const showVisualEdit = $derived.by(() => {
return diagramType ? visualEditDiagramTypes.has(diagramType) : false;
});
interface EnhancedEditAction {
campaign: string;
label: string;
medium: 'ai_edit' | 'visual_edit' | 'voice_edit';
source: string;
}
let currentActionMedium = $state<EnhancedEditAction['medium'] | undefined>(undefined);
let previousDiagramType = $state<string | undefined>(undefined);
const availableActions = $derived.by<EnhancedEditAction[]>(() => {
if (!$stateStore.diagramType) {
return [];
}
const actions: EnhancedEditAction[] = [
{ campaign: 'ai_1', label: 'with AI', medium: 'ai_edit', source: 'aiEdit' },
{ campaign: 'voice_1', label: 'with Voice', medium: 'voice_edit', source: 'voiceEdit' }
];
if (showVisualEdit) {
actions.unshift({
campaign: 'visual_1',
label: 'Visually',
medium: 'visual_edit',
source: 'visualEdit'
});
}
return actions;
});
const currentAction = $derived.by(() => {
const actions = availableActions;
if (actions.length === 0) {
return undefined;
}
return actions.find(({ medium }) => medium === currentActionMedium) ?? actions[0];
});
$effect(() => {
const actions = availableActions;
if (!diagramType || actions.length === 0) {
return;
}
const hasCurrentAction = actions.some(({ medium }) => medium === currentActionMedium);
if (diagramType !== previousDiagramType || !hasCurrentAction) {
currentActionMedium = actions[Math.floor(Math.random() * actions.length)].medium;
previousDiagramType = diagramType;
}
});
$effect(() => {
if (diagramType) {
return;
}
previousDiagramType = undefined;
});
</script>
{#if currentAction}
<McWrapper>
<Button
variant="secondary"
size="sm"
href={$urlsStore.mermaidChart({
medium: currentAction.medium,
campaign: currentAction.campaign
}).save}
target="_blank"
onclick={() => logMermaidChartClick(currentAction.source)}>
<MermaidChartIcon />
Edit
{#key currentAction.label}
<span
class="-ml-1"
in:slide={{ axis: 'x', easing: quintInOut, delay: 400 }}
out:slide={{ axis: 'x', easing: quintInOut }}>
{currentAction.label}
</span>
{/key}
</Button>
</McWrapper>
{/if}
+2 -1
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import { asset } from '$app/paths';
import type { ClassValue } from 'svelte/elements';
let { class: className }: { class?: ClassValue } = $props();
</script>
<img class={['size-4', className]} src="/mermaidchart-logo.svg" alt="Mermaid Chart" />
<img class={['size-4', className]} src={asset('/mermaidchart-logo.svg')} alt="Mermaid Chart" />
+58 -37
View File
@@ -1,45 +1,66 @@
<script>
import ExternalLinkWrapper from '$/components/ExternalLinkWrapper.svelte';
import * as Dialog from '$/components/ui/dialog';
import { env } from '$/util/env';
import { isOnMermaidLive } from '$/util/migration/domainMigration';
import ShieldIcon from '~icons/material-symbols/shield-lock-outline-rounded';
</script>
<Dialog.Root>
<Dialog.Trigger>
{#if env.privacyPolicyUrl}
<a href={env.privacyPolicyUrl} target="_blank">
<ShieldIcon />
</Dialog.Trigger>
<Dialog.Content class="max-h-full overflow-hidden overflow-y-auto p-12">
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2 text-xl">
<ShieldIcon class="size-8 text-green-700" />
Data security
</Dialog.Title>
</Dialog.Header>
</a>
{:else}
<Dialog.Root>
<Dialog.Trigger>
<ShieldIcon />
</Dialog.Trigger>
<Dialog.Content class="max-h-full overflow-hidden overflow-y-auto p-12">
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2 text-xl">
<ShieldIcon class="size-8 text-green-700" />
Data security
</Dialog.Title>
</Dialog.Header>
<p class="text-xl font-semibold">Your diagrams never leave your browser.</p>
<p>They're only stored in the URL and your browser's local storage.</p>
<p>
This is a fully open source, client-side app deployed on <a
href="https://github.com/mermaid-js/mermaid-live-editor/deployments"
class="underline"
target="_blank">GitHub Pages</a>
that works offline as a
<a href="https://web.dev/explore/progressive-web-apps" target="_blank">Progressive Web App</a
>.
</p>
<p>
We use self hosted, privacy-friendly Plausible Analytics to collect anonymous usage metadata
(diagram types, feature usage, etc.). All data is <a
href="https://p.mermaid.live/mermaid.live"
class="underline"
target="_blank">publicly available</a
>.
</p>
<ExternalLinkWrapper domain="example.com" isVisible>
<p class="text-left">
External services (PNG/SVG/Kroki exports, "Save to Mermaid Chart", "Repair with AI", etc)
will share your diagram with those 3rd parties, and are highlighted in the UI on hover.
</p>
</ExternalLinkWrapper>
</Dialog.Content>
</Dialog.Root>
{#if isOnMermaidLive()}
<p class="text-xl font-semibold">Your diagrams never leave your browser.</p>
<p>They're only stored in the URL and your browser's local storage.</p>
<p>
This is a fully open source, client-side app deployed on <a
href="https://github.com/mermaid-js/mermaid-live-editor/deployments"
class="underline"
target="_blank">GitHub Pages</a>
that works offline as a
<a href="https://web.dev/explore/progressive-web-apps" target="_blank"
>Progressive Web App</a
>.
</p>
<p>
We use self hosted, privacy-friendly Plausible Analytics to collect anonymous usage
metadata (diagram types, feature usage, etc.). All data is <a
href="https://p.mermaid.live/mermaid.live"
class="underline"
target="_blank">publicly available</a
>.
</p>
<ExternalLinkWrapper domain="example.com" isVisible>
<p class="text-left">
External services (PNG/SVG/Kroki exports, "Save to Mermaid Chart", "Repair with AI",
etc) will share your diagram with those 3rd parties, and are highlighted in the UI on
hover.
</p>
</ExternalLinkWrapper>
{:else}
<p>No privacy policy has been configured for this deployment.</p>
<p>
If you are self-hosting the Mermaid Live Editor, set the
<code class="rounded bg-muted px-1.5 py-0.5 text-sm">MERMAID_PRIVACY_POLICY_URL</code>
environment variable at build time to link to your privacy policy, or set
<code class="rounded bg-muted px-1.5 py-0.5 text-sm">MERMAID_HIDE_PRIVACY_POLICY</code>
to <code class="rounded bg-muted px-1.5 py-0.5 text-sm">true</code> to hide this button.
</p>
{/if}
</Dialog.Content>
</Dialog.Root>
{/if}
+2 -1
View File
@@ -4,6 +4,7 @@
import { Separator } from '$/components/ui/separator';
import { env } from '$/util/env';
import { urlsStore } from '$/util/state';
import { asset } from '$app/paths';
import ShareIcon from '~icons/material-symbols/share';
import CopyInput from './CopyInput.svelte';
import MermaidChartIcon from './MermaidChartIcon.svelte';
@@ -22,7 +23,7 @@
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<h2 class="flex items-center gap-2">
<img class="size-5" src="/favicon.svg" alt="Mermaid Live Editor" />
<img class="size-5" src={asset('/favicon.svg')} alt="Mermaid Live Editor" />
Mermaid Live Editor
</h2>
<CopyInput value={window.location.href} />
@@ -4,6 +4,7 @@
import { Button } from '$/components/ui/button';
import { Separator } from '$/components/ui/separator';
import { TID } from '$/constants';
import { env } from '$/util/env';
import { version } from 'mermaid/package.json';
import { mode, setMode } from 'mode-watcher';
import ThemeIcon from './ThemeIcon.svelte';
@@ -11,11 +12,13 @@
<FloatingToolbar>
<span class="text-sm font-semibold opacity-60">v{version}</span>
<Button variant="ghost" size="icon" title="Privacy & Security">
<Privacy />
</Button>
{#if !env.hidePrivacyPolicy}
<Button variant="ghost" size="icon" title="Privacy & Security">
<Privacy />
</Button>
<Separator orientation="vertical" />
<Separator orientation="vertical" />
{/if}
<Button
variant="ghost"
size="icon"
+1 -2
View File
@@ -4,7 +4,7 @@
import { render as renderDiagram } from '$/util/mermaid';
import { PanZoomState } from '$/util/panZoom';
import { inputStateStore, stateStore, updateCodeStore } from '$/util/state';
import { logEvent, saveStatistics } from '$/util/stats';
import { saveStatistics } from '$/util/stats';
import FontAwesome, { mayContainFontAwesome } from '$lib/components/FontAwesome.svelte';
import uniqueID from 'lodash-es/uniqueId';
import type { MermaidConfig } from 'mermaid';
@@ -30,7 +30,6 @@
const setupPanZoomObserver = () => {
panZoomState.onPanZoomChange = (pan, zoom) => {
updateCodeStore({ pan, zoom });
logEvent('panZoom');
};
};
@@ -5,6 +5,7 @@
import { logEvent, logMermaidChartClick } from '$/util/stats';
import { getCheckoutUrl } from '$/util/util';
import CodeIcon from '~icons/custom/code';
import ArrowIcon from '~icons/material-symbols/arrow-forward-rounded';
import OpenSourceIcon from '~icons/material-symbols/book-2-outline-rounded';
import ChatIcon from '~icons/material-symbols/chat-outline-rounded';
import EditIcon from '~icons/material-symbols/edit-outline-rounded';
@@ -39,6 +40,12 @@
logEvent('chooseEditor', { choice: 'openSource' });
close();
};
const handleContinueToNewHome = () => {
logEvent('chooseEditor', { choice: 'newHome' });
close();
window.open('https://mermaid.ai/live', '_blank');
};
</script>
<Dialog.Root
@@ -46,15 +53,15 @@
onOpenChange={(v) => {
if (!v) handleStartFree();
}}>
<Dialog.Content class="max-w-2xl bg-pink-50 p-0 dark:bg-background">
<Dialog.Header class="px-8 pt-8 pb-0">
<Dialog.Content class="flex max-w-2xl flex-col gap-6 bg-pink-50 p-6 dark:bg-background">
<Dialog.Header>
<Dialog.Title class="text-center text-2xl font-semibold">Choose your editor</Dialog.Title>
<Dialog.Description class="-mt-2 text-center text-sm font-light">
<Dialog.Description class="text-center text-sm font-light">
You'll never see this again
</Dialog.Description>
</Dialog.Header>
<div class="grid gap-4 px-6 pt-4 pb-8 sm:grid-cols-2">
<div class="grid gap-4 sm:grid-cols-2">
<!-- Mermaid Plus Card -->
<div
class="relative flex flex-col overflow-hidden rounded-xl border-2 border-accent bg-white shadow dark:bg-card">
@@ -62,20 +69,24 @@
Recommended
</div>
<div class="flex flex-col p-6">
<h3 class="text-xl font-bold">Mermaid Plus</h3>
<p class="mb-4 text-sm text-muted-foreground">Unlock AI, storage and collaboration</p>
<div class="mb-2 flex justify-center">
<span
class="rounded-full bg-pink-100 px-3 py-0.5 text-xs font-semibold text-pink-700 dark:bg-pink-950 dark:text-pink-300">
10% off with code JS26
</span>
<div class="flex flex-col gap-4 p-6">
<div>
<h3 class="text-xl font-bold">Mermaid Plus</h3>
<p class="text-sm text-muted-foreground">Unlock AI, storage and collaboration</p>
</div>
<Button variant="accent" class="mb-6 w-full" onclick={handleStartTrial}>
Start free trial
</Button>
<div class="flex flex-col gap-2">
<div class="flex justify-center">
<span
class="rounded-full bg-pink-100 px-3 py-0.5 text-xs font-semibold text-pink-700 dark:bg-pink-950 dark:text-pink-300">
10% off with code JS26
</span>
</div>
<Button variant="accent" class="w-full" onclick={handleStartTrial}>
Start free trial
</Button>
</div>
<ul class="space-y-3 text-sm">
<li class="flex items-center gap-2">
@@ -103,13 +114,22 @@
</div>
<!-- Open Source Card -->
<div class="flex flex-col rounded-xl border bg-white p-6 shadow dark:bg-card">
<h3 class="mt-10 text-xl font-bold">Open Source</h3>
<p class="mb-4 text-sm text-muted-foreground">Code only, no login, always free</p>
<div class="flex flex-col gap-4 rounded-xl border bg-white p-6 shadow dark:bg-card">
<div class="flex flex-col justify-end pt-10">
<h3 class="text-xl font-bold">Open Source</h3>
<p class="text-sm text-muted-foreground">Code only, no login, always free</p>
</div>
<Button variant="outline" class="mb-6 w-full border-accent" onclick={handleStartFree}>
Start free
</Button>
<div class="flex flex-col gap-2">
<p class="mt-2 text-sm text-muted-foreground">Mermaid has a new home</p>
<Button variant="outline" class="w-full border-accent" onclick={handleContinueToNewHome}>
Continue to mermaid.ai/live
<ArrowIcon class="ml-1 size-4" />
</Button>
<Button variant="outline" class="w-full" onclick={handleStartFree}>
Stay on mermaid.live
</Button>
</div>
<ul class="space-y-3 text-sm">
<li class="flex items-center gap-2">
@@ -131,5 +151,14 @@
</ul>
</div>
</div>
<div class="text-center">
<a
href="https://mermaid.ai/privacy-policy"
target="_blank"
class="text-sm text-foreground underline hover:text-accent">
mermaid.ai Privacy Policy
</a>
</div>
</Dialog.Content>
</Dialog.Root>
+2
View File
@@ -2,7 +2,9 @@ export const env = {
analyticsUrl: import.meta.env.MERMAID_ANALYTICS_URL ?? '',
docsUrl: import.meta.env.MERMAID_DOCS_URL ?? 'https://mermaid.js.org',
domain: import.meta.env.MERMAID_DOMAIN ?? '',
hidePrivacyPolicy: import.meta.env.MERMAID_HIDE_PRIVACY_POLICY === 'true',
isEnabledMermaidChartLinks: import.meta.env.MERMAID_IS_ENABLED_MERMAID_CHART_LINKS === 'true',
krokiRendererUrl: import.meta.env.MERMAID_KROKI_RENDERER_URL ?? '',
privacyPolicyUrl: import.meta.env.MERMAID_PRIVACY_POLICY_URL ?? '',
rendererUrl: import.meta.env.MERMAID_RENDERER_URL ?? ''
} as const;
+11 -19
View File
@@ -2,6 +2,7 @@ import { C } from '$/constants';
import { env } from '$/util/env';
const mermaidAiDomain = 'mermaid.ai';
const mermaidLiveDomain = 'mermaid.live';
/**
* Check if we're on mermaid.ai
@@ -11,25 +12,12 @@ export const isOnMermaidAI = (): boolean => {
return domain === mermaidAiDomain || domain.endsWith(`.${mermaidAiDomain}`);
};
// localStorage keys that indicate a returning user.
// Note: codeStore is excluded because it's always populated with the default state on first load.
const userDataStorageKeys = [
'manualHistoryStore', // Manual history entries
'autoHistoryStore' // Auto history entries
];
/**
* Check if user has any stored data in localStorage.
* This includes saved diagrams, history entries, etc.
* Check if we're on mermaid.live
*/
const hasStoredUserData = (): boolean => {
for (const key of userDataStorageKeys) {
const value = window.localStorage.getItem(key);
if (value && value !== '[]' && value !== 'null' && value !== '{}') {
return true;
}
}
return false;
export const isOnMermaidLive = (): boolean => {
const domain = window.location.hostname;
return domain === mermaidLiveDomain || domain.endsWith(`.${mermaidLiveDomain}`);
};
/**
@@ -54,7 +42,9 @@ const isReferredFromMermaid = (): boolean => {
hostname === 'mermaid.ai' ||
hostname.endsWith('.mermaid.ai') ||
hostname === 'mermaid.js.org' ||
hostname.endsWith('.mermaid.js.org')
hostname.endsWith('.mermaid.js.org') ||
hostname === 'mermaid.live' ||
hostname.endsWith('.mermaid.live')
);
} catch {
return false;
@@ -64,11 +54,13 @@ const isReferredFromMermaid = (): boolean => {
/**
* Check if the editor chooser modal should be shown.
* Shows for new users who haven't dismissed it and aren't viewing a shared link.
* Not shown on mobile (viewport width < 640px).
*/
export const shouldShowEditorChooser = (): boolean => {
if (!env.isEnabledMermaidChartLinks) return false;
if (!isOnMermaidAI() && !isOnMermaidLive()) return false;
if (window.innerWidth < 640) return false;
if (window.localStorage.getItem(C.editorChooserDismissedKey) === 'true') return false;
if (hasStoredUserData()) return false;
if (hasPakoData()) return false;
if (isReferredFromMermaid()) return false;
return true;
+51
View File
@@ -0,0 +1,51 @@
import { describe, expect, it } from 'vitest';
import { buildRedirectUrl } from './redirect';
const mockLocation = (url: string): Location => {
const parsed = new URL(url);
return { hash: parsed.hash, search: parsed.search } as Location;
};
describe('buildRedirectUrl', () => {
it('should redirect to /edit by default when hash is empty', () => {
expect(buildRedirectUrl(mockLocation('https://mermaid.live/'))).toBe('/edit');
});
it('should preserve search params', () => {
expect(buildRedirectUrl(mockLocation('https://mermaid.live/?utm_source=github'))).toBe(
'/edit?utm_source=github'
);
});
it('should extract route and fragment from old hash format', () => {
expect(buildRedirectUrl(mockLocation('https://mermaid.live/#/edit/pako:abc123'))).toBe(
'/edit#pako:abc123'
);
});
it('should place search params before the hash fragment', () => {
expect(
buildRedirectUrl(mockLocation('https://mermaid.live/?utm_source=twitter#/edit/pako:abc123'))
).toBe('/edit?utm_source=twitter#pako:abc123');
});
it('should handle hash with view route', () => {
expect(buildRedirectUrl(mockLocation('https://mermaid.live/#/view/pako:xyz'))).toBe(
'/view#pako:xyz'
);
});
it('should default to edit when hash has no route', () => {
expect(buildRedirectUrl(mockLocation('https://mermaid.live/#somethingelse'))).toBe('/edit');
});
it('should handle multiple search params with hash', () => {
expect(
buildRedirectUrl(
mockLocation(
'https://mermaid.live/?utm_source=gh&utm_medium=link&utm_campaign=test#/edit/pako:data'
)
)
).toBe('/edit?utm_source=gh&utm_medium=link&utm_campaign=test#pako:data');
});
});
+17
View File
@@ -0,0 +1,17 @@
import { resolve } from '$app/paths';
/**
* Build the redirect URL for legacy root-path links.
* Extracts the route and fragment from the old hash-based URL format,
* and ensures search params (e.g. UTM) come before the hash fragment.
*/
export const buildRedirectUrl = (location: Location): string => {
const parts = location.hash.split('/');
let path = 'edit';
let fragment = '';
if (parts.length > 2) {
path = parts[1];
fragment = `#${parts[2]}`;
}
return `${resolve(`/${path}`, {})}${location.search}${fragment}`;
};
+15 -4
View File
@@ -80,7 +80,7 @@ const processState = async (state: State) => {
processed.error = error as Error;
errorDebug();
console.error(error);
if ('hash' in error) {
if (error && typeof error === 'object' && 'hash' in error) {
try {
let errorString = processed.error.toString();
const errorLineText = extractErrorLineText(errorString);
@@ -137,14 +137,25 @@ export const urlsStore = derived([stateStore], ([{ code, serialized }]) => {
? `[![](${png})](${window.location.protocol}//${window.location.host}${window.location.pathname}#${serialized})`
: '',
mermaidChart: ({
medium
medium,
campaign
}: {
medium: 'ai_repair' | 'main_menu' | 'save_diagram' | 'share' | 'vibe_diagramming';
medium:
| 'ai_repair'
| 'ai_edit'
| 'main_menu'
| 'save_diagram'
| 'share'
| 'vibe_diagramming'
| 'visual_edit'
| 'voice_edit';
campaign?: string;
}) => {
const utmSource = getUTMSource();
const params = new URLSearchParams({
utm_source: utmSource,
utm_medium: medium
utm_medium: medium,
...(campaign ? { utm_campaign: campaign } : {})
}).toString();
return {
save: `${MCBaseURL}/app/plugin/save?state=${serialized}&${params}`,
+47
View File
@@ -0,0 +1,47 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { getAnalyticsSafeUrl } from './stats';
describe('getAnalyticsUrl', () => {
beforeEach(() => {
// Reset location to a clean state before each test
window.history.replaceState(null, '', '/');
});
it('should return origin and pathname for a simple URL', () => {
window.history.replaceState(null, '', '/edit');
const url = getAnalyticsSafeUrl();
expect(url).toBe(`${window.location.origin}/edit`);
});
it('should include search/query params (UTM parameters)', () => {
window.history.replaceState(null, '', '/edit?utm_source=github&utm_medium=docs');
const url = getAnalyticsSafeUrl();
expect(url).toBe(`${window.location.origin}/edit?utm_source=github&utm_medium=docs`);
});
it('should never include the hash', () => {
window.history.replaceState(null, '', '/edit#pako:someDiagramData');
// replaceState doesn't set hash, so set it via location
window.location.hash = '#pako:someDiagramData';
const url = getAnalyticsSafeUrl();
expect(url).not.toContain('#');
expect(url).not.toContain('pako:');
expect(url).toBe(`${window.location.origin}/edit`);
});
it('should include search params but exclude hash when both are present', () => {
window.history.replaceState(null, '', '/edit?utm_campaign=launch');
window.location.hash = '#pako:diagramDataHere';
const url = getAnalyticsSafeUrl();
expect(url).not.toContain('#');
expect(url).not.toContain('pako:');
expect(url).toContain('utm_campaign=launch');
expect(url).toBe(`${window.location.origin}/edit?utm_campaign=launch`);
});
it('should return just origin for root path with no params', () => {
window.history.replaceState(null, '', '/');
const url = getAnalyticsSafeUrl();
expect(url).toBe(`${window.location.origin}/`);
});
});
+10 -6
View File
@@ -23,6 +23,15 @@ export const initAnalytics = async (): Promise<void> => {
}
};
/**
* Build the current page URL for analytics tracking.
* Includes origin, pathname, and search (for UTM params),
* but never the hash (which contains diagram data).
*/
export const getAnalyticsSafeUrl = (): string => {
return window.location.origin + window.location.pathname + window.location.search;
};
export const countLines = (code: string): number => {
return (code.match(/\n/g)?.length ?? 0) + 1;
};
@@ -91,7 +100,6 @@ const delaysPerEvent = {
mermaidChartClick: noDelay,
migration: defaultDelay,
mobileViewToggle: defaultDelay,
panZoom: minutesToMilliSeconds(10),
pwaInstalled: defaultDelay,
render: minutesToMilliSeconds(5),
renderDiagram: defaultDelay,
@@ -112,11 +120,7 @@ export const logEvent = (
if (timeouts.has(key)) {
clearTimeout(timeouts.get(key));
} else {
plausible.trackEvent(
name,
{ props: data },
{ url: window.location.origin + window.location.pathname }
);
plausible.trackEvent(name, { props: data }, { url: getAnalyticsSafeUrl() });
}
timeouts.set(
key,
+4 -2
View File
@@ -5,7 +5,7 @@ import { initLoading } from './loading';
import { isOnMermaidAI } from './migration/domainMigration';
import { applyMigrations } from './migrations';
import { initURLSubscription, loadState, updateCodeStore, verifyState } from './state';
import { initAnalytics, plausible } from './stats';
import { getAnalyticsSafeUrl, initAnalytics, plausible } from './stats';
export const getDomain = (url?: string): string => {
if (!url) return '';
@@ -30,7 +30,9 @@ export const initHandler = async (): Promise<void> => {
syncDiagram();
initURLSubscription();
await initAnalytics();
plausible?.trackPageview({ url: window.location.origin + window.location.pathname });
plausible?.trackPageview({
url: getAnalyticsSafeUrl()
});
verifyState();
};
+2 -8
View File
@@ -1,16 +1,10 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { buildRedirectUrl } from '$lib/util/redirect';
import { onMount } from 'svelte';
import { base } from '$app/paths';
onMount(async () => {
// Handle old live editor links and redirect to new version
const hash = window.location.hash.split('/');
let newURL = 'edit';
if (hash.length > 2) {
newURL = `${hash[1]}#${hash[2]}`;
}
await goto(`${base}/${newURL}`, {
await goto(buildRedirectUrl(window.location), {
replaceState: true
});
});
+2
View File
@@ -3,6 +3,7 @@
import Card from '$/components/Card/Card.svelte';
import DiagramDocButton from '$/components/DiagramDocumentationButton.svelte';
import Editor from '$/components/Editor.svelte';
import EnhancedEditsButton from '$/components/EnhancedEditsButton.svelte';
import History from '$/components/History/History.svelte';
import McWrapper from '$/components/McWrapper.svelte';
import MermaidChartIcon from '$/components/MermaidChartIcon.svelte';
@@ -136,6 +137,7 @@
<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 top-0 left-5 hidden md:block"><EnhancedEditsButton /></div>
<div class="absolute top-0 right-0"><PanZoomToolbar {panZoomState} /></div>
<div class="absolute right-0 bottom-0"><VersionSecurityToolbar /></div>
<div class="absolute bottom-0 left-0 sm:left-5"><SyncRoughToolbar /></div>
+4
View File
@@ -1,4 +1,5 @@
import adapter from '@sveltejs/adapter-static';
import 'dotenv/config';
import { sveltePreprocess } from 'svelte-preprocess';
/** @type {import('@sveltejs/kit').Config} */
@@ -10,6 +11,9 @@ const config = {
alias: {
'$/*': './src/lib/*'
},
paths: {
base: process.env.MERMAID_BASE_PATH ?? ''
},
adapter: adapter({
pages: 'docs',
fallback: '404.html'