diff --git a/.env b/.env index c9bdfa82..d0a789a3 100644 --- a/.env +++ b/.env @@ -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 diff --git a/.eslintrc.cjs b/.eslintrc.cjs index bc6f10b9..8300399a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -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, diff --git a/.prettierrc b/.prettierrc index e0366fc9..a0a85589 100644 --- a/.prettierrc +++ b/.prettierrc @@ -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"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 4d231aa1..5c2a6e7b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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 } diff --git a/components.json b/components.json new file mode 100644 index 00000000..4398a0f7 --- /dev/null +++ b/components.json @@ -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" +} diff --git a/package.json b/package.json index 408e7ffc..d9e43dd5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/playwright.config.ts b/playwright.config.ts index 89202d15..8386822f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -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 }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a6e4407..93265e3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/app.html b/src/app.html index 827bd6bc..229ff125 100644 --- a/src/app.html +++ b/src/app.html @@ -17,7 +17,7 @@ %sveltekit.head% - +
%sveltekit.body%
diff --git a/src/app.postcss b/src/app.postcss index 1f611bac..f5df0bf3 100644 --- a/src/app.postcss +++ b/src/app.postcss @@ -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 { diff --git a/src/lib/components/Actions.svelte b/src/lib/components/Actions.svelte index 8e703412..a1773723 100644 --- a/src/lib/components/Actions.svelte +++ b/src/lib/components/Actions.svelte @@ -1,23 +1,37 @@ - -
- {#if isClipboardAvailable()} - - {/if} - - - {#if rendererUrl} - - - - - - - {/if} - {#if krokiRendererUrl} - - - +{#snippet dualActionButton(text: string, download: (event: Event) => unknown, url?: string)} +
+ + {#if url} + {/if} +
+{/snippet} -
+ +
+
PNG size - - - - - - - {#if imagemodeselected !== 'auto'} - + + Auto + Width + Height + + {#if imageSizeMode !== 'auto'} + + {/if} + +
+
+ {@render dualActionButton('PNG', onDownloadPNG, $urlsStore.png)} + {@render dualActionButton('SVG', onDownloadSVG, $urlsStore.svg)} + {#if env.krokiRendererUrl} + + + {/if}
- - {#if rendererUrl} -
- - -
+ + {#if isClipboardAvailable()} + + {/if} + {#if $urlsStore.mdCode} + {/if}
- - + +
{#if isNetlify}
diff --git a/src/lib/components/Card/Card.svelte b/src/lib/components/Card/Card.svelte index dbc92dc7..3cc2f078 100644 --- a/src/lib/components/Card/Card.svelte +++ b/src/lib/components/Card/Card.svelte @@ -1,26 +1,35 @@ -
+
{#if isOpen} -
- {@render children?.()} +
+ {@render children()}
{/if}
diff --git a/src/lib/components/Card/Tabs.svelte b/src/lib/components/Card/Tabs.svelte index 6796ed8a..363644f6 100644 --- a/src/lib/components/Card/Tabs.svelte +++ b/src/lib/components/Card/Tabs.svelte @@ -1,19 +1,15 @@ -
- - {#if isClosable} - - {/if} - {title} - - {#if isOpen && tabs} -
    - {#each tabs as tab} - - {/each} -
- {/if} -
+
+
    + {#each tabs as tab, index} + - + {#if index < tabs.length - 1} +
    + +
    + {/if} + {/each} +
+
diff --git a/src/lib/components/Card/card.test.ts b/src/lib/components/Card/card.test.ts deleted file mode 100644 index 8c79fc41..00000000 --- a/src/lib/components/Card/card.test.ts +++ /dev/null @@ -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'); - }); -}); diff --git a/src/lib/components/CopyButton.svelte b/src/lib/components/CopyButton.svelte new file mode 100644 index 00000000..0ccb3c74 --- /dev/null +++ b/src/lib/components/CopyButton.svelte @@ -0,0 +1,41 @@ + + + diff --git a/src/lib/components/CopyInput.svelte b/src/lib/components/CopyInput.svelte new file mode 100644 index 00000000..e3eeac8f --- /dev/null +++ b/src/lib/components/CopyInput.svelte @@ -0,0 +1,25 @@ + + +
+ { + event.currentTarget.setSelectionRange(0, event.currentTarget.value.length); + }} /> + + copyToClipboard(value)} {label} /> +
diff --git a/src/lib/components/DiagramDocumentationButton.svelte b/src/lib/components/DiagramDocumentationButton.svelte new file mode 100644 index 00000000..b2449c01 --- /dev/null +++ b/src/lib/components/DiagramDocumentationButton.svelte @@ -0,0 +1,109 @@ + + + diff --git a/src/lib/components/DropdownNavMenu.svelte b/src/lib/components/DropdownNavMenu.svelte index abc1881f..3ba8beb8 100644 --- a/src/lib/components/DropdownNavMenu.svelte +++ b/src/lib/components/DropdownNavMenu.svelte @@ -1,34 +1,28 @@ - + + diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index ff29e56c..e6c25350 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -1,14 +1,19 @@ -
+
{#if $stateStore.error instanceof Error} -
-
- -

Diagram syntax error

+
+
+
+
+ + +
- +
{$stateStore.error?.toString()}
diff --git a/src/lib/components/FloatingToolbar.svelte b/src/lib/components/FloatingToolbar.svelte new file mode 100644 index 00000000..128078b8 --- /dev/null +++ b/src/lib/components/FloatingToolbar.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/lib/components/History/History.svelte b/src/lib/components/History/History.svelte index 811b1668..2f2b3ba0 100644 --- a/src/lib/components/History/History.svelte +++ b/src/lib/components/History/History.svelte @@ -1,6 +1,4 @@ - + {#snippet actions()} -
- + onclick={uploadHistory} + title="Upload history"> {#if $historyStore.length > 0} - + size="icon" + variant="ghost" + onclick={downloadHistory} + title="Download history"> {/if} - | - + size="icon" + variant="ghost" + onclick={() => saveHistory()} + title="Save current state"> {#if $historyModeStore !== 'loader'} - + size="icon" + variant="ghost" + class="hover:text-destructive" + onclick={() => clearHistory()} + title="Delete all saved states"> {/if}
{/snippet} -
    +
      {#if $historyStore.length > 0} - {#each $historyStore as { id, state, time, name, url, type }} -
    • -
    • +
      +
      + {#if url} + {name} + {:else} + {name} + {/if} + + {new Date(time).toLocaleString()} +
      -
      - + +
      + + {dayjs(time).fromNow()} + + {#if type !== 'loader'} - + {/if}
      +
    • {/each} {:else} -
      +
      No items in History
      Click the Save button to save current state and restore it later.
      Timeline will automatically be saved every minute. diff --git a/src/lib/components/History/history.ts b/src/lib/components/History/history.ts index 97cf5ea4..8965def3 100644 --- a/src/lib/components/History/history.ts +++ b/src/lib/components/History/history.ts @@ -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; diff --git a/src/lib/components/MainMenu.svelte b/src/lib/components/MainMenu.svelte new file mode 100644 index 00000000..35d31815 --- /dev/null +++ b/src/lib/components/MainMenu.svelte @@ -0,0 +1,83 @@ + + +{#snippet menuItem(options: { label: string; icon: Component; href: string; class?: string })} + + + {options.label} + +{/snippet} + + + + Mermaid Live Editor + + + {#each menuItems as item} + {@render menuItem(item)} + {/each} + +
      + + + Dark Mode + + setMode(dark ? 'dark' : 'light')} /> +
      + + {#each mermaidChartMenuItems as item} + + {@render menuItem({ + ...item, + class: 'text-accent bg-background hover:bg-muted' + })} + + {/each} +
      +
      diff --git a/src/lib/components/McWrapper.svelte b/src/lib/components/McWrapper.svelte new file mode 100644 index 00000000..5fedf0b1 --- /dev/null +++ b/src/lib/components/McWrapper.svelte @@ -0,0 +1,50 @@ + + +{#if env.isEnabledMermaidChartLinks} + + + +
      + {@render children()} +
      +
      + +
      + {#if shouldDisableComponent} +
      + This diagram type is not supported in MermaidChart.com +
      + {:else} + + + {labelPrefix} +
      MermaidChart.com
      +
      + {/if} +
      +
      +
      +
      +{/if} diff --git a/src/lib/components/MermaidChartIcon.svelte b/src/lib/components/MermaidChartIcon.svelte new file mode 100644 index 00000000..98c54110 --- /dev/null +++ b/src/lib/components/MermaidChartIcon.svelte @@ -0,0 +1,7 @@ + + +Mermaid Chart diff --git a/src/lib/components/Navbar.svelte b/src/lib/components/Navbar.svelte index 50ad17e0..93964362 100644 --- a/src/lib/components/Navbar.svelte +++ b/src/lib/components/Navbar.svelte @@ -1,29 +1,34 @@ - + + + + + + + + + diff --git a/src/lib/components/Preset.svelte b/src/lib/components/Preset.svelte index 8d5f6498..43447ea6 100644 --- a/src/lib/components/Preset.svelte +++ b/src/lib/components/Preset.svelte @@ -1,20 +1,30 @@ - -
      + +
      {#each diagramOrder as sample} - + {/each}
      diff --git a/src/lib/components/Privacy.svelte b/src/lib/components/Privacy.svelte index 3644076a..6567def9 100644 --- a/src/lib/components/Privacy.svelte +++ b/src/lib/components/Privacy.svelte @@ -1,31 +1,46 @@ - + - - + + diff --git a/src/lib/components/Share.svelte b/src/lib/components/Share.svelte new file mode 100644 index 00000000..b0dddf5c --- /dev/null +++ b/src/lib/components/Share.svelte @@ -0,0 +1,48 @@ + + + + Share + + + + Shareable links + + Share your diagrams with others. + + +
      +
      +

      + Mermaid Live Editor + Mermaid Live Editor +

      + + + The content of the diagrams you create never leaves your browser. + +
      + {#if env.isEnabledMermaidChartLinks} + +
      +

      + + Mermaid Chart Playground +

      + + + Opens the Mermaid Chart Playground with Mermaid AI, Visual Editor, and more. + +
      + {/if} +
      +
      +
      diff --git a/src/lib/components/SyncRoughToolbar.svelte b/src/lib/components/SyncRoughToolbar.svelte new file mode 100644 index 00000000..62b4be58 --- /dev/null +++ b/src/lib/components/SyncRoughToolbar.svelte @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/src/lib/components/Theme.svelte b/src/lib/components/Theme.svelte deleted file mode 100644 index f60c09ac..00000000 --- a/src/lib/components/Theme.svelte +++ /dev/null @@ -1,66 +0,0 @@ - - - diff --git a/src/lib/components/ThemeIcon.svelte b/src/lib/components/ThemeIcon.svelte new file mode 100644 index 00000000..2a072a20 --- /dev/null +++ b/src/lib/components/ThemeIcon.svelte @@ -0,0 +1,39 @@ + + +
      + {#key $mode} +
      + {#if $mode === 'dark'} + + {:else} + + {/if} +
      + {/key} +
      diff --git a/src/lib/components/VersionSecurityToolbar.svelte b/src/lib/components/VersionSecurityToolbar.svelte new file mode 100644 index 00000000..14b319bf --- /dev/null +++ b/src/lib/components/VersionSecurityToolbar.svelte @@ -0,0 +1,28 @@ + + + + v{version} + + + + + diff --git a/src/lib/components/View.svelte b/src/lib/components/View.svelte index 0fb12dd7..e0702f64 100644 --- a/src/lib/components/View.svelte +++ b/src/lib/components/View.svelte @@ -1,56 +1,39 @@ -{#if outOfSync} -
      - Diagram out of sync.
      - {#if $stateStore.autoSync} - It will be updated automatically. - {:else} - Press (Sync button) or {cmdKey} + Enter to sync. - {/if} -
      -{/if} - -
      +
      diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte new file mode 100644 index 00000000..88920c9b --- /dev/null +++ b/src/lib/components/ui/button/button.svelte @@ -0,0 +1,69 @@ + + + + +{#if href} + + {@render children?.()} + +{:else} + +{/if} diff --git a/src/lib/components/ui/button/index.ts b/src/lib/components/ui/button/index.ts new file mode 100644 index 00000000..b79e44a8 --- /dev/null +++ b/src/lib/components/ui/button/index.ts @@ -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'; diff --git a/src/lib/components/ui/dialog/dialog-content.svelte b/src/lib/components/ui/dialog/dialog-content.svelte new file mode 100644 index 00000000..b1de6663 --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-content.svelte @@ -0,0 +1,36 @@ + + + + + + {@render children?.()} + + + Close + + + diff --git a/src/lib/components/ui/dialog/dialog-description.svelte b/src/lib/components/ui/dialog/dialog-description.svelte new file mode 100644 index 00000000..6d2b04fd --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,15 @@ + + + diff --git a/src/lib/components/ui/dialog/dialog-footer.svelte b/src/lib/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 00000000..88af15c0 --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,19 @@ + + +
      + {@render children?.()} +
      diff --git a/src/lib/components/ui/dialog/dialog-header.svelte b/src/lib/components/ui/dialog/dialog-header.svelte new file mode 100644 index 00000000..0bc0d2ad --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,19 @@ + + +
      + {@render children?.()} +
      diff --git a/src/lib/components/ui/dialog/dialog-overlay.svelte b/src/lib/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 00000000..07932582 --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/ui/dialog/dialog-title.svelte b/src/lib/components/ui/dialog/dialog-title.svelte new file mode 100644 index 00000000..5da14f94 --- /dev/null +++ b/src/lib/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,15 @@ + + + diff --git a/src/lib/components/ui/dialog/index.ts b/src/lib/components/ui/dialog/index.ts new file mode 100644 index 00000000..c3483281 --- /dev/null +++ b/src/lib/components/ui/dialog/index.ts @@ -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 +}; diff --git a/src/lib/components/ui/input/index.ts b/src/lib/components/ui/input/index.ts new file mode 100644 index 00000000..be0783a3 --- /dev/null +++ b/src/lib/components/ui/input/index.ts @@ -0,0 +1,7 @@ +import Root from './input.svelte'; + +export { + Root, + // + Root as Input +}; diff --git a/src/lib/components/ui/input/input.svelte b/src/lib/components/ui/input/input.svelte new file mode 100644 index 00000000..5d1c70e3 --- /dev/null +++ b/src/lib/components/ui/input/input.svelte @@ -0,0 +1,43 @@ + + +{#if type === 'file'} + +{:else} + +{/if} diff --git a/src/lib/components/ui/popover/index.ts b/src/lib/components/ui/popover/index.ts new file mode 100644 index 00000000..244795d8 --- /dev/null +++ b/src/lib/components/ui/popover/index.ts @@ -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 +}; diff --git a/src/lib/components/ui/popover/popover-content.svelte b/src/lib/components/ui/popover/popover-content.svelte new file mode 100644 index 00000000..abe2949f --- /dev/null +++ b/src/lib/components/ui/popover/popover-content.svelte @@ -0,0 +1,27 @@ + + + + + diff --git a/src/lib/components/ui/resizable/index.ts b/src/lib/components/ui/resizable/index.ts new file mode 100644 index 00000000..927c86d7 --- /dev/null +++ b/src/lib/components/ui/resizable/index.ts @@ -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 +}; diff --git a/src/lib/components/ui/resizable/resizable-handle.svelte b/src/lib/components/ui/resizable/resizable-handle.svelte new file mode 100644 index 00000000..22755a57 --- /dev/null +++ b/src/lib/components/ui/resizable/resizable-handle.svelte @@ -0,0 +1,29 @@ + + +div]:rotate-90', + className + )} + {...restProps}> + {#if withHandle} +
      + +
      + {/if} +
      diff --git a/src/lib/components/ui/resizable/resizable-pane-group.svelte b/src/lib/components/ui/resizable/resizable-pane-group.svelte new file mode 100644 index 00000000..c798226b --- /dev/null +++ b/src/lib/components/ui/resizable/resizable-pane-group.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/ui/separator/index.ts b/src/lib/components/ui/separator/index.ts new file mode 100644 index 00000000..2402e13e --- /dev/null +++ b/src/lib/components/ui/separator/index.ts @@ -0,0 +1,7 @@ +import Root from './separator.svelte'; + +export { + Root, + // + Root as Separator +}; diff --git a/src/lib/components/ui/separator/separator.svelte b/src/lib/components/ui/separator/separator.svelte new file mode 100644 index 00000000..a1590bf4 --- /dev/null +++ b/src/lib/components/ui/separator/separator.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/ui/sonner/index.ts b/src/lib/components/ui/sonner/index.ts new file mode 100644 index 00000000..fcaf06bf --- /dev/null +++ b/src/lib/components/ui/sonner/index.ts @@ -0,0 +1 @@ +export { default as Toaster } from './sonner.svelte'; diff --git a/src/lib/components/ui/sonner/sonner.svelte b/src/lib/components/ui/sonner/sonner.svelte new file mode 100644 index 00000000..06609dcb --- /dev/null +++ b/src/lib/components/ui/sonner/sonner.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/switch/index.ts b/src/lib/components/ui/switch/index.ts new file mode 100644 index 00000000..7fdb93ac --- /dev/null +++ b/src/lib/components/ui/switch/index.ts @@ -0,0 +1,7 @@ +import Root from './switch.svelte'; + +export { + Root, + // + Root as Switch +}; diff --git a/src/lib/components/ui/switch/switch.svelte b/src/lib/components/ui/switch/switch.svelte new file mode 100644 index 00000000..158e1836 --- /dev/null +++ b/src/lib/components/ui/switch/switch.svelte @@ -0,0 +1,25 @@ + + + + + diff --git a/src/lib/components/ui/toggle-group/index.ts b/src/lib/components/ui/toggle-group/index.ts new file mode 100644 index 00000000..1ee4c602 --- /dev/null +++ b/src/lib/components/ui/toggle-group/index.ts @@ -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 +}; diff --git a/src/lib/components/ui/toggle-group/toggle-group-item.svelte b/src/lib/components/ui/toggle-group/toggle-group-item.svelte new file mode 100644 index 00000000..087ccba3 --- /dev/null +++ b/src/lib/components/ui/toggle-group/toggle-group-item.svelte @@ -0,0 +1,29 @@ + + + diff --git a/src/lib/components/ui/toggle-group/toggle-group.svelte b/src/lib/components/ui/toggle-group/toggle-group.svelte new file mode 100644 index 00000000..027ba590 --- /dev/null +++ b/src/lib/components/ui/toggle-group/toggle-group.svelte @@ -0,0 +1,40 @@ + + + + + + diff --git a/src/lib/components/ui/toggle/index.ts b/src/lib/components/ui/toggle/index.ts new file mode 100644 index 00000000..4a0d9d7e --- /dev/null +++ b/src/lib/components/ui/toggle/index.ts @@ -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 +}; diff --git a/src/lib/components/ui/toggle/toggle.svelte b/src/lib/components/ui/toggle/toggle.svelte new file mode 100644 index 00000000..6cc3e5a4 --- /dev/null +++ b/src/lib/components/ui/toggle/toggle.svelte @@ -0,0 +1,50 @@ + + + + + diff --git a/src/lib/components/ui/tooltip/index.ts b/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 00000000..eae8afaf --- /dev/null +++ b/src/lib/components/ui/tooltip/index.ts @@ -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 +}; diff --git a/src/lib/components/ui/tooltip/tooltip-content.svelte b/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 00000000..8402ed93 --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/constants.ts b/src/lib/constants.ts new file mode 100644 index 00000000..85e0b108 --- /dev/null +++ b/src/lib/constants.ts @@ -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; diff --git a/src/lib/types.d.ts b/src/lib/types.d.ts index 7092203d..8e49b1cc 100644 --- a/src/lib/types.d.ts +++ b/src/lib/types.d.ts @@ -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; diff --git a/src/lib/util/autoSync.ts b/src/lib/util/autoSync.ts index 7b7faf06..ca5557e6 100644 --- a/src/lib/util/autoSync.ts +++ b/src/lib/util/autoSync.ts @@ -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) { diff --git a/src/lib/util/env.ts b/src/lib/util/env.ts index c9961211..28350049 100644 --- a/src/lib/util/env.ts +++ b/src/lib/util/env.ts @@ -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; diff --git a/src/lib/util/fileLoaders/gist.ts b/src/lib/util/fileLoaders/gist.ts index 8875e4b9..828a07e7 100644 --- a/src/lib/util/fileLoaders/gist.ts +++ b/src/lib/util/fileLoaders/gist.ts @@ -61,12 +61,12 @@ const getGistData = async (gistURL: string): Promise => { } 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 => { 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; diff --git a/src/lib/util/fileLoaders/loader.ts b/src/lib/util/fileLoaders/loader.ts index ce2ea348..137b26c9 100644 --- a/src/lib/util/fileLoaders/loader.ts +++ b/src/lib/util/fileLoaders/loader.ts @@ -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 = { gist: loadGistData @@ -27,14 +27,14 @@ export const loadDataFromUrl = async (): Promise => { } 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 => { loaded && updateCodeStore({ ...state, - autoSync: true, updateDiagram: true }); }; diff --git a/src/lib/util/mermaid.ts b/src/lib/util/mermaid.ts index c4a5c6c5..12693c10 100644 --- a/src/lib/util/mermaid.ts +++ b/src/lib/util/mermaid.ts @@ -18,6 +18,24 @@ export const render = async ( return await mermaid.render(id, code); }; -export const parse = async (code: string): Promise => { +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; + } + } +}; diff --git a/src/lib/util/monacoExtra.ts b/src/lib/util/monacoExtra.ts index a0b4db76..e54d1777 100644 --- a/src/lib/util/monacoExtra.ts +++ b/src/lib/util/monacoExtra.ts @@ -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: [ diff --git a/src/lib/util/notify.ts b/src/lib/util/notify.ts index 3c9e6ded..5c1c7023 100644 --- a/src/lib/util/notify.ts +++ b/src/lib/util/notify.ts @@ -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 => { diff --git a/src/lib/util/panZoom.ts b/src/lib/util/panZoom.ts new file mode 100644 index 00000000..97844bd6 --- /dev/null +++ b/src/lib/util/panZoom.ts @@ -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) { + 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; + } +} diff --git a/src/lib/util/persist.ts b/src/lib/util/persist.ts index 2c63b9a2..dfdbaa13 100644 --- a/src/lib/util/persist.ts +++ b/src/lib/util/persist.ts @@ -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)); } diff --git a/src/lib/util/promos/January2025.svelte b/src/lib/util/promos/January2025.svelte index 339a4fb2..f59dd24a 100644 --- a/src/lib/util/promos/January2025.svelte +++ b/src/lib/util/promos/January2025.svelte @@ -1,4 +1,5 @@ -
      - {@render children?.()} + + + +
      + {@render children()}
      {#if $loadingStateStore.loading} diff --git a/src/routes/edit/+page.svelte b/src/routes/edit/+page.svelte index e8c19b47..ad91fe2d 100644 --- a/src/routes/edit/+page.svelte +++ b/src/routes/edit/+page.svelte @@ -1,263 +1,112 @@
      - + + + + + + + + + +
      - - - diff --git a/src/routes/view/+page.svelte b/src/routes/view/+page.svelte index b2d25334..0f915b09 100644 --- a/src/routes/view/+page.svelte +++ b/src/routes/view/+page.svelte @@ -1,6 +1,6 @@ @@ -9,4 +9,4 @@ - + diff --git a/src/tests/setup.ts b/src/tests/setup.ts index b2e0c4d6..2240083c 100644 --- a/src/tests/setup.ts +++ b/src/tests/setup.ts @@ -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(() => { diff --git a/static/icons/code.svg b/static/icons/code.svg new file mode 100644 index 00000000..84006ab9 --- /dev/null +++ b/static/icons/code.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/static/mermaidchart-logo.svg b/static/mermaidchart-logo.svg index d7c8b59e..040245d2 100755 --- a/static/mermaidchart-logo.svg +++ b/static/mermaidchart-logo.svg @@ -1 +1,4 @@ - + + + + diff --git a/svelte.config.js b/svelte.config.js index 0180e1c2..7d17fce0 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -11,6 +11,9 @@ const config = { }) ], kit: { + alias: { + '$/*': './src/lib/*' + }, adapter: adapter({ pages: 'docs' }) diff --git a/tailwind.config.cjs b/tailwind.config.cjs deleted file mode 100644 index 18cb32c1..00000000 --- a/tailwind.config.cjs +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - plugins: [require('daisyui')], - content: ['./src/**/*.{html,js,svelte,ts}'], - theme: { - extend: {} - }, - variants: { - extend: {} - } -}; diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 00000000..8eb91f36 --- /dev/null +++ b/tailwind.config.js @@ -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) / )', + input: 'hsl(var(--input) / )', + ring: 'hsl(var(--ring) / )', + background: 'hsl(var(--background) / )', + foreground: 'hsl(var(--foreground) / )', + primary: { + DEFAULT: 'hsl(var(--primary) / )', + foreground: 'hsl(var(--primary-foreground) / )' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary) / )', + foreground: 'hsl(var(--secondary-foreground) / )' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive) / )', + foreground: 'hsl(var(--destructive-foreground) / )' + }, + muted: { + DEFAULT: 'hsl(var(--muted) / )', + foreground: 'hsl(var(--muted-foreground) / )' + }, + accent: { + DEFAULT: 'hsl(var(--accent) / )', + foreground: 'hsl(var(--accent-foreground) / )' + }, + popover: { + DEFAULT: 'hsl(var(--popover) / )', + foreground: 'hsl(var(--popover-foreground) / )' + }, + card: { + DEFAULT: 'hsl(var(--card) / )', + foreground: 'hsl(var(--card-foreground) / )' + }, + 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; diff --git a/tests/actions.spec.ts b/tests/actions.spec.ts index f710e55c..4f9aae94 100644 --- a/tests/actions.spec.ts +++ b/tests/actions.spec.ts @@ -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); diff --git a/tests/diagramUpdate.spec.ts b/tests/diagramUpdate.spec.ts index aaa1dfec..08a1d778 100644 --- a/tests/diagramUpdate.spec.ts +++ b/tests/diagramUpdate.spec.ts @@ -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'); }); }); diff --git a/tests/docMap.spec.ts b/tests/docMap.spec.ts index d7c35daf..649ac56a 100644 --- a/tests/docMap.spec.ts +++ b/tests/docMap.spec.ts @@ -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/); }); }); diff --git a/tests/history.spec.ts b/tests/history.spec.ts index 6b0d003d..0b81569e 100644 --- a/tests/history.spec.ts +++ b/tests/history.spec.ts @@ -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', { diff --git a/tests/loadSite.spec.ts b/tests/loadSite.spec.ts index fff88111..1e78791f 100644 --- a/tests/loadSite.spec.ts +++ b/tests/loadSite.spec.ts @@ -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'); }); }); }); diff --git a/tests/test.ts b/tests/test.ts new file mode 100644 index 00000000..3c4fec2c --- /dev/null +++ b/tests/test.ts @@ -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'; diff --git a/tests/themes.spec.ts b/tests/themes.spec.ts index 55c7f905..42c3774c 100644 --- a/tests/themes.spec.ts +++ b/tests/themes.spec.ts @@ -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'); }); }); }); diff --git a/tests/utils.ts b/tests/utils.ts index 0c4864c1..f2842c9a 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -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'); diff --git a/vite.config.js b/vite.config.js index 2aabcaa0..55510981 100644 --- a/vite.config.js +++ b/vite.config.js @@ -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