From 25b3bea49f874b13bdb31bb56d09b81e28d10197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Z?= Date: Tue, 27 Oct 2026 00:09:40 +0100 Subject: [PATCH 01/17] feat: render safe lazy Mermaid diagrams in commit blocks Preserve Mermaid source in PHP and TypeScript body segments; render diagrams only when near the viewport using strict Mermaid mode and an inert SVG image. Keep native code-block controls, escaped fallback, source copy and zoom. Add parser and React tests, docs and a pinned Bun lockfile. Validation: 4/4 fork CI; full 4530 PHP tests; live Chromium desktop/mobile proof at https://github.com/Xauele/dev-null.sh/actions/runs/40416945958 --- KNOWLEDGE_BASE.md | 5 + app/Services/CodeSpans.php | 18 +- bun.lock | 219 ++++++++++++++++ package.json | 1 + .../js/components/chat/message-parts.tsx | 1 + resources/js/components/commit-body.tsx | 21 ++ resources/js/components/mermaid-diagram.tsx | 245 ++++++++++++++++++ resources/js/lib/code.ts | 5 +- resources/js/lib/mermaid-renderer.ts | 86 ++++++ resources/js/types/commit.ts | 2 +- tests/Unit/MermaidDiagram.test.tsx | 87 +++++++ tests/Unit/Services/CodeSpansTest.php | 13 + 12 files changed, 698 insertions(+), 5 deletions(-) create mode 100644 resources/js/components/mermaid-diagram.tsx create mode 100644 resources/js/lib/mermaid-renderer.ts create mode 100644 tests/Unit/MermaidDiagram.test.tsx diff --git a/KNOWLEDGE_BASE.md b/KNOWLEDGE_BASE.md index 2bfc962a5..f65944f1a 100644 --- a/KNOWLEDGE_BASE.md +++ b/KNOWLEDGE_BASE.md @@ -204,6 +204,11 @@ Sources: [translation endpoint](app/Http/Controllers/CommitTranslationController Commit and reply text supports inline code in backticks and fenced code blocks with syntax highlighting, a **Wrap lines** toggle, and a copy control. Text outside code supports emoji shortcodes, clickable URLs, and `@username` mentions. This is developer-oriented text formatting, not a full rich-text editor. +A fenced `mermaid` block in a commit or reply renders as a diagram when it approaches the viewport, including in the draft preview. Switch to **Show source** to read or copy the exact diagram definition; zoom controls appear on rendered diagrams. The diagram engine is loaded on demand, not with every log page. Invalid and oversized definitions (more than 6,000 characters) remain readable as source rather than disappearing. Rendering runs in Mermaid's strict security mode and its SVG is shown as an inert image, not inserted as live HTML. Static excerpts and private Remotes keep ordinary code blocks. No images or other diagram data are saved separately. + +Sources: [diagram component](resources/js/components/mermaid-diagram.tsx), [lazy renderer](resources/js/lib/mermaid-renderer.ts), [source parser](app/Services/CodeSpans.php). + + In commits, replies, and remotes, start a line with `>` to render a quote block. Consecutive quoted lines stay together; an unquoted line ends the block. Quotes keep inline code, links, mentions, and emoji formatting. A `>` inside code or in the middle of a line stays literal. Typing **@** in a composer opens developer suggestions. Use ↑/↓ to move, Enter or Tab to select, and Esc to dismiss. A bare @ suggests people you follow; typing more searches names and usernames. In replies, matching suggestions put the direct recipient first, then the root commit author, then other visible participants in that thread, followed by ordinary suggestions. Quotes prioritize only the quoted commit author. These priorities also apply to a bare @; typing a term still filters everyone, and yourself, muted developers, and blocked developers are excluded. In an org's messages, suggestions search the loaded member list instantly and update as membership refreshes. Suggestions are limited to the org's members, but you can still type anyone's handle yourself. Mentions link to profiles and can notify the mentioned developer after approval. Email addresses and scoped packages such as `@inertiajs/react` are not treated as mentions. diff --git a/app/Services/CodeSpans.php b/app/Services/CodeSpans.php index 3c1a2f9a4..27e27f2ff 100644 --- a/app/Services/CodeSpans.php +++ b/app/Services/CodeSpans.php @@ -76,7 +76,7 @@ public function withoutCode(string $text): string * Splits the text into prose, inline code and highlighted code blocks. The highlighter escapes the code, so a * block's HTML is safe to render as is. The line breaks around a block are dropped, since it is a block already. * - * @return list + * @return list */ public function highlight(string $text): array { @@ -124,17 +124,29 @@ private function isBlock(?string $part): bool } /** - * @return array{type: 'block', language: string|null, html: string} + * @return array{type: 'block', language: string|null, html: string, source?: string} */ private function block(string $part): array { $lines = explode("\n", str_replace("\r\n", "\n", $part)); $language = strtok(mb_ltrim($lines[0], '`'), " \t") ?: null; + $source = implode("\n", array_slice($lines, 1, -1)); + + // Preserve exact Mermaid source instead of sending highlighted HTML to the diagram renderer. + // The regular HTML field stays escaped for previews and non-interactive surfaces. + if ($language !== null && mb_strtolower($language) === 'mermaid') { + return [ + 'type' => 'block', + 'language' => $language, + 'html' => htmlspecialchars($source, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), + 'source' => $source, + ]; + } return [ 'type' => 'block', 'language' => $language, - 'html' => $this->highlighter->parse(implode("\n", array_slice($lines, 1, -1)), $language === null ? null : mb_strtolower($language)), + 'html' => $this->highlighter->parse($source, $language === null ? null : mb_strtolower($language)), ]; } } diff --git a/bun.lock b/bun.lock index 7d60285e7..fd97d9820 100644 --- a/bun.lock +++ b/bun.lock @@ -27,6 +27,7 @@ "clsx": "^2.1.1", "laravel-vite-plugin": "^3.2.0", "lucide-react": "^1.61.0", + "mermaid": "11.17.2", "react": "^19.3.1", "react-dom": "^19.3.1", "sonner": "^2.0.8", @@ -64,6 +65,8 @@ "packages": { "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.1", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-cUD0PT0YX+rImRhfn7BfMRduP/tX/Zqt/N0dHlAyn2RlcJssCb5KsCJkhln61eh9vuJDkH8wKn3L8zJzzWXPFg=="], + "@antfu/install-pkg": ["@antfu/install-pkg@2.1.0", "", { "dependencies": { "package-manager-detector": "^1.8.0", "tinyexec": "^1.3.1" } }, "sha512-sdg9NxU3zR4Mnawfbc/x6GB5Wf17WYud5qOuEuxXjaKpYpMkISSJEjItGebXJ2bQ4DIcly4NYH23mtkGJjvKUw=="], + "@babel/code-frame": ["@babel/code-frame@8.0.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^8.0.6", "js-tokens": "^10.0.0" } }, "sha512-uMHhvmDmwFiLuiozaoRYTZHqMFuu3fem/X9JY78lohAuLoh8LSGGC8JqUzo2cVuBsBxkjMZNk56CE7ock8Xxkw=="], "@babel/compat-data": ["@babel/compat-data@8.0.5", "", {}, "sha512-YLsYoQMvL8l8WrGpN3Zj7O1wK5LEBN+cQtux7BcuHyxIXve724XG+zuJ1n3U1cUweRtTzQOA4IHbuQw3N34SZw=="], @@ -96,8 +99,12 @@ "@blazediff/core": ["@blazediff/core@1.10.0", "", {}, "sha512-AOQff0zgR7cGsZL+4E7hVkmujoPUpm0J9xzWGWZj5wCjd3gmxESXAPfKyuzs93VdpQNFhHlBhfOjrcZ+XTERtQ=="], + "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], + "@capsizecss/unpack": ["@capsizecss/unpack@4.0.1", "", { "dependencies": { "fontkitten": "^1.0.3" } }, "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ=="], + "@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="], + "@date-fns/tz": ["@date-fns/tz@1.5.0", "", {}, "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg=="], "@daypicker/react": ["@daypicker/react@10.0.2", "", { "dependencies": { "react-day-picker": "10.0.2" }, "peerDependencies": { "@types/react": ">=16.8.0", "react": ">=16.8.0" }, "optionalPeers": ["@types/react"] }, "sha512-Z/gvGXnc/xjRr5Z6l9OhIey00C7kwbIyJ85Pff9B9EWDkjTlkTQztB3qeylmGJDQ9xmxRJUV1W+02rskqarUBw=="], @@ -114,6 +121,10 @@ "@headlessui/react": ["@headlessui/react@2.2.10", "", { "dependencies": { "@floating-ui/react": "^0.26.16", "@react-aria/focus": "^3.20.2", "@react-aria/interactions": "^3.25.0", "@tanstack/react-virtual": "^3.13.9", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA=="], + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + + "@iconify/utils": ["@iconify/utils@3.1.7", "", { "dependencies": { "@antfu/install-pkg": "^2.0.1", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-JZHlwdID+dy+lTgbYC8NEC4zeugqeYsc6jewvzb4c58kHauJn+X7rNwQjxz5p2qSjqaEeQoLkCIQ9v/H4PK0/w=="], + "@inertiajs/core": ["@inertiajs/core@3.9.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "es-toolkit": "^1.33.0", "laravel-precognition": "^2.1.0" }, "peerDependencies": { "axios": "^1.15.2" }, "optionalPeers": ["axios"] }, "sha512-ScLQZ1/KXlvmqCEJMEZABSK5updJZIli1kaG/coWXpWMmpSLsazSJkR7esITYp7X5vTgrOGNIrcRmdMp/GQ0NA=="], "@inertiajs/react": ["@inertiajs/react@3.9.1", "", { "dependencies": { "@inertiajs/core": "3.9.1", "es-toolkit": "^1.33.0", "laravel-precognition": "^2.1.0" }, "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" } }, "sha512-fRWElF5XIm1j01ecQCEehx0KPLEYVA6fCdWvig7XUhwhmSQG6EIyNum3eG8ZEfYuY7mWhNyqae/MSRIv+bAsjw=="], @@ -138,6 +149,8 @@ "@laravel/vite-plugin-wayfinder": ["@laravel/vite-plugin-wayfinder@0.1.11", "", {}, "sha512-KU7g7+2aVs3OzvdD9RtZaIwh2Bydk/bCzjFDz8G+G6QfloTJpmgF9jTJrrlyqGspi8DhgNYGzSReMwHQKY2OCg=="], + "@mermaid-js/parser": ["@mermaid-js/parser@1.2.1", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw=="], + "@oxc-project/runtime": ["@oxc-project/runtime@0.155.0", "", {}, "sha512-VbW9raczGS5ZZ1FMunUBYHe4uV/R684qlQCoMdE3n5JT1PcqQsvmFpMYkF8AD+W04fBLGAq6Tr7YsTk0jjvDBA=="], "@oxc-project/types": ["@oxc-project/types@0.155.0", "", {}, "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q=="], @@ -392,12 +405,76 @@ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], + + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], + + "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], + + "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], + + "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], + + "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], + + "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], + + "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], + + "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], + + "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], + + "@types/d3-geo": ["@types/d3-geo@3.1.1", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w=="], + + "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], + + "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], + + "@types/d3-random": ["@types/d3-random@3.0.4", "", {}, "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], + + "@types/d3-selection": ["@types/d3-selection@3.0.12", "", {}, "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw=="], + + "@types/d3-shape": ["@types/d3-shape@3.2.0", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], + + "@types/d3-zoom": ["@types/d3-zoom@3.0.9", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/gensync": ["@types/gensync@1.0.5", "", {}, "sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg=="], + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + "@types/jsesc": ["@types/jsesc@2.5.1", "", {}, "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw=="], "@types/node": ["@types/node@26.6.5", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg=="], @@ -406,6 +483,8 @@ "@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], @@ -446,6 +525,8 @@ "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], + "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.1.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "oxc-transform-react": "^0.152.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler", "oxc-transform-react"] }, "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw=="], "@vitest/browser": ["@vitest/browser@5.0.5", "", { "dependencies": { "@blazediff/core": "1.10.0", "@vitest/mocker": "5.0.5", "@vitest/ui": "5.0.5", "@vitest/utils": "5.0.5", "magic-string": "^1.4.3", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.2.0", "ws": "^8.22.0" }, "peerDependencies": { "vitest": "5.0.5" } }, "sha512-IDMoZX/iZ8rkilw7weaSgN8tJ7iZ9hUv0wYoKXyiyjWjWFwLSWAY707DcVTgxTYOudWC1E3HA9LYn7kVa9a7uQ=="], @@ -552,12 +633,90 @@ "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], + "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "cytoscape": ["cytoscape@3.34.4", "", {}, "sha512-msY99tMY8iWTg+sIPGfSqDcPNOt4xcj+v/K/7qUFaCUxxlqnMkUYLBjRUzn9W5CU/R2hrmtJa5+UtovrEM2ctg=="], + + "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], + + "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], + + "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], + + "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], + + "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], + + "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], + + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + + "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], + + "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], + + "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], + + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], + + "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], + + "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], + + "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], + + "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], + + "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], + + "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + + "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], + "date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="], + "dayjs": ["dayjs@1.11.23", "", {}, "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ=="], + + "delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], @@ -566,6 +725,8 @@ "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + "dompurify": ["dompurify@3.4.17", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w=="], + "electron-to-chromium": ["electron-to-chromium@1.5.456", "", {}, "sha512-8hboqI6xesozteFb8aa7OGknvxUtTeRJLz0FoePysUEh/oWbddykIGdgpEJRbPRg/nIMfBf2DFos+H2MtvJl2A=="], "empathic": ["empathic@2.1.0", "", {}, "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw=="], @@ -586,6 +747,8 @@ "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], + "fastdom": ["fastdom@1.0.12", "", { "dependencies": { "strictdom": "^1.0.1" } }, "sha512-LB+xjSTEbjHE1cWsxu+tN2Xqr1kpi+V9aADI7sVM5ZMaXyYGPHULQMzpJMYqOTULK/73pUkWVzzObFRBkPr+hg=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], @@ -608,12 +771,18 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="], "ink": ["ink@7.1.1", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.3", "auto-bind": "^5.0.1", "chalk": "^5.6.2", "cli-boxes": "^4.0.1", "cli-cursor": "^4.0.0", "cli-truncate": "^6.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.45.1", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^9.0.0", "stack-utils": "^2.0.6", "string-width": "^8.2.0", "terminal-size": "^4.0.1", "type-fest": "^5.5.0", "widest-line": "^6.0.0", "wrap-ansi": "^10.0.0", "ws": "^8.20.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.2.0", "react": ">=19.2.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-Y43xxa1ZSPvpmfLHcN5o+OdP8Rf8ykkNJEuKYOUNZKT8wXVNLFTtEm1nSDMQkfBH+YANF4Xuu0hhZ4ejqAtN2w=="], + "internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], "is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="], @@ -626,10 +795,16 @@ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "katex": ["katex@0.16.47", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="], + + "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], + "laravel-precognition": ["laravel-precognition@2.1.0", "", { "dependencies": { "es-toolkit": "^1.32.0" }, "peerDependencies": { "axios": "^1.4.0" }, "optionalPeers": ["axios"] }, "sha512-W9ZobjRAReiIawSWxXUPR6q+Bvq5seO6tn+scdURl/JKOFKveXvR4Sno2KxGqQt2cI81nh8Vt2cFlaOMqXqLNw=="], "laravel-vite-plugin": ["laravel-vite-plugin@3.2.0", "", { "dependencies": { "picocolors": "^1.0.0", "tinyglobby": "^0.2.12", "vite-plugin-full-reload": "^1.1.0" }, "peerDependencies": { "fontaine": "^0.8.0", "vite": "^8.0.0" }, "optionalPeers": ["fontaine"], "bin": { "clean-orphaned-assets": "bin/clean.js" } }, "sha512-xSxY9Gzeb/eancd8WeK09piAFP+a6i5QIBqNCKNv9L0Eq6wziwzSem7F1GvMSrtjMh5F/QVKFxn8t9naGOA66A=="], + "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], @@ -654,14 +829,20 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], + "lucide-react": ["lucide-react@1.61.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-v2SQPPPNjnr+BlhL9Spuzmo5yR+hLkftfuE4E3jaxdNPgSH0hJKPcLJBP7yAta87jQNPsSERS59PJjCJ/CorBA=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], "magic-string": ["magic-string@1.4.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ=="], + "marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + "mermaid": ["mermaid@11.17.2", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.2.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.34.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.21", "dompurify": "^3.3.3", "es-toolkit": "^1.45.1", "fastdom": "1.0.12", "katex": "^0.16.47", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-V6K3C8EBdEsPFZXSKMJe6ppQOENxuHARr9GvHX4hh47lAbhMRD9qf4oEK7LoaRQxULMa80/qt5gHO73aCleBBg=="], + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], @@ -682,8 +863,12 @@ "oxlint-tsgolint": ["oxlint-tsgolint@7.0.2004", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "7.0.2004", "@oxlint-tsgolint/darwin-x64": "7.0.2004", "@oxlint-tsgolint/linux-arm64": "7.0.2004", "@oxlint-tsgolint/linux-x64": "7.0.2004", "@oxlint-tsgolint/win32-arm64": "7.0.2004", "@oxlint-tsgolint/win32-x64": "7.0.2004" }, "bin": { "tsgolint": "./bin/tsgolint.js" } }, "sha512-vQ9WDF3fiZJvRdSDosNOflerOCOpwol6vWzVDxW76RcU19cjTh7engvkHLvmv+tzedR8fAiJUVUGr6IVZdkzXQ=="], + "package-manager-detector": ["package-manager-detector@1.9.0", "", {}, "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA=="], + "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], + "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -696,6 +881,10 @@ "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], + "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], + + "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], + "postcss": ["postcss@8.5.30", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA=="], "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], @@ -722,8 +911,16 @@ "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], + "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], + "rolldown": ["rolldown@1.2.13", "", { "dependencies": { "@oxc-project/types": "=0.153.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.13", "@rolldown/binding-android-arm64": "1.2.13", "@rolldown/binding-darwin-arm64": "1.2.13", "@rolldown/binding-darwin-x64": "1.2.13", "@rolldown/binding-freebsd-x64": "1.2.13", "@rolldown/binding-linux-arm-gnueabihf": "1.2.13", "@rolldown/binding-linux-arm64-gnu": "1.2.13", "@rolldown/binding-linux-arm64-musl": "1.2.13", "@rolldown/binding-linux-ppc64-gnu": "1.2.13", "@rolldown/binding-linux-s390x-gnu": "1.2.13", "@rolldown/binding-linux-x64-gnu": "1.2.13", "@rolldown/binding-linux-x64-musl": "1.2.13", "@rolldown/binding-openharmony-arm64": "1.2.13", "@rolldown/binding-win32-arm64-msvc": "1.2.13", "@rolldown/binding-win32-x64-msvc": "1.2.13" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ=="], + "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], + + "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="], "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], @@ -740,10 +937,14 @@ "std-env": ["std-env@4.3.0", "", {}, "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag=="], + "strictdom": ["strictdom@1.0.1", "", {}, "sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg=="], + "string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="], "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "stylis": ["stylis@4.4.0", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="], + "tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="], "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], @@ -770,6 +971,8 @@ "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + "ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], @@ -790,6 +993,8 @@ "use-sync-external-store": ["use-sync-external-store@1.7.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A=="], + "uuid": ["uuid@14.0.3", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Sc5u1VVln93y1Xnz4R+CunOZc7R9LUcTmhj7Rc7Eg3HVcf7rVn2FOoa812fe0W8p3jD7xeoGSYbfqCGGH2B6xw=="], + "verkit": ["verkit@0.5.0", "", {}, "sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g=="], "vite": ["@voidzero-dev/vite-plus-core@1.4.0", "", { "dependencies": { "@oxc-project/runtime": "=0.155.0", "@oxc-project/types": "=0.155.0", "lightningcss": "^1.33.0", "postcss": "^8.5.30", "yuku-parser": "0.19.0" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "1.4.0", "@voidzero-dev/vite-plus-darwin-x64": "1.4.0", "@voidzero-dev/vite-plus-linux-arm64-gnu": "1.4.0", "@voidzero-dev/vite-plus-linux-arm64-musl": "1.4.0", "@voidzero-dev/vite-plus-linux-x64-gnu": "1.4.0", "@voidzero-dev/vite-plus-linux-x64-musl": "1.4.0", "@voidzero-dev/vite-plus-win32-arm64-msvc": "1.4.0", "@voidzero-dev/vite-plus-win32-x64-msvc": "1.4.0", "fsevents": "~2.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@types/node": "^20.19.0 || >=22.12.0", "@typescript/native-preview": "*", "@vitejs/devtools": "^0.7.1 || ^0.8.0", "@volar/typescript": "~2.4.0", "@vue/language-core": "~3.2.0 || ~3.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "publint": "^0.3.8", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0", "unplugin-unused": ">=0.5.0", "unrun": "*", "vue-tsc": "~3.2.0 || ~3.3.0", "yaml": "^2.4.2" }, "optionalPeers": ["@arethetypeswrong/core", "@types/node", "@typescript/native-preview", "@vitejs/devtools", "@volar/typescript", "@vue/language-core", "esbuild", "jiti", "less", "publint", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "typescript", "unplugin-unused", "unrun", "vue-tsc", "yaml"] }, "sha512-LTGXCdZ58qJKfuCOmf0hnZOVA64eOgzDQOqYpI+71tPxk/0cGqPQhYDGQQZoEvNLAn1G2Lo4gKadsQdWxb99WQ=="], @@ -854,10 +1059,20 @@ "@testing-library/dom/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], + + "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + + "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], + "ink/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "ink/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + "pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -919,5 +1134,9 @@ "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], "@testing-library/dom/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], + + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], } } diff --git a/package.json b/package.json index 0fed34885..0deee8bec 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "clsx": "^2.1.1", "laravel-vite-plugin": "^3.2.0", "lucide-react": "^1.61.0", + "mermaid": "11.17.2", "react": "^19.3.1", "react-dom": "^19.3.1", "sonner": "^2.0.8", diff --git a/resources/js/components/chat/message-parts.tsx b/resources/js/components/chat/message-parts.tsx index 604c0758b..49a51186f 100644 --- a/resources/js/components/chat/message-parts.tsx +++ b/resources/js/components/chat/message-parts.tsx @@ -48,6 +48,7 @@ export function MessageText({ segments={segments} mentions={message.mentions ?? null} className={cn('flow-root leading-snug', { [onPrimary]: isMine })} + renderDiagrams={false} /> ); } diff --git a/resources/js/components/commit-body.tsx b/resources/js/components/commit-body.tsx index fac23d0a0..903cf859a 100644 --- a/resources/js/components/commit-body.tsx +++ b/resources/js/components/commit-body.tsx @@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from 'react'; import { CertificateText } from '@/components/certificate-text'; import { CodeBlock } from '@/components/code-block'; import { LinkifiedText } from '@/components/linkified-text'; +import { MermaidDiagram } from '@/components/mermaid-diagram'; import { bodyBlocks } from '@/lib/code'; import { emojify } from '@/lib/emoji'; import { cn } from '@/lib/utils'; @@ -13,11 +14,13 @@ export function CommitBody({ mentions, className, isStatic = false, + renderDiagrams = true, }: { segments: BodySegment[]; mentions: Commit['mentions']; className?: string; isStatic?: boolean; + renderDiagrams?: boolean; }) { return (
) : ( @@ -45,6 +49,7 @@ export function CommitBody({ segments={block.segments} mentions={mentions} isStatic={isStatic} + renderDiagrams={renderDiagrams} /> ), )} @@ -56,10 +61,12 @@ export function BodySegments({ segments, mentions, isStatic = false, + renderDiagrams = true, }: { segments: BodySegment[]; mentions: Commit['mentions']; isStatic?: boolean; + renderDiagrams?: boolean; }) { const { auth } = usePage().props; @@ -87,6 +94,20 @@ export function BodySegments({ ); case 'block': + if ( + renderDiagrams && + !isStatic && + segment.language?.toLowerCase() === 'mermaid' && + segment.source !== undefined + ) { + return ( + + ); + } + return ( ('light'); + + useEffect(() => { + const update = () => + setTheme( + document.documentElement.classList.contains('dark') + ? 'dark' + : 'light', + ); + update(); + + const observer = new MutationObserver(update); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'], + }); + + return () => observer.disconnect(); + }, []); + + return theme; +} + +/** + * Existing code-block geometry and colors, with an inert SVG image instead of injected SVG + * markup. Invalid, large or unsupported diagrams keep their exact copyable source. + */ +export function MermaidDiagram({ source }: { source: string }) { + const root = useRef(null); + const [nearViewport, setNearViewport] = useState(false); + const [view, setView] = useState<'diagram' | 'source'>('diagram'); + const [imageUrl, setImageUrl] = useState(null); + const [error, setError] = useState(false); + const [zoom, setZoom] = useState(100); + const [copied, copy] = useClipboard(); + const theme = useDiagramTheme(); + const tooLarge = source.length > MAX_DIAGRAM_LENGTH; + + useEffect(() => { + const element = root.current; + + if (!element) { + return; + } + + if (!('IntersectionObserver' in window)) { + setNearViewport(true); + + return; + } + + const observer = new IntersectionObserver( + (entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + setNearViewport(true); + observer.disconnect(); + } + }, + { rootMargin: '240px' }, + ); + + observer.observe(element); + + return () => observer.disconnect(); + }, []); + + useEffect(() => { + if (!nearViewport || tooLarge) { + return; + } + + let disposed = false; + let url: string | null = null; + + setImageUrl(null); + setError(false); + + void renderMermaid(source, theme) + .then((svg) => { + if (disposed) { + return; + } + + url = URL.createObjectURL( + new Blob([svg], { type: 'image/svg+xml' }), + ); + setImageUrl(url); + }) + .catch(() => { + if (!disposed) { + setError(true); + setView('source'); + } + }); + + return () => { + disposed = true; + + if (url) { + URL.revokeObjectURL(url); + } + }; + }, [source, theme, nearViewport, tooLarge]); + + const copySource = () => { + void copy(source); + }; + + const showSource = view === 'source' || error || tooLarge; + const status = tooLarge + ? 'Diagram exceeds the 6,000-character limit. Source is preserved.' + : error + ? 'This diagram could not be rendered. Source is preserved.' + : 'Loading diagram…'; + + return ( +
+
+ + mermaid + +
+ + +
+
+ + {showSource ? ( + <> + {(error || tooLarge) && ( +

+ {status} +

+ )} +
+                        {source}
+                    
+ + ) : imageUrl === null ? ( +
+ {status} +
+ ) : ( + <> +
+ {/* An SVG in an img has no script, DOM access or interactive links. */} + Mermaid diagram. Use Show source to read its definition. +
+
+ + + {zoom}% + + + +
+ + )} +
+ ); +} diff --git a/resources/js/lib/code.ts b/resources/js/lib/code.ts index fece440b2..62e7d9245 100644 --- a/resources/js/lib/code.ts +++ b/resources/js/lib/code.ts @@ -25,10 +25,13 @@ function block(part: string): BodySegment { const lines = part.replaceAll('\r\n', '\n').split('\n'); const language = lines[0].replace(/^`+/, '').trim().split(/[ \t]/)[0]; + const source = lines.slice(1, -1).join('\n'); + return { type: 'block', language: language === '' ? null : language, - html: escapeHtml(lines.slice(1, -1).join('\n')), + html: escapeHtml(source), + ...(language.toLowerCase() === 'mermaid' ? { source } : {}), }; } diff --git a/resources/js/lib/mermaid-renderer.ts b/resources/js/lib/mermaid-renderer.ts new file mode 100644 index 000000000..9a33665e5 --- /dev/null +++ b/resources/js/lib/mermaid-renderer.ts @@ -0,0 +1,86 @@ +/** + * Mermaid stays out of the normal feed bundle. The renderer is loaded only for a diagram + * approaching the viewport, and its SVG is displayed as an inert image, never as DOM HTML. + */ +export const MAX_DIAGRAM_LENGTH = 6000; + +type DiagramTheme = 'light' | 'dark'; +type Mermaid = (typeof import('mermaid'))['default']; + +let engine: Promise | null = null; +let renderQueue: Promise = Promise.resolve(); +let nextId = 0; + +function loadMermaid(): Promise { + engine ??= import('mermaid').then(({ default: mermaid }) => mermaid); + + return engine; +} + +/** + * Mermaid writes temporary DOM nodes during rendering. Its shared configuration is global, + * so serialize renders to avoid cross-diagram races and theme bleeding. + */ +export function renderMermaid( + source: string, + theme: DiagramTheme, +): Promise { + if (!source.trim() || source.length > MAX_DIAGRAM_LENGTH) { + return Promise.reject( + new Error('Diagram source is empty or too long.'), + ); + } + + const task = renderQueue.then(async (): Promise => { + const mermaid = await loadMermaid(); + + mermaid.initialize({ + startOnLoad: false, + securityLevel: 'strict', + maxTextSize: MAX_DIAGRAM_LENGTH, + maxEdges: 80, + theme: 'base', + flowchart: { htmlLabels: false }, + themeVariables: + theme === 'dark' + ? { + background: '#18181a', + primaryColor: '#232326', + primaryTextColor: '#ececec', + primaryBorderColor: '#57575c', + lineColor: '#7aa2f7', + secondaryColor: '#232326', + tertiaryColor: '#18181a', + fontFamily: 'Geist, sans-serif', + } + : { + background: '#ffffff', + primaryColor: '#eef0f2', + primaryTextColor: '#121417', + primaryBorderColor: '#8a929e', + lineColor: '#3b6fe0', + secondaryColor: '#eef0f2', + tertiaryColor: '#ffffff', + fontFamily: 'Geist, sans-serif', + }, + }); + + if (!(await mermaid.parse(source, { suppressErrors: true }))) { + throw new Error('Invalid Mermaid syntax.'); + } + + const rendered = await mermaid.render( + `devnull-diagram-${++nextId}`, + source, + ); + + return rendered.svg; + }); + + renderQueue = task.then( + () => undefined, + () => undefined, + ); + + return task; +} diff --git a/resources/js/types/commit.ts b/resources/js/types/commit.ts index 8af6a4e68..bd23f1341 100644 --- a/resources/js/types/commit.ts +++ b/resources/js/types/commit.ts @@ -50,7 +50,7 @@ export type CommitImage = { export type BodySegment = | { type: 'text'; text: string } | { type: 'code'; code: string } - | { type: 'block'; language: string | null; html: string }; + | { type: 'block'; language: string | null; html: string; source?: string }; export type CommitTranslation = { language: string; diff --git a/tests/Unit/MermaidDiagram.test.tsx b/tests/Unit/MermaidDiagram.test.tsx new file mode 100644 index 000000000..540e90c00 --- /dev/null +++ b/tests/Unit/MermaidDiagram.test.tsx @@ -0,0 +1,87 @@ +/// +import assert from 'node:assert/strict'; +import { App } from '@inertiajs/react'; +import type { ComponentProps, ReactElement } from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { test } from 'vite-plus/test'; +import { CommitBody } from '../../resources/js/components/commit-body'; +import { MermaidDiagram } from '../../resources/js/components/mermaid-diagram'; +import { bodySegments } from '../../resources/js/lib/code'; + +function renderPage(content: ReactElement): string { + return renderToStaticMarkup( + content} + initialPage={ + { + component: 'Commit', + url: '/', + version: null, + props: { errors: {}, auth: { user: null, pasteis: null } }, + clearHistory: false, + encryptHistory: false, + } as unknown as ComponentProps['initialPage'] + } + />, + ); +} + +const source = 'flowchart LR\nA[] --> B[okay]'; +const segments = bodySegments('Before\n```MERMAID\n' + source + '\n```\nAfter'); + +test('preserves Mermaid source verbatim alongside an escaped fallback', () => { + const mermaid = segments.find((segment) => segment.type === 'block'); + assert.ok(mermaid && mermaid.type === 'block'); + assert.equal(mermaid.language, 'MERMAID'); + assert.equal(mermaid.source, source); + assert.ok(mermaid.html.includes('<script>')); +}); + +test('server-renders a native diagram placeholder without executing or injecting SVG', () => { + const markup = renderPage(); + assert.ok(markup.includes('data-test="mermaid-diagram"')); + assert.ok(markup.includes('Loading diagram')); + assert.ok(!markup.includes(' & welcome] --> B[Done]'; + + expect(resolve(CodeSpans::class)->highlight("```{$language}\n{$source}\n```"))->toBe([ + [ + 'type' => 'block', + 'language' => $language, + 'html' => 'flowchart LR'."\n".'A[<script>alert(1)</script> & welcome] --> B[Done]', + 'source' => $source, + ], + ]); +})->with(['mermaid', 'MERMAID']); From 5f36d27c53c81048383341856be4fde996902032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Z?= Date: Tue, 27 Oct 2026 01:09:19 +0100 Subject: [PATCH 02/17] feat(mermaid): align diagram palette with the native dev-null colors --- resources/js/lib/mermaid-theme.ts | 105 ++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 resources/js/lib/mermaid-theme.ts diff --git a/resources/js/lib/mermaid-theme.ts b/resources/js/lib/mermaid-theme.ts new file mode 100644 index 000000000..12bb33dbc --- /dev/null +++ b/resources/js/lib/mermaid-theme.ts @@ -0,0 +1,105 @@ +/** + * Diagram colors follow resources/css/app.css: + * blue is the site's primary action, green/purple/amber are existing lane colors. + * Distinct fills are intentionally muted, with readable labels in both modes. + */ +export type DiagramTheme = 'light' | 'dark'; + +const palettes = { + dark: { + canvas: '#18181a', + text: '#ececec', + primary: '#202b40', + primaryBorder: '#7aa2f7', + secondary: '#1d3026', + secondaryBorder: '#51cf66', + tertiary: '#30243c', + tertiaryBorder: '#da77f2', + decision: '#362c1d', + decisionBorder: '#fcc419', + line: '#91a2b9', + note: '#382d1c', + noteBorder: '#caa34a', + noteText: '#f2e6ca', + pie: ['#7aa2f7', '#51cf66', '#da77f2', '#ff8a4c', '#3bc9db', '#f06595', '#fcc419'], + }, + light: { + canvas: '#ffffff', + text: '#121417', + primary: '#e8effc', + primaryBorder: '#3b6fe0', + secondary: '#e7f5eb', + secondaryBorder: '#2f9e44', + tertiary: '#f3eafa', + tertiaryBorder: '#9c36b5', + decision: '#fff4dc', + decisionBorder: '#b7791f', + line: '#718199', + note: '#fff4dc', + noteBorder: '#b7791f', + noteText: '#513c1d', + pie: ['#3b6fe0', '#2f9e44', '#9c36b5', '#d9480f', '#0c8599', '#c2255c', '#b7791f'], + }, +} as const; + +export function mermaidTheme(theme: DiagramTheme) { + const palette = palettes[theme]; + + return { + darkMode: theme === 'dark', + background: palette.canvas, + mainBkg: palette.primary, + primaryColor: palette.primary, + primaryTextColor: palette.text, + primaryBorderColor: palette.primaryBorder, + secondaryColor: palette.secondary, + secondaryTextColor: palette.text, + secondaryBorderColor: palette.secondaryBorder, + tertiaryColor: palette.tertiary, + tertiaryTextColor: palette.text, + tertiaryBorderColor: palette.tertiaryBorder, + textColor: palette.text, + lineColor: palette.line, + nodeBorder: palette.primaryBorder, + nodeTextColor: palette.text, + clusterBkg: palette.tertiary, + clusterBorder: palette.tertiaryBorder, + edgeLabelBackground: palette.canvas, + noteBkgColor: palette.note, + noteBorderColor: palette.noteBorder, + noteTextColor: palette.noteText, + actorBkg: palette.primary, + actorBorder: palette.primaryBorder, + actorTextColor: palette.text, + actorLineColor: palette.line, + signalColor: palette.line, + signalTextColor: palette.text, + activationBkgColor: palette.secondary, + activationBorderColor: palette.secondaryBorder, + fontFamily: 'Geist, ui-sans-serif, system-ui, sans-serif', + fontSize: '15px', + pie1: palette.pie[0], + pie2: palette.pie[1], + pie3: palette.pie[2], + pie4: palette.pie[3], + pie5: palette.pie[4], + pie6: palette.pie[5], + pie7: palette.pie[6], + }; +} + +/** + * Only default decision diamonds get an amber tint. Explicit author-supplied + * Mermaid classes keep their own styling, so colors retain semantic meaning. + * The CSS is embedded into the generated inert SVG, never into the host DOM. + */ +export function mermaidThemeCSS(theme: DiagramTheme): string { + const palette = palettes[theme]; + + return [ + '.node.default polygon {', + ' fill: ' + palette.decision + ' !important;', + ' stroke: ' + palette.decisionBorder + ' !important;', + '}', + ].join('\n'); +} From 649f9ddc7f81929230f7372e3fbaa439c189c281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Z?= Date: Tue, 27 Oct 2026 01:09:34 +0100 Subject: [PATCH 03/17] feat(mermaid): apply native light/dark lane palette to rendered diagrams --- resources/js/lib/mermaid-renderer.ts | 29 +++++----------------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/resources/js/lib/mermaid-renderer.ts b/resources/js/lib/mermaid-renderer.ts index 9a33665e5..3a200ad03 100644 --- a/resources/js/lib/mermaid-renderer.ts +++ b/resources/js/lib/mermaid-renderer.ts @@ -2,9 +2,10 @@ * Mermaid stays out of the normal feed bundle. The renderer is loaded only for a diagram * approaching the viewport, and its SVG is displayed as an inert image, never as DOM HTML. */ -export const MAX_DIAGRAM_LENGTH = 6000; +import { mermaidTheme, mermaidThemeCSS } from '@/lib/mermaid-theme'; +import type { DiagramTheme } from '@/lib/mermaid-theme'; -type DiagramTheme = 'light' | 'dark'; +export const MAX_DIAGRAM_LENGTH = 6000; type Mermaid = (typeof import('mermaid'))['default']; let engine: Promise | null = null; @@ -41,28 +42,8 @@ export function renderMermaid( maxEdges: 80, theme: 'base', flowchart: { htmlLabels: false }, - themeVariables: - theme === 'dark' - ? { - background: '#18181a', - primaryColor: '#232326', - primaryTextColor: '#ececec', - primaryBorderColor: '#57575c', - lineColor: '#7aa2f7', - secondaryColor: '#232326', - tertiaryColor: '#18181a', - fontFamily: 'Geist, sans-serif', - } - : { - background: '#ffffff', - primaryColor: '#eef0f2', - primaryTextColor: '#121417', - primaryBorderColor: '#8a929e', - lineColor: '#3b6fe0', - secondaryColor: '#eef0f2', - tertiaryColor: '#ffffff', - fontFamily: 'Geist, sans-serif', - }, + themeVariables: mermaidTheme(theme), + themeCSS: mermaidThemeCSS(theme), }); if (!(await mermaid.parse(source, { suppressErrors: true }))) { From 76e4d8a4052650d7c377eeae29cf0642cbe6fa78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Z?= Date: Tue, 27 Oct 2026 01:10:24 +0100 Subject: [PATCH 04/17] feat(mermaid): native code-block polish and readable scrolling on mobile --- resources/js/components/mermaid-diagram.tsx | 59 +++++++++++++++++---- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/resources/js/components/mermaid-diagram.tsx b/resources/js/components/mermaid-diagram.tsx index 844216ff7..23c84eaf6 100644 --- a/resources/js/components/mermaid-diagram.tsx +++ b/resources/js/components/mermaid-diagram.tsx @@ -3,15 +3,17 @@ import { Code2, Copy, Image, + GitBranch, + ArrowLeftRight, Minus, Plus, RotateCcw, } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { useClipboard } from '@/hooks/use-clipboard'; +import { useMediaQuery } from '@/hooks/use-media-query'; import { MAX_DIAGRAM_LENGTH, renderMermaid } from '@/lib/mermaid-renderer'; - -type DiagramTheme = 'light' | 'dark'; +import type { DiagramTheme } from '@/lib/mermaid-theme'; function useDiagramTheme(): DiagramTheme { const [theme, setTheme] = useState('light'); @@ -48,6 +50,8 @@ export function MermaidDiagram({ source }: { source: string }) { const [imageUrl, setImageUrl] = useState(null); const [error, setError] = useState(false); const [zoom, setZoom] = useState(100); + const [isWide, setIsWide] = useState(false); + const isSmallScreen = useMediaQuery('(max-width: 639px)'); const [copied, copy] = useClipboard(); const theme = useDiagramTheme(); const tooLarge = source.length > MAX_DIAGRAM_LENGTH; @@ -97,6 +101,16 @@ export function MermaidDiagram({ source }: { source: string }) { return; } + // Horizontal charts must stay readable on phones instead of shrinking + // their labels to a few pixels. Keep vertical charts fitted to the feed. + const viewBox = svg.match(/viewBox="([^"]+)"/)?.[1] + ?.trim() + .split(/[\\s,]+/) + .map(Number); + const width = viewBox?.[2] ?? 0; + const height = viewBox?.[3] ?? 0; + setIsWide(height > 0 && width / height > 1.6); + url = URL.createObjectURL( new Blob([svg], { type: 'image/svg+xml' }), ); @@ -136,7 +150,8 @@ export function MermaidDiagram({ source }: { source: string }) { className="my-2 min-w-0 overflow-hidden rounded-md border bg-muted/60" >
- + + mermaid
@@ -148,7 +163,8 @@ export function MermaidDiagram({ source }: { source: string }) { aria-label={showSource ? 'Show diagram' : 'Show source'} aria-pressed={showSource} disabled={error || tooLarge} - className="grid size-6 place-items-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-40" + title={showSource ? 'Show diagram' : 'Show source'} + className="grid size-6 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:opacity-40" > {showSource ? ( @@ -160,7 +176,8 @@ export function MermaidDiagram({ source }: { source: string }) { type="button" onClick={copySource} aria-label="Copy Mermaid source" - className="grid size-6 place-items-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring" + title="Copy Mermaid source" + className="grid size-6 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring" > {copied ? ( @@ -194,16 +211,39 @@ export function MermaidDiagram({ source }: { source: string }) {
) : ( <> -
+
{/* An SVG in an img has no script, DOM access or interactive links. */} Mermaid diagram. Use Show source to read its definition.
-
+
+ + {isWide && isSmallScreen && ( + <> + + Scroll to explore + + )} + +
+
)} From 4928de08a4dd3b878108fa14cfcd15719e28d40a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Z?= Date: Tue, 27 Oct 2026 01:10:47 +0100 Subject: [PATCH 05/17] fix(mermaid): parse SVG viewBox correctly for responsive diagrams --- resources/js/components/mermaid-diagram.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/js/components/mermaid-diagram.tsx b/resources/js/components/mermaid-diagram.tsx index 23c84eaf6..100d197f4 100644 --- a/resources/js/components/mermaid-diagram.tsx +++ b/resources/js/components/mermaid-diagram.tsx @@ -105,7 +105,7 @@ export function MermaidDiagram({ source }: { source: string }) { // their labels to a few pixels. Keep vertical charts fitted to the feed. const viewBox = svg.match(/viewBox="([^"]+)"/)?.[1] ?.trim() - .split(/[\\s,]+/) + .split(/[\s,]+/) .map(Number); const width = viewBox?.[2] ?? 0; const height = viewBox?.[3] ?? 0; @@ -215,7 +215,7 @@ export function MermaidDiagram({ source }: { source: string }) { role="region" aria-label="Diagram viewport" tabIndex={0} - className="max-h-[440px] min-h-32 overflow-auto overscroll-contain bg-card px-3 py-4 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring sm:px-4 sm:py-5" + className="max-h-[440px] min-h-32 overflow-auto overscroll-contain bg-card px-3 py-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring sm:px-4 sm:py-5" style={{ backgroundImage: 'radial-gradient(circle, color-mix(in srgb, var(--border) 55%, transparent) 0.8px, transparent 1px)', backgroundSize: '20px 20px', From 514e56eb06571a0a325a5ed572d4d0b4ac1ecef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Z?= Date: Tue, 27 Oct 2026 01:11:29 +0100 Subject: [PATCH 06/17] test(mermaid): lock native light-dark palette and semantic decision colors --- tests/Unit/MermaidDiagram.test.tsx | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/Unit/MermaidDiagram.test.tsx b/tests/Unit/MermaidDiagram.test.tsx index 540e90c00..a396a2575 100644 --- a/tests/Unit/MermaidDiagram.test.tsx +++ b/tests/Unit/MermaidDiagram.test.tsx @@ -7,6 +7,7 @@ import { test } from 'vite-plus/test'; import { CommitBody } from '../../resources/js/components/commit-body'; import { MermaidDiagram } from '../../resources/js/components/mermaid-diagram'; import { bodySegments } from '../../resources/js/lib/code'; +import { mermaidTheme, mermaidThemeCSS } from '../../resources/js/lib/mermaid-theme'; function renderPage(content: ReactElement): string { return renderToStaticMarkup( @@ -85,3 +86,39 @@ test('shows long diagrams as escaped copyable source, without a dead view switch assert.ok(markup.includes('<svg onload=alert(1)>')); assert.ok(!markup.includes(' { + const dark = mermaidTheme('dark'); + const light = mermaidTheme('light'); + + assert.equal(dark.background, '#18181a'); + assert.equal(dark.primaryBorderColor, '#7aa2f7'); + assert.equal(dark.secondaryBorderColor, '#51cf66'); + assert.equal(dark.tertiaryBorderColor, '#da77f2'); + assert.equal(dark.darkMode, true); + assert.equal(light.background, '#ffffff'); + assert.equal(light.primaryBorderColor, '#3b6fe0'); + assert.equal(light.darkMode, false); + + for (const palette of [dark, light]) { + assert.notEqual(palette.primaryColor, palette.secondaryColor); + assert.notEqual(palette.secondaryColor, palette.tertiaryColor); + assert.equal(new Set([ + palette.pie1, + palette.pie2, + palette.pie3, + palette.pie4, + palette.pie5, + ]).size, 5); + } +}); + +test('tints only unstyled decision diamonds and keeps custom Mermaid classes intact', () => { + const darkCSS = mermaidThemeCSS('dark'); + const lightCSS = mermaidThemeCSS('light'); + + assert.ok(darkCSS.includes('.node.default polygon')); + assert.ok(darkCSS.includes('#fcc419')); + assert.ok(lightCSS.includes('#b7791f')); + assert.ok(!darkCSS.includes('.node rect')); +}); From 0a739cd3f9c0bc476118d66a2332274c7d01b5b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Z?= Date: Tue, 27 Oct 2026 01:11:57 +0100 Subject: [PATCH 07/17] docs(mermaid): native palette and mobile pan behavior --- KNOWLEDGE_BASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KNOWLEDGE_BASE.md b/KNOWLEDGE_BASE.md index f65944f1a..74a66eb46 100644 --- a/KNOWLEDGE_BASE.md +++ b/KNOWLEDGE_BASE.md @@ -204,7 +204,7 @@ Sources: [translation endpoint](app/Http/Controllers/CommitTranslationController Commit and reply text supports inline code in backticks and fenced code blocks with syntax highlighting, a **Wrap lines** toggle, and a copy control. Text outside code supports emoji shortcodes, clickable URLs, and `@username` mentions. This is developer-oriented text formatting, not a full rich-text editor. -A fenced `mermaid` block in a commit or reply renders as a diagram when it approaches the viewport, including in the draft preview. Switch to **Show source** to read or copy the exact diagram definition; zoom controls appear on rendered diagrams. The diagram engine is loaded on demand, not with every log page. Invalid and oversized definitions (more than 6,000 characters) remain readable as source rather than disappearing. Rendering runs in Mermaid's strict security mode and its SVG is shown as an inert image, not inserted as live HTML. Static excerpts and private Remotes keep ordinary code blocks. No images or other diagram data are saved separately. +A fenced `mermaid` block in a commit or reply renders as a diagram when it approaches the viewport, including in the draft preview. Switch to **Show source** to read or copy the exact diagram definition; zoom controls appear on rendered diagrams. Colors follow /dev/null's blue primary and green/purple/amber lane palette in both themes. Wide diagrams scroll horizontally on narrow screens instead of shrinking their labels; Mermaid `classDef` supports explicit semantic colors for success and failure. The diagram engine is loaded on demand, not with every log page. Invalid and oversized definitions (more than 6,000 characters) remain readable as source rather than disappearing. Rendering runs in Mermaid's strict security mode and its SVG is shown as an inert image, not inserted as live HTML. Static excerpts and private Remotes keep ordinary code blocks. No images or other diagram data are saved separately. Sources: [diagram component](resources/js/components/mermaid-diagram.tsx), [lazy renderer](resources/js/lib/mermaid-renderer.ts), [source parser](app/Services/CodeSpans.php). From 02b7cb8afcc6f87748886d1ed8fdd59b330b7130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Z?= Date: Tue, 27 Oct 2026 01:16:40 +0100 Subject: [PATCH 08/17] style(mermaid): apply native Vite+ formatting to visual polish --- resources/js/components/mermaid-diagram.tsx | 91 ++++++++++++--------- resources/js/lib/mermaid-theme.ts | 20 ++++- 2 files changed, 69 insertions(+), 42 deletions(-) diff --git a/resources/js/components/mermaid-diagram.tsx b/resources/js/components/mermaid-diagram.tsx index 100d197f4..3931cbfc7 100644 --- a/resources/js/components/mermaid-diagram.tsx +++ b/resources/js/components/mermaid-diagram.tsx @@ -103,7 +103,8 @@ export function MermaidDiagram({ source }: { source: string }) { // Horizontal charts must stay readable on phones instead of shrinking // their labels to a few pixels. Keep vertical charts fitted to the feed. - const viewBox = svg.match(/viewBox="([^"]+)"/)?.[1] + const viewBox = svg + .match(/viewBox="([^"]+)"/)?.[1] ?.trim() .split(/[\s,]+/) .map(Number); @@ -151,7 +152,10 @@ export function MermaidDiagram({ source }: { source: string }) { >
- + mermaid
@@ -217,7 +221,8 @@ export function MermaidDiagram({ source }: { source: string }) { tabIndex={0} className="max-h-[440px] min-h-32 overflow-auto overscroll-contain bg-card px-3 py-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring sm:px-4 sm:py-5" style={{ - backgroundImage: 'radial-gradient(circle, color-mix(in srgb, var(--border) 55%, transparent) 0.8px, transparent 1px)', + backgroundImage: + 'radial-gradient(circle, color-mix(in srgb, var(--border) 55%, transparent) 0.8px, transparent 1px)', backgroundSize: '20px 20px', }} > @@ -228,14 +233,18 @@ export function MermaidDiagram({ source }: { source: string }) { className="mx-auto block h-auto max-w-none" style={{ width: `${zoom}%`, - minWidth: isWide && isSmallScreen - ? `${Math.round(600 * zoom / 100)}px` - : undefined, + minWidth: + isWide && isSmallScreen + ? `${Math.round((600 * zoom) / 100)}px` + : undefined, }} />
- + {isWide && isSmallScreen && ( <> @@ -244,39 +253,41 @@ export function MermaidDiagram({ source }: { source: string }) { )}
- - - {zoom}% - - - + + + {zoom}% + + +
diff --git a/resources/js/lib/mermaid-theme.ts b/resources/js/lib/mermaid-theme.ts index 12bb33dbc..3ad31ad0f 100644 --- a/resources/js/lib/mermaid-theme.ts +++ b/resources/js/lib/mermaid-theme.ts @@ -21,7 +21,15 @@ const palettes = { note: '#382d1c', noteBorder: '#caa34a', noteText: '#f2e6ca', - pie: ['#7aa2f7', '#51cf66', '#da77f2', '#ff8a4c', '#3bc9db', '#f06595', '#fcc419'], + pie: [ + '#7aa2f7', + '#51cf66', + '#da77f2', + '#ff8a4c', + '#3bc9db', + '#f06595', + '#fcc419', + ], }, light: { canvas: '#ffffff', @@ -38,7 +46,15 @@ const palettes = { note: '#fff4dc', noteBorder: '#b7791f', noteText: '#513c1d', - pie: ['#3b6fe0', '#2f9e44', '#9c36b5', '#d9480f', '#0c8599', '#c2255c', '#b7791f'], + pie: [ + '#3b6fe0', + '#2f9e44', + '#9c36b5', + '#d9480f', + '#0c8599', + '#c2255c', + '#b7791f', + ], }, } as const; From 6133b4f744b8640f0fcd1f7fa5aa882a561b87a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Z?= Date: Tue, 27 Oct 2026 01:19:23 +0100 Subject: [PATCH 09/17] fix(mermaid): prevent clipped SVG labels across font contexts --- resources/js/lib/mermaid-theme.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/js/lib/mermaid-theme.ts b/resources/js/lib/mermaid-theme.ts index 3ad31ad0f..5be735e70 100644 --- a/resources/js/lib/mermaid-theme.ts +++ b/resources/js/lib/mermaid-theme.ts @@ -92,8 +92,10 @@ export function mermaidTheme(theme: DiagramTheme) { signalTextColor: palette.text, activationBkgColor: palette.secondary, activationBorderColor: palette.secondaryBorder, - fontFamily: 'Geist, ui-sans-serif, system-ui, sans-serif', - fontSize: '15px', + // SVGs shown via cannot rely on the page's Geist @font-face. + // Use the same system font for Mermaid's layout measurements and image paint. + fontFamily: 'system-ui, sans-serif', + fontSize: '14px', pie1: palette.pie[0], pie2: palette.pie[1], pie3: palette.pie[2], From 4d44c3a3bc1287d2c6932c78ded9d38f70ef249b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Z?= Date: Tue, 27 Oct 2026 01:19:53 +0100 Subject: [PATCH 10/17] test(mermaid): keep image-safe font to avoid clipped labels --- tests/Unit/MermaidDiagram.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Unit/MermaidDiagram.test.tsx b/tests/Unit/MermaidDiagram.test.tsx index a396a2575..ab7312a83 100644 --- a/tests/Unit/MermaidDiagram.test.tsx +++ b/tests/Unit/MermaidDiagram.test.tsx @@ -96,6 +96,7 @@ test('uses the real dev-null theme colors rather than Mermaid defaults', () => { assert.equal(dark.secondaryBorderColor, '#51cf66'); assert.equal(dark.tertiaryBorderColor, '#da77f2'); assert.equal(dark.darkMode, true); + assert.equal(dark.fontFamily, 'system-ui, sans-serif'); assert.equal(light.background, '#ffffff'); assert.equal(light.primaryBorderColor, '#3b6fe0'); assert.equal(light.darkMode, false); From 93871668ff2aa519d0c976f9d2ebc5cd06a8eacd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Z?= Date: Tue, 27 Oct 2026 01:20:40 +0100 Subject: [PATCH 11/17] style(mermaid): preserve native right-aligned controls on desktop --- resources/js/components/mermaid-diagram.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/js/components/mermaid-diagram.tsx b/resources/js/components/mermaid-diagram.tsx index 3931cbfc7..54ecec282 100644 --- a/resources/js/components/mermaid-diagram.tsx +++ b/resources/js/components/mermaid-diagram.tsx @@ -240,7 +240,7 @@ export function MermaidDiagram({ source }: { source: string }) { }} />
-
+
)} -
+