"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.1",
"highlight.js": "^11.11.1",
+ "mermaid": "^11.15.0",
"mode-watcher": "^1.1.0",
"pdfjs-dist": "^5.4.54",
"rehype-highlight": "^7.0.2",
"node": ">=6.0.0"
}
},
+ "node_modules/@antfu/install-pkg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
+ "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "package-manager-detector": "^1.3.0",
+ "tinyexec": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@antfu/install-pkg/node_modules/tinyexec": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz",
+ "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"node": ">=18"
}
},
+ "node_modules/@braintree/sanitize-url": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz",
+ "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==",
+ "license": "MIT"
+ },
+ "node_modules/@chevrotain/types": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz",
+ "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==",
+ "license": "Apache-2.0"
+ },
"node_modules/@chromatic-com/storybook": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@chromatic-com/storybook/-/storybook-5.0.0.tgz",
"url": "https://github.com/sponsors/nzakas"
}
},
+ "node_modules/@iconify/types": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
+ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
+ "license": "MIT"
+ },
+ "node_modules/@iconify/utils": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.3.tgz",
+ "integrity": "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==",
+ "license": "MIT",
+ "dependencies": {
+ "@antfu/install-pkg": "^1.1.0",
+ "@iconify/types": "^2.0.0",
+ "import-meta-resolve": "^4.2.0"
+ }
+ },
"node_modules/@internationalized/date": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.1.tgz",
"react": ">=16"
}
},
+ "node_modules/@mermaid-js/parser": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.1.tgz",
+ "integrity": "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@chevrotain/types": "~11.1.1"
+ }
+ },
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.26.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/d3": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+ "license": "MIT",
+ "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": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-axis": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-brush": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-chord": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-contour": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-dispatch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
+ "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-dsv": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-polygon": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-quadtree": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
"node_modules/@types/hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC"
},
+ "node_modules/@upsetjs/venn.js": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz",
+ "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==",
+ "license": "MIT",
+ "optionalDependencies": {
+ "d3-selection": "^3.0.0",
+ "d3-transition": "^3.0.1"
+ }
+ },
"node_modules/@vitest/browser": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-3.2.4.tgz",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 12"
"node": ">= 0.4.0"
}
},
+ "node_modules/cose-base": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
+ "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==",
+ "license": "MIT",
+ "dependencies": {
+ "layout-base": "^1.0.0"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"license": "MIT",
"peer": true
},
+ "node_modules/cytoscape": {
+ "version": "3.33.4",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.4.tgz",
+ "integrity": "sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/cytoscape-cose-bilkent": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz",
+ "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cose-base": "^1.0.0"
+ },
+ "peerDependencies": {
+ "cytoscape": "^3.2.0"
+ }
+ },
+ "node_modules/cytoscape-fcose": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz",
+ "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cose-base": "^2.2.0"
+ },
+ "peerDependencies": {
+ "cytoscape": "^3.2.0"
+ }
+ },
+ "node_modules/cytoscape-fcose/node_modules/cose-base": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz",
+ "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==",
+ "license": "MIT",
+ "dependencies": {
+ "layout-base": "^2.0.0"
+ }
+ },
+ "node_modules/cytoscape-fcose/node_modules/layout-base": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz",
+ "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==",
+ "license": "MIT"
+ },
+ "node_modules/d3": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+ "license": "ISC",
+ "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"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-contour": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "license": "ISC",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "license": "ISC",
+ "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"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv/node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dsv": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-sankey": {
+ "version": "0.12.3",
+ "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
+ "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-array": "1 - 2",
+ "d3-shape": "^1.2.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-array": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
+ "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "internmap": "^1.0.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-path": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
+ "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/d3-sankey/node_modules/d3-shape": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
+ "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "d3-path": "1"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/internmap": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
+ "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==",
+ "license": "ISC"
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "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"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/dagre-d3-es": {
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz",
+ "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==",
+ "license": "MIT",
+ "dependencies": {
+ "d3": "^7.9.0",
+ "lodash-es": "^4.17.21"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.20",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
+ "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
+ "license": "MIT"
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/delaunator": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
+ "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
+ "license": "ISC",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"dev": true,
"license": "MIT"
},
+ "node_modules/dompurify": {
+ "version": "3.4.5",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.5.tgz",
+ "integrity": "sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
}
},
"node_modules/es-toolkit": {
- "version": "1.39.7",
- "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.7.tgz",
- "integrity": "sha512-ek/wWryKouBrZIjkwW2BFf91CWOIMvoy2AE5YYgUrfWsJQM2Su1LoLtrw8uusEpN9RfqLlV/0FVNjT0WMv8Bxw==",
- "dev": true,
+ "version": "1.46.1",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz",
+ "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==",
"license": "MIT",
"workspaces": [
"docs",
"dev": true,
"license": "ISC"
},
+ "node_modules/hachure-fill": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
+ "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==",
+ "license": "MIT"
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/import-meta-resolve": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
+ "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
"license": "MIT"
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/ip-address": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
"version": "0.16.47",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz",
"integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==",
- "dev": true,
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
"json-buffer": "3.0.1"
}
},
+ "node_modules/khroma": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz",
+ "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
+ },
"node_modules/kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
"dev": true,
"license": "MIT"
},
+ "node_modules/layout-base": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
+ "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==",
+ "license": "MIT"
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash-es": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+ "license": "MIT"
+ },
"node_modules/lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/marked": {
+ "version": "16.4.2",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz",
+ "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/mermaid": {
+ "version": "11.15.0",
+ "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.15.0.tgz",
+ "integrity": "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@braintree/sanitize-url": "^7.1.1",
+ "@iconify/utils": "^3.0.2",
+ "@mermaid-js/parser": "^1.1.1",
+ "@types/d3": "^7.4.3",
+ "@upsetjs/venn.js": "^2.0.0",
+ "cytoscape": "^3.33.1",
+ "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.19",
+ "dompurify": "^3.3.1",
+ "es-toolkit": "^1.45.1",
+ "katex": "^0.16.25",
+ "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"
+ }
+ },
"node_modules/micromark": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
"dev": true,
"license": "BlueOak-1.0.0"
},
+ "node_modules/package-manager-detector": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz",
+ "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==",
+ "license": "MIT"
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"node": ">= 0.8"
}
},
+ "node_modules/path-data-parser": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
+ "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==",
+ "license": "MIT"
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"node": ">=18"
}
},
+ "node_modules/points-on-curve": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
+ "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==",
+ "license": "MIT"
+ },
+ "node_modules/points-on-path": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz",
+ "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==",
+ "license": "MIT",
+ "dependencies": {
+ "path-data-parser": "0.1.0",
+ "points-on-curve": "0.2.0"
+ }
+ },
"node_modules/portfinder": {
"version": "1.0.38",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz",
"node": ">=4"
}
},
+ "node_modules/robust-predicates": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
+ "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
+ "license": "Unlicense"
+ },
"node_modules/rollup": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
"fsevents": "~2.3.2"
}
},
+ "node_modules/roughjs": {
+ "version": "4.6.6",
+ "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz",
+ "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "hachure-fill": "^0.5.2",
+ "path-data-parser": "^0.1.0",
+ "points-on-curve": "^0.2.0",
+ "points-on-path": "^0.2.1"
+ }
+ },
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
"svelte": "^5.7.0"
}
},
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
"inline-style-parser": "0.2.4"
}
},
+ "node_modules/stylis": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
+ "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",
+ "license": "MIT"
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
"integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.10"
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz",
"integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==",
- "dev": true,
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.1",
"highlight.js": "^11.11.1",
+ "mermaid": "^11.15.0",
"mode-watcher": "^1.1.0",
"pdfjs-dist": "^5.4.54",
"rehype-highlight": "^7.0.2",
@apply bg-background text-foreground;
scrollbar-width: thin;
scrollbar-gutter: stable;
+ overflow: hidden; /* Added due to Mermaid rendering somehow causing the double scrollbar */
}
/* Global scrollbar styling - visible only on hover */
import { rehypeRestoreTableHtml } from './plugins/rehype/table-html-restorer';
import { rehypeEnhanceLinks } from './plugins/rehype/enhance-links';
import { rehypeEnhanceCodeBlocks } from './plugins/rehype/enhance-code-blocks';
+ import { rehypeEnhanceMermaidBlocks } from './plugins/rehype/enhance-mermaid-blocks';
+ import { rehypeMermaidPre } from './plugins/rehype/mermaid-pre';
import { rehypeResolveAttachmentImages } from './plugins/rehype/resolve-attachment-images';
import { rehypeRtlSupport } from './plugins/rehype/rehype-rtl-support';
import { remarkLiteralHtml } from './plugins/remark/literal-html';
- import { copyCodeToClipboard, preprocessLaTeX, getImageErrorFallbackHtml } from '$lib/utils';
+ import {
+ getHastNodeId,
+ getMdastNodeHash,
+ isAppendMode,
+ getCodeInfoFromTarget
+ } from './markdown-utils';
+ import {
+ preprocessLaTeX,
+ getImageErrorFallbackHtml,
+ copyCodeToClipboard,
+ copyToClipboard
+ } from '$lib/utils';
import {
IMAGE_NOT_ERROR_BOUND_SELECTOR,
DATA_ERROR_BOUND_ATTR,
import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
import githubLightCss from 'highlight.js/styles/github.css?inline';
import { mode } from 'mode-watcher';
- import { CodeBlockActions, DialogCodePreview } from '$lib/components/app';
+ import {
+ CodeBlockActions,
+ DialogCodePreview,
+ DialogMermaidPreview,
+ ActionIconCopyToClipboard
+ } from '$lib/components/app';
import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
import type { DatabaseMessageExtra } from '$lib/types/database';
import { config } from '$lib/stores/settings.svelte';
let previewDialogOpen = $state(false);
let previewCode = $state('');
let previewLanguage = $state('text');
+ let mermaidPreviewOpen = $state(false);
+ let mermaidPreviewSvgHtml = $state('');
+
let streamingCodeScrollContainer = $state<HTMLDivElement>();
// Auto-scroll controller for streaming code block content
}) // Add syntax highlighting
.use(rehypeRestoreTableHtml) // Restore limited HTML (e.g., <br>, <ul>) inside Markdown tables
.use(rehypeEnhanceLinks) // Add target="_blank" to links
+ .use(rehypeMermaidPre) // Convert mermaid blocks to <pre class="mermaid">
.use(rehypeEnhanceCodeBlocks) // Wrap code blocks with header and actions
+ .use(rehypeEnhanceMermaidBlocks) // Wrap mermaid blocks with header and actions
.use(rehypeResolveAttachmentImages, { attachments })
.use(rehypeRtlSupport) // Add bidirectional text support
.use(rehypeStringify, { allowDangerousHtml: true }); // Convert to HTML string
}
/**
- * Extracts code information from a button click target within a code block.
- * @param target - The clicked button element
- * @returns Object with rawCode and language, or null if extraction fails
- */
- function getCodeInfoFromTarget(target: HTMLElement) {
- const wrapper = target.closest('.code-block-wrapper');
-
- if (!wrapper) {
- console.error('No wrapper found');
- return null;
- }
-
- const codeElement = wrapper.querySelector<HTMLElement>('code[data-code-id]');
-
- if (!codeElement) {
- console.error('No code element found in wrapper');
- return null;
- }
-
- const rawCode = codeElement.textContent ?? '';
-
- const languageLabel = wrapper.querySelector<HTMLElement>('.code-language');
- const language = languageLabel?.textContent?.trim() || 'text';
-
- return { rawCode, language };
- }
-
- /**
- * Generates a unique identifier for a HAST node based on its position.
- * Used for stable block identification during incremental rendering.
- * @param node - The HAST root content node
- * @param indexFallback - Fallback index if position is unavailable
- * @returns Unique string identifier for the node
- */
- function getHastNodeId(node: HastRootContent, indexFallback: number): string {
- const position = node.position;
-
- if (position?.start?.offset != null && position?.end?.offset != null) {
- return `hast-${position.start.offset}-${position.end.offset}`;
- }
-
- return `${node.type}-${indexFallback}`;
- }
-
- /**
- * Generates a hash for MDAST node based on its position.
- * Used for cache lookup during incremental rendering.
- */
- function getMdastNodeHash(node: unknown, index: number): string {
- const n = node as {
- type?: string;
- position?: { start?: { offset?: number }; end?: { offset?: number } };
- };
-
- if (n.position?.start?.offset != null && n.position?.end?.offset != null) {
- return `${n.type}-${n.position.start.offset}-${n.position.end.offset}`;
- }
-
- return `${n.type}-idx${index}`;
- }
-
- /**
- * Check if we're in append-only mode (streaming).
- */
- function isAppendMode(newContent: string): boolean {
- return previousContent.length > 0 && newContent.startsWith(previousContent);
- }
+ * Transforms a single MDAST node to HTML string with caching.
/**
* Transforms a single MDAST node to HTML string with caching.
const nextBlocks: MarkdownBlock[] = [];
// Check if we're in append mode for cache reuse
- const appendMode = isAppendMode(prefixMarkdown);
+ const appendMode = isAppendMode(prefixMarkdown, previousContent);
const previousBlockCount = appendMode ? renderedBlocks.length : 0;
// All prefix blocks are now stable since code block is separate
const nextBlocks: MarkdownBlock[] = [];
// Check if we're in append mode for cache reuse
- const appendMode = isAppendMode(markdown);
+ const appendMode = isAppendMode(markdown, previousContent);
const previousBlockCount = appendMode ? renderedBlocks.length : 0;
for (let index = 0; index < stableCount; index++) {
}
}
+ /**
+ * Opens the mermaid diagram in a full-screen preview dialog with zoom/pan support.
+ * Also handles copy and preview button clicks for mermaid blocks.
+ * Uses event delegation: a single handler on the container.
+ */
+ async function handleMermaidClick(event: MouseEvent) {
+ const target = event.target as HTMLElement;
+
+ // Check if clicking on copy or preview button in mermaid block
+ const copyBtn = target.closest('.mermaid-block-wrapper .copy-code-btn');
+ const previewBtn = target.closest('.mermaid-block-wrapper .preview-code-btn');
+
+ if (copyBtn || previewBtn) {
+ const wrapper = target.closest('.mermaid-block-wrapper');
+ if (!wrapper) return;
+
+ const preElement = wrapper.querySelector<HTMLElement>('pre.mermaid[data-mermaid-syntax]');
+ if (!preElement) return;
+
+ const mermaidSyntax = preElement.dataset.mermaidSyntax ?? '';
+
+ if (copyBtn) {
+ event.preventDefault();
+ event.stopPropagation();
+ try {
+ await copyToClipboard(mermaidSyntax);
+ } catch (error) {
+ console.error('Failed to copy mermaid syntax:', error);
+ }
+ return;
+ }
+
+ if (previewBtn) {
+ event.preventDefault();
+ event.stopPropagation();
+ const svg = preElement.querySelector('svg');
+ if (!svg) return;
+ mermaidPreviewSvgHtml = svg.outerHTML;
+ mermaidPreviewOpen = true;
+ return;
+ }
+ }
+
+ // Otherwise, open preview when clicking on the mermaid diagram itself
+ const mermaidEl = target.closest('.mermaid');
+ if (!mermaidEl) return;
+
+ const svg = mermaidEl.querySelector('svg');
+ if (!svg) return;
+
+ mermaidPreviewSvgHtml = svg.outerHTML;
+ mermaidPreviewOpen = true;
+ }
+
+ /**
+ * Handles mermaid preview dialog open state changes.
+ * Cleans up SVG content when dialog is closed.
+ */
+ function handleMermaidPreviewOpenChange(open: boolean) {
+ mermaidPreviewOpen = open;
+ if (!open) {
+ mermaidPreviewSvgHtml = '';
+ }
+ }
+
+ /**
+ * Renders mermaid diagrams that haven't been rendered yet.
+ * Called after each markdown content update.
+ * Marks nodes immediately to prevent duplicate renders during streaming.
+ * Reads mode.current before await to ensure reactive tracking.
+ */
+ async function renderMermaidDiagrams() {
+ if (!containerRef) return;
+
+ const nodes = containerRef.querySelectorAll('pre.mermaid:not([data-mermaid-rendered])');
+ if (nodes.length === 0) return;
+
+ // Mark nodes immediately to prevent duplicate renders if called again during streaming.
+ // This avoids needing a guard that would block node discovery.
+ nodes.forEach((node) => node.setAttribute('data-mermaid-rendered', 'true'));
+
+ // Read mode before await so Svelte tracks it reactively.
+ const isDark = mode.current === ColorMode.DARK;
+
+ // lazy load the mermaid dependecy only when needed to reduce bundle size.
+ const { default: mermaid } = await import('mermaid');
+
+ mermaid.initialize({
+ startOnLoad: false,
+ theme: isDark ? 'dark' : 'default',
+ securityLevel: 'strict',
+ flowchart: {
+ useMaxWidth: false,
+ htmlLabels: true
+ },
+ sequence: {
+ useMaxWidth: false
+ },
+ gantt: {
+ useMaxWidth: false
+ }
+ });
+
+ try {
+ await mermaid.run({
+ nodes: Array.from(nodes) as unknown as NodeListOf<HTMLElement>
+ });
+ } catch (error) {
+ console.error('Failed to render mermaid diagram:', error);
+ }
+ }
+
/**
* Handles image load errors by replacing the image with a fallback UI.
* Shows a placeholder with a link to open the image in a new tab.
if ((hasRenderedBlocks || hasUnstableBlock) && containerRef) {
setupCodeBlockActions();
setupImageErrorHandlers();
+ renderMermaidDiagrams();
}
});
});
</script>
+<!-- svelte-ignore a11y_click_events_have_key_events -->
+<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
bind:this={containerRef}
- class="{className}{config()[SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS]
+ onclick={handleMermaidClick}
+ class="markdown-content {className}{config()[SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS]
? ' full-height-code-blocks'
: ''}"
>
{#each renderedBlocks as block (block.id)}
<div class="markdown-block" data-block-id={block.id} use:fadeInView={{ skipIfVisible: true }}>
- <!-- eslint-disable-next-line no-at-html-tags -->
{@html block.html}
</div>
{/each}
{/if}
{#if incompleteCodeBlock}
- <div class="code-block-wrapper streaming-code-block relative">
- <div class="code-block-header">
- <span class="code-language">{incompleteCodeBlock.language || 'text'}</span>
- <CodeBlockActions
- code={incompleteCodeBlock.code}
- language={incompleteCodeBlock.language || 'text'}
- disabled
- onPreview={(code, lang) => {
- previewCode = code;
- previewLanguage = lang;
- previewDialogOpen = true;
- }}
- />
+ {#if incompleteCodeBlock.language === 'mermaid'}
+ <div class="mermaid-block-wrapper streaming-mermaid-block">
+ <div class="code-block-header">
+ <span class="code-language">mermaid</span>
+ <div class="code-block-actions">
+ <ActionIconCopyToClipboard
+ text={incompleteCodeBlock.code}
+ canCopy={false}
+ ariaLabel="Diagram incomplete"
+ />
+ </div>
+ </div>
+ <div class="mermaid-loading-placeholder">
+ <span class="mermaid-loading-text">Generating diagram...</span>
+ </div>
</div>
- <div
- bind:this={streamingCodeScrollContainer}
- class="streaming-code-scroll-container"
- onscroll={() => streamingAutoScroll.handleScroll()}
- >
- <pre class="streaming-code-pre"><code
- class="hljs language-{incompleteCodeBlock.language || 'text'}"
- >{@html highlightCode(
- incompleteCodeBlock.code,
- incompleteCodeBlock.language || 'text'
- )}</code
- ></pre>
+ {:else}
+ <div class="code-block-wrapper streaming-code-block relative">
+ <div class="code-block-header">
+ <span class="code-language">{incompleteCodeBlock.language || 'text'}</span>
+ <CodeBlockActions
+ code={incompleteCodeBlock.code}
+ language={incompleteCodeBlock.language || 'text'}
+ disabled
+ onPreview={(code, lang) => {
+ previewCode = code;
+ previewLanguage = lang;
+ previewDialogOpen = true;
+ }}
+ />
+ </div>
+
+ <div
+ bind:this={streamingCodeScrollContainer}
+ class="streaming-code-scroll-container"
+ onscroll={() => streamingAutoScroll.handleScroll()}
+ >
+ <pre class="streaming-code-pre"><code
+ class="hljs language-{incompleteCodeBlock.language || 'text'}"
+ >{@html highlightCode(
+ incompleteCodeBlock.code,
+ incompleteCodeBlock.language || 'text'
+ )}</code
+ ></pre>
+ </div>
</div>
- </div>
+ {/if}
{/if}
</div>
onOpenChange={handlePreviewDialogOpenChange}
/>
-<style>
- .markdown-block--unstable {
- display: contents;
- }
-
- /* Streaming code block uses .code-block-wrapper styles */
- .streaming-code-block .streaming-code-pre {
- background: transparent;
- padding: 0.5rem;
- margin: 0;
- overflow-x: visible;
- border-radius: 0;
- border: none;
- font-size: 0.875rem;
- }
-
- /* Base typography styles */
- div :global(p) {
- margin-block: 1rem;
- line-height: 1.75;
- }
-
- div :global(:is(h1, h2, h3, h4, h5, h6):first-child) {
- margin-top: 0;
- }
-
- /* Headers with consistent spacing */
- div :global(h1) {
- font-size: 1.875rem;
- font-weight: 700;
- line-height: 1.2;
- margin: 1.5rem 0 0.75rem 0;
- }
-
- div :global(h2) {
- font-size: 1.5rem;
- font-weight: 600;
- line-height: 1.3;
- margin: 1.25rem 0 0.5rem 0;
- }
-
- div :global(h3) {
- font-size: 1.25rem;
- font-weight: 600;
- margin: 1.5rem 0 0.5rem 0;
- line-height: 1.4;
- }
-
- div :global(h4) {
- font-size: 1.125rem;
- font-weight: 600;
- margin: 0.75rem 0 0.25rem 0;
- }
-
- div :global(h5) {
- font-size: 1rem;
- font-weight: 600;
- margin: 0.5rem 0 0.25rem 0;
- }
-
- div :global(h6) {
- font-size: 0.875rem;
- font-weight: 600;
- margin: 0.5rem 0 0.25rem 0;
- }
-
- /* Text formatting */
- div :global(strong) {
- font-weight: 600;
- }
-
- div :global(em) {
- font-style: italic;
- }
-
- div :global(del) {
- text-decoration: line-through;
- opacity: 0.7;
- }
-
- /* Inline code */
- div :global(code:not(pre code)) {
- background: var(--muted);
- color: var(--muted-foreground);
- padding: 0.125rem 0.375rem;
- border-radius: 0.375rem;
- font-size: 0.875rem;
- }
-
- div :global(pre) {
- display: inline;
- margin: 0 !important;
- overflow: hidden !important;
- background: var(--muted);
- overflow-x: auto;
- border-radius: 1rem;
- border: none;
- line-height: 1 !important;
- }
-
- div :global(pre code) {
- padding: 0 !important;
- display: inline !important;
- }
-
- div :global(code) {
- background: transparent;
- color: var(--code-foreground);
- }
-
- /* Links */
- div :global(a) {
- color: var(--primary);
- text-decoration: underline;
- text-underline-offset: 2px;
- transition: color 0.2s ease;
- overflow-wrap: anywhere;
- word-break: break-all;
- }
-
- div :global(a:hover) {
- color: var(--primary);
- }
-
- /* Lists */
- div :global(ul) {
- list-style-type: disc;
- margin-inline-start: 1.5rem;
- margin-bottom: 1rem;
- }
-
- div :global(ol) {
- list-style-type: decimal;
- margin-inline-start: 1.5rem;
- margin-bottom: 1rem;
- }
-
- div :global(li) {
- margin-bottom: 0.25rem;
- padding-inline-start: 0.5rem;
- }
-
- div :global(li::marker) {
- color: var(--muted-foreground);
- }
-
- /* Nested lists */
- div :global(ul ul) {
- list-style-type: circle;
- margin-top: 0.25rem;
- margin-bottom: 0.25rem;
- }
-
- div :global(ol ol) {
- list-style-type: lower-alpha;
- margin-top: 0.25rem;
- margin-bottom: 0.25rem;
- }
-
- /* Task lists */
- div :global(.task-list-item) {
- list-style: none;
- margin-inline-start: 0;
- padding-inline-start: 0;
- }
-
- div :global(.task-list-item-checkbox) {
- margin-right: 0.5rem;
- margin-top: 0.125rem;
- }
-
- /* Blockquotes */
- div :global(blockquote) {
- border-left: 4px solid var(--border);
- padding: 0.5rem 1rem;
- margin: 1.5rem 0;
- font-style: italic;
- color: var(--muted-foreground);
- background: var(--muted);
- border-radius: 0 0.375rem 0.375rem 0;
- }
-
- /* Tables */
- div :global(table) {
- width: 100%;
- margin: 1.5rem 0;
- border-collapse: collapse;
- border: 1px solid var(--border);
- border-radius: 0.375rem;
- overflow: hidden;
- }
-
- div :global(th) {
- background: hsl(var(--muted) / 0.3);
- border: 1px solid var(--border);
- padding: 0.5rem 0.75rem;
- text-align: left;
- font-weight: 600;
- }
-
- div :global(td) {
- border: 1px solid var(--border);
- padding: 0.5rem 0.75rem;
- }
-
- div :global(tr:nth-child(even)) {
- background: hsl(var(--muted) / 0.1);
- }
-
- /* User message markdown should keep table borders visible on light primary backgrounds */
- div.markdown-user-content :global(table),
- div.markdown-user-content :global(th),
- div.markdown-user-content :global(td),
- div.markdown-user-content :global(.table-wrapper) {
- border-color: currentColor;
- }
-
- /* Horizontal rules */
- div :global(hr) {
- border: none;
- border-top: 1px solid var(--border);
- margin: 1.5rem 0;
- }
-
- /* Images */
- div :global(img) {
- border-radius: 0.5rem;
- box-shadow:
- 0 1px 3px 0 rgb(0 0 0 / 0.1),
- 0 1px 2px -1px rgb(0 0 0 / 0.1);
- margin: 1.5rem 0;
- max-width: 100%;
- height: auto;
- }
-
- /* Code blocks */
-
- div :global(.code-block-wrapper) {
- margin: 1.5rem 0;
- border-radius: 0.75rem;
- overflow: hidden;
- border: 1px solid color-mix(in oklch, var(--border) 30%, transparent);
- background: var(--code-background);
- box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
- min-height: var(--min-message-height);
- max-height: var(--max-message-height);
- }
-
- :global(.dark) div :global(.code-block-wrapper) {
- border-color: color-mix(in oklch, var(--border) 20%, transparent);
- }
-
- /* Scroll container for code blocks (both streaming and completed) */
- div :global(.code-block-scroll-container),
- .streaming-code-scroll-container {
- min-height: var(--min-message-height);
- max-height: var(--max-message-height);
- overflow-y: auto;
- overflow-x: auto;
- padding: 3rem 1rem 1rem;
- line-height: 1.3;
- }
-
- .full-height-code-blocks :global(.code-block-wrapper) {
- max-height: none;
- }
-
- .full-height-code-blocks :global(.code-block-scroll-container),
- .full-height-code-blocks .streaming-code-scroll-container {
- max-height: none;
- overflow-y: visible;
- }
-
- div :global(.code-block-header) {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0.5rem 1rem 0;
- font-size: 0.875rem;
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- }
-
- div :global(.code-language) {
- color: var(--color-foreground);
- font-weight: 500;
- font-family:
- ui-monospace, SFMono-Regular, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas,
- 'Liberation Mono', Menlo, monospace;
- text-transform: uppercase;
- font-size: 0.75rem;
- letter-spacing: 0.05em;
- }
-
- div :global(.code-block-actions) {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- }
-
- div :global(.copy-code-btn),
- div :global(.preview-code-btn) {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0;
- background: transparent;
- color: var(--code-foreground);
- cursor: pointer;
- transition: all 0.2s ease;
- }
-
- div :global(.copy-code-btn:hover),
- div :global(.preview-code-btn:hover) {
- transform: scale(1.05);
- }
-
- div :global(.copy-code-btn:active),
- div :global(.preview-code-btn:active) {
- transform: scale(0.95);
- }
-
- div :global(.code-block-wrapper pre) {
- background: transparent;
- margin: 0;
- border-radius: 0;
- border: none;
- font-size: 0.875rem;
- }
-
- /* Mentions and hashtags */
- div :global(.mention) {
- color: hsl(var(--primary));
- font-weight: 500;
- text-decoration: none;
- }
-
- div :global(.mention:hover) {
- text-decoration: underline;
- }
-
- div :global(.hashtag) {
- color: hsl(var(--primary));
- font-weight: 500;
- text-decoration: none;
- }
-
- div :global(.hashtag:hover) {
- text-decoration: underline;
- }
-
- /* Advanced table enhancements */
- div :global(table) {
- transition: all 0.2s ease;
- }
-
- div :global(table:hover) {
- box-shadow:
- 0 4px 6px -1px rgb(0 0 0 / 0.1),
- 0 2px 4px -2px rgb(0 0 0 / 0.1);
- }
-
- div :global(th:hover),
- div :global(td:hover) {
- background: var(--muted);
- }
-
- /* Disable hover effects when rendering user messages */
- .markdown-user-content :global(a),
- .markdown-user-content :global(a:hover) {
- color: inherit;
- }
-
- .markdown-user-content :global(table:hover) {
- box-shadow: none;
- }
-
- .markdown-user-content :global(th:hover),
- .markdown-user-content :global(td:hover) {
- background: inherit;
- }
-
- /* Enhanced blockquotes */
- div :global(blockquote) {
- transition: all 0.2s ease;
- position: relative;
- }
-
- div :global(blockquote:hover) {
- border-left-width: 6px;
- background: var(--muted);
- transform: translateX(2px);
- }
-
- div :global(blockquote::before) {
- content: '"';
- position: absolute;
- top: -0.5rem;
- left: 0.5rem;
- font-size: 3rem;
- color: var(--muted-foreground);
- font-family: serif;
- line-height: 1;
- }
-
- /* Enhanced images */
- div :global(img) {
- transition: all 0.3s ease;
- cursor: pointer;
- }
-
- div :global(img:hover) {
- transform: scale(1.02);
- box-shadow:
- 0 10px 15px -3px rgb(0 0 0 / 0.1),
- 0 4px 6px -4px rgb(0 0 0 / 0.1);
- }
-
- /* Image zoom overlay */
- div :global(.image-zoom-overlay) {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.8);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1000;
- cursor: pointer;
- }
-
- div :global(.image-zoom-overlay img) {
- max-width: 90vw;
- max-height: 90vh;
- border-radius: 0.5rem;
- box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
- }
-
- /* Enhanced horizontal rules */
- div :global(hr) {
- border: none;
- height: 2px;
- background: linear-gradient(to right, transparent, var(--border), transparent);
- margin: 2rem 0;
- position: relative;
- }
-
- div :global(hr::after) {
- content: '';
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- width: 1rem;
- height: 1rem;
- background: var(--border);
- border-radius: 50%;
- }
-
- /* Scrollable tables */
- div :global(.table-wrapper) {
- overflow-x: auto;
- margin: 1.5rem 0;
- border-radius: 0.5rem;
- border: 1px solid var(--border);
- }
-
- div :global(.table-wrapper table) {
- margin: 0;
- border: none;
- }
-
- /* Responsive adjustments */
- @media (max-width: 640px) {
- div :global(h1) {
- font-size: 1.5rem;
- }
-
- div :global(h2) {
- font-size: 1.25rem;
- }
-
- div :global(h3) {
- font-size: 1.125rem;
- }
-
- div :global(table) {
- font-size: 0.875rem;
- }
-
- div :global(th),
- div :global(td) {
- padding: 0.375rem 0.5rem;
- }
-
- div :global(.table-wrapper) {
- margin: 0.5rem -1rem;
- border-radius: 0;
- border-left: none;
- border-right: none;
- }
- }
-
- /* Dark mode adjustments */
- @media (prefers-color-scheme: dark) {
- div :global(blockquote:hover) {
- background: var(--muted);
- }
- }
-
- /* Image load error fallback */
- div :global(.image-load-error) {
- display: flex;
- align-items: center;
- justify-content: center;
- margin: 1.5rem 0;
- padding: 1.5rem;
- border-radius: 0.5rem;
- background: var(--muted);
- border: 1px dashed var(--border);
- }
-
- div :global(.image-error-content) {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.75rem;
- color: var(--muted-foreground);
- text-align: center;
- }
-
- div :global(.image-error-content svg) {
- opacity: 0.5;
- }
-
- div :global(.image-error-text) {
- font-size: 0.875rem;
- }
-
- div :global(.image-error-link) {
- display: inline-flex;
- align-items: center;
- gap: 0.375rem;
- padding: 0.5rem 1rem;
- font-size: 0.875rem;
- font-weight: 500;
- color: var(--primary);
- background: var(--background);
- border: 1px solid var(--border);
- border-radius: 0.375rem;
- text-decoration: none;
- transition: all 0.2s ease;
- }
+<DialogMermaidPreview
+ open={mermaidPreviewOpen}
+ svgHtml={mermaidPreviewSvgHtml}
+ onOpenChange={handleMermaidPreviewOpenChange}
+/>
- div :global(.image-error-link:hover) {
- background: var(--muted);
- border-color: var(--primary);
- }
+<style>
+ @import './markdown-content.css';
</style>
--- /dev/null
+.markdown-block--unstable {
+ display: contents;
+}
+
+/* Streaming code block uses .code-block-wrapper styles */
+.streaming-code-block .streaming-code-pre {
+ background: transparent;
+ padding: 0.5rem;
+ margin: 0;
+ overflow-x: visible;
+ border-radius: 0;
+ border: none;
+ font-size: 0.875rem;
+}
+
+/* Base typography styles */
+.markdown-content :global(p) {
+ margin-block: 1rem;
+ line-height: 1.75;
+}
+
+.markdown-content :global(:is(h1, h2, h3, h4, h5, h6):first-child) {
+ margin-top: 0;
+}
+
+/* Headers with consistent spacing */
+.markdown-content :global(h1) {
+ font-size: 1.875rem;
+ font-weight: 700;
+ line-height: 1.2;
+ margin: 1.5rem 0 0.75rem 0;
+}
+
+.markdown-content :global(h2) {
+ font-size: 1.5rem;
+ font-weight: 600;
+ line-height: 1.3;
+ margin: 1.25rem 0 0.5rem 0;
+}
+
+.markdown-content :global(h3) {
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin: 1.5rem 0 0.5rem 0;
+ line-height: 1.4;
+}
+
+.markdown-content :global(h4) {
+ font-size: 1.125rem;
+ font-weight: 600;
+ margin: 0.75rem 0 0.25rem 0;
+}
+
+.markdown-content :global(h5) {
+ font-size: 1rem;
+ font-weight: 600;
+ margin: 0.5rem 0 0.25rem 0;
+}
+
+.markdown-content :global(h6) {
+ font-size: 0.875rem;
+ font-weight: 600;
+ margin: 0.5rem 0 0.25rem 0;
+}
+
+/* Text formatting */
+.markdown-content :global(strong) {
+ font-weight: 600;
+}
+
+.markdown-content :global(em) {
+ font-style: italic;
+}
+
+.markdown-content :global(del) {
+ text-decoration: line-through;
+ opacity: 0.7;
+}
+
+/* Inline code */
+.markdown-content :global(code:not(pre code)) {
+ background: var(--muted);
+ color: var(--muted-foreground);
+ padding: 0.125rem 0.375rem;
+ border-radius: 0.375rem;
+ font-size: 0.875rem;
+}
+
+.markdown-content :global(pre) {
+ display: inline;
+ margin: 0 !important;
+ overflow: hidden !important;
+ background: var(--muted);
+ overflow-x: auto;
+ border-radius: 1rem;
+ border: none;
+ line-height: 1 !important;
+}
+
+.markdown-content :global(pre code) {
+ padding: 0 !important;
+ display: inline !important;
+}
+
+.markdown-content :global(code) {
+ background: transparent;
+ color: var(--code-foreground);
+}
+
+/* Links */
+.markdown-content :global(a) {
+ color: var(--primary);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ transition: color 0.2s ease;
+ overflow-wrap: anywhere;
+ word-break: break-all;
+}
+
+.markdown-content :global(a:hover) {
+ color: var(--primary);
+}
+
+/* Lists */
+.markdown-content :global(ul) {
+ list-style-type: disc;
+ margin-inline-start: 1.5rem;
+ margin-bottom: 1rem;
+}
+
+.markdown-content :global(ol) {
+ list-style-type: decimal;
+ margin-inline-start: 1.5rem;
+ margin-bottom: 1rem;
+}
+
+.markdown-content :global(li) {
+ margin-bottom: 0.25rem;
+ padding-inline-start: 0.5rem;
+}
+
+.markdown-content :global(li::marker) {
+ color: var(--muted-foreground);
+}
+
+/* Nested lists */
+.markdown-content :global(ul ul) {
+ list-style-type: circle;
+ margin-top: 0.25rem;
+ margin-bottom: 0.25rem;
+}
+
+.markdown-content :global(ol ol) {
+ list-style-type: lower-alpha;
+ margin-top: 0.25rem;
+ margin-bottom: 0.25rem;
+}
+
+/* Task lists */
+.markdown-content :global(.task-list-item) {
+ list-style: none;
+ margin-inline-start: 0;
+ padding-inline-start: 0;
+}
+
+.markdown-content :global(.task-list-item-checkbox) {
+ margin-right: 0.5rem;
+ margin-top: 0.125rem;
+}
+
+/* Blockquotes */
+.markdown-content :global(blockquote) {
+ border-left: 4px solid var(--border);
+ padding: 0.5rem 1rem;
+ margin: 1.5rem 0;
+ font-style: italic;
+ color: var(--muted-foreground);
+ background: var(--muted);
+ border-radius: 0 0.375rem 0.375rem 0;
+}
+
+/* Tables */
+.markdown-content :global(table) {
+ width: 100%;
+ margin: 1.5rem 0;
+ border-collapse: collapse;
+ border: 1px solid var(--border);
+ border-radius: 0.375rem;
+ overflow: hidden;
+}
+
+.markdown-content :global(th) {
+ background: hsl(var(--muted) / 0.3);
+ border: 1px solid var(--border);
+ padding: 0.5rem 0.75rem;
+ text-align: left;
+ font-weight: 600;
+}
+
+.markdown-content :global(td) {
+ border: 1px solid var(--border);
+ padding: 0.5rem 0.75rem;
+}
+
+.markdown-content :global(tr:nth-child(even)) {
+ background: hsl(var(--muted) / 0.1);
+}
+
+/* User message markdown should keep table borders visible on light primary backgrounds */
+div.markdown-user-content :global(table),
+div.markdown-user-content :global(th),
+div.markdown-user-content :global(td),
+div.markdown-user-content :global(.table-wrapper) {
+ border-color: currentColor;
+}
+
+/* Horizontal rules */
+.markdown-content :global(hr) {
+ border: none;
+ border-top: 1px solid var(--border);
+ margin: 1.5rem 0;
+}
+
+/* Images */
+.markdown-content :global(img) {
+ border-radius: 0.5rem;
+ box-shadow:
+ 0 1px 3px 0 rgb(0 0 0 / 0.1),
+ 0 1px 2px -1px rgb(0 0 0 / 0.1);
+ margin: 1.5rem 0;
+ max-width: 100%;
+ height: auto;
+}
+
+/* Code blocks */
+
+.markdown-content :global(.code-block-wrapper) {
+ margin: 1.5rem 0;
+ border-radius: 0.75rem;
+ overflow: hidden;
+ border: 1px solid color-mix(in oklch, var(--border) 30%, transparent);
+ background: var(--code-background);
+ box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+ min-height: var(--min-message-height);
+ max-height: var(--max-message-height);
+}
+
+.markdown-content:global(.dark) :global(.code-block-wrapper) {
+ border-color: color-mix(in oklch, var(--border) 20%, transparent);
+}
+
+/* Scroll container for code blocks (both streaming and completed) */
+.markdown-content :global(.code-block-scroll-container),
+.streaming-code-scroll-container {
+ min-height: var(--min-message-height);
+ max-height: var(--max-message-height);
+ overflow-y: auto;
+ overflow-x: auto;
+ padding: 3rem 1rem 1rem;
+ line-height: 1.3;
+}
+
+.full-height-code-blocks :global(.code-block-wrapper) {
+ max-height: none;
+}
+
+.full-height-code-blocks :global(.code-block-scroll-container),
+.full-height-code-blocks .streaming-code-scroll-container {
+ max-height: none;
+ overflow-y: visible;
+}
+
+.markdown-content :global(.code-block-header) {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.5rem 1rem 0;
+ font-size: 0.875rem;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+}
+
+.markdown-content :global(.code-language) {
+ color: var(--color-foreground);
+ font-weight: 500;
+ font-family:
+ ui-monospace, SFMono-Regular, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas,
+ 'Liberation Mono', Menlo, monospace;
+ text-transform: uppercase;
+ font-size: 0.75rem;
+ letter-spacing: 0.05em;
+}
+
+.markdown-content :global(.code-block-actions) {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.markdown-content :global(.copy-code-btn),
+.markdown-content :global(.preview-code-btn) {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ background: transparent;
+ color: var(--code-foreground);
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.markdown-content :global(.copy-code-btn:hover),
+.markdown-content :global(.preview-code-btn:hover) {
+ transform: scale(1.05);
+}
+
+.markdown-content :global(.copy-code-btn:active),
+.markdown-content :global(.preview-code-btn:active) {
+ transform: scale(0.95);
+}
+
+.markdown-content :global(.code-block-wrapper pre) {
+ background: transparent;
+ margin: 0;
+ border-radius: 0;
+ border: none;
+ font-size: 0.875rem;
+}
+
+/* Mentions and hashtags */
+.markdown-content :global(.mention) {
+ color: hsl(var(--primary));
+ font-weight: 500;
+ text-decoration: none;
+}
+
+.markdown-content :global(.mention:hover) {
+ text-decoration: underline;
+}
+
+.markdown-content :global(.hashtag) {
+ color: hsl(var(--primary));
+ font-weight: 500;
+ text-decoration: none;
+}
+
+.markdown-content :global(.hashtag:hover) {
+ text-decoration: underline;
+}
+
+/* Advanced table enhancements */
+.markdown-content :global(table) {
+ transition: all 0.2s ease;
+}
+
+.markdown-content :global(table:hover) {
+ box-shadow:
+ 0 4px 6px -1px rgb(0 0 0 / 0.1),
+ 0 2px 4px -2px rgb(0 0 0 / 0.1);
+}
+
+.markdown-content :global(th:hover),
+.markdown-content :global(td:hover) {
+ background: var(--muted);
+}
+
+/* Disable hover effects when rendering user messages */
+.markdown-user-content :global(a),
+.markdown-user-content :global(a:hover) {
+ color: inherit;
+}
+
+.markdown-user-content :global(table:hover) {
+ box-shadow: none;
+}
+
+.markdown-user-content :global(th:hover),
+.markdown-user-content :global(td:hover) {
+ background: inherit;
+}
+
+/* Enhanced blockquotes */
+.markdown-content :global(blockquote) {
+ transition: all 0.2s ease;
+ position: relative;
+}
+
+.markdown-content :global(blockquote:hover) {
+ border-left-width: 6px;
+ background: var(--muted);
+ transform: translateX(2px);
+}
+
+.markdown-content :global(blockquote::before) {
+ content: '"';
+ position: absolute;
+ top: -0.5rem;
+ left: 0.5rem;
+ font-size: 3rem;
+ color: var(--muted-foreground);
+ font-family: serif;
+ line-height: 1;
+}
+
+/* Enhanced images */
+.markdown-content :global(img) {
+ transition: all 0.3s ease;
+ cursor: pointer;
+}
+
+.markdown-content :global(img:hover) {
+ transform: scale(1.02);
+ box-shadow:
+ 0 10px 15px -3px rgb(0 0 0 / 0.1),
+ 0 4px 6px -4px rgb(0 0 0 / 0.1);
+}
+
+/* Image zoom overlay */
+.markdown-content :global(.image-zoom-overlay) {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.8);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ cursor: pointer;
+}
+
+.markdown-content :global(.image-zoom-overlay img) {
+ max-width: 90vw;
+ max-height: 90vh;
+ border-radius: 0.5rem;
+ box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
+}
+
+/* Enhanced horizontal rules */
+.markdown-content :global(hr) {
+ border: none;
+ height: 2px;
+ background: linear-gradient(to right, transparent, var(--border), transparent);
+ margin: 2rem 0;
+ position: relative;
+}
+
+.markdown-content :global(hr::after) {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 1rem;
+ height: 1rem;
+ background: var(--border);
+ border-radius: 50%;
+}
+
+/* Scrollable tables */
+.markdown-content :global(.table-wrapper) {
+ overflow-x: auto;
+ margin: 1.5rem 0;
+ border-radius: 0.5rem;
+ border: 1px solid var(--border);
+}
+
+.markdown-content :global(.table-wrapper table) {
+ margin: 0;
+ border: none;
+}
+
+/* Responsive adjustments */
+@media (max-width: 640px) {
+ .markdown-content :global(h1) {
+ font-size: 1.5rem;
+ }
+
+ .markdown-content :global(h2) {
+ font-size: 1.25rem;
+ }
+
+ .markdown-content :global(h3) {
+ font-size: 1.125rem;
+ }
+
+ .markdown-content :global(table) {
+ font-size: 0.875rem;
+ }
+
+ .markdown-content :global(th),
+ .markdown-content :global(td) {
+ padding: 0.375rem 0.5rem;
+ }
+
+ .markdown-content :global(.table-wrapper) {
+ margin: 0.5rem -1rem;
+ border-radius: 0;
+ border-left: none;
+ border-right: none;
+ }
+}
+
+/* Dark mode adjustments */
+@media (prefers-color-scheme: dark) {
+ .markdown-content :global(blockquote:hover) {
+ background: var(--muted);
+ }
+}
+
+/* Image load error fallback */
+.markdown-content :global(.image-load-error) {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 1.5rem 0;
+ padding: 1.5rem;
+ border-radius: 0.5rem;
+ background: var(--muted);
+ border: 1px dashed var(--border);
+}
+
+.markdown-content :global(.image-error-content) {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.75rem;
+ color: var(--muted-foreground);
+ text-align: center;
+}
+
+.markdown-content :global(.image-error-content svg) {
+ opacity: 0.5;
+}
+
+.markdown-content :global(.image-error-text) {
+ font-size: 0.875rem;
+}
+
+.markdown-content :global(.image-error-link) {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.375rem;
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: var(--primary);
+ background: var(--background);
+ border: 1px solid var(--border);
+ border-radius: 0.375rem;
+ text-decoration: none;
+ transition: all 0.2s ease;
+}
+
+.markdown-content :global(.image-error-link:hover) {
+ background: var(--muted);
+ border-color: var(--primary);
+}
+
+/* Mermaid diagrams */
+.markdown-content :global(pre.mermaid) {
+ background: transparent;
+ border: none;
+ padding: 0;
+ text-align: center;
+ font-family: inherit;
+ cursor: pointer;
+ transition: opacity 0.15s ease;
+ position: relative;
+}
+
+/* Hide mermaid code text until rendered - prevents flash */
+.markdown-content :global(pre.mermaid:not([data-mermaid-rendered])),
+.markdown-content :global(pre.mermaid[data-mermaid-rendered]:not(:has(svg))) {
+ display: none;
+}
+
+.markdown-content :global(pre.mermaid:hover) {
+ opacity: 0.85;
+}
+
+.markdown-content :global(pre.mermaid svg) {
+ max-width: 90%;
+ margin: 0 auto;
+ height: auto;
+ display: block;
+ padding: 3rem 1rem;
+}
+
+/* Mermaid block wrapper - matches code block styling */
+.markdown-content :global(.mermaid-block-wrapper) {
+ margin: 1.5rem 0;
+ border-radius: 0.75rem;
+ overflow: hidden;
+ border: 1px solid color-mix(in oklch, var(--border) 30%, transparent);
+ background: var(--code-background);
+ box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+ position: relative;
+ min-height: var(--min-message-height);
+ max-height: var(--max-message-height);
+}
+
+.markdown-content:global(.dark) :global(.mermaid-block-wrapper) {
+ border-color: color-mix(in oklch, var(--border) 20%, transparent);
+}
+
+.markdown-content :global(.mermaid-scroll-container) {
+ min-height: 350px;
+ max-height: var(--max-message-height);
+ overflow-y: auto;
+ overflow-x: auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 3rem 1rem 1rem;
+}
+
+.full-height-code-blocks :global(.mermaid-block-wrapper) {
+ max-height: none;
+}
+
+.full-height-code-blocks :global(.mermaid-scroll-container) {
+ max-height: none;
+ overflow-y: visible;
+}
+
+/* Mermaid block uses same header styling as code blocks */
+.markdown-content :global(.mermaid-block-wrapper .code-block-header) {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.5rem 1rem 0;
+ font-size: 0.875rem;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+}
+
+.markdown-content :global(.mermaid-block-wrapper .code-block-actions) {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+/* Mermaid pre element - remove default margins */
+.markdown-content :global(.mermaid-block-wrapper pre.mermaid) {
+ background: transparent;
+ border: none;
+ padding: 0;
+ margin: 0;
+ text-align: center;
+}
+
+/* Mermaid SVG should be bigger */
+.markdown-content :global(.mermaid-block-wrapper pre.mermaid svg) {
+ width: unset !important;
+ height: auto;
+ display: block;
+ padding: 3rem 1rem;
+}
+
+/* Streaming mermaid block - empty preview box */
+.mermaid-streaming-block {
+ min-height: 300px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.mermaid-loading-placeholder {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 3rem;
+ color: var(--muted-foreground);
+}
+
+.mermaid-loading-text {
+ font-size: 0.875rem;
+ font-style: italic;
+}
--- /dev/null
+/**
+ * Event handler factories for markdown content components.
+ * Uses dependency injection pattern to avoid direct component state access.
+ */
+
+import { copyCodeToClipboard, copyToClipboard } from '$lib/utils';
+
+export interface PreviewState {
+ previewDialogOpen: boolean;
+ previewCode: string;
+ previewLanguage: string;
+ setPreviewDialogOpen: (open: boolean) => void;
+ setPreviewCode: (code: string) => void;
+ setPreviewLanguage: (lang: string) => void;
+}
+
+export interface MermaidPreviewState {
+ mermaidPreviewOpen: boolean;
+ mermaidPreviewSvgHtml: string;
+ setMermaidPreviewOpen: (open: boolean) => void;
+ setMermaidPreviewSvgHtml: (html: string) => void;
+}
+
+export interface RenderedBlocksState {
+ renderedBlocks: Array<{ id: string; html: string; contentHash?: string }>;
+ setRenderedBlocks: (blocks: Array<{ id: string; html: string; contentHash?: string }>) => void;
+}
+
+/**
+ * Creates a click handler for copy buttons in code blocks.
+ * Copies the code content to clipboard.
+ */
+export function createHandleCopyClick() {
+ return async function handleCopyClick(event: Event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const target = event.currentTarget as HTMLButtonElement | null;
+ if (!target) return;
+
+ const wrapper = target.closest('.code-block-wrapper');
+ if (!wrapper) return;
+
+ const codeElement = wrapper.querySelector<HTMLElement>('code[data-code-id]');
+ if (!codeElement) return;
+
+ const rawCode = codeElement.textContent ?? '';
+
+ try {
+ await copyCodeToClipboard(rawCode);
+ } catch (error) {
+ console.error('Failed to copy code:', error);
+ }
+ };
+}
+
+/**
+ * Creates a handler for preview dialog open state changes.
+ * Clears preview content when dialog is closed.
+ */
+export function createHandlePreviewDialogOpenChange(previewState: PreviewState) {
+ return function handlePreviewDialogOpenChange(open: boolean) {
+ previewState.setPreviewDialogOpen(open);
+
+ if (!open) {
+ previewState.setPreviewCode('');
+ previewState.setPreviewLanguage('text');
+ }
+ };
+}
+
+/**
+ * Creates a click handler for preview buttons within HTML code blocks.
+ * Opens a preview dialog with the rendered HTML content.
+ */
+export function createHandlePreviewClick(previewState: PreviewState) {
+ return async function handlePreviewClick(event: Event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const target = event.currentTarget as HTMLButtonElement | null;
+ if (!target) return;
+
+ const wrapper = target.closest('.code-block-wrapper');
+ if (!wrapper) return;
+
+ const codeElement = wrapper.querySelector<HTMLElement>('code[data-code-id]');
+ if (!codeElement) return;
+
+ const rawCode = codeElement.textContent ?? '';
+ const languageLabel = wrapper.querySelector<HTMLElement>('.code-language');
+ const language = languageLabel?.textContent?.trim() || 'text';
+
+ previewState.setPreviewCode(rawCode);
+ previewState.setPreviewLanguage(language);
+ previewState.setPreviewDialogOpen(true);
+ };
+}
+
+/**
+ * Creates a click handler for mermaid block interactions.
+ * Handles copy, preview, and diagram click events via event delegation.
+ */
+export function createHandleMermaidClick(mermaidState: MermaidPreviewState) {
+ return async function handleMermaidClick(event: MouseEvent) {
+ const target = event.target as HTMLElement;
+
+ // Check if clicking on copy or preview button in mermaid block
+ const copyBtn = target.closest('.mermaid-block-wrapper .copy-code-btn');
+ const previewBtn = target.closest('.mermaid-block-wrapper .preview-code-btn');
+
+ if (copyBtn || previewBtn) {
+ const wrapper = target.closest('.mermaid-block-wrapper');
+ if (!wrapper) return;
+
+ const preElement = wrapper.querySelector<HTMLElement>('pre.mermaid[data-mermaid-syntax]');
+ if (!preElement) return;
+
+ const mermaidSyntax = preElement.dataset.mermaidSyntax ?? '';
+
+ if (copyBtn) {
+ event.preventDefault();
+ event.stopPropagation();
+ try {
+ await copyToClipboard(mermaidSyntax);
+ } catch (error) {
+ console.error('Failed to copy mermaid syntax:', error);
+ }
+ return;
+ }
+
+ if (previewBtn) {
+ event.preventDefault();
+ event.stopPropagation();
+ const svg = preElement.querySelector('svg');
+ if (!svg) return;
+ mermaidState.setMermaidPreviewSvgHtml(svg.outerHTML);
+ mermaidState.setMermaidPreviewOpen(true);
+ return;
+ }
+ }
+
+ // Otherwise, open preview when clicking on the mermaid diagram itself
+ const mermaidEl = target.closest('.mermaid');
+ if (!mermaidEl) return;
+
+ const svg = mermaidEl.querySelector('svg');
+ if (!svg) return;
+
+ mermaidState.setMermaidPreviewSvgHtml(svg.outerHTML);
+ mermaidState.setMermaidPreviewOpen(true);
+ };
+}
+
+/**
+ * Creates a handler for mermaid preview dialog open state changes.
+ * Cleans up SVG content when dialog is closed.
+ */
+export function createHandleMermaidPreviewOpenChange(mermaidState: MermaidPreviewState) {
+ return function handleMermaidPreviewOpenChange(open: boolean) {
+ mermaidState.setMermaidPreviewOpen(open);
+ if (!open) {
+ mermaidState.setMermaidPreviewSvgHtml('');
+ }
+ };
+}
+
+/**
+ * Creates an error handler for images that fail to load (e.g., CORS issues).
+ * Shows fallback UI for broken images.
+ */
+export function createHandleImageError(
+ renderedBlocksState: RenderedBlocksState,
+ IMAGE_NOT_ERROR_BOUND_SELECTOR: string,
+ DATA_ERROR_BOUND_ATTR: string,
+ BOOL_TRUE_STRING: string
+) {
+ return async function handleImageError(event: Event) {
+ const img = event.target as HTMLImageElement;
+ if (!img) return;
+
+ const blockId = img.closest('[data-block-id]')?.getAttribute('data-block-id');
+ if (!blockId) return;
+
+ const block = renderedBlocksState.renderedBlocks.find((b) => b.id === blockId);
+ if (!block) return;
+
+ // Skip if already handled
+ if (img.dataset[DATA_ERROR_BOUND_ATTR] === BOOL_TRUE_STRING) return;
+ img.dataset[DATA_ERROR_BOUND_ATTR] = BOOL_TRUE_STRING;
+
+ // Get the fallback HTML and replace the image
+ const fallbackHtml = `<div class="image-error-placeholder" data-original-src="${img.src}">
+ <span class="image-error-icon">⚠️</span>
+ <span class="image-error-text">Failed to load image</span>
+ </div>`;
+
+ // Replace the img element with fallback in the block's HTML
+ const newHtml = block.html.replace(/img[^>]*src=["']([^"']*)[^>]*>/g, (match, src) => {
+ if (src === img.src) {
+ return fallbackHtml.replace('data-original-src=""', `data-original-src="${src}"`);
+ }
+ return match;
+ });
+
+ // Update the block
+ const newBlocks = renderedBlocksState.renderedBlocks.map((b) =>
+ b.id === blockId ? { ...b, html: newHtml } : b
+ );
+ renderedBlocksState.setRenderedBlocks(newBlocks);
+ };
+}
+
+/**
+ * Creates a function to set up code block action event listeners.
+ * Binds click handlers to copy and preview buttons within code blocks.
+ */
+export function createSetupCodeBlockActions(
+ handleCopyClick: (event: Event) => void,
+ handlePreviewClick: (event: Event) => void
+) {
+ return function setupCodeBlockActions(containerRef: HTMLElement | null) {
+ if (!containerRef) return;
+
+ const wrappers = containerRef.querySelectorAll<HTMLElement>('.code-block-wrapper');
+
+ for (const wrapper of wrappers) {
+ const copyButton = wrapper.querySelector<HTMLButtonElement>('.copy-code-btn');
+ const previewButton = wrapper.querySelector<HTMLButtonElement>('.preview-code-btn');
+
+ if (copyButton && copyButton.dataset.listenerBound !== 'true') {
+ copyButton.dataset.listenerBound = 'true';
+ copyButton.addEventListener('click', handleCopyClick);
+ }
+
+ if (previewButton && previewButton.dataset.listenerBound !== 'true') {
+ previewButton.dataset.listenerBound = 'true';
+ previewButton.addEventListener('click', handlePreviewClick);
+ }
+ }
+ };
+}
+
+/**
+ * Creates a function to set up image error handlers.
+ * Attaches error handlers to images to show fallback UI when loading fails.
+ */
+export function createSetupImageErrorHandlers(
+ handleImageError: (event: Event) => void,
+ IMAGE_NOT_ERROR_BOUND_SELECTOR: string,
+ DATA_ERROR_BOUND_ATTR: string,
+ BOOL_TRUE_STRING: string
+) {
+ return function setupImageErrorHandlers(containerRef: HTMLElement | null) {
+ if (!containerRef) return;
+
+ const images = containerRef.querySelectorAll<HTMLImageElement>(IMAGE_NOT_ERROR_BOUND_SELECTOR);
+
+ for (const img of images) {
+ img.dataset[DATA_ERROR_BOUND_ATTR] = BOOL_TRUE_STRING;
+ img.addEventListener('error', handleImageError);
+ }
+ };
+}
--- /dev/null
+/**
+ * Utility functions for markdown processing in MarkdownContent component.
+ */
+
+import type { RootContent as HastRootContent } from 'hast';
+
+/**
+ * Generates a unique identifier for a HAST node based on its position.
+ * Used for stable block identification during incremental rendering.
+ * @param node - The HAST root content node
+ * @param indexFallback - Fallback index if position is unavailable
+ * @returns Unique string identifier for the node
+ */
+export function getHastNodeId(node: HastRootContent, indexFallback: number): string {
+ const position = node.position;
+
+ if (position?.start?.offset != null && position?.end?.offset != null) {
+ return `hast-${position.start.offset}-${position.end.offset}`;
+ }
+
+ return `${node.type}-${indexFallback}`;
+}
+
+/**
+ * Generates a hash for MDAST node based on its position.
+ * Used for cache lookup during incremental rendering.
+ */
+export function getMdastNodeHash(node: unknown, index: number): string {
+ const n = node as {
+ type?: string;
+ position?: { start?: { offset?: number }; end?: { offset?: number } };
+ };
+
+ if (n.position?.start?.offset != null && n.position?.end?.offset != null) {
+ return `${n.type}-${n.position.start.offset}-${n.position.end.offset}`;
+ }
+
+ return `${n.type}-idx${index}`;
+}
+
+/**
+ * Determines if the new content is an append (new content added to existing blocks).
+ * This is used to optimize cache reuse during streaming updates.
+ *
+ * @param newContent - The new markdown content
+ * @param previousContent - The previous markdown content to check against
+ * @returns true if the content appears to be an append operation
+ */
+export function isAppendMode(newContent: string, previousContent: string): boolean {
+ return previousContent.length > 0 && newContent.startsWith(previousContent);
+}
+
+export interface CodeInfo {
+ rawCode: string;
+ language: string;
+}
+
+/**
+ * Extracts code information from a button click target within a code block.
+ * @param target - The clicked button element
+ * @returns Object with rawCode and language, or null if extraction fails
+ */
+export function getCodeInfoFromTarget(target: HTMLElement): CodeInfo | null {
+ const wrapper = target.closest('.code-block-wrapper');
+
+ if (!wrapper) {
+ console.error('No wrapper found');
+ return null;
+ }
+
+ const codeElement = wrapper.querySelector<HTMLElement>('code[data-code-id]');
+
+ if (!codeElement) {
+ console.error('No code element found in wrapper');
+ return null;
+ }
+
+ const rawCode = codeElement.textContent ?? '';
+
+ const languageLabel = wrapper.querySelector<HTMLElement>('.code-language');
+ const language = languageLabel?.textContent?.trim() || 'text';
+
+ return { rawCode, language };
+}
--- /dev/null
+/**
+ * Shared utilities for enhanced code blocks and mermaid diagram blocks.
+ * Contains common HAST element creation functions to avoid code duplication.
+ */
+
+import type { Element, ElementContent } from 'hast';
+import {
+ CODE_BLOCK_HEADER_CLASS,
+ CODE_BLOCK_ACTIONS_CLASS,
+ CODE_LANGUAGE_CLASS,
+ COPY_CODE_BTN_CLASS,
+ PREVIEW_CODE_BTN_CLASS,
+ RELATIVE_CLASS,
+ COPY_ICON_SVG,
+ PREVIEW_ICON_SVG
+} from '$lib/constants';
+
+export interface BlockIdGenerator {
+ (id: number): string;
+}
+
+/**
+ * Creates an icon element with the given SVG content.
+ */
+export function createIconElement(svg: string): Element {
+ return {
+ type: 'element',
+ tagName: 'span',
+ properties: {},
+ children: [{ type: 'raw', value: svg } as unknown as ElementContent]
+ };
+}
+
+/**
+ * Creates a button element with icon.
+ */
+export function createButton(
+ className: string,
+ title: string,
+ iconSvg: string,
+ id: string,
+ idAttribute: string
+): Element {
+ return {
+ type: 'element',
+ tagName: 'button',
+ properties: {
+ className: [className],
+ [idAttribute]: id,
+ title,
+ type: 'button'
+ },
+ children: [createIconElement(iconSvg)]
+ };
+}
+
+/**
+ * Creates a copy button element.
+ */
+export function createCopyButton(id: string, idAttribute: string, title: string = 'Copy'): Element {
+ return createButton(COPY_CODE_BTN_CLASS, title, COPY_ICON_SVG, id, idAttribute);
+}
+
+/**
+ * Creates a preview button element.
+ */
+export function createPreviewButton(
+ id: string,
+ idAttribute: string,
+ title: string = 'Preview'
+): Element {
+ return createButton(PREVIEW_CODE_BTN_CLASS, title, PREVIEW_ICON_SVG, id, idAttribute);
+}
+
+/**
+ * Creates a block header with language label and action buttons.
+ */
+export function createBlockHeader(
+ language: string,
+ id: string,
+ idAttribute: string,
+ actions: Element[],
+ languageClassName: string = CODE_LANGUAGE_CLASS
+): Element {
+ return {
+ type: 'element',
+ tagName: 'div',
+ properties: { className: [CODE_BLOCK_HEADER_CLASS] },
+ children: [
+ {
+ type: 'element',
+ tagName: 'span',
+ properties: { className: [languageClassName] },
+ children: [{ type: 'text', value: language }]
+ },
+ {
+ type: 'element',
+ tagName: 'div',
+ properties: { className: [CODE_BLOCK_ACTIONS_CLASS] },
+ children: actions
+ }
+ ]
+ };
+}
+
+/**
+ * Creates a scroll container element.
+ */
+export function createScrollContainer(preElement: Element, scrollContainerClass: string): Element {
+ return {
+ type: 'element',
+ tagName: 'div',
+ properties: { className: [scrollContainerClass] },
+ children: [preElement]
+ };
+}
+
+/**
+ * Creates a wrapper element with header and scroll container.
+ */
+export function createWrapper(
+ header: Element,
+ preElement: Element,
+ wrapperClass: string,
+ scrollContainerClass: string,
+ additionalAttributes?: Record<string, string>
+): Element {
+ return {
+ type: 'element',
+ tagName: 'div',
+ properties: {
+ className: [wrapperClass, RELATIVE_CLASS],
+ ...additionalAttributes
+ } as Element['properties'],
+ children: [header, createScrollContainer(preElement, scrollContainerClass)]
+ };
+}
+
+/**
+ * Generates a unique block ID using a global counter.
+ */
+export function generateBlockId(prefix: string, windowKey: keyof Window): string {
+ if (typeof window !== 'undefined') {
+ const idx = window[windowKey] as number | undefined;
+ const next = (idx ?? 0) + 1;
+ (window as unknown as Record<string, number>)[windowKey] = next;
+ return `${prefix}-${next}`;
+ }
+ // Fallback for SSR - use timestamp + random
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
+}
import type { Plugin } from 'unified';
import type { Root, Element, ElementContent } from 'hast';
import { visit } from 'unist-util-visit';
+import { CODE_BLOCK_SCROLL_CONTAINER_CLASS, CODE_BLOCK_WRAPPER_CLASS } from '$lib/constants';
import {
- CODE_BLOCK_SCROLL_CONTAINER_CLASS,
- CODE_BLOCK_WRAPPER_CLASS,
- CODE_BLOCK_HEADER_CLASS,
- CODE_BLOCK_ACTIONS_CLASS,
- CODE_LANGUAGE_CLASS,
- COPY_CODE_BTN_CLASS,
- PREVIEW_CODE_BTN_CLASS,
- RELATIVE_CLASS
-} from '$lib/constants';
+ createBlockHeader,
+ createCopyButton,
+ createPreviewButton,
+ createWrapper,
+ generateBlockId
+} from './code-block-utils';
declare global {
interface Window {
}
}
-const COPY_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy-icon lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
-
-const PREVIEW_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye lucide-eye-icon"><path d="M2.062 12.345a1 1 0 0 1 0-.69C3.5 7.73 7.36 5 12 5s8.5 2.73 9.938 6.655a1 1 0 0 1 0 .69C20.5 16.27 16.64 19 12 19s-8.5-2.73-9.938-6.655"/><circle cx="12" cy="12" r="3"/></svg>`;
-
-function createIconElement(svg: string): Element {
- return {
- type: 'element',
- tagName: 'span',
- properties: {},
- children: [{ type: 'raw', value: svg } as unknown as ElementContent]
- };
-}
-
-function createButton(className: string, title: string, iconSvg: string, codeId: string): Element {
- return {
- type: 'element',
- tagName: 'button',
- properties: {
- className: [className],
- 'data-code-id': codeId,
- title,
- type: 'button'
- },
- children: [createIconElement(iconSvg)]
- };
-}
-
-function createCopyButton(codeId: string): Element {
- return createButton(COPY_CODE_BTN_CLASS, 'Copy code', COPY_ICON_SVG, codeId);
-}
-
-function createPreviewButton(codeId: string): Element {
- return createButton(PREVIEW_CODE_BTN_CLASS, 'Preview code', PREVIEW_ICON_SVG, codeId);
-}
-
-function createHeader(language: string, codeId: string): Element {
- const actions: Element[] = [createCopyButton(codeId)];
-
- if (language.toLowerCase() === 'html') {
- actions.push(createPreviewButton(codeId));
- }
-
- return {
- type: 'element',
- tagName: 'div',
- properties: { className: [CODE_BLOCK_HEADER_CLASS] },
- children: [
- {
- type: 'element',
- tagName: 'span',
- properties: { className: [CODE_LANGUAGE_CLASS] },
- children: [{ type: 'text', value: language }]
- },
- {
- type: 'element',
- tagName: 'div',
- properties: { className: [CODE_BLOCK_ACTIONS_CLASS] },
- children: actions
- }
- ]
- };
-}
-
-function createScrollContainer(preElement: Element): Element {
- return {
- type: 'element',
- tagName: 'div',
- properties: { className: [CODE_BLOCK_SCROLL_CONTAINER_CLASS] },
- children: [preElement]
- };
-}
-
-function createWrapper(header: Element, preElement: Element): Element {
- return {
- type: 'element',
- tagName: 'div',
- properties: { className: [CODE_BLOCK_WRAPPER_CLASS, RELATIVE_CLASS] },
- children: [header, createScrollContainer(preElement)]
- };
-}
-
function extractLanguage(codeElement: Element): string {
const className = codeElement.properties?.className;
if (!Array.isArray(className)) return 'text';
return 'text';
}
-/**
- * Generates a unique code block ID using a global counter.
- */
-function generateCodeId(): string {
- if (typeof window !== 'undefined') {
- return `code-${(window.idxCodeBlock = (window.idxCodeBlock ?? 0) + 1)}`;
- }
- // Fallback for SSR - use timestamp + random
- return `code-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
-}
-
/**
* Rehype plugin to enhance code blocks with wrapper, header, and action buttons.
* This plugin wraps <pre><code> elements with a container that includes:
if (!codeElement) return;
const language = extractLanguage(codeElement);
- const codeId = generateCodeId();
+ const codeId = generateBlockId('code', 'idxCodeBlock');
codeElement.properties = {
...codeElement.properties,
'data-code-id': codeId
};
- const header = createHeader(language, codeId);
- const wrapper = createWrapper(header, node);
+ const actions: Element[] = [createCopyButton(codeId, 'data-code-id', 'Copy code')];
+
+ if (language.toLowerCase() === 'html') {
+ actions.push(createPreviewButton(codeId, 'data-code-id', 'Preview code'));
+ }
+
+ const header = createBlockHeader(language, codeId, 'data-code-id', actions);
+ const wrapper = createWrapper(
+ header,
+ node,
+ CODE_BLOCK_WRAPPER_CLASS,
+ CODE_BLOCK_SCROLL_CONTAINER_CLASS
+ );
// Replace pre with wrapper in parent
(parent.children as ElementContent[])[index] = wrapper;
--- /dev/null
+/**
+ * Rehype plugin to enhance mermaid diagram blocks with wrapper, header, and action buttons.
+ *
+ * Wraps <pre class="mermaid"> elements with a container that includes:
+ * - Language label ("mermaid")
+ * - Copy button (copies mermaid syntax to clipboard)
+ * - Preview button (opens fullscreen preview dialog)
+ *
+ * This operates directly on the HAST tree for better performance,
+ * avoiding the need to stringify and re-parse HTML.
+ */
+
+import type { Plugin } from 'unified';
+import type { Root, Element, ElementContent } from 'hast';
+import { visit } from 'unist-util-visit';
+import { MERMAID_WRAPPER_CLASS, MERMAID_SCROLL_CONTAINER_CLASS } from '$lib/constants';
+import {
+ createBlockHeader,
+ createCopyButton,
+ createPreviewButton,
+ createWrapper,
+ generateBlockId
+} from './code-block-utils';
+
+declare global {
+ interface Window {
+ idxMermaidBlock?: number;
+ }
+}
+
+/**
+ * Rehype plugin to enhance mermaid diagram blocks with wrapper, header, and action buttons.
+ * This plugin wraps <pre class="mermaid"> elements with a container that includes:
+ * - Language label ("mermaid")
+ * - Copy button
+ * - Preview button
+ */
+export const rehypeEnhanceMermaidBlocks: Plugin<[], Root> = () => {
+ return (tree: Root) => {
+ visit(tree, 'element', (node: Element, index, parent) => {
+ if (node.tagName !== 'pre' || !parent || index === undefined) return;
+
+ const className = node.properties?.className;
+ if (!Array.isArray(className)) return;
+
+ const isMermaid = className.some((cls) => typeof cls === 'string' && cls === 'mermaid');
+
+ if (!isMermaid) return;
+
+ const mermaidId = generateBlockId('mermaid', 'idxMermaidBlock');
+
+ // Extract the mermaid syntax (text content of the pre element)
+ const diagramText = node.children
+ .map((child) => {
+ if (child.type === 'text') return child.value;
+ return '';
+ })
+ .join('');
+
+ // Store the mermaid syntax in data attribute for copy functionality
+ node.properties = {
+ ...node.properties,
+ 'data-mermaid-syntax': diagramText,
+ 'data-mermaid-id': mermaidId
+ };
+
+ const actions = [
+ createCopyButton(mermaidId, 'data-mermaid-id', 'Copy mermaid syntax'),
+ createPreviewButton(mermaidId, 'data-mermaid-id', 'Preview diagram')
+ ];
+
+ const header = createBlockHeader('mermaid', mermaidId, 'data-mermaid-id', actions);
+ const wrapper = createWrapper(
+ header,
+ node,
+ MERMAID_WRAPPER_CLASS,
+ MERMAID_SCROLL_CONTAINER_CLASS,
+ { 'data-mermaid-id': mermaidId }
+ );
+
+ // Replace pre with wrapper in parent
+ (parent.children as ElementContent[])[index] = wrapper;
+ });
+ };
+};
--- /dev/null
+import type { Plugin } from 'unified';
+import type { Root, Element, ElementContent, Text } from 'hast';
+import { visit } from 'unist-util-visit';
+
+/**
+ * Recursively extracts all text content from a HAST node.
+ * Handles nested elements (e.g., span wrappers from syntax highlighting).
+ */
+function extractText(node: ElementContent): string {
+ if (node.type === 'text') return node.value;
+ if (node.type === 'element') {
+ return (node.children ?? []).map(extractText).join('');
+ }
+ return '';
+}
+
+/**
+ * Rehype plugin to convert mermaid code blocks to <pre class="mermaid"> elements.
+ *
+ * Transforms:
+ * <pre><code class="language-mermaid">graph TD; A-->B</code></pre>
+ * into:
+ * <pre class="mermaid">graph TD; A-->B</pre>
+ *
+ * The mermaid library renders these client-side via mermaid.run().
+ *
+ * Must run BEFORE rehypeEnhanceCodeBlocks so mermaid blocks are not wrapped
+ * with code block headers/buttons (they have no <code> child, so they're skipped).
+ */
+export const rehypeMermaidPre: Plugin<[], Root> = () => {
+ return (tree: Root) => {
+ visit(tree, 'element', (node: Element, index, parent) => {
+ if (node.tagName !== 'pre' || !parent || index === undefined) return;
+
+ const codeElement = node.children.find(
+ (child): child is Element => child.type === 'element' && child.tagName === 'code'
+ );
+
+ if (!codeElement) return;
+
+ const className = codeElement.properties?.className;
+ if (!Array.isArray(className)) return;
+
+ const isMermaid = className.some(
+ (cls) => typeof cls === 'string' && cls === 'language-mermaid'
+ );
+
+ if (!isMermaid) return;
+
+ // Recursively extract text to handle nested spans from syntax highlighting
+ const diagramText = codeElement.children.map(extractText).join('').trim();
+
+ if (!diagramText) return;
+
+ const mermaidPre: Element = {
+ type: 'element',
+ tagName: 'pre',
+ properties: {
+ className: ['mermaid']
+ },
+ children: [{ type: 'text', value: diagramText } as Text]
+ };
+
+ (parent.children as ElementContent[])[index] = mermaidPre;
+ });
+ };
+};
--- /dev/null
+<script lang="ts">
+ import MermaidPreviewControls from './MermaidPreviewControls.svelte';
+
+ interface Props {
+ svgHtml: string;
+ }
+
+ let { svgHtml }: Props = $props();
+
+ // Zoom and pan state
+ let scale = $state(1);
+ let translateX = $state(0);
+ let translateY = $state(0);
+ let isDragging = $state(false);
+ const containerRef = { current: null as HTMLDivElement | null };
+
+ // Drag start position
+ let dragStartX = 0;
+ let dragStartY = 0;
+ let dragStartTranslateX = 0;
+ let dragStartTranslateY = 0;
+
+ const MIN_SCALE = 0.1;
+ const MAX_SCALE = 10;
+ const ZOOM_STEP = 0.15;
+
+ function resetView() {
+ scale = 1;
+ translateX = 0;
+ translateY = 0;
+ }
+
+ function zoomIn() {
+ scale = Math.min(scale + ZOOM_STEP, MAX_SCALE);
+ }
+
+ function zoomOut() {
+ scale = Math.max(scale - ZOOM_STEP, MIN_SCALE);
+ }
+
+ function handleWheel(event: WheelEvent) {
+ event.preventDefault();
+
+ const delta = event.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
+ scale = Math.min(Math.max(scale + delta, MIN_SCALE), MAX_SCALE);
+ }
+
+ // Imperatively attach a non-passive wheel listener so preventDefault() actually works
+ // (Svelte 5 wheel listeners are passive by default, making preventDefault() a no-op)
+ $effect(() => {
+ const el = containerRef.current;
+ if (!el) return;
+
+ function onWheel(e: WheelEvent) {
+ handleWheel(e);
+ }
+
+ el.addEventListener('wheel', onWheel, { passive: false });
+ return () => el.removeEventListener('wheel', onWheel);
+ });
+
+ function handlePointerDown(event: PointerEvent) {
+ if (event.button !== 0 && event.pointerType === 'mouse') return;
+
+ isDragging = true;
+ dragStartX = event.clientX;
+ dragStartY = event.clientY;
+ dragStartTranslateX = translateX;
+ dragStartTranslateY = translateY;
+
+ (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
+ }
+
+ function handlePointerMove(event: PointerEvent) {
+ if (!isDragging) return;
+
+ translateX = dragStartTranslateX + (event.clientX - dragStartX);
+ translateY = dragStartTranslateY + (event.clientY - dragStartY);
+ }
+
+ function handlePointerUp() {
+ isDragging = false;
+ }
+</script>
+
+<div
+ bind:this={containerRef.current}
+ class="mermaid-preview relative flex items-center justify-center overflow-hidden bg-muted/20"
+>
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
+ <div
+ class="mermaid-preview-diagram transform-origin-center inline-block min-h-fit min-w-fit will-change-transform {isDragging &&
+ 'select-none'}"
+ style="transform: translate({translateX}px, {translateY}px) scale({scale}); cursor: {isDragging
+ ? 'grabbing'
+ : 'grab'};"
+ onpointerdown={handlePointerDown}
+ onpointermove={handlePointerMove}
+ onpointerup={handlePointerUp}
+ onpointerleave={handlePointerUp}
+ >
+ <!-- eslint-disable-next-line no-at-html-tags -->
+ {@html svgHtml}
+ </div>
+
+ <MermaidPreviewControls
+ {scale}
+ {svgHtml}
+ onZoomIn={zoomIn}
+ onZoomOut={zoomOut}
+ onResetView={resetView}
+ />
+</div>
+
+<style lang="postcss" scoped>
+ /* Styles for SVGs rendered via {@html} — no Tailwind class can target child elements */
+ .mermaid-preview-diagram :global(svg) {
+ min-height: min(50vh, 12rem);
+ min-width: min(80vw, 20rem);
+ max-width: none !important;
+ max-height: none !important;
+ height: auto !important;
+ width: auto !important;
+ display: block;
+ }
+</style>
--- /dev/null
+<script lang="ts">
+ import { Download } from '@lucide/svelte';
+ import ZoomInIcon from '@lucide/svelte/icons/zoom-in';
+ import ZoomOutIcon from '@lucide/svelte/icons/zoom-out';
+ import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
+
+ interface Props {
+ scale: number;
+ svgHtml: string;
+ onZoomIn: () => void;
+ onZoomOut: () => void;
+ onResetView: () => void;
+ }
+
+ let { scale, svgHtml, onZoomIn, onZoomOut, onResetView }: Props = $props();
+
+ function downloadSvg() {
+ if (!svgHtml) return;
+ const blob = new Blob([svgHtml], { type: 'image/svg+xml' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'diagram.svg';
+ a.click();
+ URL.revokeObjectURL(url);
+ }
+</script>
+
+<div
+ class="mermaid-preview-controls absolute bottom-8 flex shrink-0 items-center justify-center p-3"
+>
+ <div class="mermaid-preview-controls-inner flex items-center gap-1 rounded-lg bg-muted p-1">
+ <button
+ class="mermaid-preview-btn flex h-8 w-8 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent text-foreground transition-colors hover:bg-muted-foreground/15 active:bg-muted-foreground/25"
+ onclick={onZoomOut}
+ title="Zoom out"
+ aria-label="Zoom out"
+ >
+ <ZoomOutIcon class="mermaid-preview-btn-icon h-4 w-4" />
+ </button>
+ <span
+ class="mermaid-preview-zoom-label min-w-[3.5rem] px-0.5 text-center text-xs font-medium text-muted-foreground tabular-nums select-none"
+ >{Math.round(scale * 100)}%</span
+ >
+ <button
+ class="mermaid-preview-btn flex h-8 w-8 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent text-foreground transition-colors hover:bg-muted-foreground/15 active:bg-muted-foreground/25"
+ onclick={onZoomIn}
+ title="Zoom in"
+ aria-label="Zoom in"
+ >
+ <ZoomInIcon class="mermaid-preview-btn-icon h-4 w-4" />
+ </button>
+ <div class="mermaid-preview-controls-separator mx-1 h-5 w-px bg-border/50"></div>
+
+ <button
+ class="mermaid-preview-btn flex h-8 w-8 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent text-foreground transition-colors hover:bg-muted-foreground/15 active:bg-muted-foreground/25"
+ onclick={onResetView}
+ title="Reset view"
+ aria-label="Reset view"
+ >
+ <RotateCcwIcon class="mermaid-preview-btn-icon h-4 w-4" />
+ </button>
+ <div class="mermaid-preview-controls-separator mx-1 h-5 w-px bg-border/50"></div>
+
+ <button
+ class="mermaid-preview-btn flex h-8 w-8 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent text-foreground transition-colors hover:bg-muted-foreground/15 active:bg-muted-foreground/25"
+ onclick={downloadSvg}
+ title="Download SVG"
+ aria-label="Download SVG"
+ >
+ <Download class="mermaid-preview-btn-icon h-4 w-4" />
+ </button>
+ </div>
+</div>
* ```
*/
export { default as CollapsibleContentBlock } from './CollapsibleContentBlock.svelte';
+
+/**
+ * **MermaidPreview** - Interactive Mermaid diagram viewer
+ *
+ * Renders Mermaid-generated SVG diagrams with zoom, pan, and fit-to-view controls.
+ *
+ * **Features:**
+ * - Mouse wheel zoom in/out
+ * - Click-drag panning with pointer capture
+ * - Fit to view and reset view controls
+ * - Download as SVG
+ * - Responsive scaling with viewBox detection
+ *
+ * @example
+ * ```svelte
+ * <MermaidPreview svgHtml={diagramSvg} />
+ * ```
+ */
+export { default as MermaidPreview } from './MermaidPreview.svelte';
--- /dev/null
+<script lang="ts">
+ import * as Dialog from '$lib/components/ui/dialog/index.js';
+ import { MermaidPreview } from '$lib/components/app/content';
+
+ interface Props {
+ open: boolean;
+ svgHtml: string;
+ onOpenChange?: (open: boolean) => void;
+ }
+
+ let { open = $bindable(), svgHtml, onOpenChange }: Props = $props();
+</script>
+
+<Dialog.Root bind:open {onOpenChange}>
+ <Dialog.Content
+ class="z-999999 grid max-h-full max-w-full! grid-rows-[1fr_auto] overflow-hidden p-0 md:h-[90vh] md:max-w-[90vw]!"
+ >
+ <MermaidPreview {svgHtml} />
+ </Dialog.Content>
+</Dialog.Root>
* ```
*/
export { default as DialogMcpResourcePreview } from './DialogMcpResourcePreview.svelte';
+
+/**
+ * **DialogMermaidPreview** - Full-screen Mermaid diagram preview with zoom and pan
+ *
+ * Full-screen dialog for previewing Mermaid diagrams with interactive controls.
+ * Supports mouse wheel zoom, drag-to-pan, and toolbar buttons for zoom in/out,
+ * fit to view, and reset.
+ *
+ * **Architecture:**
+ * - Uses UI dialog components (`Dialog.Root`, `Dialog.Overlay`, `Dialog.Content`)
+ * for consistent styling, animations, and accessibility
+ * - CSS transform-based zoom and pan (no external dependencies)
+ * - Pointer events for cross-device drag support (mouse + touch)
+ * - Wheel events for zoom-to-cursor functionality
+ *
+ * **Features:**
+ * - Scroll wheel zoom centered on cursor position
+ * - Click and drag to pan the diagram
+ * - Toolbar with zoom in, zoom out, fit to view, reset controls
+ * - Zoom percentage indicator
+ * - Keyboard accessible close button
+ * - Dark/light theme support
+ *
+ * @example
+ * ```svelte
+ * <DialogMermaidPreview
+ * bind:open={showMermaidPreview}
+ * svgHtml={mermaidSvgContent}
+ * />
+ * ```
+ */
+export { default as DialogMermaidPreview } from './DialogMermaidPreview.svelte';
[ModelModality.AUDIO]: 'Audio',
[ModelModality.VIDEO]: 'Video'
} as const;
+
+// Shared SVG icon strings for copy and preview buttons
+export const COPY_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy-icon lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
+
+export const PREVIEW_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye lucide-eye-icon"><path d="M2.062 12.345a1 1 0 0 1 0-.69C3.5 7.73 7.36 5 12 5s8.5 2.73 9.938 6.655a1 1 0 0 1 0 .69C20.5 16.27 16.64 19 12 19s-8.5-2.73-9.938-6.655"/><circle cx="12" cy="12" r="3"/></svg>`;
export * from './chat-form';
export * from './cli-flags';
export * from './code-blocks';
+export * from './icons';
export * from './code';
export * from './context-keys';
export * from './control-actions';
export * from './latex-protection';
export * from './literal-html';
export * from './markdown';
+export * from './mermaid-blocks';
export * from './max-bundle-size';
export * from './mcp';
export * from './mcp-form';
--- /dev/null
+export const MERMAID_WRAPPER_CLASS = 'mermaid-block-wrapper';
+export const MERMAID_SCROLL_CONTAINER_CLASS = 'mermaid-scroll-container';