@@ -1,7 +1,7 @@
|
||||
MERMAID_DOMAIN=''
|
||||
MERMAID_ANALYTICS_URL=''
|
||||
MERMAID_RENDERER_URL='https://mermaid.ink'
|
||||
MERMAID_KROKI_RENDERER='https://kroki.io'
|
||||
MERMAID_KROKI_RENDERER_URL='https://kroki.io'
|
||||
MERMAID_IS_ENABLED_MERMAID_CHART_LINKS=''
|
||||
|
||||
# cp .env .env.local to make local changes
|
||||
|
||||
+21
-1
@@ -11,7 +11,15 @@ module.exports = {
|
||||
'plugin:svelte/prettier',
|
||||
'prettier'
|
||||
],
|
||||
plugins: ['tailwindcss', '@typescript-eslint', 'es', 'vitest', 'no-only-tests', 'unicorn'],
|
||||
plugins: [
|
||||
'tailwindcss',
|
||||
'@typescript-eslint',
|
||||
'es',
|
||||
'vitest',
|
||||
'no-only-tests',
|
||||
'sort-keys',
|
||||
'unicorn'
|
||||
],
|
||||
ignorePatterns: [
|
||||
'docs/*',
|
||||
'*.cjs',
|
||||
@@ -40,6 +48,15 @@ module.exports = {
|
||||
'plugin:@typescript-eslint/strict',
|
||||
'prettier'
|
||||
]
|
||||
},
|
||||
{
|
||||
files: ['**/components/ui/**'],
|
||||
rules: {
|
||||
'unicorn/prefer-export-from': 'off',
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
'unicorn/explicit-length-check': 'off',
|
||||
'sort-keys/sort-keys-fix': 'off'
|
||||
}
|
||||
}
|
||||
],
|
||||
parserOptions: {
|
||||
@@ -54,6 +71,7 @@ module.exports = {
|
||||
es2020: true
|
||||
},
|
||||
rules: {
|
||||
'sort-keys/sort-keys-fix': ['error', 'asc', { minKeys: 5 }],
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
'error',
|
||||
{
|
||||
@@ -77,6 +95,7 @@ module.exports = {
|
||||
'error',
|
||||
{
|
||||
allowList: {
|
||||
args: true,
|
||||
ctx: true,
|
||||
db: true,
|
||||
doc: true,
|
||||
@@ -87,6 +106,7 @@ module.exports = {
|
||||
k: true,
|
||||
param: true,
|
||||
Props: true,
|
||||
props: true,
|
||||
req: true,
|
||||
res: true,
|
||||
str: true,
|
||||
|
||||
+1
-1
@@ -4,6 +4,6 @@
|
||||
"bracketSameLine": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"tailwindConfig": "./tailwind.config.cjs",
|
||||
"tailwindConfig": "./tailwind.config.js",
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"]
|
||||
}
|
||||
|
||||
Vendored
+10
-2
@@ -7,7 +7,6 @@
|
||||
"ckppp",
|
||||
"corg",
|
||||
"cssnano",
|
||||
"daisyui",
|
||||
"esserializer",
|
||||
"gantt",
|
||||
"gitgraph",
|
||||
@@ -22,6 +21,7 @@
|
||||
"roughjs",
|
||||
"Serde",
|
||||
"serdes",
|
||||
"Stackable",
|
||||
"tailwindcss",
|
||||
"uparrow"
|
||||
],
|
||||
@@ -29,5 +29,13 @@
|
||||
"vitest.enable": true,
|
||||
"testing.autoRun.mode": "rerun",
|
||||
"svelte.enable-ts-plugin": true,
|
||||
"githubPullRequests.ignoredPullRequestBranches": ["develop"]
|
||||
"githubPullRequests.ignoredPullRequestBranches": ["develop"],
|
||||
"[svelte]": {
|
||||
"editor.defaultFormatter": "svelte.svelte-vscode"
|
||||
},
|
||||
"tailwindCSS.classAttributes": ["class", "className", ".*Classes"],
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["([\"'`][^\"'`]*.*?[\"'`])", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
],
|
||||
"tailwindCSS.emmetCompletions": true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://next.shadcn-svelte.com/schema.json",
|
||||
"style": "new-york",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/app.postcss",
|
||||
"baseColor": "slate"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://next.shadcn-svelte.com/registry"
|
||||
}
|
||||
+14
-3
@@ -24,11 +24,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@iconify-json/material-symbols": "^1.2.15",
|
||||
"@iconify-json/mdi": "^1.2.3",
|
||||
"@sveltejs/adapter-static": "3.0.8",
|
||||
"@sveltejs/kit": "2.20.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/svelte": "^5.2.7",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/pako": "2.0.3",
|
||||
"@types/uuid": "9.0.8",
|
||||
@@ -37,14 +37,17 @@
|
||||
"@vitest/coverage-v8": "2.1.9",
|
||||
"@vitest/ui": "^2.1.9",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"bits-ui": "^1.3.12",
|
||||
"c8": "7.14.0",
|
||||
"chai": "^4.3.10",
|
||||
"clsx": "^2.1.1",
|
||||
"cssnano": "^6.0.5",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-es": "^4.1.0",
|
||||
"eslint-plugin-no-only-tests": "^3.3.0",
|
||||
"eslint-plugin-postcss-modules": "^2.0.0",
|
||||
"eslint-plugin-sort-keys": "^2.3.5",
|
||||
"eslint-plugin-svelte": "^2.46.1",
|
||||
"eslint-plugin-tailwindcss": "^3.18.0",
|
||||
"eslint-plugin-unicorn": "^50.0.1",
|
||||
@@ -53,7 +56,9 @@
|
||||
"husky": "^8.0.3",
|
||||
"jsdom": "^25.0.1",
|
||||
"lint-staged": "^15.2.11",
|
||||
"lucide-svelte": "^0.479.0",
|
||||
"node-html-parser": "^6.1.13",
|
||||
"paneforge": "^1.0.0-next.4",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-load-config": "5.1.0",
|
||||
"prettier": "^3.1.1",
|
||||
@@ -61,22 +66,28 @@
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"svelte": "^5.23.1",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"svelte-sonner": "^0.3.28",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwind-variants": "^0.3.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.5.4",
|
||||
"unplugin-icons": "^22.1.0",
|
||||
"vite": "^5.4.14",
|
||||
"vitest": "^2.1.9",
|
||||
"vitest-dom": "^0.1.1",
|
||||
"@playwright/test": "^1.42.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/recursive": "^5.2.5",
|
||||
"@mermaid-js/layout-elk": "^0.1.7",
|
||||
"@mermaid-js/mermaid-zenuml": "^0.2.0",
|
||||
"daisyui": "2.52.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"js-base64": "3.7.7",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mermaid": "^11.5.0",
|
||||
"mode-watcher": "^0.5.1",
|
||||
"monaco-editor": "0.52.2",
|
||||
"pako": "2.1.0",
|
||||
"plausible-tracker": "^0.3.9",
|
||||
|
||||
+11
-11
@@ -1,12 +1,17 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 3 : undefined,
|
||||
fullyParallel: true,
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] }
|
||||
}
|
||||
],
|
||||
reporter: process.env.CI ? [['github'], ['list']] : 'list',
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
testDir: './tests',
|
||||
use: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
browserName: 'chromium',
|
||||
@@ -14,15 +19,10 @@ export default defineConfig({
|
||||
trace: 'retain-on-failure',
|
||||
viewport: { width: 1920, height: 1080 }
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] }
|
||||
}
|
||||
],
|
||||
webServer: {
|
||||
command: `pnpm ${process.env.CI ? 'preview' : 'dev'}`,
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI
|
||||
}
|
||||
},
|
||||
workers: process.env.CI ? 3 : undefined
|
||||
});
|
||||
|
||||
Generated
+254
-182
@@ -8,15 +8,15 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@fontsource-variable/recursive':
|
||||
specifier: ^5.2.5
|
||||
version: 5.2.5
|
||||
'@mermaid-js/layout-elk':
|
||||
specifier: ^0.1.7
|
||||
version: 0.1.7(mermaid@11.5.0)
|
||||
'@mermaid-js/mermaid-zenuml':
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0(mermaid@11.5.0)(typescript@5.8.2)
|
||||
daisyui:
|
||||
specifier: 2.52.0
|
||||
version: 2.52.0(autoprefixer@10.4.21(postcss@8.5.3))(postcss@8.5.3)
|
||||
dayjs:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.13
|
||||
@@ -29,6 +29,9 @@ importers:
|
||||
mermaid:
|
||||
specifier: ^11.5.0
|
||||
version: 11.5.0
|
||||
mode-watcher:
|
||||
specifier: ^0.5.1
|
||||
version: 0.5.1(svelte@5.23.1)
|
||||
monaco-editor:
|
||||
specifier: 0.52.2
|
||||
version: 0.52.2
|
||||
@@ -54,6 +57,12 @@ importers:
|
||||
'@fortawesome/fontawesome-free':
|
||||
specifier: ^6.5.2
|
||||
version: 6.7.2
|
||||
'@iconify-json/material-symbols':
|
||||
specifier: ^1.2.15
|
||||
version: 1.2.16
|
||||
'@iconify-json/mdi':
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.3
|
||||
'@playwright/test':
|
||||
specifier: ^1.42.1
|
||||
version: 1.51.1
|
||||
@@ -66,12 +75,6 @@ importers:
|
||||
'@sveltejs/vite-plugin-svelte':
|
||||
specifier: ^4.0.4
|
||||
version: 4.0.4(svelte@5.23.1)(vite@5.4.14)
|
||||
'@testing-library/jest-dom':
|
||||
specifier: ^6.6.3
|
||||
version: 6.6.3
|
||||
'@testing-library/svelte':
|
||||
specifier: ^5.2.7
|
||||
version: 5.2.7(svelte@5.23.1)(vite@5.4.14)(vitest@2.1.9)
|
||||
'@types/lodash-es':
|
||||
specifier: ^4.17.12
|
||||
version: 4.17.12
|
||||
@@ -96,12 +99,18 @@ importers:
|
||||
autoprefixer:
|
||||
specifier: ^10.4.21
|
||||
version: 10.4.21(postcss@8.5.3)
|
||||
bits-ui:
|
||||
specifier: ^1.3.12
|
||||
version: 1.3.12(svelte@5.23.1)
|
||||
c8:
|
||||
specifier: 7.14.0
|
||||
version: 7.14.0
|
||||
chai:
|
||||
specifier: ^4.3.10
|
||||
version: 4.5.0
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
cssnano:
|
||||
specifier: ^6.0.5
|
||||
version: 6.1.2(postcss@8.5.3)
|
||||
@@ -120,6 +129,9 @@ importers:
|
||||
eslint-plugin-postcss-modules:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(eslint@8.57.1)
|
||||
eslint-plugin-sort-keys:
|
||||
specifier: ^2.3.5
|
||||
version: 2.3.5
|
||||
eslint-plugin-svelte:
|
||||
specifier: ^2.46.1
|
||||
version: 2.46.1(eslint@8.57.1)(svelte@5.23.1)
|
||||
@@ -144,9 +156,15 @@ importers:
|
||||
lint-staged:
|
||||
specifier: ^15.2.11
|
||||
version: 15.5.0
|
||||
lucide-svelte:
|
||||
specifier: ^0.479.0
|
||||
version: 0.479.0(svelte@5.23.1)
|
||||
node-html-parser:
|
||||
specifier: ^6.1.13
|
||||
version: 6.1.13
|
||||
paneforge:
|
||||
specifier: ^1.0.0-next.4
|
||||
version: 1.0.0-next.4(svelte@5.23.1)
|
||||
postcss:
|
||||
specifier: ^8.4.49
|
||||
version: 8.5.3
|
||||
@@ -168,15 +186,30 @@ importers:
|
||||
svelte-preprocess:
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3(postcss-load-config@5.1.0(jiti@1.21.7)(postcss@8.5.3))(postcss@8.5.3)(svelte@5.23.1)(typescript@5.8.2)
|
||||
svelte-sonner:
|
||||
specifier: ^0.3.28
|
||||
version: 0.3.28(svelte@5.23.1)
|
||||
tailwind-merge:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
tailwind-variants:
|
||||
specifier: ^0.3.1
|
||||
version: 0.3.1(tailwindcss@3.4.17)
|
||||
tailwindcss:
|
||||
specifier: ^3.4.17
|
||||
version: 3.4.17
|
||||
tailwindcss-animate:
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(tailwindcss@3.4.17)
|
||||
tslib:
|
||||
specifier: ^2.6.3
|
||||
version: 2.8.1
|
||||
typescript:
|
||||
specifier: ^5.5.4
|
||||
version: 5.8.2
|
||||
unplugin-icons:
|
||||
specifier: ^22.1.0
|
||||
version: 22.1.0(@vue/compiler-sfc@3.5.13)(svelte@5.23.1)
|
||||
vite:
|
||||
specifier: ^5.4.14
|
||||
version: 5.4.14
|
||||
@@ -189,9 +222,6 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@adobe/css-tools@4.4.2':
|
||||
resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==}
|
||||
|
||||
'@alloc/quick-lru@5.2.0':
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -226,10 +256,6 @@ packages:
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@babel/runtime@7.26.10':
|
||||
resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/types@7.26.10':
|
||||
resolution: {integrity: sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -451,6 +477,9 @@ packages:
|
||||
'@floating-ui/vue@1.1.6':
|
||||
resolution: {integrity: sha512-XFlUzGHGv12zbgHNk5FN2mUB7ROul3oG2ENdTpWdE+qMFxyNxWSRmsoyhiEnpmabNm6WnUvR1OvJfUfN4ojC1A==}
|
||||
|
||||
'@fontsource-variable/recursive@5.2.5':
|
||||
resolution: {integrity: sha512-tgz93CoozxkF23TeAr+ihEwwKKvZ9lFosrCjx63XPn7keeMl3nZS+iFMDsY8s1U6+jVHMHpOU8ZAOorsWNtdsA==}
|
||||
|
||||
'@fortawesome/fontawesome-free@6.7.2':
|
||||
resolution: {integrity: sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -486,12 +515,21 @@ packages:
|
||||
resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
|
||||
deprecated: Use @eslint/object-schema instead
|
||||
|
||||
'@iconify-json/material-symbols@1.2.16':
|
||||
resolution: {integrity: sha512-hCUbIYLWPX9YMChvqEScG5tvFLZ2arXl+bnsvUQDrtsGq00wu4S4qIofH3uZgClYxXhvApkDKtJDlXhoygzmMQ==}
|
||||
|
||||
'@iconify-json/mdi@1.2.3':
|
||||
resolution: {integrity: sha512-O3cLwbDOK7NNDf2ihaQOH5F9JglnulNDFV7WprU2dSoZu3h3cWH//h74uQAB87brHmvFVxIOkuBX2sZSzYhScg==}
|
||||
|
||||
'@iconify/types@2.0.0':
|
||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||
|
||||
'@iconify/utils@2.3.0':
|
||||
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
|
||||
|
||||
'@internationalized/date@3.7.0':
|
||||
resolution: {integrity: sha512-VJ5WS3fcVx0bejE/YHfbDKR/yawZgKqn/if+oEeLqNwBtPzVB06olkfcnojTmEMX+gTpH+FlQ69SHNitJ8/erQ==}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -684,6 +722,9 @@ packages:
|
||||
svelte: ^5.0.0-next.96 || ^5.0.0
|
||||
vite: ^5.0.0
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
'@tanstack/virtual-core@3.13.4':
|
||||
resolution: {integrity: sha512-fNGO9fjjSLns87tlcto106enQQLycCKR4DPNpgq3djP5IdcPFdPAmaKjsgzIeRhH7hWrELgW12hYnRthS5kLUw==}
|
||||
|
||||
@@ -692,34 +733,10 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^2.7.0 || ^3.0.0
|
||||
|
||||
'@testing-library/dom@10.4.0':
|
||||
resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@testing-library/jest-dom@6.6.3':
|
||||
resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==}
|
||||
engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
|
||||
|
||||
'@testing-library/svelte@5.2.7':
|
||||
resolution: {integrity: sha512-aGhUaFmEXEVost4QOsbHUUbHLwi7ZZRRxAHFDO2Cmr0BZD3/3+XvaYEPq70Rdw0NRNjdqZHdARBEcrCOkPuAqw==}
|
||||
engines: {node: '>= 10'}
|
||||
peerDependencies:
|
||||
svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0
|
||||
vite: '*'
|
||||
vitest: '*'
|
||||
peerDependenciesMeta:
|
||||
vite:
|
||||
optional: true
|
||||
vitest:
|
||||
optional: true
|
||||
|
||||
'@trysound/sax@0.2.0':
|
||||
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
'@types/aria-query@5.0.4':
|
||||
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
|
||||
|
||||
'@types/assert@1.5.11':
|
||||
resolution: {integrity: sha512-FjS1mxq2dlGr9N4z72/DO+XmyRS3ZZIoVn998MEopAN/OmyN28F4yumRL5pOw2z+hbFLuWGYuF2rrw5p11xM5A==}
|
||||
|
||||
@@ -1064,10 +1081,6 @@ packages:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@5.2.0:
|
||||
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
ansi-styles@6.2.1:
|
||||
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1089,9 +1102,6 @@ packages:
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
aria-query@5.3.0:
|
||||
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
|
||||
|
||||
aria-query@5.3.2:
|
||||
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1138,6 +1148,12 @@ packages:
|
||||
bindings@1.5.0:
|
||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||
|
||||
bits-ui@1.3.12:
|
||||
resolution: {integrity: sha512-RhPvg2e7mTQSXR9WMdsyR5eTC2DSa4ch5MT/TjIaN3suMJ3RGlbzYPNvf4n56Lgck3W4Xm+L4jSgrzTdynuM8Q==}
|
||||
engines: {node: '>=18', pnpm: '>=8.7.0'}
|
||||
peerDependencies:
|
||||
svelte: ^5.11.0
|
||||
|
||||
boolbase@1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
|
||||
@@ -1202,10 +1218,6 @@ packages:
|
||||
resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
chalk@3.0.0:
|
||||
resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
chalk@4.1.2:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1266,10 +1278,6 @@ packages:
|
||||
color-string@1.9.1:
|
||||
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
||||
|
||||
color@4.2.3:
|
||||
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
||||
engines: {node: '>=12.5.0'}
|
||||
|
||||
colord@2.9.3:
|
||||
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
|
||||
|
||||
@@ -1334,9 +1342,6 @@ packages:
|
||||
css-select@5.1.0:
|
||||
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
|
||||
|
||||
css-selector-tokenizer@0.8.0:
|
||||
resolution: {integrity: sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==}
|
||||
|
||||
css-tree@2.2.1:
|
||||
resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
|
||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
|
||||
@@ -1542,12 +1547,6 @@ packages:
|
||||
dagre-d3-es@7.0.11:
|
||||
resolution: {integrity: sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==}
|
||||
|
||||
daisyui@2.52.0:
|
||||
resolution: {integrity: sha512-LQTA5/IVXAJHBMFoeaEMfd7/akAFPPcdQPR3O9fzzcFiczneJFM73CFPnScmW2sOgn/D83cvkP854ep2T9OfTg==}
|
||||
peerDependencies:
|
||||
autoprefixer: ^10.0.2
|
||||
postcss: ^8.1.6
|
||||
|
||||
data-urls@5.0.0:
|
||||
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1593,10 +1592,6 @@ packages:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
dequal@2.0.3:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
devalue@5.1.1:
|
||||
resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==}
|
||||
|
||||
@@ -1614,9 +1609,6 @@ packages:
|
||||
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
dom-accessibility-api@0.5.16:
|
||||
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
|
||||
|
||||
dom-accessibility-api@0.6.3:
|
||||
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
|
||||
|
||||
@@ -1735,6 +1727,9 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: '>=0.8.0'
|
||||
|
||||
eslint-plugin-sort-keys@2.3.5:
|
||||
resolution: {integrity: sha512-2j/XKQ9sNJwK8kIp/U0EvuF6stS6/8aIc53/NskE4C5NRNh4dt3xzbZyOdrVC11cTH6Zo59/pdzA0Kb+2fQGWg==}
|
||||
|
||||
eslint-plugin-svelte@2.46.1:
|
||||
resolution: {integrity: sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==}
|
||||
engines: {node: ^14.17.0 || >=16.0.0}
|
||||
@@ -1866,9 +1861,6 @@ packages:
|
||||
resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
fastparse@1.1.2:
|
||||
resolution: {integrity: sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==}
|
||||
|
||||
fastq@1.19.1:
|
||||
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
|
||||
|
||||
@@ -2100,6 +2092,9 @@ packages:
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
inline-style-parser@0.2.4:
|
||||
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
|
||||
|
||||
internmap@1.0.1:
|
||||
resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==}
|
||||
|
||||
@@ -2326,9 +2321,10 @@ packages:
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lz-string@1.5.0:
|
||||
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
||||
hasBin: true
|
||||
lucide-svelte@0.479.0:
|
||||
resolution: {integrity: sha512-epCj6WL86ykxg7oCQTmPEth5e11pwJUzIfG9ROUsWsTP+WPtb3qat+VmAjfx/r4TRW7memTFcbTPvMrZvKthqw==}
|
||||
peerDependencies:
|
||||
svelte: ^3 || ^4 || ^5.0.0-next.42
|
||||
|
||||
magic-string@0.30.17:
|
||||
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
||||
@@ -2412,6 +2408,11 @@ packages:
|
||||
mlly@1.7.4:
|
||||
resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==}
|
||||
|
||||
mode-watcher@0.5.1:
|
||||
resolution: {integrity: sha512-adEC6T7TMX/kzQlaO/MtiQOSFekZfQu4MC+lXyoceQG+U5sKpJWZ4yKXqw846ExIuWJgedkOIPqAYYRk/xHm+w==}
|
||||
peerDependencies:
|
||||
svelte: ^4.0.0 || ^5.0.0-next.1
|
||||
|
||||
monaco-editor@0.52.2:
|
||||
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
|
||||
|
||||
@@ -2523,6 +2524,11 @@ packages:
|
||||
pako@2.1.0:
|
||||
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
|
||||
|
||||
paneforge@1.0.0-next.4:
|
||||
resolution: {integrity: sha512-xB7iqIx6sT+XDRx/Eps+lddN3X7zRy8RIqXXFimGuFF6d+3s4YoXrk8SdNwEK5U/mqwupeKX0BbSfy0z9W9feA==}
|
||||
peerDependencies:
|
||||
svelte: ^5.20.0
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2981,10 +2987,6 @@ packages:
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
pretty-format@27.5.1:
|
||||
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||
|
||||
process-warning@3.0.0:
|
||||
resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==}
|
||||
|
||||
@@ -3011,9 +3013,6 @@ packages:
|
||||
random-word-slugs@0.1.7:
|
||||
resolution: {integrity: sha512-8cyzxOIDeLFvwSPTgCItMXHGT5ZPkjhuFKUTww06Xg1dNMXuGxIKlARvS7upk6JXIm41ZKXmtlKR1iCRWklKmg==}
|
||||
|
||||
react-is@17.0.2:
|
||||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||
|
||||
read-cache@1.0.0:
|
||||
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
|
||||
|
||||
@@ -3037,17 +3036,10 @@ packages:
|
||||
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
|
||||
engines: {node: '>= 12.13.0'}
|
||||
|
||||
redent@3.0.0:
|
||||
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
redent@4.0.0:
|
||||
resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
regenerator-runtime@0.14.1:
|
||||
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
||||
|
||||
regexp-tree@0.1.27:
|
||||
resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==}
|
||||
hasBin: true
|
||||
@@ -3109,6 +3101,11 @@ packages:
|
||||
run-parallel@1.2.0:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
|
||||
runed@0.23.4:
|
||||
resolution: {integrity: sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==}
|
||||
peerDependencies:
|
||||
svelte: ^5.7.0
|
||||
|
||||
rw@1.3.3:
|
||||
resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
|
||||
|
||||
@@ -3251,6 +3248,9 @@ packages:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
style-to-object@1.0.8:
|
||||
resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==}
|
||||
|
||||
stylehacks@6.1.1:
|
||||
resolution: {integrity: sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==}
|
||||
engines: {node: ^14 || ^16 || >=18.0}
|
||||
@@ -3319,6 +3319,17 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
svelte-sonner@0.3.28:
|
||||
resolution: {integrity: sha512-K3AmlySeFifF/cKgsYNv5uXqMVNln0NBAacOYgmkQStLa/UoU0LhfAACU6Gr+YYC8bOCHdVmFNoKuDbMEsppJg==}
|
||||
peerDependencies:
|
||||
svelte: ^3.0.0 || ^4.0.0 || ^5.0.0-next.1
|
||||
|
||||
svelte-toolbelt@0.7.1:
|
||||
resolution: {integrity: sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==}
|
||||
engines: {node: '>=18', pnpm: '>=8.7.0'}
|
||||
peerDependencies:
|
||||
svelte: ^5.0.0
|
||||
|
||||
svelte@5.23.1:
|
||||
resolution: {integrity: sha512-DUu3e5tQDO+PtKffjqJ548YfeKtw2Rqc9/+nlP26DZ0AopWTJNylkNnTOP/wcgIt1JSnovyISxEZ/lDR1OhbOw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3341,6 +3352,26 @@ packages:
|
||||
symbol-tree@3.2.4:
|
||||
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||
|
||||
tabbable@6.2.0:
|
||||
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
||||
|
||||
tailwind-merge@2.5.4:
|
||||
resolution: {integrity: sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==}
|
||||
|
||||
tailwind-merge@3.0.2:
|
||||
resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==}
|
||||
|
||||
tailwind-variants@0.3.1:
|
||||
resolution: {integrity: sha512-krn67M3FpPwElg4FsZrOQd0U26o7UDH/QOkK8RNaiCCrr052f6YJPBUfNKnPo/s/xRzNPtv1Mldlxsg8Tb46BQ==}
|
||||
engines: {node: '>=16.x', pnpm: '>=7.x'}
|
||||
peerDependencies:
|
||||
tailwindcss: '*'
|
||||
|
||||
tailwindcss-animate@1.0.7:
|
||||
resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
|
||||
peerDependencies:
|
||||
tailwindcss: '>=3.0.0 || insiders'
|
||||
|
||||
tailwindcss@3.4.17:
|
||||
resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -3462,6 +3493,33 @@ packages:
|
||||
ufo@1.5.4:
|
||||
resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
|
||||
|
||||
unplugin-icons@22.1.0:
|
||||
resolution: {integrity: sha512-ect2ZNtk1Zgwb0NVHd0C1IDW/MV+Jk/xaq4t8o6rYdVS3+L660ZdD5kTSQZvsgdwCvquRw+/wYn75hsweRjoIA==}
|
||||
peerDependencies:
|
||||
'@svgr/core': '>=7.0.0'
|
||||
'@svgx/core': ^1.0.1
|
||||
'@vue/compiler-sfc': ^3.0.2 || ^2.7.0
|
||||
svelte: ^3.0.0 || ^4.0.0 || ^5.0.0
|
||||
vue-template-compiler: ^2.6.12
|
||||
vue-template-es2015-compiler: ^1.9.0
|
||||
peerDependenciesMeta:
|
||||
'@svgr/core':
|
||||
optional: true
|
||||
'@svgx/core':
|
||||
optional: true
|
||||
'@vue/compiler-sfc':
|
||||
optional: true
|
||||
svelte:
|
||||
optional: true
|
||||
vue-template-compiler:
|
||||
optional: true
|
||||
vue-template-es2015-compiler:
|
||||
optional: true
|
||||
|
||||
unplugin@2.2.0:
|
||||
resolution: {integrity: sha512-m1ekpSwuOT5hxkJeZGRxO7gXbXT3gF26NjQ7GdVHoLoF8/nopLcd/QfPigpCy7i51oFHiRJg/CyHhj4vs2+KGw==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
||||
update-browserslist-db@1.1.3:
|
||||
resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
|
||||
hasBin: true
|
||||
@@ -3615,6 +3673,9 @@ packages:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
webpack-virtual-modules@0.6.2:
|
||||
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
|
||||
|
||||
whatwg-encoding@3.1.1:
|
||||
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3705,8 +3766,6 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@adobe/css-tools@4.4.2': {}
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
@@ -3743,10 +3802,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/types': 7.26.10
|
||||
|
||||
'@babel/runtime@7.26.10':
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.1
|
||||
|
||||
'@babel/types@7.26.10':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 7.25.9
|
||||
@@ -3905,6 +3960,8 @@ snapshots:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@fontsource-variable/recursive@5.2.5': {}
|
||||
|
||||
'@fortawesome/fontawesome-free@6.7.2': {}
|
||||
|
||||
'@headlessui-float/vue@0.14.4(@headlessui/vue@1.7.23(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2))':
|
||||
@@ -3938,6 +3995,14 @@ snapshots:
|
||||
|
||||
'@humanwhocodes/object-schema@2.0.3': {}
|
||||
|
||||
'@iconify-json/material-symbols@1.2.16':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify-json/mdi@1.2.3':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify/types@2.0.0': {}
|
||||
|
||||
'@iconify/utils@2.3.0':
|
||||
@@ -3953,6 +4018,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@internationalized/date@3.7.0':
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.15
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
@@ -4125,6 +4194,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@tanstack/virtual-core@3.13.4': {}
|
||||
|
||||
'@tanstack/vue-virtual@3.13.4(vue@3.5.13(typescript@5.8.2))':
|
||||
@@ -4132,39 +4205,8 @@ snapshots:
|
||||
'@tanstack/virtual-core': 3.13.4
|
||||
vue: 3.5.13(typescript@5.8.2)
|
||||
|
||||
'@testing-library/dom@10.4.0':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.26.2
|
||||
'@babel/runtime': 7.26.10
|
||||
'@types/aria-query': 5.0.4
|
||||
aria-query: 5.3.0
|
||||
chalk: 4.1.2
|
||||
dom-accessibility-api: 0.5.16
|
||||
lz-string: 1.5.0
|
||||
pretty-format: 27.5.1
|
||||
|
||||
'@testing-library/jest-dom@6.6.3':
|
||||
dependencies:
|
||||
'@adobe/css-tools': 4.4.2
|
||||
aria-query: 5.3.2
|
||||
chalk: 3.0.0
|
||||
css.escape: 1.5.1
|
||||
dom-accessibility-api: 0.6.3
|
||||
lodash: 4.17.21
|
||||
redent: 3.0.0
|
||||
|
||||
'@testing-library/svelte@5.2.7(svelte@5.23.1)(vite@5.4.14)(vitest@2.1.9)':
|
||||
dependencies:
|
||||
'@testing-library/dom': 10.4.0
|
||||
svelte: 5.23.1
|
||||
optionalDependencies:
|
||||
vite: 5.4.14
|
||||
vitest: 2.1.9(@vitest/ui@2.1.9)(jsdom@25.0.1)
|
||||
|
||||
'@trysound/sax@0.2.0': {}
|
||||
|
||||
'@types/aria-query@5.0.4': {}
|
||||
|
||||
'@types/assert@1.5.11': {}
|
||||
|
||||
'@types/cookie@0.6.0': {}
|
||||
@@ -4632,8 +4674,6 @@ snapshots:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@5.2.0: {}
|
||||
|
||||
ansi-styles@6.2.1: {}
|
||||
|
||||
antlr4@4.11.0: {}
|
||||
@@ -4649,10 +4689,6 @@ snapshots:
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
aria-query@5.3.0:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
aria-query@5.3.2: {}
|
||||
|
||||
array-union@2.1.0: {}
|
||||
@@ -4687,6 +4723,17 @@ snapshots:
|
||||
dependencies:
|
||||
file-uri-to-path: 1.0.0
|
||||
|
||||
bits-ui@1.3.12(svelte@5.23.1):
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.6.9
|
||||
'@floating-ui/dom': 1.6.13
|
||||
'@internationalized/date': 3.7.0
|
||||
esm-env: 1.2.2
|
||||
runed: 0.23.4(svelte@5.23.1)
|
||||
svelte: 5.23.1
|
||||
svelte-toolbelt: 0.7.1(svelte@5.23.1)
|
||||
tabbable: 6.2.0
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
|
||||
brace-expansion@1.1.11:
|
||||
@@ -4771,11 +4818,6 @@ snapshots:
|
||||
loupe: 3.1.3
|
||||
pathval: 2.0.0
|
||||
|
||||
chalk@3.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
chalk@4.1.2:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
@@ -4849,11 +4891,6 @@ snapshots:
|
||||
color-name: 1.1.4
|
||||
simple-swizzle: 0.2.2
|
||||
|
||||
color@4.2.3:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
color-string: 1.9.1
|
||||
|
||||
colord@2.9.3: {}
|
||||
|
||||
colorette@2.0.20: {}
|
||||
@@ -4910,11 +4947,6 @@ snapshots:
|
||||
domutils: 3.2.2
|
||||
nth-check: 2.1.1
|
||||
|
||||
css-selector-tokenizer@0.8.0:
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
fastparse: 1.1.2
|
||||
|
||||
css-tree@2.2.1:
|
||||
dependencies:
|
||||
mdn-data: 2.0.28
|
||||
@@ -5170,17 +5202,6 @@ snapshots:
|
||||
d3: 7.9.0
|
||||
lodash-es: 4.17.21
|
||||
|
||||
daisyui@2.52.0(autoprefixer@10.4.21(postcss@8.5.3))(postcss@8.5.3):
|
||||
dependencies:
|
||||
autoprefixer: 10.4.21(postcss@8.5.3)
|
||||
color: 4.2.3
|
||||
css-selector-tokenizer: 0.8.0
|
||||
postcss: 8.5.3
|
||||
postcss-js: 4.0.1(postcss@8.5.3)
|
||||
tailwindcss: 3.4.17
|
||||
transitivePeerDependencies:
|
||||
- ts-node
|
||||
|
||||
data-urls@5.0.0:
|
||||
dependencies:
|
||||
whatwg-mimetype: 4.0.0
|
||||
@@ -5215,8 +5236,6 @@ snapshots:
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
dequal@2.0.3: {}
|
||||
|
||||
devalue@5.1.1: {}
|
||||
|
||||
didyoumean@1.2.2: {}
|
||||
@@ -5231,8 +5250,6 @@ snapshots:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
|
||||
dom-accessibility-api@0.5.16: {}
|
||||
|
||||
dom-accessibility-api@0.6.3: {}
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
@@ -5367,6 +5384,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- ts-node
|
||||
|
||||
eslint-plugin-sort-keys@2.3.5:
|
||||
dependencies:
|
||||
natural-compare: 1.4.0
|
||||
|
||||
eslint-plugin-svelte@2.46.1(eslint@8.57.1)(svelte@5.23.1):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.5.1(eslint@8.57.1)
|
||||
@@ -5551,8 +5572,6 @@ snapshots:
|
||||
|
||||
fast-redact@3.5.0: {}
|
||||
|
||||
fastparse@1.1.2: {}
|
||||
|
||||
fastq@1.19.1:
|
||||
dependencies:
|
||||
reusify: 1.1.0
|
||||
@@ -5771,6 +5790,8 @@ snapshots:
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
inline-style-parser@0.2.4: {}
|
||||
|
||||
internmap@1.0.1: {}
|
||||
|
||||
internmap@2.0.3: {}
|
||||
@@ -6001,7 +6022,9 @@ snapshots:
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lz-string@1.5.0: {}
|
||||
lucide-svelte@0.479.0(svelte@5.23.1):
|
||||
dependencies:
|
||||
svelte: 5.23.1
|
||||
|
||||
magic-string@0.30.17:
|
||||
dependencies:
|
||||
@@ -6094,6 +6117,10 @@ snapshots:
|
||||
pkg-types: 1.3.1
|
||||
ufo: 1.5.4
|
||||
|
||||
mode-watcher@0.5.1(svelte@5.23.1):
|
||||
dependencies:
|
||||
svelte: 5.23.1
|
||||
|
||||
monaco-editor@0.52.2: {}
|
||||
|
||||
mri@1.2.0: {}
|
||||
@@ -6195,6 +6222,11 @@ snapshots:
|
||||
|
||||
pako@2.1.0: {}
|
||||
|
||||
paneforge@1.0.0-next.4(svelte@5.23.1):
|
||||
dependencies:
|
||||
svelte: 5.23.1
|
||||
svelte-toolbelt: 0.7.1(svelte@5.23.1)
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
@@ -6549,12 +6581,6 @@ snapshots:
|
||||
|
||||
prettier@3.5.3: {}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
ansi-styles: 5.2.0
|
||||
react-is: 17.0.2
|
||||
|
||||
process-warning@3.0.0: {}
|
||||
|
||||
process@0.11.10: {}
|
||||
@@ -6571,8 +6597,6 @@ snapshots:
|
||||
|
||||
random-word-slugs@0.1.7: {}
|
||||
|
||||
react-is@17.0.2: {}
|
||||
|
||||
read-cache@1.0.0:
|
||||
dependencies:
|
||||
pify: 2.3.0
|
||||
@@ -6604,18 +6628,11 @@ snapshots:
|
||||
|
||||
real-require@0.2.0: {}
|
||||
|
||||
redent@3.0.0:
|
||||
dependencies:
|
||||
indent-string: 4.0.0
|
||||
strip-indent: 3.0.0
|
||||
|
||||
redent@4.0.0:
|
||||
dependencies:
|
||||
indent-string: 5.0.0
|
||||
strip-indent: 4.0.0
|
||||
|
||||
regenerator-runtime@0.14.1: {}
|
||||
|
||||
regexp-tree@0.1.27: {}
|
||||
|
||||
regexpp@3.2.0: {}
|
||||
@@ -6689,6 +6706,11 @@ snapshots:
|
||||
dependencies:
|
||||
queue-microtask: 1.2.3
|
||||
|
||||
runed@0.23.4(svelte@5.23.1):
|
||||
dependencies:
|
||||
esm-env: 1.2.2
|
||||
svelte: 5.23.1
|
||||
|
||||
rw@1.3.3: {}
|
||||
|
||||
sade@1.8.1:
|
||||
@@ -6815,6 +6837,10 @@ snapshots:
|
||||
|
||||
strip-json-comments@3.1.1: {}
|
||||
|
||||
style-to-object@1.0.8:
|
||||
dependencies:
|
||||
inline-style-parser: 0.2.4
|
||||
|
||||
stylehacks@6.1.1(postcss@8.5.3):
|
||||
dependencies:
|
||||
browserslist: 4.24.4
|
||||
@@ -6857,6 +6883,17 @@ snapshots:
|
||||
postcss-load-config: 5.1.0(jiti@1.21.7)(postcss@8.5.3)
|
||||
typescript: 5.8.2
|
||||
|
||||
svelte-sonner@0.3.28(svelte@5.23.1):
|
||||
dependencies:
|
||||
svelte: 5.23.1
|
||||
|
||||
svelte-toolbelt@0.7.1(svelte@5.23.1):
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
runed: 0.23.4(svelte@5.23.1)
|
||||
style-to-object: 1.0.8
|
||||
svelte: 5.23.1
|
||||
|
||||
svelte@5.23.1:
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
@@ -6896,6 +6933,21 @@ snapshots:
|
||||
|
||||
symbol-tree@3.2.4: {}
|
||||
|
||||
tabbable@6.2.0: {}
|
||||
|
||||
tailwind-merge@2.5.4: {}
|
||||
|
||||
tailwind-merge@3.0.2: {}
|
||||
|
||||
tailwind-variants@0.3.1(tailwindcss@3.4.17):
|
||||
dependencies:
|
||||
tailwind-merge: 2.5.4
|
||||
tailwindcss: 3.4.17
|
||||
|
||||
tailwindcss-animate@1.0.7(tailwindcss@3.4.17):
|
||||
dependencies:
|
||||
tailwindcss: 3.4.17
|
||||
|
||||
tailwindcss@3.4.17:
|
||||
dependencies:
|
||||
'@alloc/quick-lru': 5.2.0
|
||||
@@ -7014,6 +7066,24 @@ snapshots:
|
||||
|
||||
ufo@1.5.4: {}
|
||||
|
||||
unplugin-icons@22.1.0(@vue/compiler-sfc@3.5.13)(svelte@5.23.1):
|
||||
dependencies:
|
||||
'@antfu/install-pkg': 1.0.0
|
||||
'@iconify/utils': 2.3.0
|
||||
debug: 4.4.0
|
||||
local-pkg: 1.1.1
|
||||
unplugin: 2.2.0
|
||||
optionalDependencies:
|
||||
'@vue/compiler-sfc': 3.5.13
|
||||
svelte: 5.23.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
unplugin@2.2.0:
|
||||
dependencies:
|
||||
acorn: 8.14.1
|
||||
webpack-virtual-modules: 0.6.2
|
||||
|
||||
update-browserslist-db@1.1.3(browserslist@4.24.4):
|
||||
dependencies:
|
||||
browserslist: 4.24.4
|
||||
@@ -7159,6 +7229,8 @@ snapshots:
|
||||
|
||||
webidl-conversions@7.0.0: {}
|
||||
|
||||
webpack-virtual-modules@0.6.2: {}
|
||||
|
||||
whatwg-encoding@3.1.1:
|
||||
dependencies:
|
||||
iconv-lite: 0.6.3
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body style="overflow: hidden">
|
||||
<body>
|
||||
<div id="svelte">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+56
-8
@@ -1,19 +1,67 @@
|
||||
@import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||
@import '@fontsource-variable/recursive/crsv.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.input {
|
||||
@apply flex-1 rounded border-2 border-solid border-primary;
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 240 10% 91%;
|
||||
--primary-foreground: 255 20% 15%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 228 24% 96%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 340 100% 44%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 72.22% 50.59%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 210 40% 30%;
|
||||
--primary-foreground: 222.2 47.4% 90%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 340 100% 44%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
.action-btn {
|
||||
@apply btn btn-primary;
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply h-screen w-screen overflow-hidden bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
#graph-div sub,
|
||||
#graph-div sup {
|
||||
position: initial;
|
||||
vertical-align: revert;
|
||||
|
||||
body {
|
||||
font-family: 'Recursive Variable', sans-serif;
|
||||
}
|
||||
|
||||
.d {
|
||||
|
||||
+129
-144
@@ -1,23 +1,37 @@
|
||||
<script lang="ts">
|
||||
import Card from '$/components/Card/Card.svelte';
|
||||
import CopyButton from '$/components/CopyButton.svelte';
|
||||
import CopyInput from '$/components/CopyInput.svelte';
|
||||
import { Button } from '$/components/ui/button';
|
||||
import { Input } from '$/components/ui/input';
|
||||
import { Separator } from '$/components/ui/separator';
|
||||
import * as ToggleGroup from '$/components/ui/toggle-group';
|
||||
import { TID } from '$/constants';
|
||||
import { env } from '$/util/env';
|
||||
import { browser } from '$app/environment';
|
||||
import Card from '$lib/components/Card/Card.svelte';
|
||||
import { waitForRender } from '$lib/util/autoSync';
|
||||
import { env } from '$lib/util/env';
|
||||
import { pakoSerde } from '$lib/util/serde';
|
||||
import { stateStore } from '$lib/util/state';
|
||||
import { inputStateStore, stateStore, urlsStore } from '$lib/util/state';
|
||||
import { logEvent } from '$lib/util/stats';
|
||||
import { version as FAVersion } from '@fortawesome/fontawesome-free/package.json';
|
||||
import dayjs from 'dayjs';
|
||||
import { toBase64 } from 'js-base64';
|
||||
import DownloadIcon from '~icons/material-symbols/download';
|
||||
import ExternalLinkIcon from '~icons/material-symbols/open-in-new-rounded';
|
||||
import WidthIcon from '~icons/material-symbols/width-rounded';
|
||||
|
||||
const FONT_AWESOME_URL = `https://cdnjs.cloudflare.com/ajax/libs/font-awesome/${FAVersion}/css/all.min.css`;
|
||||
|
||||
const { krokiRendererUrl, rendererUrl } = env;
|
||||
type Exporter = (context: CanvasRenderingContext2D, image: HTMLImageElement) => () => void;
|
||||
|
||||
const getFileName = (extension: string) =>
|
||||
`mermaid-diagram-${dayjs().format('YYYY-MM-DD-HHmmss')}.${extension}`;
|
||||
|
||||
const getSvgElement = () => {
|
||||
const svgElement = document.querySelector('#container svg')?.cloneNode(true) as HTMLElement;
|
||||
svgElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
||||
return svgElement;
|
||||
};
|
||||
|
||||
const getBase64SVG = (svg?: HTMLElement, width?: number, height?: number): string => {
|
||||
if (svg) {
|
||||
// Prevents the SVG size of the interface from being changed
|
||||
@@ -25,9 +39,11 @@
|
||||
}
|
||||
height && svg?.setAttribute('height', `${height}px`);
|
||||
width && svg?.setAttribute('width', `${width}px`); // Workaround https://stackoverflow.com/questions/28690643/firefox-error-rendering-an-svg-image-to-html5-canvas-with-drawimage
|
||||
|
||||
if (!svg) {
|
||||
svg = getSvgElement();
|
||||
}
|
||||
|
||||
const svgString = svg.outerHTML
|
||||
.replaceAll('<br>', '<br/>')
|
||||
.replaceAll(/<img([^>]*)>/g, (m, g: string) => `<img ${g} />`);
|
||||
@@ -37,50 +53,6 @@
|
||||
${svgString}`);
|
||||
};
|
||||
|
||||
const exportImage = async (event: Event, exporter: Exporter) => {
|
||||
await waitForRender();
|
||||
if (document.querySelector('.outOfSync')) {
|
||||
throw new Error('Diagram is out of sync');
|
||||
}
|
||||
const canvas: HTMLCanvasElement = document.createElement('canvas');
|
||||
const svg = document.querySelector<HTMLElement>('#container svg');
|
||||
if (!svg) {
|
||||
throw new Error('svg not found');
|
||||
}
|
||||
const box: DOMRect = svg.getBoundingClientRect();
|
||||
canvas.width = box.width;
|
||||
canvas.height = box.height;
|
||||
if (imagemodeselected === 'width') {
|
||||
const ratio = box.height / box.width;
|
||||
canvas.width = userimagesize;
|
||||
canvas.height = userimagesize * ratio;
|
||||
} else if (imagemodeselected === 'height') {
|
||||
const ratio = box.width / box.height;
|
||||
canvas.width = userimagesize * ratio;
|
||||
canvas.height = userimagesize;
|
||||
}
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) {
|
||||
throw new Error('context not found');
|
||||
}
|
||||
context.fillStyle = `hsl(${window.getComputedStyle(document.body).getPropertyValue('--b1')})`;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const image = new Image();
|
||||
image.addEventListener('load', exporter(context, image));
|
||||
image.src = `data:image/svg+xml;base64,${getBase64SVG(svg, canvas.width, canvas.height)}`;
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const getSvgElement = () => {
|
||||
const svgElement = document.querySelector('#container svg')?.cloneNode(true) as HTMLElement;
|
||||
svgElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
||||
return svgElement;
|
||||
};
|
||||
|
||||
const simulateDownload = (download: string, href: string): void => {
|
||||
const a = document.createElement('a');
|
||||
a.download = download;
|
||||
@@ -88,6 +60,52 @@ ${svgString}`);
|
||||
a.click();
|
||||
a.remove();
|
||||
};
|
||||
|
||||
const exportImage = async (event: Event, exporter: Exporter) => {
|
||||
$inputStateStore.panZoom = false;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await waitForRender();
|
||||
const canvas = document.createElement('canvas');
|
||||
const svg = document.querySelector<HTMLElement>('#container svg');
|
||||
if (!svg) {
|
||||
throw new Error('svg not found');
|
||||
}
|
||||
|
||||
const box = svg.getBoundingClientRect();
|
||||
|
||||
if (imageSizeMode === 'width') {
|
||||
const ratio = box.height / box.width;
|
||||
canvas.width = imageSize;
|
||||
canvas.height = imageSize * ratio;
|
||||
} else if (imageSizeMode === 'height') {
|
||||
const ratio = box.width / box.height;
|
||||
canvas.width = imageSize * ratio;
|
||||
canvas.height = imageSize;
|
||||
} else {
|
||||
const multiplier = 2;
|
||||
canvas.width = box.width * multiplier;
|
||||
canvas.height = box.height * multiplier;
|
||||
}
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) {
|
||||
throw new Error('context not found');
|
||||
}
|
||||
|
||||
context.fillStyle = `hsl(${window.getComputedStyle(document.body).getPropertyValue('--background')})`;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const image = new Image();
|
||||
image.addEventListener('load', () => {
|
||||
exporter(context, image)();
|
||||
$inputStateStore.panZoom = true;
|
||||
});
|
||||
image.src = `data:image/svg+xml;base64,${getBase64SVG(svg, canvas.width, canvas.height)}`;
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const downloadImage: Exporter = (context, image) => {
|
||||
return () => {
|
||||
const { canvas } = context;
|
||||
@@ -100,7 +118,7 @@ ${svgString}`);
|
||||
};
|
||||
|
||||
const isClipboardAvailable = (): boolean => {
|
||||
return Object.prototype.hasOwnProperty.call(window, 'ClipboardItem') as boolean;
|
||||
return Object.prototype.hasOwnProperty.call(window, 'ClipboardItem');
|
||||
};
|
||||
|
||||
const clipboardCopy: Exporter = (context, image) => {
|
||||
@@ -143,126 +161,93 @@ ${svgString}`);
|
||||
});
|
||||
};
|
||||
|
||||
const onCopyMarkdown = () => {
|
||||
document.querySelector<HTMLInputElement>('#markdown')?.select();
|
||||
document.execCommand('Copy');
|
||||
logEvent('copyMarkdown');
|
||||
};
|
||||
|
||||
let gistURL = $state('');
|
||||
stateStore.subscribe(({ loader }) => {
|
||||
if (loader?.type === 'gist') {
|
||||
// @ts-expect-error Gist will have url
|
||||
gistURL = loader.config.url;
|
||||
}
|
||||
});
|
||||
|
||||
const loadGist = () => {
|
||||
if (!gistURL) {
|
||||
alert('Please enter a Gist URL first');
|
||||
return alert('Please enter a Gist URL first');
|
||||
}
|
||||
window.location.href = `${window.location.pathname}?gist=${gistURL}`;
|
||||
logEvent('loadGist');
|
||||
};
|
||||
|
||||
let iUrl: string | undefined = $state();
|
||||
let svgUrl: string | undefined = $state();
|
||||
let krokiUrl: string | undefined = $state();
|
||||
let mdCode: string | undefined = $state();
|
||||
let imagemodeselected = $state('auto');
|
||||
let userimagesize = $state(1080);
|
||||
let imageSizeMode: 'auto' | 'width' | 'height' = $state('auto');
|
||||
|
||||
let isNetlify = $state(false);
|
||||
if (browser && ['mermaid.live', 'netlify'].some((path) => window.location.host.includes(path))) {
|
||||
isNetlify = true;
|
||||
}
|
||||
stateStore.subscribe(({ code, serialized }) => {
|
||||
iUrl = `${rendererUrl}/img/${serialized}?type=png`;
|
||||
svgUrl = `${rendererUrl}/svg/${serialized}`;
|
||||
krokiUrl = `${krokiRendererUrl}/mermaid/svg/${pakoSerde.serialize(code)}`;
|
||||
mdCode = `[](${window.location.protocol}//${window.location.host}${window.location.pathname}#${serialized})`;
|
||||
$effect(() => {
|
||||
if (!imageSizeMode) {
|
||||
imageSizeMode = 'auto';
|
||||
}
|
||||
});
|
||||
|
||||
let imageSize = $state(1080);
|
||||
|
||||
const isNetlify = browser && window.location.host.includes('netlify');
|
||||
</script>
|
||||
|
||||
<Card title="Actions" isOpen={false}>
|
||||
<div class="m-2 flex flex-wrap gap-2">
|
||||
{#if isClipboardAvailable()}
|
||||
<button class="action-btn w-full" onclick={onCopyClipboard}
|
||||
><i class="far fa-copy mr-2"></i> Copy Image to clipboard
|
||||
</button>
|
||||
{/if}
|
||||
<button id="downloadPNG" class="action-btn flex-grow" onclick={onDownloadPNG}>
|
||||
<i class="fas fa-download mr-2"></i> PNG
|
||||
</button>
|
||||
<button id="downloadSVG" class="action-btn flex-grow" onclick={onDownloadSVG}>
|
||||
<i class="fas fa-download mr-2"></i> SVG
|
||||
</button>
|
||||
{#if rendererUrl}
|
||||
<a target="_blank" rel="noreferrer" class="flex-grow" href={iUrl}>
|
||||
<button class="action-btn w-full">
|
||||
<i class="fas fa-external-link-alt mr-2"></i> PNG
|
||||
</button>
|
||||
</a>
|
||||
<a target="_blank" rel="noreferrer" class="flex-grow" href={svgUrl}>
|
||||
<button class="action-btn w-full">
|
||||
<i class="fas fa-external-link-alt mr-2"></i> SVG
|
||||
</button>
|
||||
</a>
|
||||
{/if}
|
||||
{#if krokiRendererUrl}
|
||||
<a target="_blank" rel="noreferrer" class="flex-grow" href={krokiUrl}>
|
||||
<button class="action-btn w-full">
|
||||
<i class="fas fa-external-link-alt mr-2"></i> Kroki
|
||||
</button>
|
||||
</a>
|
||||
{#snippet dualActionButton(text: string, download: (event: Event) => unknown, url?: string)}
|
||||
<div class="flex flex-grow gap-0.5">
|
||||
<Button
|
||||
class={['flex-grow', url && 'rounded-r-none']}
|
||||
onclick={download}
|
||||
data-testid="download-{text}">
|
||||
<DownloadIcon />
|
||||
{text}
|
||||
</Button>
|
||||
{#if url}
|
||||
<Button class="rounded-l-none" href={url} target="_blank" rel="noreferrer noopener">
|
||||
<ExternalLinkIcon />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Card title="Actions" isStackable icon={{ component: DownloadIcon, class: 'rotate-180' }}>
|
||||
<div class="flex min-w-fit flex-col gap-2 p-2">
|
||||
<div class="flex w-full items-center gap-2 whitespace-nowrap py-2">
|
||||
PNG size
|
||||
<label for="autosize">
|
||||
<input type="radio" value="auto" id="autosize" bind:group={imagemodeselected} /> Auto
|
||||
</label>
|
||||
|
||||
<label for="width">
|
||||
<input type="radio" value="width" id="width" bind:group={imagemodeselected} /> Width
|
||||
</label>
|
||||
|
||||
<label for="height">
|
||||
<input type="radio" value="height" id="height" bind:group={imagemodeselected} /> Height
|
||||
</label>
|
||||
|
||||
{#if imagemodeselected !== 'auto'}
|
||||
<input
|
||||
id="height"
|
||||
class="input"
|
||||
type="number"
|
||||
min="3"
|
||||
max="10000"
|
||||
bind:value={userimagesize} />
|
||||
<ToggleGroup.Root type="single" variant="outline" bind:value={imageSizeMode}>
|
||||
<ToggleGroup.Item value="auto">Auto</ToggleGroup.Item>
|
||||
<ToggleGroup.Item value="width">Width</ToggleGroup.Item>
|
||||
<ToggleGroup.Item value="height">Height</ToggleGroup.Item>
|
||||
</ToggleGroup.Root>
|
||||
{#if imageSizeMode !== 'auto'}
|
||||
<WidthIcon
|
||||
class={['size-6 shrink-0 transition-all', imageSizeMode === 'width' && 'rotate-90']} />
|
||||
{/if}
|
||||
<Input
|
||||
type="number"
|
||||
min="3"
|
||||
max="10000"
|
||||
disabled={imageSizeMode === 'auto'}
|
||||
bind:value={imageSize} />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{@render dualActionButton('PNG', onDownloadPNG, $urlsStore.png)}
|
||||
{@render dualActionButton('SVG', onDownloadSVG, $urlsStore.svg)}
|
||||
{#if env.krokiRendererUrl}
|
||||
<a target="_blank" rel="noreferrer" class="flex-grow" href={$urlsStore.kroki}>
|
||||
<Button class="action-btn flex w-full items-center gap-2">
|
||||
<ExternalLinkIcon /> Kroki
|
||||
</Button>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if rendererUrl}
|
||||
<div class="flex w-full items-center gap-2">
|
||||
<input class="input" id="markdown" type="text" value={mdCode} onclick={onCopyMarkdown} />
|
||||
<label for="markdown">
|
||||
<button class="btn btn-primary btn-md flex-auto" onclick={onCopyMarkdown}>
|
||||
Copy Markdown
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
<Separator />
|
||||
{#if isClipboardAvailable()}
|
||||
<CopyButton onclick={onCopyClipboard} label="Copy Image" />
|
||||
{/if}
|
||||
{#if $urlsStore.mdCode}
|
||||
<CopyInput value={$urlsStore.mdCode} label="Copy Markdown" testID={TID.copyMarkdown} />
|
||||
{/if}
|
||||
|
||||
<div class="flex w-full items-center gap-2">
|
||||
<input
|
||||
class="input"
|
||||
id="gist"
|
||||
type="text"
|
||||
bind:value={gistURL}
|
||||
placeholder="Enter Gist URL" />
|
||||
<label for="gist">
|
||||
<button class="btn btn-primary btn-md flex-auto" onclick={loadGist}> Load Gist </button>
|
||||
</label>
|
||||
<Input type="url" bind:value={gistURL} placeholder="Enter Gist URL" />
|
||||
<Button onclick={loadGist}>Load Gist</Button>
|
||||
</div>
|
||||
{#if isNetlify}
|
||||
<div class="flex w-full items-center justify-center">
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
<script lang="ts">
|
||||
import type { Tab } from '$lib/types';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { Tab } from '$/types';
|
||||
import type { Component, Snippet } from 'svelte';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { slide } from 'svelte/transition';
|
||||
import CollapseAllIcon from '~icons/material-symbols/collapse-all-rounded';
|
||||
import Tabs from './Tabs.svelte';
|
||||
|
||||
interface Props {
|
||||
isClosable?: boolean;
|
||||
isOpen?: boolean;
|
||||
isStackable?: boolean;
|
||||
tabs?: Tab[];
|
||||
activeTabID?: string;
|
||||
title: string;
|
||||
title?: string;
|
||||
icon?: {
|
||||
component: Component;
|
||||
class?: string;
|
||||
};
|
||||
onselect?: (tab: Tab) => void;
|
||||
actions?: Snippet;
|
||||
children?: Snippet;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
isClosable = true,
|
||||
isOpen = true,
|
||||
isOpen = false,
|
||||
isStackable = false,
|
||||
tabs = [],
|
||||
activeTabID = '',
|
||||
title,
|
||||
icon,
|
||||
onselect,
|
||||
actions,
|
||||
children
|
||||
@@ -35,23 +44,42 @@
|
||||
let isTabsShown = $derived(isOpen && tabs.length > 0);
|
||||
</script>
|
||||
|
||||
<div class="card m-2 flex flex-grow flex-col overflow-hidden rounded shadow-2xl">
|
||||
<div
|
||||
class={[
|
||||
'card flex h-fit flex-col overflow-hidden rounded-2xl border-2 border-muted',
|
||||
isOpen && 'isOpen flex-grow',
|
||||
isStackable ? 'flex-1 group-has-[.isOpen]:w-full group-has-[.isOpen]:flex-none' : 'w-full'
|
||||
]}>
|
||||
<div
|
||||
role="toolbar"
|
||||
tabindex="0"
|
||||
class="bg-primary p-2 {isTabsShown ? 'pb-0' : ''} flex-none cursor-pointer"
|
||||
class={[
|
||||
'flex h-11 flex-none cursor-pointer items-center justify-between whitespace-nowrap bg-muted p-2',
|
||||
isTabsShown && 'pb-1'
|
||||
]}
|
||||
onclick={toggleCardOpen}
|
||||
onkeypress={toggleCardOpen}>
|
||||
<div class="flex justify-between">
|
||||
<Tabs {onselect} {tabs} bind:isOpen {title} {isClosable} {activeTabID} />
|
||||
<div class="flex items-center gap-x-4 {isTabsShown ? '-mt-2' : ''}">
|
||||
{@render actions?.()}
|
||||
</div>
|
||||
</div>
|
||||
{#if icon || title}
|
||||
<span role="menubar" tabindex="0" class="flex w-fit items-center gap-3">
|
||||
{#if icon}
|
||||
<icon.component class={icon.class} />
|
||||
{/if}
|
||||
{title}
|
||||
</span>
|
||||
{/if}
|
||||
{#if isOpen && tabs && tabs.length > 0}
|
||||
<Tabs {onselect} {tabs} {activeTabID} />
|
||||
{/if}
|
||||
|
||||
{@render actions?.()}
|
||||
|
||||
{#if isOpen && isClosable}
|
||||
<CollapseAllIcon />
|
||||
{/if}
|
||||
</div>
|
||||
{#if isOpen}
|
||||
<div class="card-body flex-grow overflow-auto p-0 text-base-content" transition:slide>
|
||||
{@render children?.()}
|
||||
<div class="flex-grow overflow-x-auto" transition:slide={{ easing: quintOut }}>
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$/components/ui/button';
|
||||
import { Separator } from '$/components/ui/separator';
|
||||
import type { Tab } from '$lib/types';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
let {
|
||||
isClosable = true,
|
||||
tabs,
|
||||
title,
|
||||
isOpen = $bindable(false),
|
||||
activeTabID = $bindable(),
|
||||
activeTabID,
|
||||
onselect
|
||||
}: {
|
||||
isClosable?: boolean;
|
||||
tabs: Tab[];
|
||||
title: string;
|
||||
isOpen?: boolean;
|
||||
activeTabID: string;
|
||||
onselect?: (tab: Tab) => void;
|
||||
} = $props();
|
||||
@@ -25,41 +21,32 @@
|
||||
const toggleTabs = (tab: Tab) => {
|
||||
return (event: Event) => {
|
||||
event.stopPropagation();
|
||||
activeTabID = tab.id;
|
||||
onselect?.(tab);
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex cursor-default">
|
||||
<span role="menubar" tabindex="0" class="mr-2 font-semibold">
|
||||
{#if isClosable}
|
||||
<i class="fas fa-chevron-right icon mr-1" class:isOpen></i>
|
||||
{/if}
|
||||
{title}
|
||||
</span>
|
||||
{#if isOpen && tabs}
|
||||
<ul class="tabs" transition:fade>
|
||||
{#each tabs as tab}
|
||||
<div
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
class="tab tab-lifted {activeTabID === tab.id ? 'tab-active' : 'text-primary-content'}"
|
||||
onclick={toggleTabs(tab)}
|
||||
onkeypress={toggleTabs(tab)}>
|
||||
<i class="mr-1 {tab.icon}"></i>
|
||||
{tab.title}
|
||||
</div>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex w-fit cursor-default items-center gap-2">
|
||||
<ul class="flex gap-2 align-middle" transition:fade>
|
||||
{#each tabs as tab, index}
|
||||
<Button
|
||||
role="tab"
|
||||
variant="ghost"
|
||||
class={[
|
||||
'px-2',
|
||||
activeTabID === tab.id && 'rounded-b-none border-b-2 border-b-primary-foreground/50'
|
||||
]}
|
||||
onclick={toggleTabs(tab)}
|
||||
onkeypress={toggleTabs(tab)}>
|
||||
<tab.icon />
|
||||
{tab.title}
|
||||
</Button>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
transition-duration: 0.5s;
|
||||
}
|
||||
.isOpen {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
</style>
|
||||
{#if index < tabs.length - 1}
|
||||
<div class="my-2">
|
||||
<Separator orientation="vertical" class="w-0.5 bg-slate-300" />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { cleanup, render } from '@testing-library/svelte';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import Card from './Card.svelte';
|
||||
|
||||
describe('card.svelte', () => {
|
||||
// TODO: @testing-library/svelte claims to add this automatically but it doesn't work without explicit afterEach
|
||||
afterEach(() => cleanup());
|
||||
|
||||
it('mounts', () => {
|
||||
const { container } = render(Card, {
|
||||
title: 'TabTest',
|
||||
tabs: [
|
||||
{ id: 't1', title: 'title1', icon: 'fab fa-git-alt' },
|
||||
{ id: 't2', title: 'title2', icon: 'far fa-bookmark' }
|
||||
]
|
||||
});
|
||||
expect(container).toBeTruthy();
|
||||
expect(container).toHaveTextContent('TabTest');
|
||||
expect(container).toHaveTextContent('title1');
|
||||
expect(container).toHaveTextContent('title2');
|
||||
expect(container).not.toHaveTextContent('title3');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$/components/ui/button';
|
||||
import type { InputType } from '$/types';
|
||||
import { notify } from '$/util/notify';
|
||||
import { scale } from 'svelte/transition';
|
||||
import CheckIcon from '~icons/material-symbols/check-rounded';
|
||||
import CopyIcon from '~icons/material-symbols/content-copy-outline-rounded';
|
||||
|
||||
let {
|
||||
onclick,
|
||||
label = 'Copy'
|
||||
}: { onclick: (event?: Event) => Promise<unknown>; label?: string; type?: InputType } = $props();
|
||||
|
||||
let showCheckIcon = $state(false);
|
||||
</script>
|
||||
|
||||
<Button
|
||||
onclick={async (event) => {
|
||||
try {
|
||||
showCheckIcon = true;
|
||||
setTimeout(() => {
|
||||
showCheckIcon = false;
|
||||
}, 1000);
|
||||
await onclick(event);
|
||||
} catch {
|
||||
notify('Failed to copy');
|
||||
}
|
||||
}}>
|
||||
<div class="grid">
|
||||
{#key showCheckIcon}
|
||||
<span transition:scale class="col-start-1 row-start-1">
|
||||
{#if showCheckIcon}
|
||||
<CheckIcon />
|
||||
{:else}
|
||||
<CopyIcon />
|
||||
{/if}
|
||||
</span>
|
||||
{/key}
|
||||
</div>
|
||||
{label}
|
||||
</Button>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import CopyButton from '$/components/CopyButton.svelte';
|
||||
import { Input } from '$/components/ui/input';
|
||||
import type { InputType } from '$/types';
|
||||
import { copyToClipboard } from '$/util/util';
|
||||
|
||||
let {
|
||||
value,
|
||||
label = 'Copy',
|
||||
type = 'url',
|
||||
testID
|
||||
}: { value: string; label?: string; type?: InputType; testID?: string } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex w-full items-center gap-2">
|
||||
<Input
|
||||
{type}
|
||||
{value}
|
||||
data-testid={testID}
|
||||
onclick={(event) => {
|
||||
event.currentTarget.setSelectionRange(0, event.currentTarget.value.length);
|
||||
}} />
|
||||
|
||||
<CopyButton onclick={() => copyToClipboard(value)} {label} />
|
||||
</div>
|
||||
@@ -0,0 +1,109 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$/components/ui/button';
|
||||
import { TID } from '$/constants';
|
||||
import type { DocumentationConfig } from '$/types';
|
||||
import { standardizeDiagramType } from '$/util/mermaid';
|
||||
import { stateStore } from '$/util/state';
|
||||
import BookIcon from '~icons/material-symbols/book-2-outline-rounded';
|
||||
|
||||
const docURLBase = 'https://mermaid.js.org';
|
||||
const docMap = {
|
||||
architecture: {
|
||||
code: '/syntax/architecture.html'
|
||||
},
|
||||
block: {
|
||||
code: '/syntax/block.html'
|
||||
},
|
||||
c4: {
|
||||
code: '/syntax/c4.html'
|
||||
},
|
||||
class: {
|
||||
code: '/syntax/classDiagram.html',
|
||||
config: '/syntax/classDiagram.html#configuration'
|
||||
},
|
||||
er: {
|
||||
code: '/syntax/entityRelationshipDiagram.html',
|
||||
config: '/syntax/entityRelationshipDiagram.html#styling'
|
||||
},
|
||||
flowchart: {
|
||||
code: '/syntax/flowchart.html',
|
||||
config: '/syntax/flowchart.html#configuration'
|
||||
},
|
||||
gantt: {
|
||||
code: '/syntax/gantt.html',
|
||||
config: '/syntax/gantt.html#configuration'
|
||||
},
|
||||
gitGraph: {
|
||||
code: '/syntax/gitgraph.html',
|
||||
config: '/syntax/gitgraph.html#gitgraph-specific-configuration-options'
|
||||
},
|
||||
journey: {
|
||||
code: '/syntax/userJourney.html'
|
||||
},
|
||||
kanban: {
|
||||
code: '/syntax/kanban.html',
|
||||
config: '/syntax/kanban.html#configuration-options'
|
||||
},
|
||||
mindmap: {
|
||||
code: '/syntax/mindmap.html'
|
||||
},
|
||||
packet: {
|
||||
code: '/syntax/packet.html',
|
||||
config: '/config/schema-docs/config-defs-packet-diagram-config.html'
|
||||
},
|
||||
pie: {
|
||||
code: '/syntax/pie.html',
|
||||
config: '/syntax/pie.html#configuration'
|
||||
},
|
||||
quadrantChart: {
|
||||
code: '/syntax/quadrantChart.html',
|
||||
config: '/syntax/quadrantChart.html#chart-configurations'
|
||||
},
|
||||
requirement: {
|
||||
code: '/syntax/requirementDiagram.html'
|
||||
},
|
||||
sankey: {
|
||||
code: '/syntax/sankey.html',
|
||||
config: '/syntax/sankey.html#configuration'
|
||||
},
|
||||
sequence: {
|
||||
code: '/syntax/sequenceDiagram.html',
|
||||
config: '/syntax/sequenceDiagram.html#configuration'
|
||||
},
|
||||
stateDiagram: {
|
||||
code: '/syntax/stateDiagram.html'
|
||||
},
|
||||
timeline: {
|
||||
code: '/syntax/timeline.html',
|
||||
config: '/syntax/timeline.html#themes'
|
||||
},
|
||||
xychart: {
|
||||
code: '/syntax/xyChart.html',
|
||||
config: '/syntax/xyChart.html#chart-configurations'
|
||||
},
|
||||
zenuml: {
|
||||
code: '/syntax/zenuml.html'
|
||||
}
|
||||
} as const satisfies DocumentationConfig;
|
||||
|
||||
const doc = $derived.by(() => {
|
||||
const { editorMode, diagramType } = $stateStore;
|
||||
if (!diagramType) {
|
||||
return { key: '', url: docURLBase };
|
||||
}
|
||||
const key = standardizeDiagramType(diagramType);
|
||||
const docConfig = docMap[key] ?? { code: '' };
|
||||
const url = docURLBase + (docConfig[editorMode] ?? docConfig.code ?? '');
|
||||
return { key, url };
|
||||
});
|
||||
</script>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
data-testid={TID.diagramDocumentationButton}
|
||||
href={doc.url}
|
||||
target="_blank"
|
||||
title="View documentation for {doc.key.replace('Diagram', '')} diagram">
|
||||
<BookIcon />
|
||||
Docs
|
||||
</Button>
|
||||
@@ -1,34 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$/components/ui/button';
|
||||
import * as Popover from '$/components/ui/popover';
|
||||
import type { Component } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
links: { title: string; href: string }[];
|
||||
label?: string;
|
||||
icon?: string;
|
||||
icon?: Component;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { links, label, icon }: Props = $props();
|
||||
let props: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="dropdown dropdown-end">
|
||||
<button class="btn btn-ghost">
|
||||
{#if icon}
|
||||
<i class={icon}></i>
|
||||
{/if}
|
||||
{#if label}
|
||||
<span>{label}</span>
|
||||
{/if}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1792 1792"
|
||||
class="ml-1 inline-block h-4 w-4 fill-current"
|
||||
><path
|
||||
d="M1395 736q0 13-10 23l-466 466q-10 10-23 10t-23-10l-466-466q-10-10-10-23t10-23l50-50q10-10 23-10t23 10l393 393 393-393q10-10 23-10t23 10l50 50q10 10 10 23z" /></svg>
|
||||
</button>
|
||||
<div
|
||||
class="dropdown-content menu top-14 size-fit overflow-y-auto bg-base-200 text-base-content shadow-2xl">
|
||||
<Popover.Root>
|
||||
<Popover.Trigger class="flex items-center gap-0">
|
||||
<Button variant="ghost" size="sm">
|
||||
<props.icon class={props.class} />
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<ul tabindex="0" class="menu compact p-4">
|
||||
{#each links as { href, title }}
|
||||
<li>
|
||||
<ul tabindex="0" class="flex flex-col">
|
||||
{#each props.links as { href, title }}
|
||||
<li class="rounded-md p-2 hover:bg-muted">
|
||||
<a
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
@@ -40,5 +34,5 @@
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
<script lang="ts">
|
||||
import McWrapper from '$/components/McWrapper.svelte';
|
||||
import MermaidChartIcon from '$/components/MermaidChartIcon.svelte';
|
||||
import { Button } from '$/components/ui/button';
|
||||
import { TID } from '$/constants';
|
||||
import { env } from '$/util/env';
|
||||
import type { EditorMode } from '$lib/types';
|
||||
import { initEditor } from '$lib/util/monacoExtra';
|
||||
import { stateStore, updateCode, updateConfig } from '$lib/util/state';
|
||||
import { logEvent } from '$lib/util/stats';
|
||||
import { themeStore } from '$lib/util/theme';
|
||||
import { errorDebug, syncDiagram } from '$lib/util/util';
|
||||
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 { onDestroy, onMount } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import ExclamationCircleIcon from '~icons/material-symbols/error-outline-rounded';
|
||||
|
||||
let divElement: HTMLDivElement | undefined = $state();
|
||||
let editor: monaco.editor.IStandaloneCodeEditor | undefined;
|
||||
@@ -19,21 +24,19 @@
|
||||
theme: 'mermaid',
|
||||
overviewRulerLanes: 0
|
||||
};
|
||||
let text = '';
|
||||
let currentText = '';
|
||||
|
||||
stateStore.subscribe(({ errorMarkers, editorMode, code, mermaid }) => {
|
||||
// console.log('editor store subscription', { code, mermaid });
|
||||
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 !== text) {
|
||||
// console.log('updating editor text', newText);
|
||||
if (newText !== currentText) {
|
||||
editor.setScrollTop(0);
|
||||
editor.setValue(newText);
|
||||
text = newText;
|
||||
currentText = newText;
|
||||
}
|
||||
|
||||
// Update editor mode if it's different
|
||||
@@ -51,12 +54,7 @@
|
||||
monaco.editor.setModelMarkers(model, 'mermaid', errorMarkers);
|
||||
});
|
||||
|
||||
themeStore.subscribe(({ isDark }) => {
|
||||
editor && monaco.editor.setTheme(isDark ? 'mermaid-dark' : 'mermaid');
|
||||
});
|
||||
|
||||
const handleUpdate = (text: string, mode: EditorMode) => {
|
||||
// console.log('editor HandleUpdate', { text, mode });
|
||||
if (mode === 'code') {
|
||||
updateCode(text);
|
||||
} else {
|
||||
@@ -77,30 +75,21 @@
|
||||
if (!divElement) {
|
||||
throw new Error('divEl is undefined');
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
|
||||
initEditor(monaco);
|
||||
errorDebug();
|
||||
editor = monaco.editor.create(divElement, editorOptions);
|
||||
editor.onDidChangeModelContent(({ isFlush }) => {
|
||||
const newText = editor?.getValue();
|
||||
if (!newText || text === newText || isFlush) {
|
||||
if (!newText || currentText === newText || isFlush) {
|
||||
return;
|
||||
}
|
||||
text = newText;
|
||||
handleUpdate(text, $stateStore.editorMode);
|
||||
currentText = newText;
|
||||
handleUpdate(currentText, $stateStore.editorMode);
|
||||
});
|
||||
editor.addAction({
|
||||
id: 'mermaid-render-diagram',
|
||||
label: 'Render Diagram',
|
||||
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
|
||||
run: function () {
|
||||
syncDiagram();
|
||||
logEvent('renderDiagram', {
|
||||
method: 'keyboardShortcut'
|
||||
});
|
||||
}
|
||||
const unsubscribeMode = mode.subscribe((mode) => {
|
||||
editor && monaco.editor.setTheme(`mermaid${mode === 'dark' ? '-dark' : ''}`);
|
||||
});
|
||||
monaco.editor.setTheme($themeStore.isDark ? 'mermaid-dark' : 'mermaid');
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
editor?.layout({
|
||||
height: entries[0].contentRect.height,
|
||||
@@ -111,22 +100,38 @@
|
||||
if (divElement.parentElement) {
|
||||
resizeObserver.observe(divElement);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
editor?.dispose();
|
||||
return () => {
|
||||
unsubscribeState();
|
||||
unsubscribeMode();
|
||||
resizeObserver.disconnect();
|
||||
editor?.dispose();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="flex h-full flex-col pt-1">
|
||||
<div bind:this={divElement} id="editor" class="h-full flex-grow overflow-hidden"></div>
|
||||
{#if $stateStore.error instanceof Error}
|
||||
<div class="flex flex-col text-sm text-neutral-100">
|
||||
<div class="flex items-center gap-2 bg-red-700 p-2">
|
||||
<i class="fa fa-exclamation-circle w-4" aria-hidden="true"></i>
|
||||
<p>Diagram syntax error</p>
|
||||
<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 w-fit items-center gap-2">
|
||||
<ExclamationCircleIcon class="size-6 text-destructive" aria-hidden="true" />
|
||||
<div class="flex flex-col">
|
||||
<p>Syntax error</p>
|
||||
{#if env.isEnabledMermaidChartLinks}
|
||||
<p class="text-xs text-white/60">Create a free account to repair with AI</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<McWrapper>
|
||||
<Button variant="accent" size="sm" href={$urlsStore.mermaidChart.save}>
|
||||
<MermaidChartIcon />
|
||||
AI Repair
|
||||
</Button>
|
||||
</McWrapper>
|
||||
</div>
|
||||
<output class="max-h-32 overflow-auto bg-red-600 p-2" name="mermaid-error" for="editor">
|
||||
<output class="max-h-32 overflow-auto bg-muted p-2" name="mermaid-error" for="editor">
|
||||
<pre>{$stateStore.error?.toString()}</pre>
|
||||
</output>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="hidden h-12 items-center justify-between gap-2 rounded-2xl bg-muted p-3 md:flex">
|
||||
{@render children()}
|
||||
</div>
|
||||
@@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { stopPropagation } from 'svelte/legacy';
|
||||
|
||||
import Card from '$lib/components/Card/Card.svelte';
|
||||
import type { HistoryEntry, HistoryType, State, Tab } from '$lib/types';
|
||||
import { notify, prompt } from '$lib/util/notify';
|
||||
@@ -10,6 +8,16 @@
|
||||
import dayjsRelativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import BookmarkIcon from '~icons/material-symbols/bookmark-outline-rounded';
|
||||
import TrashAltIcon from '~icons/material-symbols/delete-outline-rounded';
|
||||
import DownloadIcon from '~icons/material-symbols/download-rounded';
|
||||
import SaveIcon from '~icons/material-symbols/save-outline-rounded';
|
||||
import UndoIcon from '~icons/material-symbols/settings-backup-restore-rounded';
|
||||
import UploadIcon from '~icons/material-symbols/upload-rounded';
|
||||
import HistoryIcon from '~icons/mdi/clock-outline';
|
||||
import GitAltIcon from '~icons/mdi/git';
|
||||
import { Button } from '../ui/button';
|
||||
import { Separator } from '../ui/separator';
|
||||
import {
|
||||
addHistoryEntry,
|
||||
clearHistoryData,
|
||||
@@ -27,16 +35,17 @@
|
||||
const tabSelectHandler = (tab: Tab) => {
|
||||
historyModeStore.set(tab.id as HistoryType);
|
||||
};
|
||||
|
||||
let tabs: Tab[] = $state([
|
||||
{
|
||||
id: 'manual',
|
||||
title: 'Saved',
|
||||
icon: 'far fa-bookmark'
|
||||
icon: BookmarkIcon
|
||||
},
|
||||
{
|
||||
id: 'auto',
|
||||
title: 'Timeline',
|
||||
icon: 'fas fa-history'
|
||||
icon: HistoryIcon
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -94,11 +103,6 @@
|
||||
inputStateStore.set({ ...state, updateDiagram: true });
|
||||
};
|
||||
|
||||
const relativeTime = (time: number) => {
|
||||
const t = new Date(time);
|
||||
return `${new Date(t).toLocaleString()} (${dayjs(t).fromNow()})`;
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
historyModeStore.set('manual');
|
||||
setInterval(() => {
|
||||
@@ -112,7 +116,7 @@
|
||||
{
|
||||
id: 'loader',
|
||||
title: 'Revisions',
|
||||
icon: 'fab fa-git-alt'
|
||||
icon: GitAltIcon
|
||||
},
|
||||
...tabs
|
||||
];
|
||||
@@ -121,72 +125,84 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<Card onselect={tabSelectHandler} isOpen={false} {tabs} title="History">
|
||||
<Card onselect={tabSelectHandler} isOpen isClosable={false} {tabs}>
|
||||
{#snippet actions()}
|
||||
<div>
|
||||
<button
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
id="uploadHistory"
|
||||
class="btn btn-secondary btn-xs w-12"
|
||||
onclick={stopPropagation(() => uploadHistory())}
|
||||
title="Upload history"
|
||||
aria-label="Upload history"><i class="fa fa-upload"></i></button>
|
||||
onclick={uploadHistory}
|
||||
title="Upload history"><UploadIcon /></Button>
|
||||
{#if $historyStore.length > 0}
|
||||
<button
|
||||
<Button
|
||||
id="downloadHistory"
|
||||
class="btn btn-secondary btn-xs w-12"
|
||||
onclick={stopPropagation(() => downloadHistory())}
|
||||
title="Download history"
|
||||
aria-label="Download history"><i class="fa fa-download"></i></button>
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onclick={downloadHistory}
|
||||
title="Download history"><DownloadIcon /></Button>
|
||||
{/if}
|
||||
|
|
||||
<button
|
||||
<Separator orientation="vertical" />
|
||||
<Button
|
||||
id="saveHistory"
|
||||
class="btn btn-success btn-xs w-12"
|
||||
onclick={stopPropagation(() => saveHistory())}
|
||||
title="Save current state"
|
||||
aria-label="Save current state"><i class="far fa-save"></i></button>
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onclick={() => saveHistory()}
|
||||
title="Save current state"><SaveIcon /></Button>
|
||||
{#if $historyModeStore !== 'loader'}
|
||||
<button
|
||||
<Button
|
||||
id="clearHistory"
|
||||
class="btn btn-error btn-xs w-12"
|
||||
onclick={stopPropagation(() => clearHistory())}
|
||||
title="Delete all saved states"
|
||||
aria-label="Delete all saved states"><i class="fas fa-trash-alt"></i></button>
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="hover:text-destructive"
|
||||
onclick={() => clearHistory()}
|
||||
title="Delete all saved states"><TrashAltIcon /></Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
<ul class="h-56 space-y-2 overflow-auto p-2" id="historyList">
|
||||
<ul class="flex h-full min-w-fit flex-col gap-2 overflow-auto p-2" id="historyList">
|
||||
{#if $historyStore.length > 0}
|
||||
{#each $historyStore as { id, state, time, name, url, type }}
|
||||
<li class="flex-col rounded p-2 shadow">
|
||||
<div class="flex">
|
||||
<div class="flex-1">
|
||||
<div class="flex flex-col text-base-content">
|
||||
{#if url}
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
title="Open revision in new tab"
|
||||
class="text-blue-500 hover:underline">{name}</a>
|
||||
{:else}
|
||||
<span>{name}</span>
|
||||
{/if}
|
||||
<span class="text-sm text-gray-400">{relativeTime(time)}</span>
|
||||
</div>
|
||||
{#each $historyStore as { id, state, time, name, url, type } (id)}
|
||||
<li class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-col">
|
||||
{#if url}
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
title="Open revision in new tab"
|
||||
class="text-blue-500 hover:underline">{name}</a>
|
||||
{:else}
|
||||
<span class="whitespace-nowrap">{name}</span>
|
||||
{/if}
|
||||
<span class="whitespace-nowrap text-xs text-primary-foreground/30">
|
||||
{new Date(time).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex content-center gap-2">
|
||||
<button class="btn btn-success" onclick={() => restoreHistoryItem(state)}
|
||||
><i class="fas fa-undo mr-1"></i>Restore</button>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="whitespace-nowrap text-sm text-primary-foreground/50">
|
||||
{dayjs(time).fromNow()}
|
||||
</span>
|
||||
<Button size="icon" variant="ghost" onclick={() => restoreHistoryItem(state)}>
|
||||
<UndoIcon />
|
||||
</Button>
|
||||
{#if type !== 'loader'}
|
||||
<button class="btn btn-error" onclick={() => clearHistory(id)}
|
||||
><i class="fas fa-trash-alt mr-1"></i>Delete</button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="hover:text-destructive"
|
||||
onclick={() => clearHistory(id)}>
|
||||
<TrashAltIcon />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
</li>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="m-2">
|
||||
<div class="m-2 text-center">
|
||||
No items in History<br />
|
||||
Click the Save button to save current state and restore it later.<br />
|
||||
Timeline will automatically be saved every minute.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { derived, writable, get } from 'svelte/store';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import { persist, localStorage } from '$lib/util/persist';
|
||||
import { generateSlug } from 'random-word-slugs';
|
||||
import type { HistoryEntry, HistoryType, Optional } from '$lib/types';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import { localStorage, persist } from '$lib/util/persist';
|
||||
import { logEvent } from '$lib/util/stats';
|
||||
import { generateSlug } from 'random-word-slugs';
|
||||
import type { Readable, Writable } from 'svelte/store';
|
||||
import { derived, get, writable } from 'svelte/store';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
const MAX_AUTO_HISTORY_LENGTH = 30;
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
import McWrapper from '$/components/McWrapper.svelte';
|
||||
import ThemeIcon from '$/components/ThemeIcon.svelte';
|
||||
import * as Popover from '$/components/ui/popover';
|
||||
import { Switch } from '$/components/ui/switch';
|
||||
import { urlsStore } from '$/util/state';
|
||||
import { cn } from '$/utils';
|
||||
import { mode, setMode } from 'mode-watcher';
|
||||
import type { Component } from 'svelte';
|
||||
import AddIcon from '~icons/material-symbols/add-2-rounded';
|
||||
import BookIcon from '~icons/material-symbols/book-2-outline-rounded';
|
||||
import PluginIcon from '~icons/material-symbols/extension-outline';
|
||||
import HomeIcon from '~icons/material-symbols/house-outline-rounded';
|
||||
import CommunityIcon from '~icons/material-symbols/person-play-outline-rounded';
|
||||
import PlaygroundIcon from '~icons/material-symbols/shape-line-outline';
|
||||
import MermaidChartIcon from './MermaidChartIcon.svelte';
|
||||
|
||||
const menuItems = $derived([
|
||||
{ label: 'New Diagram', icon: AddIcon, href: $urlsStore.new },
|
||||
{ label: 'Home', icon: HomeIcon, href: 'https://mermaid.js.org/' },
|
||||
{ label: 'Documentation', icon: BookIcon, href: 'https://mermaid.js.org/intro/' },
|
||||
{ label: 'Community', icon: CommunityIcon, href: 'https://discord.gg/sKeNQX4Wtj' }
|
||||
]);
|
||||
|
||||
const mermaidChartMenuItems = $derived([
|
||||
{ label: 'Edit in Playground', icon: PlaygroundIcon, href: $urlsStore.mermaidChart.playground },
|
||||
{
|
||||
label: 'Plugins',
|
||||
icon: PluginIcon,
|
||||
href: 'https://www.mermaidchart.com/plugins',
|
||||
checkDiagramType: false
|
||||
},
|
||||
{
|
||||
label: 'MermaidChart',
|
||||
icon: MermaidChartIcon,
|
||||
href: 'https://www.mermaidchart.com',
|
||||
checkDiagramType: false
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
{#snippet menuItem(options: { label: string; icon: Component; href: string; class?: string })}
|
||||
<a
|
||||
href={options.href}
|
||||
target="_blank"
|
||||
class={cn(
|
||||
'flex items-center justify-start gap-2 border-b bg-muted p-2 px-3 hover:bg-background',
|
||||
options.class
|
||||
)}>
|
||||
<options.icon class="size-5" />
|
||||
{options.label}
|
||||
</a>
|
||||
{/snippet}
|
||||
|
||||
<Popover.Root>
|
||||
<Popover.Trigger class="shrink-0">
|
||||
<img class="size-6" src="/favicon.svg" alt="Mermaid Live Editor" />
|
||||
</Popover.Trigger>
|
||||
<Popover.Content align="start" class="flex flex-col overflow-hidden p-0" sideOffset={16}>
|
||||
{#each menuItems as item}
|
||||
{@render menuItem(item)}
|
||||
{/each}
|
||||
|
||||
<div class="flex items-center justify-between border-b bg-muted px-3 py-2 hover:bg-background">
|
||||
<span class="flex items-center gap-2">
|
||||
<ThemeIcon />
|
||||
Dark Mode
|
||||
</span>
|
||||
<Switch
|
||||
checked={$mode === 'dark'}
|
||||
onCheckedChange={(dark) => setMode(dark ? 'dark' : 'light')} />
|
||||
</div>
|
||||
|
||||
{#each mermaidChartMenuItems as item}
|
||||
<McWrapper side="right" shouldCheckDiagramType={item.checkDiagramType}>
|
||||
{@render menuItem({
|
||||
...item,
|
||||
class: 'text-accent bg-background hover:bg-muted'
|
||||
})}
|
||||
</McWrapper>
|
||||
{/each}
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { env } from '$/util/env';
|
||||
import { stateStore } from '$/util/state';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import type { ComponentProps, Snippet } from 'svelte';
|
||||
import ExternalLinkIcon from '~icons/material-symbols/open-in-new-rounded';
|
||||
|
||||
let {
|
||||
children,
|
||||
shouldCheckDiagramType = true,
|
||||
side = 'bottom',
|
||||
labelPrefix = 'Opens a new tab in'
|
||||
}: {
|
||||
children: Snippet;
|
||||
shouldCheckDiagramType?: boolean;
|
||||
side?: ComponentProps<typeof Tooltip.Content>['side'];
|
||||
labelPrefix?: string;
|
||||
} = $props();
|
||||
|
||||
let shouldDisableComponent = $derived(
|
||||
shouldCheckDiagramType && $stateStore.diagramType === 'zenuml'
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if env.isEnabledMermaidChartLinks}
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root delayDuration={100}>
|
||||
<Tooltip.Trigger>
|
||||
<div class={[shouldDisableComponent && 'pointer-events-none cursor-not-allowed grayscale']}>
|
||||
{@render children()}
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content {side}>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if shouldDisableComponent}
|
||||
<div class="text-muted-foreground">
|
||||
This diagram type is not supported in MermaidChart.com
|
||||
</div>
|
||||
{:else}
|
||||
<ExternalLinkIcon />
|
||||
<span class="flex items-center gap-1">
|
||||
{labelPrefix}
|
||||
<div class="text-accent">MermaidChart.com</div>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
{/if}
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
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" />
|
||||
@@ -1,29 +1,34 @@
|
||||
<script module lang="ts">
|
||||
<script lang="ts" module>
|
||||
import { logEvent, plausible } from '$lib/util/stats';
|
||||
import { version } from 'mermaid/package.json';
|
||||
|
||||
void logEvent('version', {
|
||||
mermaidVersion: version
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { env } from '$lib/util/env';
|
||||
import MainMenu from '$/components/MainMenu.svelte';
|
||||
import McWrapper from '$/components/McWrapper.svelte';
|
||||
import { Button } from '$/components/ui/button';
|
||||
import { Separator } from '$/components/ui/separator';
|
||||
import { Switch } from '$/components/ui/switch';
|
||||
import { dismissPromotion, getActivePromotion } from '$lib/util/promos/promo';
|
||||
import { stateStore } from '$lib/util/state';
|
||||
import { urlsStore } from '$lib/util/state';
|
||||
import { MCBaseURL } from '$lib/util/util';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import type { ComponentProps, Snippet } from 'svelte';
|
||||
import CloseIcon from '~icons/material-symbols/close-rounded';
|
||||
import GithubIcon from '~icons/mdi/github';
|
||||
import DropdownNavMenu from './DropdownNavMenu.svelte';
|
||||
import Privacy from './Privacy.svelte';
|
||||
import Theme from './Theme.svelte';
|
||||
|
||||
const { isEnabledMermaidChartLinks } = env;
|
||||
|
||||
let isMenuOpen = $state(false);
|
||||
const isReferral = document.referrer.includes(MCBaseURL);
|
||||
function toggleMenu() {
|
||||
isMenuOpen = !isMenuOpen;
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
|
||||
const isReferral = document.referrer.includes(MCBaseURL);
|
||||
|
||||
type Links = ComponentProps<typeof DropdownNavMenu>['links'];
|
||||
|
||||
const githubLinks: Links = [
|
||||
@@ -38,15 +43,6 @@
|
||||
}
|
||||
];
|
||||
|
||||
const documentationLinks: Links = [
|
||||
{ title: 'Getting started', href: 'https://mermaid.js.org/intro/getting-started.html' },
|
||||
{ title: 'Tutorials', href: 'https://mermaid.js.org/ecosystem/tutorials.html' },
|
||||
{
|
||||
title: 'Integrations',
|
||||
href: 'https://mermaid.js.org/ecosystem/integrations-community.html'
|
||||
}
|
||||
];
|
||||
|
||||
let activePromotion = $state(getActivePromotion());
|
||||
|
||||
const trackBannerClick = () => {
|
||||
@@ -70,127 +66,67 @@
|
||||
<activePromotion.component {closeBanner} />
|
||||
</div>
|
||||
{#snippet closeBanner()}
|
||||
<button
|
||||
<Button
|
||||
title="Dismiss banner"
|
||||
aria-label="Dismiss banner"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => {
|
||||
dismissPromotion(activePromotion?.id);
|
||||
activePromotion = undefined;
|
||||
}}>
|
||||
<i class="fa fa-close px-2"></i>
|
||||
</button>
|
||||
<CloseIcon />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="navbar z-50 bg-primary p-0 shadow-lg">
|
||||
<div class="mx-2 flex flex-1 gap-2 px-2">
|
||||
<a href="/"><img class="size-6" src="./favicon.svg" alt="Mermaid Live Editor" /></a>
|
||||
<nav class="z-50 flex p-6">
|
||||
<div class="flex flex-1 items-center gap-4">
|
||||
<MainMenu />
|
||||
<div
|
||||
id="switcher"
|
||||
class="flex items-center justify-center gap-2 font-bold"
|
||||
class="flex items-center justify-center gap-4 font-medium"
|
||||
class:flex-row-reverse={isReferral}>
|
||||
<a href="/">
|
||||
<a href="/" class="whitespace-nowrap text-accent">
|
||||
{#if !isReferral}
|
||||
Mermaid
|
||||
{/if}
|
||||
Live Editor
|
||||
</a>
|
||||
{#if isEnabledMermaidChartLinks}
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
id="editorMode"
|
||||
checked={isReferral}
|
||||
onclick={() => {
|
||||
logEvent('playgroundToggle', { isReferred: isReferral });
|
||||
// Wait for the event to be logged
|
||||
setTimeout(() => {
|
||||
window.open(
|
||||
`${MCBaseURL}/play#${$stateStore.serialized}`,
|
||||
'_self',
|
||||
// Do not send referrer header, if the user already came from playground
|
||||
isReferral ? 'noreferrer' : ''
|
||||
);
|
||||
}, 100);
|
||||
}} />
|
||||
<a href="{MCBaseURL}/play#{$stateStore.serialized}">Playground</a>
|
||||
{/if}
|
||||
|
||||
<McWrapper labelPrefix="Opens the current diagram in">
|
||||
<div class="hidden items-center justify-center gap-4 md:flex">
|
||||
<Separator orientation="vertical" />
|
||||
<Switch
|
||||
id="editorMode"
|
||||
class="data-[state=checked]:bg-secondary"
|
||||
checked={isReferral}
|
||||
onclick={() => {
|
||||
logEvent('playgroundToggle', { isReferred: isReferral });
|
||||
// Wait for the event to be logged
|
||||
setTimeout(() => {
|
||||
window.open(
|
||||
$urlsStore.mermaidChart.playground,
|
||||
'_self',
|
||||
// Do not send referrer header, if the user already came from playground
|
||||
isReferral ? 'noreferrer' : ''
|
||||
);
|
||||
}, 100);
|
||||
}} />
|
||||
|
||||
<a href={$urlsStore.mermaidChart.playground} class="whitespace-nowrap">
|
||||
Playground <span class="hidden text-sm opacity-50 lg:inline"
|
||||
>- more features, no account required</span>
|
||||
</a>
|
||||
</div>
|
||||
</McWrapper>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label
|
||||
for="menu-toggle"
|
||||
class={isMenuOpen ? 'hidden' : 'pointer-cursor fixed right-4 z-[1000] lg:hidden'}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
class="fill-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20">
|
||||
<title>Menu</title>
|
||||
<path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z" />
|
||||
</svg>
|
||||
</label>
|
||||
|
||||
<!-- Cross SVG -->
|
||||
<label
|
||||
for="menu-toggle"
|
||||
class={isMenuOpen ? 'pointer-cursor fixed right-4 z-[1000] lg:hidden' : 'hidden'}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
class="fill-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20">
|
||||
<title>Cross</title>
|
||||
<line x1="5" y1="5" x2="15" y2="15" stroke="white" stroke-width="2" />
|
||||
<line x1="5" y1="15" x2="15" y2="5" stroke="white" stroke-width="2" />
|
||||
</svg>
|
||||
</label>
|
||||
|
||||
<input
|
||||
class="hidden"
|
||||
type="checkbox"
|
||||
id="menu-toggle"
|
||||
bind:checked={isMenuOpen}
|
||||
onclick={toggleMenu} />
|
||||
|
||||
<div class="hidden w-full lg:flex lg:w-auto lg:items-center" id="menu">
|
||||
<span class="text-sm">v{version}</span>
|
||||
<ul class="items-center justify-between pt-4 text-base lg:flex lg:pt-0">
|
||||
<li>
|
||||
<Privacy />
|
||||
</li>
|
||||
<li>
|
||||
<Theme />
|
||||
</li>
|
||||
<li>
|
||||
<DropdownNavMenu label="Documentation" links={documentationLinks} />
|
||||
</li>
|
||||
<li>
|
||||
<DropdownNavMenu icon="fab fa-github fa-lg" links={githubLinks} />
|
||||
</li>
|
||||
|
||||
{#if isEnabledMermaidChartLinks}
|
||||
<li>
|
||||
<a class="btn btn-ghost" target="_blank" href="https://mermaidchart.com">
|
||||
<img class="size-6" src="./mermaidchart-logo.svg" alt="Mermaid Chart" />
|
||||
</a>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
<div
|
||||
id="menu"
|
||||
class="hidden flex-nowrap items-center justify-between gap-3 overflow-hidden md:flex">
|
||||
<DropdownNavMenu icon={GithubIcon} links={githubLinks} />
|
||||
<Separator orientation="vertical" />
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#menu-toggle:checked + #menu {
|
||||
position: absolute;
|
||||
top: 2.5rem;
|
||||
padding: 1rem 0;
|
||||
background: #661ae6;
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import FloatingToolbar from '$/components/FloatingToolbar.svelte';
|
||||
import { Button } from '$/components/ui/button';
|
||||
import { Separator } from '$/components/ui/separator';
|
||||
import type { PanZoomState } from '$/util/panZoom';
|
||||
import { urlsStore } from '$/util/state';
|
||||
import ExpandIcon from '~icons/material-symbols/open-in-full-rounded';
|
||||
import ArrowsToCircleIcon from '~icons/material-symbols/screenshot-frame-2';
|
||||
import MagnifyingGlassPlusIcon from '~icons/material-symbols/zoom-in';
|
||||
import MagnifyingGlassMinusIcon from '~icons/material-symbols/zoom-out';
|
||||
|
||||
let { panZoomState }: { panZoomState: PanZoomState } = $props();
|
||||
</script>
|
||||
|
||||
<FloatingToolbar>
|
||||
<Button variant="ghost" size="icon" title="Reset view" onclick={() => panZoomState.reset()}>
|
||||
<ArrowsToCircleIcon />
|
||||
</Button>
|
||||
<Separator orientation="vertical" />
|
||||
<Button variant="ghost" size="icon" onclick={() => panZoomState.zoomOut()}>
|
||||
<MagnifyingGlassMinusIcon />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onclick={() => panZoomState.zoomIn()}>
|
||||
<MagnifyingGlassPlusIcon />
|
||||
</Button>
|
||||
<Separator orientation="vertical" />
|
||||
<Button variant="ghost" size="icon" title="Full Screen" href={$urlsStore.view} target="_blank">
|
||||
<ExpandIcon />
|
||||
</Button>
|
||||
</FloatingToolbar>
|
||||
@@ -1,20 +1,30 @@
|
||||
<script lang="ts">
|
||||
import Card from '$lib/components/Card/Card.svelte';
|
||||
import Card from '$/components/Card/Card.svelte';
|
||||
import { Button } from '$/components/ui/button';
|
||||
import { updateCode } from '$lib/util/state';
|
||||
import { logEvent } from '$lib/util/stats';
|
||||
import ShapesIcon from '~icons/material-symbols/account-tree-outline-rounded';
|
||||
|
||||
const samples = {
|
||||
Flow: `flowchart TD
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
C -->|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]`,
|
||||
Sequence: `sequenceDiagram
|
||||
Alice->>+John: Hello John, how are you?
|
||||
Alice->>+John: John, can you hear me?
|
||||
John-->>-Alice: Hi Alice, I can hear you!
|
||||
John-->>-Alice: I feel great!`,
|
||||
Block: `block-beta
|
||||
columns 3
|
||||
doc>"Document"]:3
|
||||
space down1<[" "]>(down) space
|
||||
|
||||
block:e:3
|
||||
l["left"]
|
||||
m("A wide one in the middle")
|
||||
r["right"]
|
||||
end
|
||||
space down2<[" "]>(down) space
|
||||
db[("DB")]:3
|
||||
space:3
|
||||
D space C
|
||||
db --> D
|
||||
C --> db
|
||||
D --> C
|
||||
style m fill:#d6d,stroke:#333,stroke-width:4px
|
||||
`,
|
||||
Class: `classDiagram
|
||||
Animal <|-- Duck
|
||||
Animal <|-- Fish
|
||||
@@ -36,26 +46,6 @@
|
||||
+bool is_wild
|
||||
+run()
|
||||
}`,
|
||||
State: `stateDiagram-v2
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
Still --> Moving
|
||||
Moving --> Still
|
||||
Moving --> Crash
|
||||
Crash --> [*]`,
|
||||
Gantt: `gantt
|
||||
title A Gantt Diagram
|
||||
dateFormat YYYY-MM-DD
|
||||
section Section
|
||||
A task :a1, 2014-01-01, 30d
|
||||
Another task :after a1 , 20d
|
||||
section Another
|
||||
Task in sec :2014-01-12 , 12d
|
||||
another task : 24d`,
|
||||
Pie: `pie title Pets adopted by volunteers
|
||||
"Dogs" : 386
|
||||
"Cats" : 85
|
||||
"Rats" : 15`,
|
||||
ER: `erDiagram
|
||||
CUSTOMER }|..|{ DELIVERY-ADDRESS : has
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
@@ -65,15 +55,21 @@
|
||||
ORDER ||--|{ ORDER-ITEM : includes
|
||||
PRODUCT-CATEGORY ||--|{ PRODUCT : contains
|
||||
PRODUCT ||--o{ ORDER-ITEM : "ordered in"`,
|
||||
'User Journey': `journey
|
||||
title My working day
|
||||
section Go to work
|
||||
Make tea: 5: Me
|
||||
Go upstairs: 3: Me
|
||||
Do work: 1: Me, Cat
|
||||
section Go home
|
||||
Go downstairs: 5: Me
|
||||
Sit down: 3: Me`,
|
||||
Flow: `flowchart TD
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
C -->|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]`,
|
||||
Gantt: `gantt
|
||||
title A Gantt Diagram
|
||||
dateFormat YYYY-MM-DD
|
||||
section Section
|
||||
A task :a1, 2014-01-01, 30d
|
||||
Another task :after a1 , 20d
|
||||
section Another
|
||||
Task in sec :2014-01-12 , 12d
|
||||
another task : 24d`,
|
||||
Git: `gitGraph
|
||||
commit
|
||||
commit
|
||||
@@ -93,7 +89,7 @@
|
||||
Popularisation
|
||||
British popular psychology author Tony Buzan
|
||||
Research
|
||||
On effectivness<br/>and features
|
||||
On effectiveness<br/>and features
|
||||
On Automatic creation
|
||||
Uses
|
||||
Creative techniques
|
||||
@@ -102,6 +98,32 @@
|
||||
Tools
|
||||
Pen and paper
|
||||
Mermaid`,
|
||||
Packet: `---
|
||||
title: "TCP Packet"
|
||||
---
|
||||
packet-beta
|
||||
0-15: "Source Port"
|
||||
16-31: "Destination Port"
|
||||
32-63: "Sequence Number"
|
||||
64-95: "Acknowledgment Number"
|
||||
96-99: "Data Offset"
|
||||
100-105: "Reserved"
|
||||
106: "URG"
|
||||
107: "ACK"
|
||||
108: "PSH"
|
||||
109: "RST"
|
||||
110: "SYN"
|
||||
111: "FIN"
|
||||
112-127: "Window"
|
||||
128-143: "Checksum"
|
||||
144-159: "Urgent Pointer"
|
||||
160-191: "(Options and Padding)"
|
||||
192-255: "Data (variable length)"
|
||||
`,
|
||||
Pie: `pie title Pets adopted by volunteers
|
||||
"Dogs" : 386
|
||||
"Cats" : 85
|
||||
"Rats" : 15`,
|
||||
QuadrantChart: `quadrantChart
|
||||
title Reach and engagement of campaigns
|
||||
x-axis Low Reach --> High Reach
|
||||
@@ -116,6 +138,27 @@
|
||||
Campaign D: [0.78, 0.34]
|
||||
Campaign E: [0.40, 0.34]
|
||||
Campaign F: [0.35, 0.78]`,
|
||||
Sequence: `sequenceDiagram
|
||||
Alice->>+John: Hello John, how are you?
|
||||
Alice->>+John: John, can you hear me?
|
||||
John-->>-Alice: Hi Alice, I can hear you!
|
||||
John-->>-Alice: I feel great!`,
|
||||
State: `stateDiagram-v2
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
Still --> Moving
|
||||
Moving --> Still
|
||||
Moving --> Crash
|
||||
Crash --> [*]`,
|
||||
'User Journey': `journey
|
||||
title My working day
|
||||
section Go to work
|
||||
Make tea: 5: Me
|
||||
Go upstairs: 3: Me
|
||||
Do work: 1: Me, Cat
|
||||
section Go home
|
||||
Go downstairs: 5: Me
|
||||
Sit down: 3: Me`,
|
||||
XYChart: `
|
||||
xychart-beta
|
||||
title "Sales Revenue"
|
||||
@@ -123,25 +166,6 @@
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
|
||||
line [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]`,
|
||||
Block: `block-beta
|
||||
columns 3
|
||||
doc>"Document"]:3
|
||||
space down1<[" "]>(down) space
|
||||
|
||||
block:e:3
|
||||
l["left"]
|
||||
m("A wide one in the middle")
|
||||
r["right"]
|
||||
end
|
||||
space down2<[" "]>(down) space
|
||||
db[("DB")]:3
|
||||
space:3
|
||||
D space C
|
||||
db --> D
|
||||
C --> db
|
||||
D --> C
|
||||
style m fill:#d6d,stroke:#333,stroke-width:4px
|
||||
`,
|
||||
ZenUML: `zenuml
|
||||
title Order Service
|
||||
@Actor Client #FFEBE6
|
||||
@@ -165,42 +189,18 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
Packet: `---
|
||||
title: "TCP Packet"
|
||||
---
|
||||
packet-beta
|
||||
0-15: "Source Port"
|
||||
16-31: "Destination Port"
|
||||
32-63: "Sequence Number"
|
||||
64-95: "Acknowledgment Number"
|
||||
96-99: "Data Offset"
|
||||
100-105: "Reserved"
|
||||
106: "URG"
|
||||
107: "ACK"
|
||||
108: "PSH"
|
||||
109: "RST"
|
||||
110: "SYN"
|
||||
111: "FIN"
|
||||
112-127: "Window"
|
||||
128-143: "Checksum"
|
||||
144-159: "Urgent Pointer"
|
||||
160-191: "(Options and Padding)"
|
||||
192-255: "Data (variable length)"
|
||||
`
|
||||
`
|
||||
};
|
||||
|
||||
type SampleTypes = keyof typeof samples;
|
||||
const loadSampleDiagram = (diagramType: SampleTypes): void => {
|
||||
updateCode(samples[diagramType], {
|
||||
updateDiagram: true,
|
||||
resetPanZoom: true
|
||||
resetPanZoom: true,
|
||||
updateDiagram: true
|
||||
});
|
||||
logEvent('loadSampleDiagram', { diagramType });
|
||||
};
|
||||
|
||||
// Adding in this array will add an icon to the preset menu
|
||||
const newDiagrams: SampleTypes[] = ['QuadrantChart', 'XYChart', 'Block', 'Packet'];
|
||||
const diagramOrder: SampleTypes[] = [
|
||||
'Flow',
|
||||
'Sequence',
|
||||
@@ -220,17 +220,15 @@ packet-beta
|
||||
];
|
||||
</script>
|
||||
|
||||
<Card title="Sample Diagrams" isOpen={false}>
|
||||
<div class="flex flex-wrap gap-2 p-2">
|
||||
<Card title="Sample Diagrams" isOpen isStackable icon={{ component: ShapesIcon }}>
|
||||
<div class="flex h-fit max-h-52 flex-wrap gap-2 overflow-y-auto p-2">
|
||||
{#each diagramOrder as sample}
|
||||
<button
|
||||
class="btn btn-primary btn-sm w-fit min-w-20 flex-grow normal-case"
|
||||
<Button
|
||||
size="sm"
|
||||
class="w-fit min-w-20 flex-grow normal-case"
|
||||
onclick={() => loadSampleDiagram(sample)}>
|
||||
{sample}
|
||||
{#if newDiagrams.includes(sample)}
|
||||
<span class="fa fa-heart ml-2"></span>
|
||||
{/if}
|
||||
</button>
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,31 +1,46 @@
|
||||
<label for="privacyModal" class="btn btn-ghost flex gap-2">
|
||||
<i class="fa-solid fa-shield-halved text-xl"></i> Security
|
||||
</label>
|
||||
<script>
|
||||
import * as Dialog from '$/components/ui/dialog';
|
||||
import ShieldIcon from '~icons/material-symbols/shield-lock-outline-rounded';
|
||||
</script>
|
||||
|
||||
<input type="checkbox" id="privacyModal" class="modal-toggle" />
|
||||
<div class="modal" role="dialog">
|
||||
<div class="modal-box flex flex-col gap-4 text-base-content">
|
||||
<h1 class="text-3xl font-bold">Data security</h1>
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<ShieldIcon />
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content>
|
||||
<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">
|
||||
The content of the diagrams you create never leaves your browser.
|
||||
</p>
|
||||
<p>It's only stored in the URL, and your browser's local storage.</p>
|
||||
<p>
|
||||
<span class="text-xl font-extrabold">
|
||||
The content of the diagrams you create never leaves your browser.
|
||||
</span>
|
||||
It's stored in the URL, and the browser's local storage only.
|
||||
Mermaid Live Editor is a fully open source, client side application, deployed transparently on <a
|
||||
href="https://github.com/mermaid-js/mermaid-live-editor/deployments"
|
||||
class="underline"
|
||||
target="_blank">
|
||||
GitHub Pages
|
||||
</a>.
|
||||
</p>
|
||||
<p>
|
||||
Mermaid live editor is a fully client side application, that will also work as an offline <a
|
||||
href="https://web.dev/explore/progressive-web-apps"
|
||||
class="link"
|
||||
target="_blank">
|
||||
PWA.
|
||||
It will also work as a fully offline
|
||||
<a href="https://web.dev/explore/progressive-web-apps" target="_blank">
|
||||
Progressive Web App.
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
The only server we have is a self hosted version of the open source and privacy friendly
|
||||
Plausible Analytics. We only collect data related to actions performed, like the type of
|
||||
diagram rendered, number of times a feature was used, etc. <br /> All the data we collect is
|
||||
anonymized and
|
||||
<a href="https://p.mermaid.live/mermaid.live" class="link" target="_blank">
|
||||
Plausible Analytics. We only collect anonymous data related to actions performed, like the
|
||||
type of diagram rendered, number of times a feature was used, etc.
|
||||
</p>
|
||||
<p>
|
||||
All the data we collect is anonymized and
|
||||
<a href="https://p.mermaid.live/mermaid.live" class="underline" target="_blank">
|
||||
available publicly.
|
||||
</a>
|
||||
</p>
|
||||
@@ -34,9 +49,5 @@
|
||||
Additional services like the external PNG/SVG/Kroki links and "Save to Mermaid Chart" feature
|
||||
will share your diagram with the respective 3rd party service.
|
||||
</p>
|
||||
|
||||
<label class="btn btn-circle btn-ghost btn-sm absolute right-2 top-2" for="privacyModal">
|
||||
X
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<script>
|
||||
import { buttonVariants } from '$/components/ui/button';
|
||||
import * as Dialog from '$/components/ui/dialog';
|
||||
import { Separator } from '$/components/ui/separator';
|
||||
import { env } from '$/util/env';
|
||||
import { urlsStore } from '$/util/state';
|
||||
import ShareIcon from '~icons/material-symbols/share';
|
||||
import CopyInput from './CopyInput.svelte';
|
||||
import MermaidChartIcon from './MermaidChartIcon.svelte';
|
||||
</script>
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger class={buttonVariants({ size: 'sm' })}>Share</Dialog.Trigger>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="flex items-center gap-2 text-xl">
|
||||
<ShareIcon class="size-5" /> Shareable links
|
||||
</Dialog.Title>
|
||||
<Dialog.Description>Share your diagrams with others.</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<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" />
|
||||
Mermaid Live Editor
|
||||
</h2>
|
||||
<CopyInput value={window.location.href} />
|
||||
<Dialog.Description>
|
||||
The content of the diagrams you create never leaves your browser.
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
{#if env.isEnabledMermaidChartLinks}
|
||||
<Separator />
|
||||
<div class="flex flex-col gap-2">
|
||||
<h2 class="flex items-center gap-2">
|
||||
<MermaidChartIcon class="size-5" />
|
||||
Mermaid Chart Playground
|
||||
</h2>
|
||||
<CopyInput value={$urlsStore.mermaidChart.playground} />
|
||||
<Dialog.Description>
|
||||
Opens the Mermaid Chart Playground with Mermaid AI, Visual Editor, and more.
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import FloatingToolbar from '$/components/FloatingToolbar.svelte';
|
||||
import { Toggle } from '$/components/ui/toggle';
|
||||
import { defaultState, inputStateStore } from '$/util/state';
|
||||
import RoughIcon from '~icons/material-symbols/draw-outline-rounded';
|
||||
import BackgroundIcon from '~icons/material-symbols/grid-4x4-rounded';
|
||||
|
||||
if ($inputStateStore.grid === undefined) {
|
||||
// Handle cases where old states were saved without grid option
|
||||
$inputStateStore.grid = defaultState.grid;
|
||||
}
|
||||
</script>
|
||||
|
||||
<FloatingToolbar>
|
||||
<Toggle bind:pressed={$inputStateStore.rough} size="sm" title="Hand-Drawn">
|
||||
<RoughIcon />
|
||||
</Toggle>
|
||||
<Toggle bind:pressed={$inputStateStore.grid} size="sm" title="Background Grid">
|
||||
<BackgroundIcon />
|
||||
</Toggle>
|
||||
</FloatingToolbar>
|
||||
@@ -1,66 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { setTheme, themeStore } from '$lib/util/theme';
|
||||
|
||||
const themes = [
|
||||
'🌝 light',
|
||||
'🌚 dark',
|
||||
'🧁 cupcake',
|
||||
'🐝 bumblebee',
|
||||
'✳️ emerald',
|
||||
'🏢 corporate',
|
||||
'🌃 synthwave',
|
||||
'👴 retro',
|
||||
'🤖 cyberpunk',
|
||||
'🌸 valentine',
|
||||
'🎃 halloween',
|
||||
'🌷 garden',
|
||||
'🌲 forest',
|
||||
'🐟 aqua',
|
||||
'👓 lofi',
|
||||
'🖍 pastel',
|
||||
'🧚♀️ fantasy',
|
||||
'📝 wireframe',
|
||||
'🏴 black',
|
||||
'💎 luxury',
|
||||
'🧛♂️ dracula'
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="dropdown hidden lg:block">
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<div tabindex="0" class="btn btn-ghost">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="inline-block h-6 w-6 stroke-current md:mr-2"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" /></svg>
|
||||
<span class="hidden md:inline">Theme</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1792 1792"
|
||||
class="ml-1 inline-block h-4 w-4 fill-current"
|
||||
><path
|
||||
d="M1395 736q0 13-10 23l-466 466q-10 10-23 10t-23-10l-466-466q-10-10-10-23t10-23l50-50q10-10 23-10t23 10l393 393 393-393q10-10 23-10t23 10l50 50q10 10 10 23z" /></svg>
|
||||
</div>
|
||||
<div
|
||||
class="dropdown-content top-px mt-14 h-96 w-56 overflow-y-auto bg-base-200 text-base-content shadow-2xl">
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<ul tabindex="0" class="menu compact p-4">
|
||||
{#each themes as theme}
|
||||
<li class:bordered={$themeStore.theme !== undefined && theme.includes($themeStore.theme)}>
|
||||
<span
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
class="btn btn-ghost justify-start"
|
||||
onclick={() => setTheme(theme)}
|
||||
onkeypress={() => setTheme(theme)}>{theme}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { mode } from 'mode-watcher';
|
||||
import { cubicInOut } from 'svelte/easing';
|
||||
import { type TransitionConfig } from 'svelte/transition';
|
||||
import MoonIcon from '~icons/material-symbols/dark-mode-outline-rounded';
|
||||
import SunIcon from '~icons/material-symbols/light-mode-outline-rounded';
|
||||
|
||||
const spin = (
|
||||
node: Element,
|
||||
{ duration = 400, easing = cubicInOut, clockWise = true } = {}
|
||||
): TransitionConfig => {
|
||||
const style = getComputedStyle(node);
|
||||
const opacity = +style.opacity;
|
||||
const transform = style.transform === 'none' ? '' : style.transform;
|
||||
return {
|
||||
duration,
|
||||
easing,
|
||||
css: (t, u) => `
|
||||
transform: ${transform} rotate(${u * 90 * (clockWise ? 1 : -1)}deg);
|
||||
opacity: ${opacity * t}
|
||||
`
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="inline-grid">
|
||||
{#key $mode}
|
||||
<div
|
||||
in:spin={{ clockWise: true }}
|
||||
out:spin={{ clockWise: false }}
|
||||
class="col-start-1 row-start-1">
|
||||
{#if $mode === 'dark'}
|
||||
<MoonIcon />
|
||||
{:else}
|
||||
<SunIcon />
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import FloatingToolbar from '$/components/FloatingToolbar.svelte';
|
||||
import Privacy from '$/components/Privacy.svelte';
|
||||
import { Button } from '$/components/ui/button';
|
||||
import { Separator } from '$/components/ui/separator';
|
||||
import { TID } from '$/constants';
|
||||
import { version } from 'mermaid/package.json';
|
||||
import { mode, setMode } from 'mode-watcher';
|
||||
import ThemeIcon from './ThemeIcon.svelte';
|
||||
</script>
|
||||
|
||||
<FloatingToolbar>
|
||||
<span class="text-sm font-semibold opacity-60">v{version}</span>
|
||||
<Button variant="ghost" size="icon" title="Privacy & Security">
|
||||
<Privacy />
|
||||
</Button>
|
||||
|
||||
<Separator orientation="vertical" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-testid={TID.themeToggleButton}
|
||||
title="Switch to {$mode === 'dark' ? 'light' : 'dark'} theme"
|
||||
class="[&_svg]:size-5"
|
||||
onclick={() => setMode($mode === 'dark' ? 'light' : 'dark')}>
|
||||
<ThemeIcon />
|
||||
</Button>
|
||||
</FloatingToolbar>
|
||||
@@ -1,56 +1,39 @@
|
||||
<script lang="ts">
|
||||
import type { State, ValidatedState } from '$lib/types';
|
||||
import { recordRenderTime, shouldRefreshView } from '$lib/util/autoSync';
|
||||
import { render as renderDiagram } from '$lib/util/mermaid';
|
||||
import { inputStateStore, stateStore, updateCodeStore } from '$lib/util/state';
|
||||
import { logEvent, saveStatistics } from '$lib/util/stats';
|
||||
import { cmdKey } from '$lib/util/util';
|
||||
import type { State, ValidatedState } from '$/types';
|
||||
import { recordRenderTime, shouldRefreshView } from '$/util/autoSync';
|
||||
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 uniqueID from 'lodash-es/uniqueId';
|
||||
import type { MermaidConfig } from 'mermaid';
|
||||
import { mode } from 'mode-watcher';
|
||||
import { onMount } from 'svelte';
|
||||
import panzoom from 'svg-pan-zoom';
|
||||
import { Svg2Roughjs } from 'svg2roughjs';
|
||||
|
||||
let {
|
||||
panZoomState = new PanZoomState(),
|
||||
shouldShowGrid = true
|
||||
}: { panZoomState?: PanZoomState; shouldShowGrid?: boolean } = $props();
|
||||
let code = '';
|
||||
let config = '';
|
||||
let container: HTMLDivElement | undefined = $state();
|
||||
let rough: boolean;
|
||||
let view: HTMLDivElement | undefined = $state();
|
||||
let error = $state(false);
|
||||
let outOfSync = $state(false);
|
||||
let panZoom = true;
|
||||
let manualUpdate = true;
|
||||
let panZoomEnabled = $stateStore.panZoom;
|
||||
let pzoom: typeof panzoom | undefined;
|
||||
const handlePanZoomChange = () => {
|
||||
if (!pzoom) {
|
||||
return;
|
||||
}
|
||||
const pan = pzoom.getPan();
|
||||
const zoom = pzoom.getZoom();
|
||||
updateCodeStore({ pan, zoom });
|
||||
logEvent('panZoom');
|
||||
|
||||
// Set up panZoom state observer to update the store when pan/zoom changes
|
||||
const setupPanZoomObserver = () => {
|
||||
panZoomState.onPanZoomChange = (pan, zoom) => {
|
||||
updateCodeStore({ pan, zoom });
|
||||
logEvent('panZoom');
|
||||
};
|
||||
};
|
||||
|
||||
const handlePanZoom = (state: State, graphDiv: SVGSVGElement) => {
|
||||
if (!state.panZoom) {
|
||||
return;
|
||||
}
|
||||
pzoom?.destroy();
|
||||
pzoom = undefined;
|
||||
void Promise.resolve().then(() => {
|
||||
pzoom = panzoom(graphDiv, {
|
||||
onPan: handlePanZoomChange,
|
||||
onZoom: handlePanZoomChange,
|
||||
controlIconsEnabled: true,
|
||||
fit: true,
|
||||
center: true
|
||||
});
|
||||
const { pan, zoom } = state;
|
||||
if (pan !== undefined && zoom !== undefined && Number.isFinite(zoom)) {
|
||||
pzoom.zoom(zoom);
|
||||
pzoom.pan(pan);
|
||||
}
|
||||
});
|
||||
panZoomState.updateElement(graphDiv, state);
|
||||
};
|
||||
|
||||
const handleStateChange = async (state: ValidatedState) => {
|
||||
@@ -62,31 +45,26 @@
|
||||
error = false;
|
||||
let diagramType: string | undefined;
|
||||
try {
|
||||
if (container && state && (state.updateDiagram || state.autoSync)) {
|
||||
if (!state.autoSync) {
|
||||
$inputStateStore.updateDiagram = false;
|
||||
}
|
||||
outOfSync = false;
|
||||
if (container) {
|
||||
manualUpdate = true;
|
||||
// Do not render if there is no change in Code/Config/PanZoom
|
||||
if (
|
||||
code === state.code &&
|
||||
config === state.mermaid &&
|
||||
panZoomEnabled === state.panZoom &&
|
||||
rough === state.rough
|
||||
rough === state.rough &&
|
||||
panZoom === state.panZoom
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldRefreshView()) {
|
||||
outOfSync = true;
|
||||
return;
|
||||
}
|
||||
|
||||
code = state.code;
|
||||
config = state.mermaid;
|
||||
panZoomEnabled = state.panZoom;
|
||||
rough = state.rough;
|
||||
panZoom = state.panZoom ?? true;
|
||||
const scroll = view?.parentElement?.scrollTop;
|
||||
delete container.dataset.processed;
|
||||
const viewID = uniqueID('graph-');
|
||||
@@ -113,6 +91,7 @@
|
||||
}
|
||||
const height = sketch.getAttribute('height');
|
||||
const width = sketch.getAttribute('width');
|
||||
sketch.setAttribute('id', 'graph-div');
|
||||
sketch.setAttribute('height', '100%');
|
||||
sketch.setAttribute('width', '100%');
|
||||
sketch.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
||||
@@ -125,7 +104,9 @@
|
||||
bindFunctions(graphDiv);
|
||||
}
|
||||
}
|
||||
handlePanZoom(state, graphDiv);
|
||||
if (state.panZoom) {
|
||||
handlePanZoom(state, graphDiv);
|
||||
}
|
||||
}
|
||||
if (view?.parentElement && scroll) {
|
||||
view.parentElement.scrollTop = scroll;
|
||||
@@ -133,57 +114,43 @@
|
||||
error = false;
|
||||
} else if (manualUpdate) {
|
||||
manualUpdate = false;
|
||||
} else if (code !== state.code || config !== state.mermaid) {
|
||||
outOfSync = true;
|
||||
}
|
||||
} catch (error_) {
|
||||
console.error('view fail', error_);
|
||||
error = true;
|
||||
}
|
||||
const renderTime = Date.now() - startTime;
|
||||
saveStatistics({ code, renderTime, isRough: state.rough, diagramType });
|
||||
saveStatistics({ code, diagramType, isRough: state.rough, renderTime });
|
||||
recordRenderTime(renderTime, () => {
|
||||
$inputStateStore.updateDiagram = true;
|
||||
});
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
setupPanZoomObserver();
|
||||
// Queue state changes to avoid race condition
|
||||
let pendingStateChange = Promise.resolve();
|
||||
stateStore.subscribe((state) => {
|
||||
pendingStateChange = pendingStateChange.then(() => handleStateChange(state).catch(() => {}));
|
||||
});
|
||||
window.addEventListener('resize', () => {
|
||||
if ($stateStore.panZoom && pzoom) {
|
||||
pzoom.resize();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if outOfSync}
|
||||
<div
|
||||
class="absolute z-10 w-full bg-base-100 bg-opacity-80 p-2 text-left font-mono text-yellow-600"
|
||||
id="errorContainer">
|
||||
Diagram out of sync. <br />
|
||||
{#if $stateStore.autoSync}
|
||||
It will be updated automatically.
|
||||
{:else}
|
||||
Press <i class="fas fa-sync"></i> (Sync button) or <kbd>{cmdKey} + Enter</kbd> to sync.
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div id="view" bind:this={view} class="h-full p-2" class:error class:outOfSync>
|
||||
<div
|
||||
id="view"
|
||||
bind:this={view}
|
||||
class={['h-full w-full', shouldShowGrid && `grid-bg-${$mode}`, error && 'opacity-50']}>
|
||||
<div id="container" bind:this={container} class="h-full overflow-auto"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#view {
|
||||
flex: 1;
|
||||
.grid-bg-light {
|
||||
background-size: 30px 30px;
|
||||
background-image: radial-gradient(circle, #e4e4e48c 2px, #0000 2px);
|
||||
}
|
||||
.error,
|
||||
.outOfSync {
|
||||
opacity: 0.5;
|
||||
|
||||
.grid-bg-dark {
|
||||
background-size: 30px 30px;
|
||||
background-image: radial-gradient(circle, #46464646 2px, #0000 2px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<script lang="ts" module>
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
import { type VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: 'focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-6 [&_svg]:shrink-0',
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm',
|
||||
outline:
|
||||
'border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm',
|
||||
accent: 'bg-accent text-accent-foreground hover:bg-accent/80 shadow-sm',
|
||||
ghost: 'hover:bg-primary hover:text-primary-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline'
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'size-8'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
ref = $bindable(null),
|
||||
href,
|
||||
type = 'button',
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a bind:this={ref} class={cn(buttonVariants({ variant, size }), className)} {href} {...restProps}>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{...restProps}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
@@ -0,0 +1,9 @@
|
||||
export {
|
||||
default as Button,
|
||||
buttonVariants,
|
||||
default as Root,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
type ButtonProps as Props
|
||||
} from './button.svelte';
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
|
||||
import X from 'lucide-svelte/icons/x';
|
||||
import type { Snippet } from 'svelte';
|
||||
import * as Dialog from './index.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||
portalProps?: DialogPrimitive.PortalProps;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Portal {...portalProps}>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
class={cn(
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
className
|
||||
)}
|
||||
{...restProps}>
|
||||
{@render children?.()}
|
||||
<DialogPrimitive.Close
|
||||
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:text-muted-foreground">
|
||||
<X class="size-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</Dialog.Portal>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Description
|
||||
bind:ref
|
||||
class={cn('text-sm text-muted-foreground', className)}
|
||||
{...restProps} />
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||
{...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)}
|
||||
{...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Overlay
|
||||
bind:ref
|
||||
class={cn(
|
||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className
|
||||
)}
|
||||
{...restProps} />
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Title
|
||||
bind:ref
|
||||
class={cn('text-lg font-semibold leading-none tracking-tight', className)}
|
||||
{...restProps} />
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import Content from './dialog-content.svelte';
|
||||
import Description from './dialog-description.svelte';
|
||||
import Footer from './dialog-footer.svelte';
|
||||
import Header from './dialog-header.svelte';
|
||||
import Overlay from './dialog-overlay.svelte';
|
||||
import Title from './dialog-title.svelte';
|
||||
|
||||
const Root: typeof DialogPrimitive.Root = DialogPrimitive.Root;
|
||||
const Trigger: typeof DialogPrimitive.Trigger = DialogPrimitive.Trigger;
|
||||
const Close: typeof DialogPrimitive.Close = DialogPrimitive.Close;
|
||||
const Portal: typeof DialogPrimitive.Portal = DialogPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Close,
|
||||
Content,
|
||||
Description,
|
||||
//
|
||||
Root as Dialog,
|
||||
Close as DialogClose,
|
||||
Content as DialogContent,
|
||||
Description as DialogDescription,
|
||||
Footer as DialogFooter,
|
||||
Header as DialogHeader,
|
||||
Overlay as DialogOverlay,
|
||||
Portal as DialogPortal,
|
||||
Title as DialogTitle,
|
||||
Trigger as DialogTrigger,
|
||||
Footer,
|
||||
Header,
|
||||
Overlay,
|
||||
Portal,
|
||||
Root,
|
||||
Title,
|
||||
Trigger
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import Root from './input.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import type { InputType } from '$/types';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||
|
||||
type Props = WithElementRef<
|
||||
Omit<HTMLInputAttributes, 'type'> &
|
||||
({ type: 'file'; files?: FileList } | { type?: InputType; files?: undefined })
|
||||
>;
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
type,
|
||||
files = $bindable(),
|
||||
class: className,
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if type === 'file'}
|
||||
<input
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className
|
||||
)}
|
||||
type="file"
|
||||
bind:files
|
||||
bind:value
|
||||
{...restProps} />
|
||||
{:else}
|
||||
<input
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className
|
||||
)}
|
||||
{type}
|
||||
bind:value
|
||||
{...restProps} />
|
||||
{/if}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
import Content from './popover-content.svelte';
|
||||
const Root = PopoverPrimitive.Root;
|
||||
const Trigger = PopoverPrimitive.Trigger;
|
||||
const Close = PopoverPrimitive.Close;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Trigger,
|
||||
Close,
|
||||
//
|
||||
Root as Popover,
|
||||
Content as PopoverContent,
|
||||
Trigger as PopoverTrigger,
|
||||
Close as PopoverClose
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
align = 'center',
|
||||
sideOffset = 4,
|
||||
portalProps,
|
||||
...restProps
|
||||
}: PopoverPrimitive.ContentProps & {
|
||||
portalProps?: PopoverPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Portal {...portalProps}>
|
||||
<PopoverPrimitive.Content
|
||||
bind:ref
|
||||
{align}
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
{...restProps} />
|
||||
</PopoverPrimitive.Portal>
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Pane } from 'paneforge';
|
||||
import Handle from './resizable-handle.svelte';
|
||||
import PaneGroup from './resizable-pane-group.svelte';
|
||||
|
||||
export {
|
||||
PaneGroup,
|
||||
Pane,
|
||||
Handle,
|
||||
//
|
||||
PaneGroup as ResizablePaneGroup,
|
||||
Pane as ResizablePane,
|
||||
Handle as ResizableHandle
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import GripVertical from 'lucide-svelte/icons/grip-vertical';
|
||||
import * as ResizablePrimitive from 'paneforge';
|
||||
import type { WithoutChildrenOrChild } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
withHandle = false,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<ResizablePrimitive.PaneResizerProps> & {
|
||||
withHandle?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<ResizablePrimitive.PaneResizer
|
||||
bind:ref
|
||||
class={cn(
|
||||
'relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[direction=vertical]:h-px data-[direction=vertical]:w-full data-[direction=vertical]:after:left-0 data-[direction=vertical]:after:h-1 data-[direction=vertical]:after:w-full data-[direction=vertical]:after:-translate-y-1/2 data-[direction=vertical]:after:translate-x-0 [&[data-direction=vertical]>div]:rotate-90',
|
||||
className
|
||||
)}
|
||||
{...restProps}>
|
||||
{#if withHandle}
|
||||
<div class="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
|
||||
<GripVertical class="size-2.5" />
|
||||
</div>
|
||||
{/if}
|
||||
</ResizablePrimitive.PaneResizer>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import * as ResizablePrimitive from 'paneforge';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
direction,
|
||||
this: paneGroup = $bindable(),
|
||||
...restProps
|
||||
}: ResizablePrimitive.PaneGroupProps & {
|
||||
this?: ResizablePrimitive.PaneGroup;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<ResizablePrimitive.PaneGroup
|
||||
bind:ref
|
||||
bind:this={paneGroup}
|
||||
{direction}
|
||||
class={cn('flex h-full w-full data-[direction=vertical]:flex-col', className)}
|
||||
{...restProps} />
|
||||
@@ -0,0 +1,7 @@
|
||||
import Root from './separator.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { Separator as SeparatorPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
orientation = 'horizontal',
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SeparatorPrimitive.Root
|
||||
bind:ref
|
||||
class={cn(
|
||||
'shrink-0 rounded border',
|
||||
orientation === 'horizontal' ? 'h-[1px] w-full' : 'min-h-6 w-[0px]',
|
||||
className
|
||||
)}
|
||||
{orientation}
|
||||
{...restProps} />
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Toaster } from './sonner.svelte';
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Toaster as Sonner, type ToasterProps as SonnerProps } from 'svelte-sonner';
|
||||
import { mode } from 'mode-watcher';
|
||||
|
||||
let restProps: SonnerProps = $props();
|
||||
</script>
|
||||
|
||||
<Sonner
|
||||
theme={$mode}
|
||||
class="toaster group"
|
||||
toastOptions={{
|
||||
classes: {
|
||||
toast:
|
||||
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
|
||||
description: 'group-[.toast]:text-muted-foreground',
|
||||
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
|
||||
cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground'
|
||||
}
|
||||
}}
|
||||
{...restProps} />
|
||||
@@ -0,0 +1,7 @@
|
||||
import Root from './switch.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Switch
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { Switch as SwitchPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
|
||||
</script>
|
||||
|
||||
<SwitchPrimitive.Root
|
||||
bind:ref
|
||||
bind:checked
|
||||
class={cn(
|
||||
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-accent data-[state=unchecked]:bg-slate-700',
|
||||
className
|
||||
)}
|
||||
{...restProps}>
|
||||
<SwitchPrimitive.Thumb
|
||||
class={cn(
|
||||
'pointer-events-none block size-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
|
||||
)} />
|
||||
</SwitchPrimitive.Root>
|
||||
@@ -0,0 +1,10 @@
|
||||
import Root from './toggle-group.svelte';
|
||||
import Item from './toggle-group-item.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Item,
|
||||
//
|
||||
Root as ToggleGroup,
|
||||
Item as ToggleGroupItem
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { ToggleGroup as ToggleGroupPrimitive } from 'bits-ui';
|
||||
import { getToggleGroupCtx } from './toggle-group.svelte';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { type ToggleVariants, toggleVariants } from '$lib/components/ui/toggle/index.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
class: className,
|
||||
size,
|
||||
variant,
|
||||
...restProps
|
||||
}: ToggleGroupPrimitive.ItemProps & ToggleVariants = $props();
|
||||
|
||||
const ctx = getToggleGroupCtx();
|
||||
</script>
|
||||
|
||||
<ToggleGroupPrimitive.Item
|
||||
bind:ref
|
||||
class={cn(
|
||||
toggleVariants({
|
||||
variant: ctx.variant || variant,
|
||||
size: ctx.size || size
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{value}
|
||||
{...restProps} />
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts" module>
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import type { ToggleVariants } from '$lib/components/ui/toggle/index.js';
|
||||
export function setToggleGroupCtx(props: ToggleVariants) {
|
||||
setContext('toggleGroup', props);
|
||||
}
|
||||
|
||||
export function getToggleGroupCtx() {
|
||||
return getContext<ToggleVariants>('toggleGroup');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { ToggleGroup as ToggleGroupPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
class: className,
|
||||
size = 'default',
|
||||
variant = 'default',
|
||||
...restProps
|
||||
}: ToggleGroupPrimitive.RootProps & ToggleVariants = $props();
|
||||
|
||||
setToggleGroupCtx({
|
||||
variant,
|
||||
size
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Discriminated Unions + Destructing (required for bindable) do not
|
||||
get along, so we shut typescript up by casting `value` to `never`.
|
||||
-->
|
||||
<ToggleGroupPrimitive.Root
|
||||
bind:value={value as never}
|
||||
bind:ref
|
||||
class={cn('flex items-center justify-center gap-1', className)}
|
||||
{...restProps} />
|
||||
@@ -0,0 +1,13 @@
|
||||
import Root from './toggle.svelte';
|
||||
export {
|
||||
toggleVariants,
|
||||
type ToggleSize,
|
||||
type ToggleVariant,
|
||||
type ToggleVariants
|
||||
} from './toggle.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Toggle
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const toggleVariants = tv({
|
||||
base: 'hover:bg-muted hover:text-muted-foreground focus-visible:ring-ring data-[state=on]:bg-primary data-[state=on]:text-primary-foreground inline-flex items-center justify-center gap-1 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-6 [&_svg]:shrink-0',
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-transparent',
|
||||
outline:
|
||||
'border-input hover:bg-primary/80 hover:text-primary-foreground border bg-transparent'
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 min-w-9 px-3',
|
||||
sm: 'h-8 min-w-8 px-2',
|
||||
lg: 'h-10 min-w-10 px-3'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
});
|
||||
|
||||
export type ToggleVariant = VariantProps<typeof toggleVariants>['variant'];
|
||||
export type ToggleSize = VariantProps<typeof toggleVariants>['size'];
|
||||
export type ToggleVariants = VariantProps<typeof toggleVariants>;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { Toggle as TogglePrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
pressed = $bindable(false),
|
||||
class: className,
|
||||
size = 'default',
|
||||
variant = 'default',
|
||||
...restProps
|
||||
}: TogglePrimitive.RootProps & {
|
||||
variant?: ToggleVariant;
|
||||
size?: ToggleSize;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<TogglePrimitive.Root
|
||||
bind:ref
|
||||
bind:pressed
|
||||
class={cn(toggleVariants({ variant, size }), className)}
|
||||
{...restProps} />
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Tooltip as TooltipPrimitive } from 'bits-ui';
|
||||
import Content from './tooltip-content.svelte';
|
||||
|
||||
const Root = TooltipPrimitive.Root;
|
||||
const Trigger = TooltipPrimitive.Trigger;
|
||||
const Provider = TooltipPrimitive.Provider;
|
||||
|
||||
export {
|
||||
Content,
|
||||
Provider,
|
||||
Root,
|
||||
//
|
||||
Root as Tooltip,
|
||||
Content as TooltipContent,
|
||||
Provider as TooltipProvider,
|
||||
Trigger as TooltipTrigger,
|
||||
Trigger
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Tooltip as TooltipPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
...restProps
|
||||
}: TooltipPrimitive.ContentProps = $props();
|
||||
</script>
|
||||
|
||||
<TooltipPrimitive.Content
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
{...restProps} />
|
||||
@@ -0,0 +1,8 @@
|
||||
export const TID = {
|
||||
copyMarkdown: 'copy-markdown',
|
||||
diagramDocumentationButton: 'diagram-documentation-button',
|
||||
downloadPNG: 'download-PNG',
|
||||
downloadSVG: 'download-SVG',
|
||||
errorContainer: 'error-container',
|
||||
themeToggleButton: 'theme-toggle-button'
|
||||
} as const;
|
||||
Vendored
+21
-7
@@ -1,3 +1,7 @@
|
||||
import type { Component } from 'svelte';
|
||||
import type { HTMLInputTypeAttribute } from 'svelte/elements';
|
||||
import 'unplugin-icons/types/svelte';
|
||||
|
||||
export interface MarkerData {
|
||||
severity: number;
|
||||
message: string;
|
||||
@@ -15,17 +19,19 @@ export interface TabEvents {
|
||||
export interface Tab {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
icon: Component;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
code: string;
|
||||
mermaid: string;
|
||||
updateDiagram: boolean;
|
||||
autoSync: boolean;
|
||||
rough: boolean;
|
||||
editorMode?: EditorMode;
|
||||
// All new options must be optional, as users would have old states saved
|
||||
renderCount?: number;
|
||||
panZoom?: boolean;
|
||||
grid?: boolean;
|
||||
editorMode?: EditorMode;
|
||||
pan?: { x: number; y: number };
|
||||
zoom?: number;
|
||||
loader?: LoaderConfig;
|
||||
@@ -33,6 +39,7 @@ export interface State {
|
||||
|
||||
export interface ValidatedState extends State {
|
||||
editorMode: EditorMode;
|
||||
diagramType?: string;
|
||||
error?: Error;
|
||||
errorMarkers: MarkerData[];
|
||||
serialized: string;
|
||||
@@ -50,10 +57,15 @@ export interface FileLoaderConfig {
|
||||
codeURL: string;
|
||||
configURL?: string;
|
||||
}
|
||||
export interface LoaderConfig {
|
||||
type: 'gist' | 'files';
|
||||
config: GistLoaderConfig | FileLoaderConfig;
|
||||
}
|
||||
export type LoaderConfig =
|
||||
| {
|
||||
type: 'gist';
|
||||
config: GistLoaderConfig;
|
||||
}
|
||||
| {
|
||||
type: 'files';
|
||||
config: FileLoaderConfig;
|
||||
};
|
||||
export type HistoryType = 'auto' | 'manual' | 'loader';
|
||||
export type HistoryEntry = { id: string; state: State; time: number; url?: string } & (
|
||||
| {
|
||||
@@ -87,3 +99,5 @@ export interface ErrorHash {
|
||||
last_column: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { get } from 'svelte/store';
|
||||
import { stateStore } from './state';
|
||||
|
||||
let shouldSync = true;
|
||||
let updater: () => void;
|
||||
@@ -16,10 +14,6 @@ const debouncedRender = debounce(() => {
|
||||
|
||||
export const recordRenderTime = (renderTimeMs: number, updaterFunction: () => void): void => {
|
||||
resolveRenderPromise?.();
|
||||
const { autoSync } = get(stateStore);
|
||||
if (!autoSync) {
|
||||
return;
|
||||
}
|
||||
updater = updaterFunction;
|
||||
const isSlow = renderTimeMs > slowRenderThreshold;
|
||||
if (!shouldSync) {
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
export const env = {
|
||||
rendererUrl: import.meta.env.MERMAID_RENDERER_URL ?? '',
|
||||
krokiRendererUrl: import.meta.env.MERMAID_KROKI_RENDERER_URL ?? '',
|
||||
analyticsUrl: import.meta.env.MERMAID_ANALYTICS_URL ?? '',
|
||||
domain: import.meta.env.MERMAID_DOMAIN ?? '',
|
||||
isEnabledMermaidChartLinks: import.meta.env.MERMAID_IS_ENABLED_MERMAID_CHART_LINKS == 'true'
|
||||
isEnabledMermaidChartLinks: import.meta.env.MERMAID_IS_ENABLED_MERMAID_CHART_LINKS === 'true',
|
||||
krokiRendererUrl: import.meta.env.MERMAID_KROKI_RENDERER_URL ?? '',
|
||||
rendererUrl: import.meta.env.MERMAID_RENDERER_URL ?? ''
|
||||
} as const;
|
||||
|
||||
@@ -61,12 +61,12 @@ const getGistData = async (gistURL: string): Promise<GistData> => {
|
||||
}
|
||||
const currentItem = history[0];
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
url: `${html_url}/${currentItem.version}`,
|
||||
author: currentItem.user.login,
|
||||
code,
|
||||
config,
|
||||
author: currentItem.user.login,
|
||||
time: new Date(currentItem.committed_at).getTime(),
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
url: `${html_url}/${currentItem.version}`,
|
||||
version: currentItem.version.slice(-7)
|
||||
};
|
||||
} else {
|
||||
@@ -79,10 +79,10 @@ const getStateFromGist = (gist: GistData, gistURL: string = gist.url): State =>
|
||||
...defaultState,
|
||||
code: gist.code,
|
||||
loader: {
|
||||
type: 'gist',
|
||||
config: {
|
||||
url: gistURL
|
||||
}
|
||||
},
|
||||
type: 'gist'
|
||||
}
|
||||
};
|
||||
gist.config && (state.mermaid = gist.config);
|
||||
@@ -120,11 +120,11 @@ export const loadGistData = async (gistURL: string): Promise<State> => {
|
||||
const state = getStateFromGist(entry, gistURL);
|
||||
for (const gist of gistHistory) {
|
||||
addHistoryEntry({
|
||||
name: `${gist.author} v${gist.version}`,
|
||||
state: getStateFromGist(gist),
|
||||
time: gist.time,
|
||||
type: 'loader',
|
||||
url: gist.url,
|
||||
name: `${gist.author} v${gist.version}`
|
||||
url: gist.url
|
||||
});
|
||||
}
|
||||
return state;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { loadGistData } from './gist';
|
||||
import { updateCodeStore, defaultState } from '$lib/util/state';
|
||||
import { fetchText } from '$lib/util/util';
|
||||
import type { Loader, State } from '$lib/types';
|
||||
import { defaultState, updateCodeStore } from '$lib/util/state';
|
||||
import { fetchText } from '$lib/util/util';
|
||||
import { loadGistData } from './gist';
|
||||
|
||||
const loaders: Record<string, Loader> = {
|
||||
gist: loadGistData
|
||||
@@ -27,14 +27,14 @@ export const loadDataFromUrl = async (): Promise<void> => {
|
||||
}
|
||||
state = {
|
||||
code,
|
||||
mermaid: config,
|
||||
loader: {
|
||||
type: 'files',
|
||||
config: {
|
||||
codeURL,
|
||||
configURL
|
||||
}
|
||||
}
|
||||
},
|
||||
type: 'files'
|
||||
},
|
||||
mermaid: config
|
||||
};
|
||||
} else {
|
||||
for (const [key, value] of searchParams.entries()) {
|
||||
@@ -52,7 +52,6 @@ export const loadDataFromUrl = async (): Promise<void> => {
|
||||
loaded &&
|
||||
updateCodeStore({
|
||||
...state,
|
||||
autoSync: true,
|
||||
updateDiagram: true
|
||||
});
|
||||
};
|
||||
|
||||
+19
-1
@@ -18,6 +18,24 @@ export const render = async (
|
||||
return await mermaid.render(id, code);
|
||||
};
|
||||
|
||||
export const parse = async (code: string): Promise<unknown> => {
|
||||
export const parse = async (code: string) => {
|
||||
return await mermaid.parse(code);
|
||||
};
|
||||
|
||||
export const standardizeDiagramType = (diagramType: string) => {
|
||||
switch (diagramType) {
|
||||
case 'class':
|
||||
case 'classDiagram': {
|
||||
return 'classDiagram';
|
||||
}
|
||||
case 'graph':
|
||||
case 'flowchart':
|
||||
case 'flowchart-elk':
|
||||
case 'flowchart-v2': {
|
||||
return 'flowchart';
|
||||
}
|
||||
default: {
|
||||
return diagramType;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+382
-382
@@ -20,139 +20,7 @@ export const initEditor = (monacoEditor: typeof Monaco): void => {
|
||||
keywords: string[];
|
||||
}
|
||||
> = {
|
||||
flowchart: {
|
||||
typeKeywords: ['flowchart', 'flowchart-v2', 'graph'],
|
||||
blockKeywords: ['subgraph', 'end'],
|
||||
keywords: [
|
||||
'TB',
|
||||
'TD',
|
||||
'BT',
|
||||
'RL',
|
||||
'LR',
|
||||
'click',
|
||||
'call',
|
||||
'href',
|
||||
'_self',
|
||||
'_blank',
|
||||
'_parent',
|
||||
'_top',
|
||||
'linkStyle',
|
||||
'style',
|
||||
'classDef',
|
||||
'class',
|
||||
'direction',
|
||||
'interpolate'
|
||||
]
|
||||
},
|
||||
sequenceDiagram: {
|
||||
typeKeywords: ['sequenceDiagram'],
|
||||
blockKeywords: ['alt', 'par', 'and', 'loop', 'else', 'end', 'rect', 'opt', 'alt', 'rect'],
|
||||
keywords: [
|
||||
'participant',
|
||||
'as',
|
||||
'Note',
|
||||
'note',
|
||||
'right of',
|
||||
'left of',
|
||||
'over',
|
||||
'activate',
|
||||
'deactivate',
|
||||
'autonumber',
|
||||
'title',
|
||||
'actor',
|
||||
'accDescription',
|
||||
'link',
|
||||
'links'
|
||||
]
|
||||
},
|
||||
classDiagram: {
|
||||
typeKeywords: ['classDiagram', 'classDiagram-v2'],
|
||||
blockKeywords: ['class'],
|
||||
keywords: [
|
||||
'link',
|
||||
'click',
|
||||
'callback',
|
||||
'call',
|
||||
'href',
|
||||
'cssClass',
|
||||
'direction',
|
||||
'TB',
|
||||
'BT',
|
||||
'RL',
|
||||
'LR',
|
||||
'title',
|
||||
'accDescription',
|
||||
'order'
|
||||
]
|
||||
},
|
||||
stateDiagram: {
|
||||
typeKeywords: ['stateDiagram', 'stateDiagram-v2'],
|
||||
blockKeywords: ['state', 'note', 'end'],
|
||||
keywords: ['state', 'as', 'hide empty description', 'direction', 'TB', 'BT', 'RL', 'LR']
|
||||
},
|
||||
erDiagram: {
|
||||
typeKeywords: ['erDiagram'],
|
||||
blockKeywords: [],
|
||||
keywords: ['title', 'accDescription']
|
||||
},
|
||||
journey: {
|
||||
typeKeywords: ['journey'],
|
||||
blockKeywords: ['section'],
|
||||
keywords: ['title']
|
||||
},
|
||||
info: {
|
||||
typeKeywords: ['info'],
|
||||
blockKeywords: [],
|
||||
keywords: ['showInfo']
|
||||
},
|
||||
gantt: {
|
||||
typeKeywords: ['gantt'],
|
||||
blockKeywords: [],
|
||||
keywords: [
|
||||
'title',
|
||||
'dateFormat',
|
||||
'axisFormat',
|
||||
'todayMarker',
|
||||
'section',
|
||||
'excludes',
|
||||
'inclusiveEndDates'
|
||||
]
|
||||
},
|
||||
requirementDiagram: {
|
||||
typeKeywords: ['requirement', 'requirementDiagram'],
|
||||
blockKeywords: [...requirementDiagrams, 'element'],
|
||||
keywords: []
|
||||
},
|
||||
gitGraph: {
|
||||
typeKeywords: ['gitGraph'],
|
||||
blockKeywords: [],
|
||||
keywords: [
|
||||
'accTitle',
|
||||
'accDescr',
|
||||
'commit',
|
||||
'cherry-pick',
|
||||
'branch',
|
||||
'merge',
|
||||
'reset',
|
||||
'checkout',
|
||||
'LR',
|
||||
'BT',
|
||||
'id',
|
||||
'msg',
|
||||
'type',
|
||||
'tag',
|
||||
'NORMAL',
|
||||
'REVERSE',
|
||||
'HIGHLIGHT'
|
||||
]
|
||||
},
|
||||
pie: {
|
||||
typeKeywords: ['pie'],
|
||||
blockKeywords: [],
|
||||
keywords: ['showData', 'title', 'accDescr', 'accTitle']
|
||||
},
|
||||
c4Diagram: {
|
||||
typeKeywords: ['C4Context', 'C4Container', 'C4Component', 'C4Dynamic', 'C4Deployment'],
|
||||
blockKeywords: [
|
||||
'Boundary',
|
||||
'Enterprise_Boundary',
|
||||
@@ -203,21 +71,153 @@ export const initEditor = (monacoEditor: typeof Monaco): void => {
|
||||
'Rel_R',
|
||||
'Rel_Back',
|
||||
'RelIndex'
|
||||
]
|
||||
],
|
||||
typeKeywords: ['C4Context', 'C4Container', 'C4Component', 'C4Dynamic', 'C4Deployment']
|
||||
},
|
||||
classDiagram: {
|
||||
blockKeywords: ['class'],
|
||||
keywords: [
|
||||
'link',
|
||||
'click',
|
||||
'callback',
|
||||
'call',
|
||||
'href',
|
||||
'cssClass',
|
||||
'direction',
|
||||
'TB',
|
||||
'BT',
|
||||
'RL',
|
||||
'LR',
|
||||
'title',
|
||||
'accDescription',
|
||||
'order'
|
||||
],
|
||||
typeKeywords: ['classDiagram', 'classDiagram-v2']
|
||||
},
|
||||
erDiagram: {
|
||||
blockKeywords: [],
|
||||
keywords: ['title', 'accDescription'],
|
||||
typeKeywords: ['erDiagram']
|
||||
},
|
||||
flowchart: {
|
||||
blockKeywords: ['subgraph', 'end'],
|
||||
keywords: [
|
||||
'TB',
|
||||
'TD',
|
||||
'BT',
|
||||
'RL',
|
||||
'LR',
|
||||
'click',
|
||||
'call',
|
||||
'href',
|
||||
'_self',
|
||||
'_blank',
|
||||
'_parent',
|
||||
'_top',
|
||||
'linkStyle',
|
||||
'style',
|
||||
'classDef',
|
||||
'class',
|
||||
'direction',
|
||||
'interpolate'
|
||||
],
|
||||
typeKeywords: ['flowchart', 'flowchart-v2', 'graph']
|
||||
},
|
||||
gantt: {
|
||||
blockKeywords: [],
|
||||
keywords: [
|
||||
'title',
|
||||
'dateFormat',
|
||||
'axisFormat',
|
||||
'todayMarker',
|
||||
'section',
|
||||
'excludes',
|
||||
'inclusiveEndDates'
|
||||
],
|
||||
typeKeywords: ['gantt']
|
||||
},
|
||||
gitGraph: {
|
||||
blockKeywords: [],
|
||||
keywords: [
|
||||
'accTitle',
|
||||
'accDescr',
|
||||
'commit',
|
||||
'cherry-pick',
|
||||
'branch',
|
||||
'merge',
|
||||
'reset',
|
||||
'checkout',
|
||||
'LR',
|
||||
'BT',
|
||||
'id',
|
||||
'msg',
|
||||
'type',
|
||||
'tag',
|
||||
'NORMAL',
|
||||
'REVERSE',
|
||||
'HIGHLIGHT'
|
||||
],
|
||||
typeKeywords: ['gitGraph']
|
||||
},
|
||||
info: {
|
||||
blockKeywords: [],
|
||||
keywords: ['showInfo'],
|
||||
typeKeywords: ['info']
|
||||
},
|
||||
journey: {
|
||||
blockKeywords: ['section'],
|
||||
keywords: ['title'],
|
||||
typeKeywords: ['journey']
|
||||
},
|
||||
pie: {
|
||||
blockKeywords: [],
|
||||
keywords: ['showData', 'title', 'accDescr', 'accTitle'],
|
||||
typeKeywords: ['pie']
|
||||
},
|
||||
requirementDiagram: {
|
||||
blockKeywords: [...requirementDiagrams, 'element'],
|
||||
keywords: [],
|
||||
typeKeywords: ['requirement', 'requirementDiagram']
|
||||
},
|
||||
sankey: {
|
||||
typeKeywords: ['sankey-beta'],
|
||||
blockKeywords: [],
|
||||
keywords: []
|
||||
keywords: [],
|
||||
typeKeywords: ['sankey-beta']
|
||||
},
|
||||
sequenceDiagram: {
|
||||
blockKeywords: ['alt', 'par', 'and', 'loop', 'else', 'end', 'rect', 'opt', 'alt', 'rect'],
|
||||
keywords: [
|
||||
'participant',
|
||||
'as',
|
||||
'Note',
|
||||
'note',
|
||||
'right of',
|
||||
'left of',
|
||||
'over',
|
||||
'activate',
|
||||
'deactivate',
|
||||
'autonumber',
|
||||
'title',
|
||||
'actor',
|
||||
'accDescription',
|
||||
'link',
|
||||
'links'
|
||||
],
|
||||
typeKeywords: ['sequenceDiagram']
|
||||
},
|
||||
stateDiagram: {
|
||||
blockKeywords: ['state', 'note', 'end'],
|
||||
keywords: ['state', 'as', 'hide empty description', 'direction', 'TB', 'BT', 'RL', 'LR'],
|
||||
typeKeywords: ['stateDiagram', 'stateDiagram-v2']
|
||||
}
|
||||
};
|
||||
|
||||
const configDirectiveHandler = [
|
||||
/^\s*%%(?={)/,
|
||||
{
|
||||
token: 'string',
|
||||
next: '@configDirective',
|
||||
nextEmbedded: 'javascript'
|
||||
nextEmbedded: 'javascript',
|
||||
token: 'string'
|
||||
}
|
||||
] as Monaco.languages.IShortMonarchLanguageRule1;
|
||||
|
||||
@@ -241,157 +241,26 @@ export const initEditor = (monacoEditor: typeof Monaco): void => {
|
||||
{}
|
||||
),
|
||||
tokenizer: {
|
||||
root: [
|
||||
[/^\s*gitGraph/m, 'typeKeyword', 'gitGraph'],
|
||||
[/^\s*info/m, 'typeKeyword', 'info'],
|
||||
[/^\s*pie/m, 'typeKeyword', 'pie'],
|
||||
[/^\s*(flowchart|flowchart-v2|graph)/m, 'typeKeyword', 'flowchart'],
|
||||
[/^\s*sequenceDiagram/, 'typeKeyword', 'sequenceDiagram'],
|
||||
[/^\s*classDiagram(-v2)?/, 'typeKeyword', 'classDiagram'],
|
||||
[/^\s*journey/, 'typeKeyword', 'journey'],
|
||||
[/^\s*gantt/, 'typeKeyword', 'gantt'],
|
||||
[/^\s*stateDiagram(-v2)?/, 'typeKeyword', 'stateDiagram'],
|
||||
[/^\s*er(Diagram)?/, 'typeKeyword', 'erDiagram'],
|
||||
[/^\s*requirement(Diagram)?/, 'typeKeyword', 'requirementDiagram'],
|
||||
[/^\s*sankey-beta/m, 'typeKeyword', 'sankey'],
|
||||
[
|
||||
/^\s*(C4Context|C4Container|C4Component|C4Dynamic|C4Deployment)/m,
|
||||
'typeKeyword',
|
||||
'c4Diagram'
|
||||
],
|
||||
configDirectiveHandler,
|
||||
[/%%[^${].*$/, 'comment']
|
||||
],
|
||||
configDirective: [[/%%$/, { token: 'string', next: '@pop', nextEmbedded: '@pop' }]],
|
||||
gitGraph: [
|
||||
configDirectiveHandler,
|
||||
[/option(?=s)/, { token: 'typeKeyword', next: 'optionsGitGraph' }],
|
||||
[/(accTitle|accDescr)(\s*:)(\s*[^\n\r]+$)/, ['keyword', 'delimiter.bracket', 'string']],
|
||||
[
|
||||
/(^\s*branch)(.*?)(\s+order)(:\s*)(\d+\s*$)/,
|
||||
['keyword', 'variable', 'keyword', 'delimiter.bracket', 'number']
|
||||
],
|
||||
[/".*?"/, 'string'],
|
||||
[
|
||||
/(^\s*)(branch|reset|merge|checkout)(\s*\S+)/m,
|
||||
['delimiter.bracket', 'keyword', 'variable']
|
||||
],
|
||||
[
|
||||
/[A-Za-z][\w$]*/,
|
||||
{
|
||||
cases: {
|
||||
'@gitGraphBlockKeywords': 'typeKeyword',
|
||||
'@gitGraphKeywords': 'keyword'
|
||||
}
|
||||
}
|
||||
],
|
||||
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'],
|
||||
[/\^/, 'delimiter.bracket']
|
||||
],
|
||||
optionsGitGraph: [
|
||||
[
|
||||
/s$/,
|
||||
{
|
||||
token: 'typeKeyword',
|
||||
nextEmbedded: 'json'
|
||||
}
|
||||
],
|
||||
['end', { token: 'typeKeyword', next: '@pop', nextEmbedded: '@pop' }]
|
||||
],
|
||||
info: [
|
||||
[
|
||||
/[A-Za-z][\w$]*/,
|
||||
{
|
||||
cases: {
|
||||
'@infoBlockKeywords': 'typeKeyword',
|
||||
'@infoKeywords': 'keyword'
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
pie: [
|
||||
c4Diagram: [
|
||||
configDirectiveHandler,
|
||||
[/(title|accDescription)(.*$)/, ['keyword', 'string']],
|
||||
[/\(/, { next: 'c4DiagramParenthesis', token: 'delimiter.bracket' }],
|
||||
[
|
||||
/[A-Za-z][\w$]*/,
|
||||
/[A-Z_a-z-][\w$]*/,
|
||||
{
|
||||
cases: {
|
||||
'@pieBlockKeywords': 'typeKeyword',
|
||||
'@pieKeywords': 'keyword'
|
||||
}
|
||||
}
|
||||
],
|
||||
[/".*?"/, 'string'],
|
||||
[/\s*\d+/, 'number'],
|
||||
[/:/, 'delimiter.bracket'],
|
||||
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment']
|
||||
],
|
||||
flowchart: [
|
||||
configDirectiveHandler,
|
||||
[/[ox]?(--+|==+)[ox]/, 'transition'],
|
||||
[
|
||||
/[A-Za-z][\w$]*/,
|
||||
{
|
||||
cases: {
|
||||
'@flowchartBlockKeywords': 'typeKeyword',
|
||||
'@flowchartKeywords': 'keyword',
|
||||
'@c4DiagramBlockKeywords': 'typeKeyword',
|
||||
'@c4DiagramKeywords': 'keyword',
|
||||
'@default': 'variable'
|
||||
}
|
||||
}
|
||||
],
|
||||
[/\|+.+?\|+/, 'string'],
|
||||
[/\[+(\\.+?[/\\]|\/.+?[/\\])]+/, 'string'],
|
||||
[/[>[]+[^[\]|]+?]+/, 'string'],
|
||||
[/{+.+?}+/, 'string'],
|
||||
[/\(+.+?\)+/, 'string'],
|
||||
[/-\.+->?/, 'transition'],
|
||||
[/(-[.-])([^>-][^-]+?)(-{3,}|-{2,}>|\.-+>)/, ['transition', 'string', 'transition']],
|
||||
[/(==+)([^=]+?)(={3,}|={2,}>)/, ['transition', 'string', 'transition']],
|
||||
[/<?(--+|==+)>|===+|---+/, 'transition'],
|
||||
[/:::/, 'transition'],
|
||||
[/[&;]/, 'delimiter.bracket'],
|
||||
[/".*?"/, 'string'],
|
||||
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment']
|
||||
],
|
||||
sequenceDiagram: [
|
||||
configDirectiveHandler,
|
||||
[/(title:?|accDescription)([^\n\r;]*$)/, ['keyword', 'string']],
|
||||
[/(autonumber)([^\S\n\r]+off[^\S\n\r]*$)/, ['keyword', 'keyword']],
|
||||
[/(autonumber)((?:[^\S\n\r]+\d+){2}[^\S\n\r]*$)/, ['keyword', 'number']],
|
||||
[/(autonumber)([^\S\n\r]+\d+[^\S\n\r]*$)/, ['keyword', 'number']],
|
||||
[
|
||||
/(link\s+)(.*?)(:)(\s*.*?)(\s*@)(\s*[^\n\r;]+)/,
|
||||
['keyword', 'variable', 'delimiter.bracket', 'string', 'delimiter.bracket', 'string']
|
||||
],
|
||||
[
|
||||
/((?:links|properties)\s+)([^\n\r:]*?)(:\s+)/,
|
||||
[
|
||||
{ token: 'keyword' },
|
||||
{ token: 'variable' },
|
||||
{
|
||||
token: 'delimiter.bracket',
|
||||
nextEmbedded: 'javascript',
|
||||
next: '@sequenceDiagramLinksProps'
|
||||
}
|
||||
]
|
||||
],
|
||||
[
|
||||
/[A-Za-z][\w$]*/,
|
||||
{
|
||||
cases: {
|
||||
'@sequenceDiagramBlockKeywords': 'typeKeyword',
|
||||
'@sequenceDiagramKeywords': 'keyword',
|
||||
'@default': 'variable'
|
||||
}
|
||||
}
|
||||
],
|
||||
[/(--?>?>|--?[)x])[+-]?/, 'transition'],
|
||||
[/(:)([^\n:]*?$)/, ['delimiter.bracket', 'string']],
|
||||
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment']
|
||||
],
|
||||
sequenceDiagramLinksProps: [
|
||||
// [/^:/, { token: 'delimiter.bracket', nextEmbedded: 'json' }],
|
||||
[/$|;/, { nextEmbedded: '@pop', next: '@pop', token: 'delimiter.bracket' }]
|
||||
c4DiagramParenthesis: [
|
||||
[/,/, 'delimiter.bracket'],
|
||||
[/\)/, { next: '@pop', token: 'delimiter.bracket' }],
|
||||
[/[^),]/, 'string']
|
||||
],
|
||||
classDiagram: [
|
||||
configDirectiveHandler,
|
||||
@@ -419,33 +288,43 @@ export const initEditor = (monacoEditor: typeof Monaco): void => {
|
||||
[/:::/, 'transition'],
|
||||
[/:|\+|-|#|~|\*\s*$|\$\s*$|\(|\)|{|}/, 'delimiter.bracket']
|
||||
],
|
||||
journey: [
|
||||
configDirective: [[/%%$/, { next: '@pop', nextEmbedded: '@pop', token: 'string' }]],
|
||||
erDiagram: [
|
||||
configDirectiveHandler,
|
||||
[/(title)(.*)/, ['keyword', 'string']],
|
||||
[/(section)(.*)/, ['typeKeyword', 'string']],
|
||||
[/(title|accDescription)(.*$)/, ['keyword', 'string']],
|
||||
[/[|}][o|](--|\.\.)[o|][{|]/, 'transition'],
|
||||
[/".*?"/, 'string'],
|
||||
[/(:)(.*?$)/, ['delimiter.bracket', 'string']],
|
||||
[/[:{}]/, 'delimiter.bracket'],
|
||||
[/([A-Za-z]+)(\s+[A-Za-z]+)/, ['type', 'variable']],
|
||||
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'],
|
||||
[/[A-Z_a-z-][\w$]*/, 'variable']
|
||||
],
|
||||
flowchart: [
|
||||
configDirectiveHandler,
|
||||
[/[ox]?(--+|==+)[ox]/, 'transition'],
|
||||
[
|
||||
/[A-Za-z][\w$]*/,
|
||||
{
|
||||
cases: {
|
||||
'@journeyBlockKeywords': 'typeKeyword',
|
||||
'@journeyKeywords': 'keyword',
|
||||
'@default': 'variable'
|
||||
'@default': 'variable',
|
||||
'@flowchartBlockKeywords': 'typeKeyword',
|
||||
'@flowchartKeywords': 'keyword'
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
/(^\s*.+?)(:)(.*?)(:)(.*?)([$,])/,
|
||||
[
|
||||
'string',
|
||||
'delimiter.bracket',
|
||||
'number',
|
||||
'delimiter.bracket',
|
||||
'variable',
|
||||
'delimiter.bracket'
|
||||
]
|
||||
],
|
||||
[/,/, 'delimiter.bracket'],
|
||||
[/(^\s*.+?)(:)([^:]*?)$/, ['string', 'delimiter.bracket', 'variable']],
|
||||
[/\|+.+?\|+/, 'string'],
|
||||
[/\[+(\\.+?[/\\]|\/.+?[/\\])]+/, 'string'],
|
||||
[/[>[]+[^[\]|]+?]+/, 'string'],
|
||||
[/{+.+?}+/, 'string'],
|
||||
[/\(+.+?\)+/, 'string'],
|
||||
[/-\.+->?/, 'transition'],
|
||||
[/(-[.-])([^>-][^-]+?)(-{3,}|-{2,}>|\.-+>)/, ['transition', 'string', 'transition']],
|
||||
[/(==+)([^=]+?)(={3,}|={2,}>)/, ['transition', 'string', 'transition']],
|
||||
[/<?(--+|==+)>|===+|---+/, 'transition'],
|
||||
[/:::/, 'transition'],
|
||||
[/[&;]/, 'delimiter.bracket'],
|
||||
[/".*?"/, 'string'],
|
||||
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment']
|
||||
],
|
||||
gantt: [
|
||||
@@ -465,9 +344,189 @@ export const initEditor = (monacoEditor: typeof Monaco): void => {
|
||||
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'],
|
||||
[/:/, 'delimiter.bracket']
|
||||
],
|
||||
gitGraph: [
|
||||
configDirectiveHandler,
|
||||
[/option(?=s)/, { next: 'optionsGitGraph', token: 'typeKeyword' }],
|
||||
[/(accTitle|accDescr)(\s*:)(\s*[^\n\r]+$)/, ['keyword', 'delimiter.bracket', 'string']],
|
||||
[
|
||||
/(^\s*branch)(.*?)(\s+order)(:\s*)(\d+\s*$)/,
|
||||
['keyword', 'variable', 'keyword', 'delimiter.bracket', 'number']
|
||||
],
|
||||
[/".*?"/, 'string'],
|
||||
[
|
||||
/(^\s*)(branch|reset|merge|checkout)(\s*\S+)/m,
|
||||
['delimiter.bracket', 'keyword', 'variable']
|
||||
],
|
||||
[
|
||||
/[A-Za-z][\w$]*/,
|
||||
{
|
||||
cases: {
|
||||
'@gitGraphBlockKeywords': 'typeKeyword',
|
||||
'@gitGraphKeywords': 'keyword'
|
||||
}
|
||||
}
|
||||
],
|
||||
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'],
|
||||
[/\^/, 'delimiter.bracket']
|
||||
],
|
||||
info: [
|
||||
[
|
||||
/[A-Za-z][\w$]*/,
|
||||
{
|
||||
cases: {
|
||||
'@infoBlockKeywords': 'typeKeyword',
|
||||
'@infoKeywords': 'keyword'
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
journey: [
|
||||
configDirectiveHandler,
|
||||
[/(title)(.*)/, ['keyword', 'string']],
|
||||
[/(section)(.*)/, ['typeKeyword', 'string']],
|
||||
[
|
||||
/[A-Za-z][\w$]*/,
|
||||
{
|
||||
cases: {
|
||||
'@default': 'variable',
|
||||
'@journeyBlockKeywords': 'typeKeyword',
|
||||
'@journeyKeywords': 'keyword'
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
/(^\s*.+?)(:)(.*?)(:)(.*?)([$,])/,
|
||||
[
|
||||
'string',
|
||||
'delimiter.bracket',
|
||||
'number',
|
||||
'delimiter.bracket',
|
||||
'variable',
|
||||
'delimiter.bracket'
|
||||
]
|
||||
],
|
||||
[/,/, 'delimiter.bracket'],
|
||||
[/(^\s*.+?)(:)([^:]*?)$/, ['string', 'delimiter.bracket', 'variable']],
|
||||
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment']
|
||||
],
|
||||
optionsGitGraph: [
|
||||
[
|
||||
/s$/,
|
||||
{
|
||||
nextEmbedded: 'json',
|
||||
token: 'typeKeyword'
|
||||
}
|
||||
],
|
||||
['end', { next: '@pop', nextEmbedded: '@pop', token: 'typeKeyword' }]
|
||||
],
|
||||
pie: [
|
||||
configDirectiveHandler,
|
||||
[/(title|accDescription)(.*$)/, ['keyword', 'string']],
|
||||
[
|
||||
/[A-Za-z][\w$]*/,
|
||||
{
|
||||
cases: {
|
||||
'@pieBlockKeywords': 'typeKeyword',
|
||||
'@pieKeywords': 'keyword'
|
||||
}
|
||||
}
|
||||
],
|
||||
[/".*?"/, 'string'],
|
||||
[/\s*\d+/, 'number'],
|
||||
[/:/, 'delimiter.bracket'],
|
||||
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment']
|
||||
],
|
||||
requirementDiagram: [
|
||||
configDirectiveHandler,
|
||||
[/->|<-|-/, 'transition'],
|
||||
[/(\d+\.)*\d+/, 'number'],
|
||||
[
|
||||
/[A-Z_a-z-][\w$]*/,
|
||||
{
|
||||
cases: {
|
||||
'@default': 'variable',
|
||||
'@requirementDiagramBlockKeywords': 'typeKeyword'
|
||||
}
|
||||
}
|
||||
],
|
||||
[/[/:{}]/, 'delimiter.bracket'],
|
||||
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'],
|
||||
[/".*?"/, 'string']
|
||||
],
|
||||
root: [
|
||||
[/^\s*gitGraph/m, 'typeKeyword', 'gitGraph'],
|
||||
[/^\s*info/m, 'typeKeyword', 'info'],
|
||||
[/^\s*pie/m, 'typeKeyword', 'pie'],
|
||||
[/^\s*(flowchart|flowchart-v2|graph)/m, 'typeKeyword', 'flowchart'],
|
||||
[/^\s*sequenceDiagram/, 'typeKeyword', 'sequenceDiagram'],
|
||||
[/^\s*classDiagram(-v2)?/, 'typeKeyword', 'classDiagram'],
|
||||
[/^\s*journey/, 'typeKeyword', 'journey'],
|
||||
[/^\s*gantt/, 'typeKeyword', 'gantt'],
|
||||
[/^\s*stateDiagram(-v2)?/, 'typeKeyword', 'stateDiagram'],
|
||||
[/^\s*er(Diagram)?/, 'typeKeyword', 'erDiagram'],
|
||||
[/^\s*requirement(Diagram)?/, 'typeKeyword', 'requirementDiagram'],
|
||||
[/^\s*sankey-beta/m, 'typeKeyword', 'sankey'],
|
||||
[
|
||||
/^\s*(C4Context|C4Container|C4Component|C4Dynamic|C4Deployment)/m,
|
||||
'typeKeyword',
|
||||
'c4Diagram'
|
||||
],
|
||||
configDirectiveHandler,
|
||||
[/%%[^${].*$/, 'comment']
|
||||
],
|
||||
sankey: [
|
||||
configDirectiveHandler,
|
||||
[/(title)(.*)/, ['keyword', 'string']],
|
||||
[/(accTitle|accDescr)(\s*:)(\s*[^\n\r]+$)/, ['keyword', 'delimiter.bracket', 'string']],
|
||||
[/".*?"/, 'string'],
|
||||
[/[A-Za-z]+/, 'string'],
|
||||
[/\s*\d+/, 'number'],
|
||||
[/,/, 'delimiter.bracket'],
|
||||
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment']
|
||||
],
|
||||
sequenceDiagram: [
|
||||
configDirectiveHandler,
|
||||
[/(title:?|accDescription)([^\n\r;]*$)/, ['keyword', 'string']],
|
||||
[/(autonumber)([^\S\n\r]+off[^\S\n\r]*$)/, ['keyword', 'keyword']],
|
||||
[/(autonumber)((?:[^\S\n\r]+\d+){2}[^\S\n\r]*$)/, ['keyword', 'number']],
|
||||
[/(autonumber)([^\S\n\r]+\d+[^\S\n\r]*$)/, ['keyword', 'number']],
|
||||
[
|
||||
/(link\s+)(.*?)(:)(\s*.*?)(\s*@)(\s*[^\n\r;]+)/,
|
||||
['keyword', 'variable', 'delimiter.bracket', 'string', 'delimiter.bracket', 'string']
|
||||
],
|
||||
[
|
||||
/((?:links|properties)\s+)([^\n\r:]*?)(:\s+)/,
|
||||
[
|
||||
{ token: 'keyword' },
|
||||
{ token: 'variable' },
|
||||
{
|
||||
next: '@sequenceDiagramLinksProps',
|
||||
nextEmbedded: 'javascript',
|
||||
token: 'delimiter.bracket'
|
||||
}
|
||||
]
|
||||
],
|
||||
[
|
||||
/[A-Za-z][\w$]*/,
|
||||
{
|
||||
cases: {
|
||||
'@default': 'variable',
|
||||
'@sequenceDiagramBlockKeywords': 'typeKeyword',
|
||||
'@sequenceDiagramKeywords': 'keyword'
|
||||
}
|
||||
}
|
||||
],
|
||||
[/(--?>?>|--?[)x])[+-]?/, 'transition'],
|
||||
[/(:)([^\n:]*?$)/, ['delimiter.bracket', 'string']],
|
||||
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment']
|
||||
],
|
||||
sequenceDiagramLinksProps: [
|
||||
// [/^:/, { token: 'delimiter.bracket', nextEmbedded: 'json' }],
|
||||
[/$|;/, { next: '@pop', nextEmbedded: '@pop', token: 'delimiter.bracket' }]
|
||||
],
|
||||
stateDiagram: [
|
||||
configDirectiveHandler,
|
||||
[/note[^:]*$/, { token: 'typeKeyword', next: 'stateDiagramNote' }],
|
||||
[/note[^:]*$/, { next: 'stateDiagramNote', token: 'typeKeyword' }],
|
||||
['hide empty description', 'keyword'],
|
||||
[/^\s*state\s(?!.*{)/, 'keyword'],
|
||||
[/(<<)(fork|join|choice)(>>)/, 'annotation'],
|
||||
@@ -476,9 +535,9 @@ export const initEditor = (monacoEditor: typeof Monaco): void => {
|
||||
/[A-Za-z][\w$]*/,
|
||||
{
|
||||
cases: {
|
||||
'@default': 'variable',
|
||||
'@stateDiagramBlockKeywords': 'typeKeyword',
|
||||
'@stateDiagramKeywords': 'keyword',
|
||||
'@default': 'variable'
|
||||
'@stateDiagramKeywords': 'keyword'
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -490,99 +549,40 @@ export const initEditor = (monacoEditor: typeof Monaco): void => {
|
||||
[/\[.*?]/, 'string']
|
||||
],
|
||||
stateDiagramNote: [
|
||||
[/^\s*end note$/, { token: 'typeKeyword', next: '@pop' }],
|
||||
[/^\s*end note$/, { next: '@pop', token: 'typeKeyword' }],
|
||||
[/.*/, 'string']
|
||||
],
|
||||
erDiagram: [
|
||||
configDirectiveHandler,
|
||||
[/(title|accDescription)(.*$)/, ['keyword', 'string']],
|
||||
[/[|}][o|](--|\.\.)[o|][{|]/, 'transition'],
|
||||
[/".*?"/, 'string'],
|
||||
[/(:)(.*?$)/, ['delimiter.bracket', 'string']],
|
||||
[/[:{}]/, 'delimiter.bracket'],
|
||||
[/([A-Za-z]+)(\s+[A-Za-z]+)/, ['type', 'variable']],
|
||||
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'],
|
||||
[/[A-Z_a-z-][\w$]*/, 'variable']
|
||||
],
|
||||
requirementDiagram: [
|
||||
configDirectiveHandler,
|
||||
[/->|<-|-/, 'transition'],
|
||||
[/(\d+\.)*\d+/, 'number'],
|
||||
[
|
||||
/[A-Z_a-z-][\w$]*/,
|
||||
{
|
||||
cases: {
|
||||
'@requirementDiagramBlockKeywords': 'typeKeyword',
|
||||
'@default': 'variable'
|
||||
}
|
||||
}
|
||||
],
|
||||
[/[/:{}]/, 'delimiter.bracket'],
|
||||
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'],
|
||||
[/".*?"/, 'string']
|
||||
],
|
||||
c4Diagram: [
|
||||
configDirectiveHandler,
|
||||
[/(title|accDescription)(.*$)/, ['keyword', 'string']],
|
||||
[/\(/, { token: 'delimiter.bracket', next: 'c4DiagramParenthesis' }],
|
||||
[
|
||||
/[A-Z_a-z-][\w$]*/,
|
||||
{
|
||||
cases: {
|
||||
'@c4DiagramBlockKeywords': 'typeKeyword',
|
||||
'@c4DiagramKeywords': 'keyword',
|
||||
'@default': 'variable'
|
||||
}
|
||||
}
|
||||
],
|
||||
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment']
|
||||
],
|
||||
c4DiagramParenthesis: [
|
||||
[/,/, 'delimiter.bracket'],
|
||||
[/\)/, { next: '@pop', token: 'delimiter.bracket' }],
|
||||
[/[^),]/, 'string']
|
||||
],
|
||||
sankey: [
|
||||
configDirectiveHandler,
|
||||
[/(title)(.*)/, ['keyword', 'string']],
|
||||
[/(accTitle|accDescr)(\s*:)(\s*[^\n\r]+$)/, ['keyword', 'delimiter.bracket', 'string']],
|
||||
[/".*?"/, 'string'],
|
||||
[/[A-Za-z]+/, 'string'],
|
||||
[/\s*\d+/, 'number'],
|
||||
[/,/, 'delimiter.bracket'],
|
||||
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment']
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
monacoEditor.editor.defineTheme('mermaid-dark', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
colors: {},
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: 'typeKeyword', foreground: '9650c8', fontStyle: 'bold' },
|
||||
{ token: 'transition', foreground: '008800', fontStyle: 'bold' },
|
||||
{ token: 'identifier', foreground: '9cdcfe' }
|
||||
{ fontStyle: 'bold', foreground: '9650c8', token: 'typeKeyword' },
|
||||
{ fontStyle: 'bold', foreground: '008800', token: 'transition' },
|
||||
{ foreground: '9cdcfe', token: 'identifier' }
|
||||
]
|
||||
});
|
||||
|
||||
monacoEditor.editor.defineTheme('mermaid', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
colors: {},
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: 'typeKeyword', foreground: '9650c8', fontStyle: 'bold' },
|
||||
{ token: 'keyword', foreground: '649696' },
|
||||
{ token: 'custom-error', foreground: 'ff0000', fontStyle: 'bold' },
|
||||
{ token: 'string', foreground: 'AA8500' },
|
||||
{ token: 'transition', foreground: '008800', fontStyle: 'bold' },
|
||||
{ token: 'delimiter.bracket', foreground: '000000', fontStyle: 'bold' },
|
||||
{ token: 'annotation', foreground: '4b4b96' },
|
||||
{ token: 'number', foreground: '4b4b96' },
|
||||
{ token: 'comment', foreground: '888c89' },
|
||||
{ token: 'variable', foreground: 'A22889' },
|
||||
{ token: 'type', foreground: '2BDEA8' },
|
||||
{ token: 'identifier', foreground: '9cdcfe' }
|
||||
{ fontStyle: 'bold', foreground: '9650c8', token: 'typeKeyword' },
|
||||
{ foreground: '649696', token: 'keyword' },
|
||||
{ fontStyle: 'bold', foreground: 'ff0000', token: 'custom-error' },
|
||||
{ foreground: 'AA8500', token: 'string' },
|
||||
{ fontStyle: 'bold', foreground: '008800', token: 'transition' },
|
||||
{ fontStyle: 'bold', foreground: '000000', token: 'delimiter.bracket' },
|
||||
{ foreground: '4b4b96', token: 'annotation' },
|
||||
{ foreground: '4b4b96', token: 'number' },
|
||||
{ foreground: '888c89', token: 'comment' },
|
||||
{ foreground: 'A22889', token: 'variable' },
|
||||
{ foreground: '2BDEA8', token: 'type' },
|
||||
{ foreground: '9cdcfe', token: 'identifier' }
|
||||
]
|
||||
});
|
||||
|
||||
@@ -597,16 +597,16 @@ export const initEditor = (monacoEditor: typeof Monaco): void => {
|
||||
monacoEditor.languages.setLanguageConfiguration('mermaid', {
|
||||
autoClosingPairs: [
|
||||
{
|
||||
open: '(',
|
||||
close: ')'
|
||||
close: ')',
|
||||
open: '('
|
||||
},
|
||||
{
|
||||
open: '{',
|
||||
close: '}'
|
||||
close: '}',
|
||||
open: '{'
|
||||
},
|
||||
{
|
||||
open: '[',
|
||||
close: ']'
|
||||
close: ']',
|
||||
open: '['
|
||||
}
|
||||
],
|
||||
brackets: [
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
export const notify = (message: string): void => {
|
||||
alert(message);
|
||||
toast(message);
|
||||
};
|
||||
|
||||
export const prompt = (message: string): boolean => {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { State } from '$/types';
|
||||
import type { Point } from 'mermaid/dist/types.js';
|
||||
import panzoom from 'svg-pan-zoom';
|
||||
type PanZoom = typeof panzoom;
|
||||
|
||||
export class PanZoomState {
|
||||
private pan?: Point;
|
||||
private zoom?: number;
|
||||
private pzoom: PanZoom | undefined;
|
||||
private isDirty = false;
|
||||
private resizeObserver: ResizeObserver;
|
||||
|
||||
public isPanEnabled: boolean;
|
||||
public onPanZoomChange?: (pan: Point, zoom: number) => void;
|
||||
|
||||
constructor() {
|
||||
this.isPanEnabled = true;
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.resize();
|
||||
if (!this.isDirty) {
|
||||
this.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public updateElement(diagramView: SVGElement, { pan, zoom }: Pick<State, 'pan' | 'zoom'>) {
|
||||
this.pzoom = panzoom(diagramView, {
|
||||
center: true,
|
||||
controlIconsEnabled: false,
|
||||
fit: true,
|
||||
maxZoom: 12,
|
||||
minZoom: 0.2,
|
||||
onPan: (pan) => {
|
||||
this.pan = pan;
|
||||
this.zoom = this.pzoom?.getZoom();
|
||||
this.isDirty = true;
|
||||
if (this.zoom) {
|
||||
this.onPanZoomChange?.(this.pan, this.zoom);
|
||||
}
|
||||
},
|
||||
onZoom: (zoom) => {
|
||||
this.zoom = zoom;
|
||||
this.pan = this.pzoom?.getPan();
|
||||
this.isDirty = true;
|
||||
if (this.pan) {
|
||||
this.onPanZoomChange?.(this.pan, this.zoom);
|
||||
}
|
||||
},
|
||||
panEnabled: true,
|
||||
zoomEnabled: true
|
||||
});
|
||||
|
||||
this.pzoom.disableDblClickZoom();
|
||||
|
||||
this.resizeObserver.disconnect();
|
||||
this.resizeObserver.observe(diagramView);
|
||||
|
||||
// TODO: Investigate why this is necessary
|
||||
if (pan !== undefined && zoom !== undefined && Number.isFinite(zoom)) {
|
||||
this.restorePanZoom(pan, zoom);
|
||||
}
|
||||
|
||||
// we start out with both pan and zoom enabled so that the tool can auto position view refreshed
|
||||
// then set enable/disable pan based on state
|
||||
if (this.isPanEnabled) {
|
||||
this.pzoom.enablePan();
|
||||
this.pzoom.enableZoom();
|
||||
} else {
|
||||
this.pzoom.disableZoom();
|
||||
this.pzoom.disablePan();
|
||||
}
|
||||
|
||||
if (pan === undefined && zoom === undefined) {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
public restorePanZoom(pan: Point, zoom: number) {
|
||||
if (!this.pzoom) {
|
||||
console.error('PanZoomState.restorePanZoom: pzoom is not initialized');
|
||||
return;
|
||||
}
|
||||
this.pzoom.zoom(zoom);
|
||||
this.pzoom.pan(pan);
|
||||
}
|
||||
|
||||
public resize() {
|
||||
this.pzoom?.resize();
|
||||
if (!this.isDirty) {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
public zoomIn() {
|
||||
this.pzoom?.zoomIn();
|
||||
}
|
||||
|
||||
public zoomOut() {
|
||||
this.pzoom?.zoomOut();
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.pzoom?.reset();
|
||||
// Zoom out a bit to avoid overlap with the toolbar
|
||||
this.pzoom?.zoom(0.875);
|
||||
this.isDirty = false;
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,13 @@ function getBrowserStorage(
|
||||
connect();
|
||||
}
|
||||
},
|
||||
deleteValue(key: string) {
|
||||
browserStorage.removeItem(key);
|
||||
},
|
||||
getValue(key: string): any {
|
||||
const value = browserStorage.getItem(key);
|
||||
return deserialize(value);
|
||||
},
|
||||
removeListener(key: string, listener: (newValue: any) => void) {
|
||||
const index = listeners.indexOf({ key, listener });
|
||||
if (index !== -1) {
|
||||
@@ -213,13 +220,6 @@ function getBrowserStorage(
|
||||
disconnect();
|
||||
}
|
||||
},
|
||||
getValue(key: string): any {
|
||||
const value = browserStorage.getItem(key);
|
||||
return deserialize(value);
|
||||
},
|
||||
deleteValue(key: string) {
|
||||
browserStorage.removeItem(key);
|
||||
},
|
||||
setValue(key: string, value: any) {
|
||||
browserStorage.setItem(key, serialize(value));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$/components/ui/button';
|
||||
import { onDestroy, type Snippet } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
@@ -133,13 +134,10 @@
|
||||
href={currentTagline.url}
|
||||
target="_blank"
|
||||
class="col-start-1 row-start-1 flex items-center justify-center gap-4 no-underline"
|
||||
in:fade={{ delay: 750 }}
|
||||
in:fade={{ delay: 800 }}
|
||||
out:fade={{ duration: 1000 }}>
|
||||
<span class="text-sm tracking-wider">{currentTagline.label}</span>
|
||||
<button
|
||||
class="shrink-0 rounded-lg bg-[#1E1A2E] px-3 py-1.5 text-sm font-semibold tracking-wide">
|
||||
Try now
|
||||
</button>
|
||||
<span class="text-sm tracking-wider text-white">{currentTagline.label}</span>
|
||||
<Button size="sm" class="bg-background font-semibold tracking-wider">Try now</Button>
|
||||
</a>
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
@@ -13,19 +13,19 @@ const verifySerde = (state: State, serde?: SerdeType): string => {
|
||||
describe('Serde tests', () => {
|
||||
it('should serialize and deserialize with default serde', () => {
|
||||
expect(verifySerde(defaultState)).toMatchInlineSnapshot(
|
||||
`"pako:eNpVjs1qw0AMhF9F6NRC_AI-FBq7zSXQQnPz5iBs2bvE-8NaSwi2373r-NLqJM18M2jG1neMJfajv7eaosClVg7yvDeVjmYSS9MViuJtObGA9Y4fCxxfTh4m7UMwbnjd-eMGQTWfN4xBtHG3dbeqZ_7L8QJ1c6YgPlz_Ope7X-CjMd861_93dOSc-mx6KnsqWopQUXwieEDL0ZLp8vvzpigUzZYVlnntuKc0ikLl1oxSEv_zcC2WEhMfMPo0aMyd45SvFDoSrg0NkeyOrL_WfFuF"`
|
||||
`"pako:eNpVjEFug0AMRa9iedVI4QIsKiXQZhOpXWRVyMICw4zKjEdmUFQBd--QtFLrlb_e-3_GRlrGHLtBbo0hjXApaw_pDlVh1I7R0XiFLHteThzBieevBY5PJ4HRSAjW97uHf9wkKObzpjFEY_3n-kDFvf_meYGyOlOIEq5_yeUmC7xU9t2k-f_EKKfWa9VR3lHWkEJBeldwj73aFvOoE-_RsTraIs4brTEadlxjnt6WO5qGWGPt11QL5D9E3G9TZeoNpvlhTGkKLUUuLfVKP8r6DdATXyM"`
|
||||
);
|
||||
});
|
||||
|
||||
it('should serialize and deserialize with base64 serde', () => {
|
||||
expect(verifySerde(defaultState, 'base64')).toMatchInlineSnapshot(
|
||||
`"base64:eyJjb2RlIjoiZmxvd2NoYXJ0IFREXG4gICAgQVtDaHJpc3RtYXNdIC0tPnxHZXQgbW9uZXl8IEIoR28gc2hvcHBpbmcpXG4gICAgQiAtLT4gQ3tMZXQgbWUgdGhpbmt9XG4gICAgQyAtLT58T25lfCBEW0xhcHRvcF1cbiAgICBDIC0tPnxUd298IEVbaVBob25lXVxuICAgIEMgLS0-fFRocmVlfCBGW2ZhOmZhLWNhciBDYXJdXG4gICIsIm1lcm1haWQiOiJ7XG4gIFwidGhlbWVcIjogXCJkZWZhdWx0XCJcbn0iLCJhdXRvU3luYyI6dHJ1ZSwicm91Z2giOmZhbHNlLCJ1cGRhdGVEaWFncmFtIjp0cnVlfQ"`
|
||||
`"base64:eyJjb2RlIjoiZmxvd2NoYXJ0IFREXG4gICAgQVtDaHJpc3RtYXNdIC0tPnxHZXQgbW9uZXl8IEIoR28gc2hvcHBpbmcpXG4gICAgQiAtLT4gQ3tMZXQgbWUgdGhpbmt9XG4gICAgQyAtLT58T25lfCBEW0xhcHRvcF1cbiAgICBDIC0tPnxUd298IEVbaVBob25lXVxuICAgIEMgLS0-fFRocmVlfCBGW2ZhOmZhLWNhciBDYXJdXG4gICIsImdyaWQiOnRydWUsIm1lcm1haWQiOiJ7XG4gIFwidGhlbWVcIjogXCJkZWZhdWx0XCJcbn0iLCJwYW5ab29tIjp0cnVlLCJyb3VnaCI6ZmFsc2UsInVwZGF0ZURpYWdyYW0iOnRydWV9"`
|
||||
);
|
||||
});
|
||||
|
||||
it('should serialize and deserialize with pako serde', () => {
|
||||
expect(verifySerde(defaultState, 'pako')).toMatchInlineSnapshot(
|
||||
`"pako:eNpVjs1qw0AMhF9F6NRC_AI-FBq7zSXQQnPz5iBs2bvE-8NaSwi2373r-NLqJM18M2jG1neMJfajv7eaosClVg7yvDeVjmYSS9MViuJtObGA9Y4fCxxfTh4m7UMwbnjd-eMGQTWfN4xBtHG3dbeqZ_7L8QJ1c6YgPlz_Ope7X-CjMd861_93dOSc-mx6KnsqWopQUXwieEDL0ZLp8vvzpigUzZYVlnntuKc0ikLl1oxSEv_zcC2WEhMfMPo0aMyd45SvFDoSrg0NkeyOrL_WfFuF"`
|
||||
`"pako:eNpVjEFug0AMRa9iedVI4QIsKiXQZhOpXWRVyMICw4zKjEdmUFQBd--QtFLrlb_e-3_GRlrGHLtBbo0hjXApaw_pDlVh1I7R0XiFLHteThzBieevBY5PJ4HRSAjW97uHf9wkKObzpjFEY_3n-kDFvf_meYGyOlOIEq5_yeUmC7xU9t2k-f_EKKfWa9VR3lHWkEJBeldwj73aFvOoE-_RsTraIs4brTEadlxjnt6WO5qGWGPt11QL5D9E3G9TZeoNpvlhTGkKLUUuLfVKP8r6DdATXyM"`
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
+46
-21
@@ -2,6 +2,7 @@ import type { ErrorHash, MarkerData, State, ValidatedState } from '$lib/types';
|
||||
import { debounce } from 'lodash-es';
|
||||
import type { MermaidConfig } from 'mermaid';
|
||||
import { derived, get, writable, type Readable } from 'svelte/store';
|
||||
import { env } from './env';
|
||||
import {
|
||||
extractErrorLineText,
|
||||
findMostRelevantLineNumber,
|
||||
@@ -9,8 +10,8 @@ import {
|
||||
} from './errorHandling';
|
||||
import { parse } from './mermaid';
|
||||
import { localStorage, persist } from './persist';
|
||||
import { deserializeState, serializeState } from './serde';
|
||||
import { errorDebug, formatJSON } from './util';
|
||||
import { deserializeState, pakoSerde, serializeState } from './serde';
|
||||
import { errorDebug, formatJSON, MCBaseURL } from './util';
|
||||
|
||||
export const defaultState: State = {
|
||||
code: `flowchart TD
|
||||
@@ -20,10 +21,11 @@ export const defaultState: State = {
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]
|
||||
`,
|
||||
grid: true,
|
||||
mermaid: formatJSON({
|
||||
theme: 'default'
|
||||
}),
|
||||
autoSync: true,
|
||||
panZoom: true,
|
||||
rough: false,
|
||||
updateDiagram: true
|
||||
};
|
||||
@@ -46,25 +48,33 @@ export const currentState: ValidatedState = (() => {
|
||||
const state = get(inputStateStore);
|
||||
return {
|
||||
...state,
|
||||
serialized: serializeState(state),
|
||||
errorMarkers: [],
|
||||
editorMode: state.editorMode ?? 'code',
|
||||
error: undefined,
|
||||
editorMode: state.editorMode ?? 'code'
|
||||
errorMarkers: [],
|
||||
serialized: serializeState(state)
|
||||
};
|
||||
})();
|
||||
|
||||
let lastDiagramType = '';
|
||||
|
||||
const processState = async (state: State) => {
|
||||
const processed: ValidatedState = {
|
||||
...state,
|
||||
serialized: '',
|
||||
errorMarkers: [],
|
||||
editorMode: state.editorMode ?? 'code',
|
||||
error: undefined,
|
||||
editorMode: state.editorMode ?? 'code'
|
||||
errorMarkers: [],
|
||||
serialized: ''
|
||||
};
|
||||
// No changes should be done to fields part of `state`.
|
||||
try {
|
||||
processed.serialized = serializeState(state);
|
||||
await parse(state.code);
|
||||
const { diagramType } = await parse(state.code);
|
||||
processed.diagramType = diagramType;
|
||||
if (lastDiagramType === 'zenuml' && diagramType !== lastDiagramType) {
|
||||
// Temp Hack to refresh page after displaying ZenUML.
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
}
|
||||
lastDiagramType = diagramType;
|
||||
JSON.parse(state.mermaid);
|
||||
} catch (error) {
|
||||
processed.error = error as Error;
|
||||
@@ -94,12 +104,12 @@ const processState = async (state: State) => {
|
||||
|
||||
processed.error = new Error(errorString);
|
||||
const marker: MarkerData = {
|
||||
severity: 8, // Error
|
||||
startLineNumber: realLineNumber,
|
||||
startColumn: first_column,
|
||||
endLineNumber: last_line + (realLineNumber - first_line),
|
||||
endColumn: last_column + (first_column === last_column ? 0 : 5),
|
||||
message: errorString || 'Syntax error'
|
||||
endLineNumber: last_line + (realLineNumber - first_line),
|
||||
message: errorString || 'Syntax error',
|
||||
severity: 8, // Error
|
||||
startColumn: first_column,
|
||||
startLineNumber: realLineNumber
|
||||
};
|
||||
processed.errorMarkers = [marker];
|
||||
} catch (error) {
|
||||
@@ -119,6 +129,23 @@ export const stateStore: Readable<ValidatedState> = derived(
|
||||
currentState
|
||||
);
|
||||
|
||||
export const urlsStore = derived([stateStore], ([{ code, serialized }]) => {
|
||||
const { krokiRendererUrl, rendererUrl } = env;
|
||||
const png = `${rendererUrl}/img/${serialized}?type=png`;
|
||||
return {
|
||||
kroki: `${krokiRendererUrl}/mermaid/svg/${pakoSerde.serialize(code)}`,
|
||||
mdCode: `[](${window.location.protocol}//${window.location.host}${window.location.pathname}#${serialized})`,
|
||||
mermaidChart: {
|
||||
save: `${MCBaseURL}/app/plugin/save?state=${serialized}`,
|
||||
playground: `${MCBaseURL}/play#${serialized}`
|
||||
},
|
||||
new: `${window.location.protocol}//${window.location.host}${window.location.pathname}/#${serializeState(defaultState)}`,
|
||||
png,
|
||||
svg: `${rendererUrl}/svg/${serialized}`,
|
||||
view: `/view#${serialized}`
|
||||
};
|
||||
});
|
||||
|
||||
export const loadState = (data: string): void => {
|
||||
let state: State;
|
||||
console.log(`Loading '${data}'`);
|
||||
@@ -152,9 +179,11 @@ export const loadState = (data: string): void => {
|
||||
updateCodeStore(state);
|
||||
};
|
||||
|
||||
let renderCount = 0;
|
||||
export const updateCodeStore = (newState: Partial<State>): void => {
|
||||
inputStateStore.update((state) => {
|
||||
return { ...state, ...newState };
|
||||
renderCount++;
|
||||
return { ...state, ...newState, renderCount };
|
||||
});
|
||||
};
|
||||
|
||||
@@ -177,10 +206,7 @@ export const updateCode = (
|
||||
};
|
||||
|
||||
export const updateConfig = (config: string): void => {
|
||||
// console.log('updateConfig', config);
|
||||
inputStateStore.update((state) => {
|
||||
return { ...state, mermaid: config };
|
||||
});
|
||||
updateCodeStore({ mermaid: config });
|
||||
};
|
||||
|
||||
export const toggleDarkTheme = (dark: boolean): void => {
|
||||
@@ -189,7 +215,6 @@ export const toggleDarkTheme = (dark: boolean): void => {
|
||||
if (!config.theme || ['dark', 'default'].includes(config.theme)) {
|
||||
config.theme = dark ? 'dark' : 'default';
|
||||
}
|
||||
|
||||
return { ...state, mermaid: formatJSON(config) };
|
||||
});
|
||||
};
|
||||
|
||||
+12
-12
@@ -9,10 +9,10 @@ export const initAnalytics = async (): Promise<void> => {
|
||||
try {
|
||||
const { default: Plausible } = await import('plausible-tracker');
|
||||
plausible = Plausible({
|
||||
domain: env.domain,
|
||||
hashMode: false,
|
||||
// All tracked stats are public and available at https://p.mermaid.live/mermaid.live
|
||||
apiHost: env.analyticsUrl
|
||||
apiHost: env.analyticsUrl,
|
||||
domain: env.domain,
|
||||
hashMode: false
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -42,7 +42,7 @@ export const saveStatistics = ({
|
||||
const length = countLines(code);
|
||||
const lengthBucket = getBucket(length);
|
||||
const renderTimeMsBucket = getBucket(renderTime);
|
||||
logEvent('render', { diagramType, lengthBucket, renderTimeMsBucket, isRough });
|
||||
logEvent('render', { diagramType, isRough, lengthBucket, renderTimeMsBucket });
|
||||
};
|
||||
|
||||
const getBucket = (length: number): string => {
|
||||
@@ -77,19 +77,19 @@ const minutesToMilliSeconds = (minutes: number): number => {
|
||||
|
||||
const defaultDelay = minutesToMilliSeconds(1);
|
||||
const delaysPerEvent = {
|
||||
render: minutesToMilliSeconds(5),
|
||||
panZoom: minutesToMilliSeconds(10),
|
||||
playgroundToggle: 0,
|
||||
bannerClick: defaultDelay,
|
||||
copyClipboard: defaultDelay,
|
||||
download: defaultDelay,
|
||||
copyMarkdown: defaultDelay,
|
||||
download: defaultDelay,
|
||||
history: defaultDelay,
|
||||
loadGist: defaultDelay,
|
||||
loadSampleDiagram: defaultDelay,
|
||||
renderDiagram: defaultDelay,
|
||||
history: defaultDelay,
|
||||
migration: defaultDelay,
|
||||
panZoom: minutesToMilliSeconds(10),
|
||||
playgroundToggle: 0,
|
||||
render: minutesToMilliSeconds(5),
|
||||
renderDiagram: defaultDelay,
|
||||
themeChange: defaultDelay,
|
||||
bannerClick: defaultDelay,
|
||||
version: defaultDelay
|
||||
} as const;
|
||||
export type AnalyticsEvent = keyof typeof delaysPerEvent;
|
||||
@@ -102,7 +102,7 @@ export const logEvent = (
|
||||
if (!plausible) {
|
||||
return;
|
||||
}
|
||||
const key = data ? JSON.stringify({ name, data }) : name;
|
||||
const key = data ? JSON.stringify({ data, name }) : name;
|
||||
if (timeouts.has(key)) {
|
||||
clearTimeout(timeouts.get(key));
|
||||
} else {
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { persist, localStorage } from '$lib/util/persist';
|
||||
import { logEvent } from './stats';
|
||||
|
||||
export interface ThemeConfig {
|
||||
isDark: boolean;
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
export const themeStore: Writable<ThemeConfig> = persist(
|
||||
writable({
|
||||
isDark: false
|
||||
}),
|
||||
localStorage(),
|
||||
'themeStore'
|
||||
);
|
||||
|
||||
const darkThemes = new Set([
|
||||
'dark',
|
||||
'synthwave',
|
||||
'halloween',
|
||||
'aqua',
|
||||
'forest',
|
||||
'luxury',
|
||||
'black',
|
||||
'dracula'
|
||||
]);
|
||||
|
||||
export const setTheme = (theme: string): void => {
|
||||
if (theme.includes(' ')) {
|
||||
theme = theme.split(' ')[1].trim();
|
||||
}
|
||||
const isDark = darkThemes.has(theme);
|
||||
console.log('Setting theme', theme);
|
||||
themeStore.set({ theme, isDark });
|
||||
logEvent('themeChange', { theme, isDark });
|
||||
};
|
||||
+35
-1
@@ -1,3 +1,4 @@
|
||||
import { env } from './env';
|
||||
import { loadDataFromUrl } from './fileLoaders/loader';
|
||||
import { initLoading } from './loading';
|
||||
import { applyMigrations } from './migrations';
|
||||
@@ -26,7 +27,9 @@ export const initHandler = async (): Promise<void> => {
|
||||
|
||||
export const isMac = navigator.platform.toUpperCase().includes('MAC');
|
||||
export const cmdKey = isMac ? 'Cmd' : 'Ctrl';
|
||||
export const MCBaseURL = 'https://mermaidchart.com'; // 'http://localhost:5174'
|
||||
export const MCBaseURL = env.isEnabledMermaidChartLinks
|
||||
? 'https://mermaidchart.com' // 'http://localhost:5174'
|
||||
: 'https://example.com';
|
||||
|
||||
let count = 0;
|
||||
export const errorDebug = (limit = 1000) => {
|
||||
@@ -47,3 +50,34 @@ export const fetchText = async (url: string): Promise<string> => {
|
||||
const res = await fetch(url);
|
||||
return res.text();
|
||||
};
|
||||
|
||||
export const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch {
|
||||
fallbackCopyToClipboard(text);
|
||||
}
|
||||
};
|
||||
|
||||
function fallbackCopyToClipboard(text: string) {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
// Make the textarea out of viewport
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
textArea.style.top = '-999999px';
|
||||
document.body.append(textArea);
|
||||
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
// The deprecated but widely supported method
|
||||
document.execCommand('copy');
|
||||
} catch (error) {
|
||||
console.error('Failed to copy:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
textArea.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
+14
-19
@@ -1,14 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Toaster } from '$/components/ui/sonner/index.js';
|
||||
import { loadingStateStore } from '$/util/loading';
|
||||
import { toggleDarkTheme } from '$/util/state';
|
||||
import { initHandler } from '$/util/util';
|
||||
import { base } from '$app/paths';
|
||||
import { loadingStateStore } from '$lib/util/loading';
|
||||
import { toggleDarkTheme } from '$lib/util/state';
|
||||
import { setTheme, themeStore } from '$lib/util/theme';
|
||||
import { initHandler } from '$lib/util/util';
|
||||
import { mode, ModeWatcher } from 'mode-watcher';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import '../app.postcss';
|
||||
|
||||
interface Props {
|
||||
children?: Snippet;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
@@ -31,24 +32,18 @@
|
||||
console.log('Service worker registration failed, error:', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
if ($themeStore.theme === undefined) {
|
||||
setTheme(isDarkMode ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
themeStore.subscribe(({ theme, isDark }) => {
|
||||
if (theme) {
|
||||
document.querySelectorAll('html')[0].dataset.theme = theme;
|
||||
toggleDarkTheme(isDark);
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
toggleDarkTheme($mode === 'dark');
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="h-screen text-primary-content">
|
||||
{@render children?.()}
|
||||
<ModeWatcher />
|
||||
<Toaster />
|
||||
|
||||
<main class="h-screen">
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
{#if $loadingStateStore.loading}
|
||||
|
||||
+83
-234
@@ -1,263 +1,112 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import Actions from '$lib/components/Actions.svelte';
|
||||
import Card from '$lib/components/Card/Card.svelte';
|
||||
import Editor from '$lib/components/Editor.svelte';
|
||||
import History from '$lib/components/History/History.svelte';
|
||||
import Navbar from '$lib/components/Navbar.svelte';
|
||||
import Preset from '$lib/components/Preset.svelte';
|
||||
import View from '$lib/components/View.svelte';
|
||||
import type { DocumentationConfig, EditorMode, Tab, ValidatedState } from '$lib/types';
|
||||
import { env } from '$lib/util/env';
|
||||
import { inputStateStore, stateStore, updateCodeStore } from '$lib/util/state';
|
||||
import { cmdKey, initHandler, MCBaseURL, syncDiagram } from '$lib/util/util';
|
||||
import Actions from '$/components/Actions.svelte';
|
||||
import Card from '$/components/Card/Card.svelte';
|
||||
import DiagramDocButton from '$/components/DiagramDocumentationButton.svelte';
|
||||
import Editor from '$/components/Editor.svelte';
|
||||
import History from '$/components/History/History.svelte';
|
||||
import McWrapper from '$/components/McWrapper.svelte';
|
||||
import MermaidChartIcon from '$/components/MermaidChartIcon.svelte';
|
||||
import Navbar from '$/components/Navbar.svelte';
|
||||
import PanZoomToolbar from '$/components/PanZoomToolbar.svelte';
|
||||
import Preset from '$/components/Preset.svelte';
|
||||
import Share from '$/components/Share.svelte';
|
||||
import SyncRoughToolbar from '$/components/SyncRoughToolbar.svelte';
|
||||
import { Button } from '$/components/ui/button';
|
||||
import * as Resizable from '$/components/ui/resizable';
|
||||
import { Toggle } from '$/components/ui/toggle';
|
||||
import VersionSecurityToolbar from '$/components/VersionSecurityToolbar.svelte';
|
||||
import View from '$/components/View.svelte';
|
||||
import type { EditorMode, Tab } from '$/types';
|
||||
import { PanZoomState } from '$/util/panZoom';
|
||||
import { stateStore, updateCodeStore, urlsStore } from '$/util/state';
|
||||
import { initHandler } from '$/util/util';
|
||||
import { onMount } from 'svelte';
|
||||
import CodeIcon from '~icons/custom/code';
|
||||
import HistoryIcon from '~icons/material-symbols/history';
|
||||
import GearIcon from '~icons/material-symbols/settings-outline-rounded';
|
||||
|
||||
const docURLBase = 'https://mermaid.js.org';
|
||||
const docMap: DocumentationConfig = {
|
||||
graph: {
|
||||
code: '/syntax/.html',
|
||||
config: '/syntax/.html#configuration'
|
||||
},
|
||||
flowchart: {
|
||||
code: '/syntax/flowchart.html',
|
||||
config: '/syntax/flowchart.html#configuration'
|
||||
},
|
||||
sequenceDiagram: {
|
||||
code: '/syntax/sequenceDiagram.html',
|
||||
config: '/syntax/sequenceDiagram.html#configuration'
|
||||
},
|
||||
classDiagram: {
|
||||
code: '/syntax/classDiagram.html',
|
||||
config: '/syntax/classDiagram.html#configuration'
|
||||
},
|
||||
'stateDiagram-v2': {
|
||||
code: '/syntax/stateDiagram.html'
|
||||
},
|
||||
gantt: {
|
||||
code: '/syntax/gantt.html',
|
||||
config: '/syntax/gantt.html#configuration'
|
||||
},
|
||||
pie: {
|
||||
code: '/syntax/pie.html',
|
||||
config: '/syntax/pie.html#configuration'
|
||||
},
|
||||
erDiagram: {
|
||||
code: '/syntax/entityRelationshipDiagram.html',
|
||||
config: '/syntax/entityRelationshipDiagram.html#styling'
|
||||
},
|
||||
journey: {
|
||||
code: '/syntax/userJourney.html'
|
||||
},
|
||||
gitGraph: {
|
||||
code: '/syntax/gitgraph.html',
|
||||
config: '/syntax/gitgraph.html#gitgraph-specific-configuration-options'
|
||||
},
|
||||
quadrantChart: {
|
||||
code: '/syntax/quadrantChart.html',
|
||||
config: '/syntax/quadrantChart.html#chart-configurations'
|
||||
},
|
||||
requirementDiagram: {
|
||||
code: '/syntax/requirementDiagram.html'
|
||||
},
|
||||
C4Context: {
|
||||
code: '/syntax/c4.html'
|
||||
},
|
||||
mindmap: {
|
||||
code: '/syntax/mindmap.html'
|
||||
},
|
||||
timeline: {
|
||||
code: '/syntax/timeline.html',
|
||||
config: '/syntax/timeline.html#themes'
|
||||
},
|
||||
zenuml: {
|
||||
code: '/syntax/zenuml.html'
|
||||
},
|
||||
'sankey-beta': {
|
||||
code: '/syntax/sankey.html',
|
||||
config: '/syntax/sankey.html#configuration'
|
||||
},
|
||||
'xychart-beta': {
|
||||
code: '/syntax/xyChart.html',
|
||||
config: '/syntax/xyChart.html#chart-configurations'
|
||||
}
|
||||
};
|
||||
let docURL = $state(docURLBase);
|
||||
let activeTabID = $state('code');
|
||||
let docKey = $state('');
|
||||
stateStore.subscribe(({ code, editorMode }: ValidatedState) => {
|
||||
activeTabID = editorMode;
|
||||
const codeTypeMatch = /(\S+)\s/.exec(code);
|
||||
if (codeTypeMatch && codeTypeMatch.length > 1) {
|
||||
docKey = codeTypeMatch[1];
|
||||
const docConfig = docMap[docKey] ?? { code: '' };
|
||||
docURL = docURLBase + (docConfig[editorMode] ?? docConfig.code ?? '');
|
||||
}
|
||||
});
|
||||
const panZoomState = new PanZoomState();
|
||||
|
||||
const tabSelectHandler = (tab: Tab) => {
|
||||
const editorMode: EditorMode = tab.id === 'code' ? 'code' : 'config';
|
||||
updateCodeStore({ editorMode });
|
||||
};
|
||||
|
||||
const tabs: Tab[] = [
|
||||
const editorTabs: Tab[] = [
|
||||
{
|
||||
icon: CodeIcon,
|
||||
id: 'code',
|
||||
title: 'Code',
|
||||
icon: 'fas fa-code'
|
||||
title: 'Code'
|
||||
},
|
||||
{
|
||||
icon: GearIcon,
|
||||
id: 'config',
|
||||
title: 'Config',
|
||||
icon: 'fas fa-cogs'
|
||||
title: 'Config'
|
||||
}
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
await initHandler();
|
||||
const resizer = document.querySelector<HTMLElement>('#resizeHandler');
|
||||
const element = document.querySelector<HTMLElement>('#editorPane');
|
||||
if (!resizer || !element) {
|
||||
console.debug('Failed to find resize handler or editor pane', { resizer, element });
|
||||
return;
|
||||
}
|
||||
const resize = ({ pageX }: { pageX: number }) => {
|
||||
const newWidth = pageX - element.getBoundingClientRect().left;
|
||||
if (newWidth > 50) {
|
||||
element.style.width = `${newWidth}px`;
|
||||
}
|
||||
};
|
||||
|
||||
const stopResize = () => {
|
||||
window.removeEventListener('mousemove', resize);
|
||||
};
|
||||
resizer.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault();
|
||||
window.addEventListener('mousemove', resize);
|
||||
window.addEventListener('mouseup', stopResize);
|
||||
});
|
||||
});
|
||||
|
||||
let isHistoryOpen = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col overflow-hidden">
|
||||
<Navbar />
|
||||
<Navbar>
|
||||
<Toggle bind:pressed={isHistoryOpen} size="sm">
|
||||
<HistoryIcon />
|
||||
</Toggle>
|
||||
<Share />
|
||||
<McWrapper>
|
||||
<Button variant="accent" size="sm" href={$urlsStore.mermaidChart.save} target="_blank">
|
||||
<MermaidChartIcon />
|
||||
Save diagram
|
||||
</Button>
|
||||
</McWrapper>
|
||||
</Navbar>
|
||||
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<div class="hidden flex-col md:flex" id="editorPane" style="width: 40%">
|
||||
<Card onselect={tabSelectHandler} {tabs} isClosable={false} {activeTabID} title="Mermaid">
|
||||
{#snippet actions()}
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="form-control flex-row items-center">
|
||||
<label class="label cursor-pointer" for="autoSync">
|
||||
<span> Auto sync</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle {$stateStore.autoSync ? 'btn-secondary' : 'toggle-primary'} ml-1"
|
||||
id="autoSync"
|
||||
bind:checked={$inputStateStore.autoSync} />
|
||||
</label>
|
||||
</div>
|
||||
<Resizable.PaneGroup direction="horizontal" autoSaveId="liveEditor" class="p-6 pt-0">
|
||||
<Resizable.Pane defaultSize={30} minSize={15} class="hidden md:block">
|
||||
<div class="flex h-full flex-col gap-6" id="editorPane">
|
||||
<Card
|
||||
onselect={tabSelectHandler}
|
||||
isOpen
|
||||
tabs={editorTabs}
|
||||
activeTabID={$stateStore.editorMode}
|
||||
isClosable={false}>
|
||||
{#snippet actions()}
|
||||
<DiagramDocButton />
|
||||
{/snippet}
|
||||
<Editor />
|
||||
</Card>
|
||||
|
||||
{#if !$stateStore.autoSync}
|
||||
<button
|
||||
class="btn btn-secondary btn-xs mr-1"
|
||||
title="Sync Diagram ({cmdKey} + Enter)"
|
||||
aria-label="Sync Diagram"
|
||||
data-cy="sync"
|
||||
onclick={syncDiagram}><i class="fas fa-sync"></i></button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="btn btn-secondary btn-xs"
|
||||
title="View documentation for {docKey.replace('Diagram', '')} diagram">
|
||||
<a target="_blank" href={docURL} data-cy="docs">
|
||||
<i class="fas fa-book mr-1"></i>Docs
|
||||
</a>
|
||||
</button>
|
||||
<div class="group flex flex-wrap justify-between gap-6">
|
||||
<Preset />
|
||||
<Actions />
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<Editor />
|
||||
</Card>
|
||||
|
||||
<div class="-mt-2">
|
||||
<Preset />
|
||||
<History />
|
||||
<Actions />
|
||||
</div>
|
||||
</div>
|
||||
<div id="resizeHandler" class="hidden md:block"></div>
|
||||
<div class="flex flex-1 flex-col overflow-hidden">
|
||||
<Card title="Diagram" isClosable={false}>
|
||||
{#snippet actions()}
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<label
|
||||
class="label flex cursor-pointer gap-1 py-0"
|
||||
title="Rough mode is in beta. Features like clickable nodes and ZenUML diagram will not work."
|
||||
for="rough">
|
||||
<span>Rough</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle {$stateStore.rough ? 'btn-secondary' : 'toggle-primary'}"
|
||||
id="rough"
|
||||
bind:checked={$inputStateStore.rough} />
|
||||
</label>
|
||||
<label class="label flex cursor-pointer gap-1 py-0" for="panZoom">
|
||||
<span>Pan & Zoom</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle {$stateStore.panZoom ? 'btn-secondary' : 'toggle-primary'}"
|
||||
id="panZoom"
|
||||
bind:checked={$inputStateStore.panZoom} />
|
||||
</label>
|
||||
<a
|
||||
href={`${base}/view#${$stateStore.serialized}`}
|
||||
target="_blank"
|
||||
class="btn btn-secondary btn-xs gap-1"
|
||||
title="View diagram in new page"
|
||||
><i class="fas fa-external-link-alt"></i>Full screen</a>
|
||||
{#if env.isEnabledMermaidChartLinks}
|
||||
<a
|
||||
href={`${MCBaseURL}/app/plugin/save?state=${$stateStore.serialized}`}
|
||||
target="_blank"
|
||||
class="btn btn-secondary btn-xs gap-1 bg-[#FF3570]"
|
||||
title="Save diagram in Mermaid Chart"
|
||||
><img src="./mermaidchart-logo.svg" class="h-5 w-5" alt="Mermaid chart logo" />Save
|
||||
to Mermaid Chart</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div class="flex-1 overflow-auto">
|
||||
<View />
|
||||
</div>
|
||||
</Card>
|
||||
<div class="mx-2 rounded p-2 shadow md:hidden">
|
||||
Code editing not supported on mobile. Please use a desktop browser.
|
||||
</div>
|
||||
</div>
|
||||
</Resizable.Pane>
|
||||
<Resizable.Handle class="mr-1 opacity-0" />
|
||||
<Resizable.Pane minSize={15} class="relative flex h-full flex-1 flex-col overflow-hidden">
|
||||
<View {panZoomState} shouldShowGrid={$stateStore.grid} />
|
||||
<div class="absolute right-0 top-0"><PanZoomToolbar {panZoomState} /></div>
|
||||
<div class="absolute bottom-0 right-0"><VersionSecurityToolbar /></div>
|
||||
<div class="absolute bottom-0 left-5"><SyncRoughToolbar /></div>
|
||||
<div class="rounded bg-primary p-2 text-center shadow md:hidden">
|
||||
Code editing not supported on mobile. Please use a desktop browser.
|
||||
</div>
|
||||
</Resizable.Pane>
|
||||
{#if isHistoryOpen}
|
||||
<Resizable.Handle class="ml-1 hidden opacity-0 md:block" />
|
||||
<Resizable.Pane
|
||||
minSize={15}
|
||||
defaultSize={30}
|
||||
class="hidden h-full flex-grow flex-col md:flex">
|
||||
<History />
|
||||
</Resizable.Pane>
|
||||
{/if}
|
||||
</Resizable.PaneGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#resizeHandler {
|
||||
cursor: col-resize;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
#resizeHandler::after {
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: hsla(var(--b3));
|
||||
margin-left: -1px;
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
|
||||
#resizeHandler:hover::after {
|
||||
margin-left: -2px;
|
||||
background-color: hsla(var(--p));
|
||||
width: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import View from '$lib/components/View.svelte';
|
||||
import { initHandler } from '$lib/util/util';
|
||||
import View from '$/components/View.svelte';
|
||||
import { initHandler } from '$/util/util';
|
||||
import { onMount } from 'svelte';
|
||||
onMount(initHandler);
|
||||
</script>
|
||||
@@ -9,4 +9,4 @@
|
||||
<meta name="robots" content="noindex" />
|
||||
</svelte:head>
|
||||
|
||||
<View />
|
||||
<View shouldShowGrid={false} />
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import { beforeAll, vi } from 'vitest';
|
||||
import 'vitest-dom/extend-expect';
|
||||
|
||||
// TODO: Remove once https://github.com/sveltejs/kit/issues/6259 is closed.
|
||||
beforeAll(() => {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="terminal">
|
||||
<mask id="mask0_13660_101033" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
|
||||
<rect id="Bounding box" width="24" height="24" fill="#D9D9D9"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_13660_101033)">
|
||||
<g id="terminal_2">
|
||||
<path d="M3 21C2.45 21 1.97917 20.8042 1.5875 20.4125C1.19583 20.0208 1 19.55 1 19V5C1 4.45 1.19583 3.97917 1.5875 3.5875C1.97917 3.19583 2.45 3 3 3H21C21.55 3 22.0208 3.19583 22.4125 3.5875C22.8042 3.97917 23 4.45 23 5V19C23 19.55 22.8042 20.0208 22.4125 20.4125C22.0208 20.8042 21.55 21 21 21H3ZM3 19H21V7H3V19ZM16.675 13L14.775 11.1C14.575 10.9 14.4792 10.6667 14.4875 10.4C14.4958 10.1333 14.6 9.9 14.8 9.7C15 9.51667 15.2333 9.42083 15.5 9.4125C15.7667 9.40417 16 9.5 16.2 9.7L18.8 12.3C19 12.5 19.1 12.7333 19.1 13C19.1 13.2667 19 13.5 18.8 13.7L16.2 16.3C16.0167 16.4833 15.7875 16.5792 15.5125 16.5875C15.2375 16.5958 15 16.5 14.8 16.3C14.6167 16.1167 14.525 15.8833 14.525 15.6C14.525 15.3167 14.6167 15.0833 14.8 14.9L16.675 13ZM6 14C5.71667 14 5.47917 13.9042 5.2875 13.7125C5.09583 13.5208 5 13.2833 5 13C5 12.7167 5.09583 12.4792 5.2875 12.2875C5.47917 12.0958 5.71667 12 6 12H8C8.28333 12 8.52083 12.0958 8.7125 12.2875C8.90417 12.4792 9 12.7167 9 13C9 13.2833 8.90417 13.5208 8.7125 13.7125C8.52083 13.9042 8.28333 14 8 14H6Z" fill="currentColor"/>
|
||||
<path d="M11 14C10.7167 14 10.4792 13.9042 10.2875 13.7125C10.0958 13.5208 10 13.2833 10 13C10 12.7167 10.0958 12.4792 10.2875 12.2875C10.4792 12.0958 10.7167 12 11 12H13C13.2833 12 13.5208 12.0958 13.7125 12.2875C13.9042 12.4792 14 12.7167 14 13C14 13.2833 13.9042 13.5208 13.7125 13.7125C13.5208 13.9042 13.2833 14 13 14H11Z" fill="currentColor"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -1 +1,4 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 490.16 490.16"><defs><style>.cls-1{fill:#ff3670;}.cls-2{fill:#fff;}</style></defs><rect class="cls-1" width="490.16" height="490.16" rx="84.61"/><path class="cls-2" d="M407.48,111.18A165.2,165.2,0,0,0,245.08,220,165.2,165.2,0,0,0,82.68,111.18a165.5,165.5,0,0,0,72.06,143.64,88.81,88.81,0,0,1,38.53,73.45v50.86H296.9V328.27a88.8,88.8,0,0,1,38.52-73.45,165.41,165.41,0,0,0,72.06-143.64Z"/><path class="cls-2" d="M160.63,328.27a56.09,56.09,0,0,0-24.27-46.49,198.74,198.74,0,0,1-28.54-23.66A196.87,196.87,0,0,1,82.53,227V379.13h78.1Z"/><path class="cls-2" d="M329.53,328.27a56.09,56.09,0,0,1,24.27-46.49,198.74,198.74,0,0,0,28.54-23.66A196.87,196.87,0,0,0,407.63,227V379.13h-78.1Z"/></svg>
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="48" height="48" rx="24" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.44299 3.44299C0 6.88598 0 12.4274 0 23.5102V24.4898C0 35.5726 0 41.114 3.44299 44.557C6.88598 48 12.4274 48 23.5102 48H24.4898C35.5726 48 41.114 48 44.557 44.557C48 41.114 48 35.5726 48 24.4898V24.4898V23.5102V23.5102C48 12.4274 48 6.88598 44.557 3.44299C41.114 0 35.5726 0 24.4898 0H23.5102C12.4274 0 6.88598 0 3.44299 3.44299ZM38.3865 12.0135C35.2747 11.879 32.2016 12.7511 29.6141 14.5031C27.0267 16.2551 25.0599 18.7955 23.9998 21.7549C22.9396 18.7955 20.9728 16.2551 18.3854 14.5031C15.7979 12.7511 12.7248 11.879 9.61304 12.0135C9.50856 14.5283 10.0398 17.0285 11.1567 19.2782C12.2737 21.528 13.9394 23.4531 15.9967 24.872C17.0522 25.6045 17.9148 26.5863 18.5096 27.7321C19.1044 28.878 19.4135 30.1532 19.41 31.4471V36H28.5904V31.4471C28.5867 30.1532 28.8956 28.8781 29.4903 27.7322C30.085 26.5864 30.9474 25.6046 32.0028 24.872C34.0607 23.4537 35.7268 21.5286 36.8438 19.2788C37.9608 17.0289 38.4917 14.5284 38.3865 12.0135ZM16.5185 31.4471C16.5228 30.6289 16.3292 29.822 15.9544 29.0967C15.5797 28.3713 15.0353 27.7495 14.3685 27.2854C13.4622 26.6605 12.6158 25.9514 11.8401 25.1674C11.0015 24.3182 10.2508 23.3848 9.59975 22.3816V36H16.5185V31.4471ZM32.0451 29.0967C31.6703 29.822 31.4767 30.6289 31.481 31.4471V36H38.3997V22.3816C37.7487 23.3848 36.998 24.3182 36.1594 25.1674C35.3837 25.9514 34.5373 26.6605 33.6311 27.2854C32.9642 27.7495 32.4198 28.3713 32.0451 29.0967Z" fill="#E0095F"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 772 B After Width: | Height: | Size: 1.6 KiB |
@@ -11,6 +11,9 @@ const config = {
|
||||
})
|
||||
],
|
||||
kit: {
|
||||
alias: {
|
||||
'$/*': './src/lib/*'
|
||||
},
|
||||
adapter: adapter({
|
||||
pages: 'docs'
|
||||
})
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: [require('daisyui')],
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
variants: {
|
||||
extend: {}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import tailwindcssAnimate from 'tailwindcss-animate';
|
||||
import { fontFamily } from 'tailwindcss/defaultTheme';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
const config = {
|
||||
darkMode: ['class'],
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
safelist: ['dark'],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: '2rem',
|
||||
screens: {
|
||||
'2xl': '1400px'
|
||||
}
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border) / <alpha-value>)',
|
||||
input: 'hsl(var(--input) / <alpha-value>)',
|
||||
ring: 'hsl(var(--ring) / <alpha-value>)',
|
||||
background: 'hsl(var(--background) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--foreground) / <alpha-value>)',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--primary-foreground) / <alpha-value>)'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)'
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--destructive-foreground) / <alpha-value>)'
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--muted-foreground) / <alpha-value>)'
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--accent-foreground) / <alpha-value>)'
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--popover-foreground) / <alpha-value>)'
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card) / <alpha-value>)',
|
||||
foreground: 'hsl(var(--card-foreground) / <alpha-value>)'
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: 'hsl(var(--sidebar-background))',
|
||||
foreground: 'hsl(var(--sidebar-foreground))',
|
||||
primary: 'hsl(var(--sidebar-primary))',
|
||||
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
||||
accent: 'hsl(var(--sidebar-accent))',
|
||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||
border: 'hsl(var(--sidebar-border))',
|
||||
ring: 'hsl(var(--sidebar-ring))'
|
||||
}
|
||||
},
|
||||
borderRadius: {
|
||||
xl: 'calc(var(--radius) + 4px)',
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [...fontFamily.sans]
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: { height: '0' },
|
||||
to: { height: 'var(--bits-accordion-content-height)' }
|
||||
},
|
||||
'accordion-up': {
|
||||
from: { height: 'var(--bits-accordion-content-height)' },
|
||||
to: { height: '0' }
|
||||
},
|
||||
'caret-blink': {
|
||||
'0%,70%,100%': { opacity: '1' },
|
||||
'20%,50%': { opacity: '0' }
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
'caret-blink': 'caret-blink 1.25s ease-out infinite'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [tailwindcssAnimate]
|
||||
};
|
||||
|
||||
export default config;
|
||||
+14
-27
@@ -1,17 +1,14 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { typeInEditor, verifyFileSizeGreaterThan } from './utils';
|
||||
import { expect, test } from './test';
|
||||
|
||||
test.describe('Check actions', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/edit');
|
||||
await page.getByText('Actions', { exact: true }).click();
|
||||
test.beforeEach(async ({ editPage }) => {
|
||||
await editPage.toggleActions();
|
||||
});
|
||||
|
||||
test('should update markdown code', async ({ page }) => {
|
||||
const markdown = page.locator('#markdown');
|
||||
const oldText = await markdown.inputValue();
|
||||
await typeInEditor(page, 'C --> HistoryTest', { bottom: true, newline: true });
|
||||
const newText = await markdown.inputValue();
|
||||
test('should update markdown code', async ({ editPage }) => {
|
||||
const oldText = await editPage.markdownInput.inputValue();
|
||||
await editPage.typeInEditor('C --> HistoryTest', { bottom: true, newline: true });
|
||||
const newText = await editPage.markdownInput.inputValue();
|
||||
expect(newText).not.toBe(oldText);
|
||||
});
|
||||
|
||||
@@ -23,26 +20,16 @@ test.describe('Check actions', () => {
|
||||
await expect(page.getByText('Go shopping!!')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should download png and svg', async ({ page }) => {
|
||||
const downloadPNGPromise = verifyFileSizeGreaterThan(page, 'diagram', 'png', 29_000);
|
||||
await page.locator('#downloadPNG').click();
|
||||
const firstPngSize = await downloadPNGPromise;
|
||||
|
||||
const downloadSVGPromise = verifyFileSizeGreaterThan(page, 'diagram', 'svg', 10_000);
|
||||
await page.locator('#downloadSVG').click();
|
||||
const firstSvgSize = await downloadSVGPromise;
|
||||
test('should download png and svg', async ({ editPage }) => {
|
||||
const firstPngSize = await editPage.checkAndDownloadPNG(80_000);
|
||||
const firstSvgSize = await editPage.downloadSVG(10_000);
|
||||
|
||||
// Verify downloaded file is different for different diagrams
|
||||
await page.getByText('Sample Diagrams').click();
|
||||
await page.getByText('ER', { exact: true }).click();
|
||||
await editPage.toggleSampleDiagrams();
|
||||
await editPage.loadSampleDiagram('ER');
|
||||
|
||||
const downloadPNGPromise2 = verifyFileSizeGreaterThan(page, 'diagram', 'png', 35_000);
|
||||
await page.locator('#downloadPNG').click();
|
||||
const secondPngSize = await downloadPNGPromise2;
|
||||
|
||||
const downloadSVGPromise2 = verifyFileSizeGreaterThan(page, 'diagram', 'svg', 11_000);
|
||||
await page.locator('#downloadSVG').click();
|
||||
const secondSvgSize = await downloadSVGPromise2;
|
||||
const secondPngSize = await editPage.checkAndDownloadPNG(110_000);
|
||||
const secondSvgSize = await editPage.downloadSVG(11_000);
|
||||
|
||||
// Verify files are actually different
|
||||
expect(firstPngSize).not.toBe(secondPngSize);
|
||||
|
||||
+21
-101
@@ -1,63 +1,10 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { typeInEditor } from './utils';
|
||||
import { test } from './test';
|
||||
|
||||
test.describe('Auto sync tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveURL(/.*\/edit#pako/);
|
||||
});
|
||||
|
||||
test('should dim diagram when code is edited', async ({ page }) => {
|
||||
await page.getByText('Auto sync').click();
|
||||
await expect(page.locator('#view')).not.toHaveClass(/outOfSync/);
|
||||
await expect(page.locator('#errorContainer')).not.toBeVisible();
|
||||
await typeInEditor(page, ' C --> Test', { bottom: true });
|
||||
await expect(page.locator('#view')).toHaveClass(/outOfSync/);
|
||||
await expect(page.locator('#errorContainer')).toContainText('Diagram out of sync.');
|
||||
const codeStore = await page.evaluate(() => localStorage.getItem('codeStore'));
|
||||
expect(codeStore).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should update diagram when shortcut is used', async ({ page }) => {
|
||||
await page.getByText('Auto sync').click();
|
||||
await expect(page.locator('#view')).not.toHaveClass(/outOfSync/);
|
||||
await typeInEditor(page, ' C --> Test');
|
||||
await expect(page.locator('#view')).toHaveClass(/outOfSync/);
|
||||
await expect(page.locator('#errorContainer')).toContainText('Diagram out of sync.');
|
||||
await page.locator('#editor').click();
|
||||
await page.keyboard.press('Control+Enter');
|
||||
await expect(page.locator('#errorContainer')).not.toBeVisible();
|
||||
await expect(page.locator('#view')).not.toHaveClass(/outOfSync/);
|
||||
});
|
||||
|
||||
test('should show/hide sync button with auto sync', async ({ page }) => {
|
||||
await expect(page.locator('[data-cy=sync]')).not.toBeVisible();
|
||||
await page.getByText('Auto sync').click();
|
||||
await expect(page.locator('[data-cy=sync]')).toBeVisible();
|
||||
await page.locator('#autoSync').check();
|
||||
await expect(page.locator('[data-cy=sync]')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should not dim diagram when code is in sync', async ({ page }) => {
|
||||
await page.getByText('Auto sync').click();
|
||||
await expect(page.locator('#view')).not.toHaveClass(/outOfSync/);
|
||||
await typeInEditor(page, ' C --> Test');
|
||||
await expect(page.locator('#view')).toHaveClass(/outOfSync/);
|
||||
await page.locator('[data-cy=sync]').click();
|
||||
await expect(page.locator('#view')).not.toHaveClass(/outOfSync/);
|
||||
await page.locator('#autoSync').check();
|
||||
await typeInEditor(page, 'ing');
|
||||
await expect(page.locator('#view')).not.toHaveClass(/outOfSync/);
|
||||
const codeStore = await page.evaluate(() => localStorage.getItem('codeStore'));
|
||||
expect(codeStore).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should automatically defer rendering when complex diagrams are edited', async ({
|
||||
page
|
||||
editPage
|
||||
}) => {
|
||||
await expect(page.locator('#view')).not.toHaveClass(/outOfSync/);
|
||||
await typeInEditor(
|
||||
page,
|
||||
await editPage.typeInEditor(
|
||||
`
|
||||
A & B & C & D & E --> F & G & H & I & J & K
|
||||
A & B & C & D & E --> F & G & H & I & J & K
|
||||
@@ -66,62 +13,35 @@ A & B & C & D & E --> F & G & H & I & J & K
|
||||
A & B & C & D & E --> F & G & H & I & J & K
|
||||
A & B & C & D & E --> F & G & H & I & J & K
|
||||
A & B & C & D & E --> F & G & H & I & J & K
|
||||
A & B & C & D & E --> F & G & H & I & J & K`
|
||||
A & B & C & D & E --> F & G & H & I & J & K & LongTest`
|
||||
);
|
||||
await expect(page.locator('#view')).toHaveClass(/outOfSync/);
|
||||
await expect(page.locator('#errorContainer')).toContainText(
|
||||
'It will be updated automatically.'
|
||||
);
|
||||
// The class should be removed automatically after 1 second
|
||||
await expect(page.locator('#view')).not.toHaveClass(/outOfSync/, { timeout: 2000 });
|
||||
await editPage.checkTextNotInView('LongTest');
|
||||
await editPage.checkTextInView('LongTest');
|
||||
});
|
||||
|
||||
test('supports commenting code out/in', async ({ page }) => {
|
||||
await page.locator('#editor').getByText('fa-car').click();
|
||||
await page.waitForTimeout(1000);
|
||||
await page.keyboard.press('Control+/');
|
||||
|
||||
await expect(page.locator('#view').getByText('Car')).not.toBeVisible();
|
||||
|
||||
await page.keyboard.press('Control+/');
|
||||
await expect(page.locator('#view').getByText('Car')).toBeVisible();
|
||||
test('supports commenting code out/in', async ({ editPage }) => {
|
||||
await editPage.toggleComment('fa-car');
|
||||
await editPage.checkTextNotInView('Car');
|
||||
await editPage.toggleComment('fa-car');
|
||||
await editPage.checkTextInView('Car');
|
||||
});
|
||||
|
||||
test('supports editing code when code is incorrect', async ({ page }) => {
|
||||
await page.goto(
|
||||
test('supports editing code when code is incorrect', async ({ editPage }) => {
|
||||
await editPage.start(
|
||||
'/edit#pako:eNpljjEKwzAMRa8SNOcEnlt6gK5eVFvYJsgOqkwpIXevg9smEE1PnyfxF3DFExgISW-CczQ2D21cYU7a-SGYXRwyvTp9jUhuKlVP-eHy7zA-leQsMEmg_QOM0BLG5FujZVMsaCQmC6ahR5ks2Lw2r84ela4-aREwKpVGwKrl_s7ut3fnkjAIcg_XDzuaUhs'
|
||||
);
|
||||
await expect(page.getByText('Diagram syntax error')).not.toBeVisible();
|
||||
await typeInEditor(page, 'branch test', { bottom: true, newline: true });
|
||||
await expect(page.locator('#editor')).toContainText('branch test');
|
||||
await expect(
|
||||
page.getByText('Error: Trying to checkout branch which is not yet created.')
|
||||
).toBeVisible();
|
||||
await editPage.typeInEditor('branch test', { bottom: true, newline: true });
|
||||
await editPage.checkTextNotInView('test');
|
||||
await editPage.checkInEditor('branch test');
|
||||
await editPage.checkError('Error: Trying to checkout branch which is not yet created.');
|
||||
});
|
||||
|
||||
test('should update diagram after entire text is removed', async ({ page }) => {
|
||||
await page.locator('#editor').click();
|
||||
await page.keyboard.press('Control+KeyA');
|
||||
await page.keyboard.press('Backspace');
|
||||
await typeInEditor(
|
||||
page,
|
||||
test('should update diagram after entire text is removed', async ({ editPage }) => {
|
||||
await editPage.clearEditor();
|
||||
await editPage.typeInEditor(
|
||||
`graph LR
|
||||
A-->Bike`
|
||||
);
|
||||
await expect(page.locator('#view').getByText('Bike')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Pan and Zoom', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('should toggle pan and zoom', async ({ page }) => {
|
||||
await expect(page.locator('#svg-pan-zoom-reset-pan-zoom')).not.toBeVisible();
|
||||
await page.getByText('Pan & Zoom').click();
|
||||
await expect(page.locator('#svg-pan-zoom-reset-pan-zoom')).toBeVisible();
|
||||
await page.getByText('Pan & Zoom').click();
|
||||
await expect(page.locator('#svg-pan-zoom-reset-pan-zoom')).not.toBeVisible();
|
||||
await editPage.checkTextInView('Bike');
|
||||
});
|
||||
});
|
||||
|
||||
+25
-29
@@ -1,42 +1,38 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { test } from './test';
|
||||
|
||||
test.describe('Editor docs tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/edit');
|
||||
await page.getByText('Sample Diagrams').click();
|
||||
test.beforeEach(async ({ editPage }) => {
|
||||
await editPage.toggleSampleDiagrams();
|
||||
});
|
||||
|
||||
test('Test default loading', async ({ page }) => {
|
||||
await expect(page.locator('[data-cy=docs][href^="https://mermaid.js.org/"]')).toBeVisible();
|
||||
test('Test default loading', async ({ editPage }) => {
|
||||
await editPage.checkDocURL(/mermaid\.js\.org\//);
|
||||
});
|
||||
|
||||
test('Test to see if the correct URL loads when changing from one diagram to other', async ({
|
||||
page
|
||||
editPage
|
||||
}) => {
|
||||
await page.getByText('Flow', { exact: true }).click();
|
||||
await expect(page.locator('[data-cy=docs][href$="/syntax/flowchart.html"]')).toBeVisible();
|
||||
await editPage.loadSampleDiagram('Flow');
|
||||
await editPage.checkDocURL(/syntax\/flowchart\.html/);
|
||||
await editPage.setEditorMode('Config');
|
||||
await editPage.checkDocURL(/syntax\/flowchart\.html#configuration/);
|
||||
await editPage.setEditorMode('Code');
|
||||
await editPage.checkDocURL(/syntax\/flowchart\.html/);
|
||||
|
||||
await page.getByText('Config').click();
|
||||
await expect(
|
||||
page.locator('[data-cy=docs][href$="/syntax/flowchart.html#configuration"]')
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByText('Sequence').click();
|
||||
await expect(
|
||||
page.locator('[data-cy=docs][href$="/syntax/sequenceDiagram.html#configuration"]')
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByText('Code', { exact: true }).click();
|
||||
await expect(
|
||||
page.locator('[data-cy=docs][href$="/syntax/sequenceDiagram.html"]')
|
||||
).toBeVisible();
|
||||
await editPage.loadSampleDiagram('Sequence');
|
||||
await editPage.checkDocURL(/syntax\/sequenceDiagram\.html/);
|
||||
await editPage.setEditorMode('Config');
|
||||
await editPage.checkDocURL(/syntax\/sequenceDiagram\.html#configuration/);
|
||||
await editPage.setEditorMode('Code');
|
||||
await editPage.checkDocURL(/syntax\/sequenceDiagram\.html/);
|
||||
});
|
||||
|
||||
test("Test to check URLs for a case where config URL doesn't exist", async ({ page }) => {
|
||||
await page.getByText('State').click();
|
||||
await expect(page.locator('[data-cy=docs][href$="/syntax/stateDiagram.html"]')).toBeVisible();
|
||||
|
||||
await page.getByText('Config').click();
|
||||
await expect(page.locator('[data-cy=docs][href$="/syntax/stateDiagram.html"]')).toBeVisible();
|
||||
test("Test to check URLs for a case where config URL doesn't exist", async ({ editPage }) => {
|
||||
await editPage.loadSampleDiagram('State');
|
||||
await editPage.checkDocURL(/syntax\/stateDiagram\.html/);
|
||||
await editPage.setEditorMode('Config');
|
||||
await editPage.checkDocURL(/syntax\/stateDiagram\.html/);
|
||||
await editPage.setEditorMode('Code');
|
||||
await editPage.checkDocURL(/syntax\/stateDiagram\.html/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { typeInEditor } from './utils';
|
||||
|
||||
test.describe('Save History', () => {
|
||||
test.describe.skip('Save History', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
Object.defineProperty(Date, 'now', {
|
||||
|
||||
+44
-49
@@ -1,46 +1,43 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { expect, test } from './test';
|
||||
|
||||
test.describe('Site Loads', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveURL(/.*\/edit#pako/);
|
||||
});
|
||||
|
||||
test('Check Home page load', async ({ page }) => {
|
||||
await expect(page).toHaveURL(/.*\/edit/);
|
||||
await page.getByText('History').click();
|
||||
const codeStore = await page.evaluate(() => localStorage.getItem('codeStore'));
|
||||
test('Check Home page load', async ({ editPage }) => {
|
||||
await editPage.setEditorMode('Config');
|
||||
const codeStore = await editPage.page.evaluate(() => localStorage.getItem('codeStore'));
|
||||
expect(codeStore).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should keep code after reload', async ({ page }) => {
|
||||
await expect(page.locator('#editor')).toContainText('Car');
|
||||
test('should keep code after reload', async ({ editPage, page }) => {
|
||||
await editPage.checkInEditor('Car');
|
||||
await editPage.checkTextInView('Car');
|
||||
await page.reload();
|
||||
await expect(page.locator('#editor')).toContainText('Car');
|
||||
await editPage.checkInEditor('Car');
|
||||
await editPage.checkTextInView('Car');
|
||||
});
|
||||
|
||||
test('Check Redirect from old URL', async ({ page }) => {
|
||||
await page.goto(
|
||||
test('Check Redirect from old URL', async ({ page, editPage }) => {
|
||||
await editPage.start(
|
||||
'/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZylcbiAgICBCIC0tPiBDe0xldCBtZSB0aGlua31cbiAgICBDIC0tPnxPbmV8IERbTGFwdG9wXVxuICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdXG4gICAgQyAtLT58VGhyZWV8IEZbZmE6ZmEtY2FyIENhcl0iLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ'
|
||||
);
|
||||
await expect(page).toHaveURL(/.*\/edit#pako:eNp/);
|
||||
});
|
||||
|
||||
test('should load sample diagrams when clicked', async ({ page }) => {
|
||||
await page.getByText('Sample Diagrams').click();
|
||||
await page.getByText('Pie').click();
|
||||
await expect(page.getByText('pie title Pets adopted by volunteers')).toBeVisible();
|
||||
await page.getByText('Class').click();
|
||||
await expect(page.getByText('classDiagram')).toBeVisible();
|
||||
test('should load sample diagrams when clicked', async ({ editPage }) => {
|
||||
await editPage.toggleSampleDiagrams();
|
||||
await editPage.loadSampleDiagram('Pie');
|
||||
await editPage.checkInEditor('pie title Pets adopted by volunteers');
|
||||
await editPage.checkTextInView('Pets adopted by volunteers');
|
||||
await editPage.loadSampleDiagram('Class');
|
||||
await editPage.checkInEditor('classDiagram');
|
||||
});
|
||||
|
||||
test.describe('github', () => {
|
||||
test('should load diagram from gist', async ({ page }) => {
|
||||
await page.goto(
|
||||
test.describe.skip('github', () => {
|
||||
test('should load diagram from gist', async ({ editPage, page }) => {
|
||||
await editPage.start(
|
||||
`/edit?gist=https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a`
|
||||
);
|
||||
await page.getByText('History').click();
|
||||
await expect(page.locator('#view').getByText('Go shopping!!')).toBeVisible();
|
||||
await editPage.checkTextInView('Go shopping!!');
|
||||
await expect(page.getByText('Revisions')).toBeVisible();
|
||||
await expect(page.getByText('sidharthv96 v8f8f1e2')).toBeVisible();
|
||||
await expect(page.getByText('sidharthv96 v7851e19')).toBeVisible();
|
||||
@@ -48,66 +45,64 @@ test.describe('Site Loads', () => {
|
||||
expect(codeStore).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should load diagram from gist revision', async ({ page }) => {
|
||||
await page.goto(
|
||||
test('should load diagram from gist revision', async ({ editPage, page }) => {
|
||||
await editPage.start(
|
||||
'/edit?gist=https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/ec9b4ab0e41e4ff6287326cd3cb47affd7851e19'
|
||||
);
|
||||
await page.getByText('History').click();
|
||||
await expect(page.locator('#view').getByText('Party')).toBeVisible();
|
||||
await editPage.checkTextInView('Party');
|
||||
await expect(page.getByText('Revisions')).toBeVisible();
|
||||
await expect(page.getByText('sidharthv96 v7851e19')).toBeVisible();
|
||||
const codeStore = await page.evaluate(() => localStorage.getItem('codeStore'));
|
||||
expect(codeStore).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should load diagram from raw files', async ({ page }) => {
|
||||
await page.goto(
|
||||
test('should load diagram from raw files', async ({ editPage, page }) => {
|
||||
await editPage.start(
|
||||
'/edit?code=https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/code.mmd&config=https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/config.json'
|
||||
);
|
||||
await expect(page.locator('#view').getByText('Party')).toBeVisible();
|
||||
await editPage.checkTextInView('Party');
|
||||
const codeStore = await page.evaluate(() => localStorage.getItem('codeStore'));
|
||||
expect(codeStore).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('should show troubleshooting steps if loading fails', async ({ page }) => {
|
||||
await page.goto('/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAg');
|
||||
test('should show troubleshooting steps if loading fails', async ({ editPage, page }) => {
|
||||
await editPage.start('/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAg');
|
||||
await page.reload({ waitUntil: 'networkidle' });
|
||||
await expect(
|
||||
page.locator('#view').getByText('Please Click here to Raise an issue in github.')
|
||||
).toBeVisible();
|
||||
await editPage.checkTextInView('Please Click here to Raise an issue in github.');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Verify types of URLs', () => {
|
||||
test('should load compressed URL', async ({ page }) => {
|
||||
await page.goto(
|
||||
test('should load compressed URL', async ({ editPage }) => {
|
||||
await editPage.start(
|
||||
'/edit#pako:eNpVkM2KwkAQhF-l6dMK5gVyEDRxvYi7sF6WjIcm0zqDzg_jBJEk725Hd2G3Tw31VVFUj23QjCWeEkUD-1p5kFs2O77BN1M6QFEshg1ncMHzfYDV2ybA1YQYrT_NXvhqgqDqtxPGkI315_ElVU__h-cB6mZLMYd4-Kvsb2GAdWM_jcT_V0xicb03RyqPVLSUoJI-OEfHyZHV0rqfDAqzYccKS3k1pbNC5Ufhuqgp81rbHBJKxuXKc6Quh6-7b7HMqeNfqLYkC7gfanwAlW1ZvQ'
|
||||
);
|
||||
await expect(page.locator('#view').getByText('New Year')).toBeVisible();
|
||||
await page.goto(
|
||||
await editPage.checkTextInView('New Year');
|
||||
await editPage.start(
|
||||
'/edit#pako:eNptkU1PwzAMhv9K5BOI9Q9EXBDbJA477YYqITcxndV8QD40weh_Jy1rGR0-OY_tV2_sEyivCSQogzGuGduAtnaixINji0bcf1WVWGfVXdMtx8M1faYm4B8sxR27JLClJd6nwK4VLTlN4bI4jMQd2pLe3C4KFhNNcLQ92jv9ADGLNoTdozc-zIV4ZDsNlud7RtVN7_5Sb_jYrFcN3iN_0pPbEqUZK3QbTP_Ojyv4NdR4bwTHlyMbPcOQ3WJ2CliBpWCRdbnLqFJDOpClGmRJNYauhtr1pS-_6bKMjebkA8hXNJFWgDn5_YdTIFPINDWdb3vu6r8BaWOZRQ'
|
||||
);
|
||||
await expect(page.locator('#view').getByText('Animal')).toBeVisible();
|
||||
await editPage.checkTextInView('Animal');
|
||||
});
|
||||
|
||||
test.describe('Uncompressed URLs', () => {
|
||||
test('should load URL without specifier', async ({ page }) => {
|
||||
await page.goto(
|
||||
test('should load URL without specifier', async ({ editPage }) => {
|
||||
await editPage.start(
|
||||
'/edit/#eyJjb2RlIjoiZ3JhcGhcbiAgICBUZXN0TGFiZWwiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGVmYXVsdFwiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ'
|
||||
);
|
||||
await expect(page.locator('#view').getByText('TestLabel')).toBeVisible();
|
||||
await page.goto(
|
||||
await editPage.checkTextInView('TestLabel');
|
||||
await editPage.start(
|
||||
'/edit#eyJjb2RlIjoiY2xhc3NEaWFncmFtXG4gICAgQW5pbWFsIDx8LS0gRHVja1xuICAgIEFuaW1hbCA8fC0tIEZpc2hcbiAgICBBbmltYWwgPHwtLSBaZWJyYVxuICAgIEFuaW1hbCA6ICtpbnQgYWdlXG4gICAgQW5pbWFsIDogK1N0cmluZyBnZW5kZXJcbiAgICBBbmltYWw6ICtpc01hbW1hbCgpXG4gICAgQW5pbWFsOiArbWF0ZSgpXG4gICAgY2xhc3MgRHVja3tcbiAgICAgICtTdHJpbmcgYmVha0NvbG9yXG4gICAgICArc3dpbSgpXG4gICAgICArcXVhY2soKVxuICAgIH1cbiAgICBjbGFzcyBGaXNoe1xuICAgICAgLWludCBzaXplSW5GZWV0XG4gICAgICAtY2FuRWF0KClcbiAgICB9XG4gICAgY2xhc3MgWmVicmF7XG4gICAgICArYm9vbCBpc193aWxkXG4gICAgICArcnVuKClcbiAgICB9XG4gICAgICAgICAgICAiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGFya1wiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ'
|
||||
);
|
||||
await expect(page.locator('#view').getByText('Animal')).toBeVisible();
|
||||
await editPage.checkTextInView('Animal');
|
||||
});
|
||||
|
||||
test('should load URL with "base64" specifier', async ({ page }) => {
|
||||
await page.goto(
|
||||
test('should load URL with "base64" specifier', async ({ editPage }) => {
|
||||
await editPage.start(
|
||||
'/edit/#base64:eyJjb2RlIjoiZ3JhcGhcbiAgICBUZXN0TGFiZWwiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGVmYXVsdFwiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ'
|
||||
);
|
||||
await expect(page.locator('#view').getByText('TestLabel')).toBeVisible();
|
||||
await editPage.checkTextInView('TestLabel');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import { TID } from '$/constants';
|
||||
import { test as base, expect, type Locator, type Page } from '@playwright/test';
|
||||
import { verifyFileSizeGreaterThan, type EditorOptions } from './utils';
|
||||
|
||||
export class EditorPage {
|
||||
readonly editor: Locator;
|
||||
readonly markdownInput: Locator;
|
||||
readonly view: Locator;
|
||||
|
||||
constructor(readonly page: Page) {
|
||||
this.editor = page.locator('css=.monaco-editor');
|
||||
this.markdownInput = page.getByTestId(TID.copyMarkdown);
|
||||
this.view = page.locator('#view');
|
||||
}
|
||||
|
||||
async start(url = '/edit') {
|
||||
await this.page.goto(url);
|
||||
await expect(this.page)
|
||||
.toHaveURL(/.*\/edit#pako/)
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
async typeInEditor(text: string, { bottom = true, newline = false }: EditorOptions = {}) {
|
||||
await this.editor.click();
|
||||
if (bottom) {
|
||||
await this.page.keyboard.press('PageDown');
|
||||
}
|
||||
if (newline) {
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
await this.page.keyboard.type(text, { delay: 10 });
|
||||
}
|
||||
|
||||
async clearEditor() {
|
||||
await this.editor.click();
|
||||
await this.page.keyboard.press('Control+KeyA');
|
||||
await this.page.keyboard.press('Backspace');
|
||||
}
|
||||
|
||||
async toggleActions() {
|
||||
await this.page.getByText('Actions', { exact: true }).click();
|
||||
}
|
||||
|
||||
async toggleSampleDiagrams() {
|
||||
await this.page.getByText('Sample Diagrams', { exact: true }).click();
|
||||
}
|
||||
|
||||
async checkAndDownloadPNG(expectedSize: number) {
|
||||
const downloadPNGPromise = verifyFileSizeGreaterThan(this.page, 'diagram', 'png', expectedSize);
|
||||
await this.page.getByTestId(TID.downloadPNG).click();
|
||||
return await downloadPNGPromise;
|
||||
}
|
||||
|
||||
async downloadSVG(expectedSize: number) {
|
||||
const downloadSVGPromise = verifyFileSizeGreaterThan(this.page, 'diagram', 'svg', expectedSize);
|
||||
await this.page.getByTestId(TID.downloadSVG).click();
|
||||
return await downloadSVGPromise;
|
||||
}
|
||||
|
||||
async loadSampleDiagram(diagramName: string) {
|
||||
await this.page.getByText(diagramName, { exact: true }).click();
|
||||
}
|
||||
|
||||
async checkTextInView(text: string) {
|
||||
await expect(this.view).toContainText(text);
|
||||
}
|
||||
|
||||
async checkTextNotInView(text: string) {
|
||||
await expect(this.view).not.toContainText(text);
|
||||
}
|
||||
|
||||
async checkError(text: string) {
|
||||
await expect(this.page.getByTestId(TID.errorContainer)).toContainText(text);
|
||||
}
|
||||
|
||||
async checkInEditor(text: string) {
|
||||
await expect(this.editor).toContainText(text);
|
||||
}
|
||||
|
||||
async toggleComment(text: string) {
|
||||
await this.editor.getByText(text).click();
|
||||
await this.page.keyboard.press('Control+/');
|
||||
}
|
||||
|
||||
async setEditorMode(mode: 'Code' | 'Config') {
|
||||
await this.page.getByRole('tab').getByText(mode).click();
|
||||
}
|
||||
|
||||
async checkDocURL(url: string | RegExp) {
|
||||
await expect(this.page.getByTestId(TID.diagramDocumentationButton)).toHaveAttribute(
|
||||
'href',
|
||||
url
|
||||
);
|
||||
}
|
||||
|
||||
async toggleTheme() {
|
||||
await this.page.getByTestId(TID.themeToggleButton).click();
|
||||
}
|
||||
|
||||
async checkTheme(theme: 'light' | 'dark') {
|
||||
await expect(this.page.getByTestId(TID.themeToggleButton)).toHaveAttribute(
|
||||
'title',
|
||||
`Switch to ${theme === 'light' ? 'dark' : 'light'} theme`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const test = base.extend<{ editPage: EditorPage }>({
|
||||
editPage: async ({ page }, use) => {
|
||||
const editorPage = new EditorPage(page);
|
||||
await editorPage.start();
|
||||
await editorPage.toggleSampleDiagrams();
|
||||
await use(editorPage);
|
||||
}
|
||||
});
|
||||
|
||||
export { expect } from '@playwright/test';
|
||||
+12
-32
@@ -1,46 +1,26 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { test } from './test';
|
||||
|
||||
test.describe('Test themes', () => {
|
||||
test.describe('Test color scheme preference', () => {
|
||||
test('should respect prefers-color-scheme light', async ({ page }) => {
|
||||
test('should respect prefers-color-scheme light', async ({ page, editPage }) => {
|
||||
// Test light mode
|
||||
await page.emulateMedia({ colorScheme: 'light' });
|
||||
await page.goto('/edit');
|
||||
await expect(page.getByText('light').locator('..')).toHaveClass(/bordered/);
|
||||
await expect(page.getByText('dark').locator('..')).not.toHaveClass(/bordered/);
|
||||
await editPage.start();
|
||||
await editPage.checkTheme('light');
|
||||
});
|
||||
|
||||
test('should respect prefers-color-scheme dark', async ({ page }) => {
|
||||
test('should respect prefers-color-scheme dark', async ({ page, editPage }) => {
|
||||
// Test dark mode
|
||||
await page.emulateMedia({ colorScheme: 'dark' });
|
||||
await page.goto('/edit');
|
||||
await expect(page.getByText('dark').locator('..')).toHaveClass(/bordered/);
|
||||
await expect(page.getByText('light').locator('..')).not.toHaveClass(/bordered/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Test light themes', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1440, height: 768 });
|
||||
await page.goto('/edit');
|
||||
await page.getByText('Theme').click();
|
||||
await editPage.start();
|
||||
await editPage.checkTheme('dark');
|
||||
});
|
||||
|
||||
test('should set light theme as default', async ({ page }) => {
|
||||
await expect(page.getByText('light').locator('..')).toHaveClass(/bordered/);
|
||||
await expect(page.getByText('dark').locator('..')).not.toHaveClass(/bordered/);
|
||||
const themeStore = await page.evaluate(() => localStorage.getItem('themeStore'));
|
||||
expect(themeStore).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should change themes when clicked', async ({ page }) => {
|
||||
await expect(page.getByText('light').locator('..')).toHaveClass(/bordered/);
|
||||
await page.getByText('cupcake').click();
|
||||
await expect(page.getByText('cupcake').locator('..')).toHaveClass(/bordered/);
|
||||
await expect(page.getByText('light').locator('..')).not.toHaveClass(/bordered/);
|
||||
await expect(page.getByText('dark').locator('..')).not.toHaveClass(/bordered/);
|
||||
const themeStore = await page.evaluate(() => localStorage.getItem('themeStore'));
|
||||
expect(themeStore).toBeTruthy();
|
||||
test('should change themes when clicked', async ({ editPage }) => {
|
||||
await editPage.toggleTheme();
|
||||
await editPage.checkTheme('dark');
|
||||
await editPage.toggleTheme();
|
||||
await editPage.checkTheme('light');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -21,8 +21,8 @@ export async function typeInEditor(
|
||||
text: string,
|
||||
{ bottom = true, newline = false }: EditorOptions = {}
|
||||
) {
|
||||
await page.locator('#editor').click();
|
||||
const editor = page.locator('#editor');
|
||||
const editor = page.locator('css=.monaco-editor');
|
||||
await editor.click();
|
||||
|
||||
if (bottom) {
|
||||
await editor.locator('textarea').press('PageDown');
|
||||
|
||||
+24
-3
@@ -1,11 +1,32 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { svelteTesting } from '@testing-library/svelte/vite';
|
||||
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
|
||||
import Icons from 'unplugin-icons/vite';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
/**
|
||||
* HMR creates state inconsistencies, so we always reload the page.
|
||||
* @type {import('vite').PluginOption} PluginOption
|
||||
*/
|
||||
const alwaysFullReload = {
|
||||
name: 'always-full-reload',
|
||||
handleHotUpdate({ server }) {
|
||||
server.ws.send({ type: 'full-reload' });
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit(), svelteTesting()],
|
||||
plugins: [
|
||||
sveltekit(),
|
||||
Icons({
|
||||
compiler: 'svelte',
|
||||
customCollections: {
|
||||
custom: FileSystemIconLoader('./static/icons')
|
||||
}
|
||||
}),
|
||||
alwaysFullReload
|
||||
],
|
||||
envPrefix: 'MERMAID_',
|
||||
optimizeDeps: { include: ['mermaid'] },
|
||||
server: {
|
||||
port: 3000,
|
||||
host: true
|
||||
|
||||
Reference in New Issue
Block a user