This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

web/components: add pierre file and diff viewers

Signed-off-by: dawn <dawn@tangled.org>

author
dawn
date (Aug 1, 2026, 2:21 AM +0300) commit e8b1a8e1 parent dd0f7b8b change-id qknxpxqv
+1476 -26
+1
web/package.json
··· 76 76 "@mdit/plugin-alert": "^1.0.1", 77 77 "@mdit/plugin-footnote": "^1.0.1", 78 78 "@mdit/plugin-tasklist": "^1.0.1", 79 + "@pierre/diffs": "^1.2.12", 79 80 "markdown-it": "^14.3.0", 80 81 "markdown-it-anchor": "^9.2.1", 81 82 "markdown-it-emoji": "^3.1.0",
+436 -25
web/pnpm-lock.yaml
··· 35 35 '@mdit/plugin-tasklist': 36 36 specifier: ^1.0.1 37 37 version: 1.0.1(markdown-it@14.3.0) 38 + '@pierre/diffs': 39 + specifier: ^1.2.12 40 + version: 1.2.12(@shikijs/themes@4.3.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) 38 41 markdown-it: 39 42 specifier: ^14.3.0 40 43 version: 14.3.0 ··· 128 131 version: 4.7.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(tailwindcss@4.3.2)(typescript@6.0.3) 129 132 eslint-plugin-storybook: 130 133 specifier: ^10.5.0 131 - version: 10.5.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(storybook@10.5.0(@types/react@19.2.17)(prettier@3.9.5)(react@19.2.7))(supports-color@7.2.0)(typescript@6.0.3) 134 + version: 10.5.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(storybook@10.5.0(@types/react@19.2.17)(prettier@3.9.5)(react@19.2.7))(supports-color@10.2.2)(typescript@6.0.3) 132 135 eslint-plugin-svelte: 133 136 specifier: ^3.20.0 134 137 version: 3.20.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(svelte@5.56.5(@typescript-eslint/types@8.64.0)) ··· 164 167 version: 6.0.3 165 168 typescript-eslint: 166 169 specifier: ^8.64.0 167 - version: 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) 170 + version: 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) 168 171 unplugin-icons: 169 172 specifier: ^23.0.1 170 173 version: 23.0.1(svelte@5.56.5(@typescript-eslint/types@8.64.0)) ··· 1108 1111 cpu: [x64] 1109 1112 os: [win32] 1110 1113 1114 + '@pierre/diffs@1.2.12': 1115 + resolution: {integrity: sha512-pY/gmgWL03WnagqCyCnBi3QtRXUv4hCIY6FYqd5b1ZGaoI6a4Bsji8j+yRl2RfzPh/8Hf19rCl1GE80G6a1cLQ==} 1116 + peerDependencies: 1117 + react: ^18.3.1 || ^19.0.0 1118 + react-dom: ^18.3.1 || ^19.0.0 1119 + 1120 + '@pierre/theme@1.1.0': 1121 + resolution: {integrity: sha512-GC2OWTAfTIIWWYhPCygwG8t2EtePQkRfON4MI2rwIkJylmiyqIttJID2dCL8sUD8cNdEvYkEyfEHHKMeCiDLoQ==} 1122 + engines: {vscode: ^1.0.0} 1123 + 1124 + '@pierre/theming@0.0.2': 1125 + resolution: {integrity: sha512-QM1M4stXfnzfaE8I8YbjXSApV8c+2dBsXJj8eYg9WTpBR/cTmCZIcfGnN4p13iRrYu2Br/R/OJfEL7uR8Qjctw==} 1126 + peerDependencies: 1127 + '@pierre/theme': ^1.1.0 1128 + '@shikijs/themes': ^3.0.0 || ^4.0.0 1129 + react: ^18.3.1 || ^19.0.0 1130 + react-dom: ^18.3.1 || ^19.0.0 1131 + shiki: ^3.0.0 || ^4.0.0 1132 + peerDependenciesMeta: 1133 + '@pierre/theme': 1134 + optional: true 1135 + '@shikijs/themes': 1136 + optional: true 1137 + react: 1138 + optional: true 1139 + react-dom: 1140 + optional: true 1141 + shiki: 1142 + optional: true 1143 + 1111 1144 '@pkgr/core@0.3.6': 1112 1145 resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} 1113 1146 engines: {node: ^14.18.0 || >=16.0.0} ··· 1410 1443 cpu: [x64] 1411 1444 os: [win32] 1412 1445 1446 + '@shikijs/core@4.3.1': 1447 + resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==} 1448 + engines: {node: '>=20'} 1449 + 1450 + '@shikijs/engine-javascript@4.3.1': 1451 + resolution: {integrity: sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==} 1452 + engines: {node: '>=20'} 1453 + 1454 + '@shikijs/engine-oniguruma@4.3.1': 1455 + resolution: {integrity: sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==} 1456 + engines: {node: '>=20'} 1457 + 1458 + '@shikijs/langs@4.3.1': 1459 + resolution: {integrity: sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==} 1460 + engines: {node: '>=20'} 1461 + 1462 + '@shikijs/primitive@4.3.1': 1463 + resolution: {integrity: sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==} 1464 + engines: {node: '>=20'} 1465 + 1466 + '@shikijs/themes@4.3.1': 1467 + resolution: {integrity: sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==} 1468 + engines: {node: '>=20'} 1469 + 1470 + '@shikijs/transformers@4.3.1': 1471 + resolution: {integrity: sha512-z6ir0bGDgWcF2FduktEfPgIsdOtIlDiLAjFBgBzE42Q9xHbkkIXZtORHzlLVB71iZP9elEcqKg6keajvOUwE2A==} 1472 + engines: {node: '>=20'} 1473 + 1474 + '@shikijs/types@4.3.1': 1475 + resolution: {integrity: sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==} 1476 + engines: {node: '>=20'} 1477 + 1478 + '@shikijs/vscode-textmate@10.0.2': 1479 + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} 1480 + 1413 1481 '@sindresorhus/is@7.2.0': 1414 1482 resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} 1415 1483 engines: {node: '>=18'} ··· 1703 1771 '@types/estree@1.0.9': 1704 1772 resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} 1705 1773 1774 + '@types/hast@3.0.5': 1775 + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} 1776 + 1706 1777 '@types/json-schema@7.0.15': 1707 1778 resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 1708 1779 ··· 1714 1785 1715 1786 '@types/markdown-it@14.1.2': 1716 1787 resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} 1788 + 1789 + '@types/mdast@4.0.4': 1790 + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} 1717 1791 1718 1792 '@types/mdurl@2.0.0': 1719 1793 resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} ··· 1735 1809 1736 1810 '@types/trusted-types@2.0.7': 1737 1811 resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} 1812 + 1813 + '@types/unist@3.0.3': 1814 + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} 1738 1815 1739 1816 '@typescript-eslint/eslint-plugin@8.64.0': 1740 1817 resolution: {integrity: sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==} ··· 1794 1871 '@typescript-eslint/visitor-keys@8.64.0': 1795 1872 resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==} 1796 1873 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 1874 + 1875 + '@ungap/structured-clone@1.3.3': 1876 + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} 1797 1877 1798 1878 '@valibot/to-json-schema@1.7.1': 1799 1879 resolution: {integrity: sha512-3qkmU6KXWh8GIThEAW3kuRHPQBMjWkKy+Ppz3WkUucx53DTpOa6siMn4xDGSOhlVyMrDaJTCTMLYPZVAIk1P0A==} ··· 1940 2020 bundle-name@4.1.0: 1941 2021 resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} 1942 2022 engines: {node: '>=18'} 2023 + 2024 + ccount@2.0.1: 2025 + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} 1943 2026 1944 2027 chai@5.3.3: 1945 2028 resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} ··· 1953 2036 resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 1954 2037 engines: {node: '>=10'} 1955 2038 2039 + character-entities-html4@2.1.0: 2040 + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} 2041 + 2042 + character-entities-legacy@3.0.0: 2043 + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} 2044 + 1956 2045 check-error@2.1.3: 1957 2046 resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} 1958 2047 engines: {node: '>= 16'} ··· 1986 2075 1987 2076 color-name@1.1.4: 1988 2077 resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 2078 + 2079 + comma-separated-tokens@2.0.3: 2080 + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} 1989 2081 1990 2082 commondir@1.0.1: 1991 2083 resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} ··· 2079 2171 devalue@5.8.1: 2080 2172 resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} 2081 2173 2174 + devlop@1.1.0: 2175 + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} 2176 + 2177 + diff@9.0.0: 2178 + resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} 2179 + engines: {node: '>=0.3.1'} 2180 + 2082 2181 dom-accessibility-api@0.5.16: 2083 2182 resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} 2084 2183 ··· 2349 2448 resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} 2350 2449 engines: {node: '>= 0.4'} 2351 2450 2451 + hast-util-to-html@9.0.5: 2452 + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} 2453 + 2454 + hast-util-whitespace@3.0.0: 2455 + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} 2456 + 2352 2457 html-escaper@2.0.2: 2353 2458 resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} 2459 + 2460 + html-void-elements@3.0.0: 2461 + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} 2354 2462 2355 2463 htmlparser2@10.1.0: 2356 2464 resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} ··· 2574 2682 loupe@3.2.1: 2575 2683 resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} 2576 2684 2685 + lru_map@0.4.1: 2686 + resolution: {integrity: sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg==} 2687 + 2577 2688 lz-string@1.5.0: 2578 2689 resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} 2579 2690 hasBin: true ··· 2601 2712 resolution: {integrity: sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==} 2602 2713 hasBin: true 2603 2714 2715 + mdast-util-to-hast@13.2.1: 2716 + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} 2717 + 2604 2718 mdn-data@2.29.0: 2605 2719 resolution: {integrity: sha512-pVxQFCcaYUEAH853+v7yoI/qzhxXSq1bTb9obMYGYAN1c3Hen+XDCEvr296XhstrwlSTNgOR7mCSD4JPjbJe5A==} 2606 2720 2607 2721 mdurl@2.1.0: 2608 2722 resolution: {integrity: sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==} 2723 + 2724 + micromark-util-character@2.1.1: 2725 + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} 2726 + 2727 + micromark-util-encode@2.0.1: 2728 + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} 2729 + 2730 + micromark-util-sanitize-uri@2.0.1: 2731 + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} 2732 + 2733 + micromark-util-symbol@2.0.1: 2734 + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} 2735 + 2736 + micromark-util-types@2.0.2: 2737 + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} 2609 2738 2610 2739 min-indent@1.0.1: 2611 2740 resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} ··· 2653 2782 obug@2.1.3: 2654 2783 resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} 2655 2784 engines: {node: '>=12.20.0'} 2785 + 2786 + oniguruma-parser@0.12.2: 2787 + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} 2788 + 2789 + oniguruma-to-es@4.3.6: 2790 + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} 2656 2791 2657 2792 open@10.2.0: 2658 2793 resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} ··· 2838 2973 resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} 2839 2974 engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} 2840 2975 2976 + property-information@7.2.0: 2977 + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} 2978 + 2841 2979 punycode.js@2.3.1: 2842 2980 resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} 2843 2981 engines: {node: '>=6'} ··· 2873 3011 resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} 2874 3012 engines: {node: '>=8'} 2875 3013 3014 + regex-recursion@6.0.2: 3015 + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} 3016 + 3017 + regex-utilities@2.3.0: 3018 + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} 3019 + 3020 + regex@6.1.0: 3021 + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} 3022 + 2876 3023 regexparam@3.0.0: 2877 3024 resolution: {integrity: sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==} 2878 3025 engines: {node: '>=8'} ··· 2930 3077 resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 2931 3078 engines: {node: '>=8'} 2932 3079 3080 + shiki@4.3.1: 3081 + resolution: {integrity: sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==} 3082 + engines: {node: '>=20'} 3083 + 2933 3084 siginfo@2.0.0: 2934 3085 resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 2935 3086 ··· 2945 3096 resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 2946 3097 engines: {node: '>=0.10.0'} 2947 3098 3099 + space-separated-tokens@2.0.2: 3100 + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} 3101 + 2948 3102 stackback@0.0.2: 2949 3103 resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 2950 3104 ··· 2966 3120 vite-plus: 2967 3121 optional: true 2968 3122 3123 + stringify-entities@4.0.4: 3124 + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} 3125 + 2969 3126 strip-ansi@7.2.0: 2970 3127 resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} 2971 3128 engines: {node: '>=12'} ··· 3090 3247 resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} 3091 3248 engines: {node: '>=6'} 3092 3249 3250 + trim-lines@3.0.1: 3251 + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} 3252 + 3093 3253 ts-api-utils@2.5.0: 3094 3254 resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} 3095 3255 engines: {node: '>=18.12'} ··· 3159 3319 unicode-segmenter@0.14.5: 3160 3320 resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} 3161 3321 3322 + unist-util-is@6.0.1: 3323 + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} 3324 + 3325 + unist-util-position@5.0.0: 3326 + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} 3327 + 3328 + unist-util-stringify-position@4.0.0: 3329 + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} 3330 + 3331 + unist-util-visit-parents@6.0.2: 3332 + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} 3333 + 3334 + unist-util-visit@5.1.0: 3335 + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} 3336 + 3162 3337 universalify@2.0.1: 3163 3338 resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} 3164 3339 engines: {node: '>= 10.0.0'} ··· 3202 3377 peerDependenciesMeta: 3203 3378 typescript: 3204 3379 optional: true 3380 + 3381 + vfile-message@4.0.3: 3382 + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} 3383 + 3384 + vfile@6.0.3: 3385 + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} 3205 3386 3206 3387 vite@8.1.4: 3207 3388 resolution: {integrity: sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==} ··· 3383 3564 3384 3565 zimmerframe@1.1.4: 3385 3566 resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} 3567 + 3568 + zwitch@2.0.4: 3569 + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} 3386 3570 3387 3571 snapshots: 3388 3572 ··· 4140 4324 '@oxc-resolver/binding-win32-x64-msvc@11.24.2': 4141 4325 optional: true 4142 4326 4327 + '@pierre/diffs@1.2.12(@shikijs/themes@4.3.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': 4328 + dependencies: 4329 + '@pierre/theme': 1.1.0 4330 + '@pierre/theming': 0.0.2(@pierre/theme@1.1.0)(@shikijs/themes@4.3.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(shiki@4.3.1) 4331 + '@shikijs/transformers': 4.3.1 4332 + diff: 9.0.0 4333 + hast-util-to-html: 9.0.5 4334 + lru_map: 0.4.1 4335 + react: 19.2.7 4336 + react-dom: 19.2.7(react@19.2.7) 4337 + shiki: 4.3.1 4338 + transitivePeerDependencies: 4339 + - '@shikijs/themes' 4340 + 4341 + '@pierre/theme@1.1.0': {} 4342 + 4343 + '@pierre/theming@0.0.2(@pierre/theme@1.1.0)(@shikijs/themes@4.3.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(shiki@4.3.1)': 4344 + optionalDependencies: 4345 + '@pierre/theme': 1.1.0 4346 + '@shikijs/themes': 4.3.1 4347 + react: 19.2.7 4348 + react-dom: 19.2.7(react@19.2.7) 4349 + shiki: 4.3.1 4350 + 4143 4351 '@pkgr/core@0.3.6': {} 4144 4352 4145 4353 '@playwright/test@1.61.1': ··· 4329 4537 '@rollup/rollup-win32-x64-msvc@4.62.2': 4330 4538 optional: true 4331 4539 4540 + '@shikijs/core@4.3.1': 4541 + dependencies: 4542 + '@shikijs/primitive': 4.3.1 4543 + '@shikijs/types': 4.3.1 4544 + '@shikijs/vscode-textmate': 10.0.2 4545 + '@types/hast': 3.0.5 4546 + hast-util-to-html: 9.0.5 4547 + 4548 + '@shikijs/engine-javascript@4.3.1': 4549 + dependencies: 4550 + '@shikijs/types': 4.3.1 4551 + '@shikijs/vscode-textmate': 10.0.2 4552 + oniguruma-to-es: 4.3.6 4553 + 4554 + '@shikijs/engine-oniguruma@4.3.1': 4555 + dependencies: 4556 + '@shikijs/types': 4.3.1 4557 + '@shikijs/vscode-textmate': 10.0.2 4558 + 4559 + '@shikijs/langs@4.3.1': 4560 + dependencies: 4561 + '@shikijs/types': 4.3.1 4562 + 4563 + '@shikijs/primitive@4.3.1': 4564 + dependencies: 4565 + '@shikijs/types': 4.3.1 4566 + '@shikijs/vscode-textmate': 10.0.2 4567 + '@types/hast': 3.0.5 4568 + 4569 + '@shikijs/themes@4.3.1': 4570 + dependencies: 4571 + '@shikijs/types': 4.3.1 4572 + 4573 + '@shikijs/transformers@4.3.1': 4574 + dependencies: 4575 + '@shikijs/core': 4.3.1 4576 + '@shikijs/types': 4.3.1 4577 + 4578 + '@shikijs/types@4.3.1': 4579 + dependencies: 4580 + '@shikijs/vscode-textmate': 10.0.2 4581 + '@types/hast': 3.0.5 4582 + 4583 + '@shikijs/vscode-textmate@10.0.2': {} 4584 + 4332 4585 '@sindresorhus/is@7.2.0': {} 4333 4586 4334 4587 '@speed-highlight/core@1.2.17': {} ··· 4628 4881 4629 4882 '@types/estree@1.0.9': {} 4630 4883 4884 + '@types/hast@3.0.5': 4885 + dependencies: 4886 + '@types/unist': 3.0.3 4887 + 4631 4888 '@types/json-schema@7.0.15': {} 4632 4889 4633 4890 '@types/linkify-it@5.0.0': {} ··· 4640 4897 dependencies: 4641 4898 '@types/linkify-it': 5.0.0 4642 4899 '@types/mdurl': 2.0.0 4900 + 4901 + '@types/mdast@4.0.4': 4902 + dependencies: 4903 + '@types/unist': 3.0.3 4643 4904 4644 4905 '@types/mdurl@2.0.0': {} 4645 4906 ··· 4661 4922 4662 4923 '@types/trusted-types@2.0.7': {} 4663 4924 4664 - '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': 4925 + '@types/unist@3.0.3': {} 4926 + 4927 + '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3)': 4665 4928 dependencies: 4666 4929 '@eslint-community/regexpp': 4.12.2 4667 - '@typescript-eslint/parser': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) 4930 + '@typescript-eslint/parser': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) 4668 4931 '@typescript-eslint/scope-manager': 8.64.0 4669 - '@typescript-eslint/type-utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) 4670 - '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) 4932 + '@typescript-eslint/type-utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) 4933 + '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) 4671 4934 '@typescript-eslint/visitor-keys': 8.64.0 4672 4935 eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) 4673 4936 ignore: 7.0.6 ··· 4677 4940 transitivePeerDependencies: 4678 4941 - supports-color 4679 4942 4680 - '@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': 4943 + '@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3)': 4681 4944 dependencies: 4682 4945 '@typescript-eslint/scope-manager': 8.64.0 4683 4946 '@typescript-eslint/types': 8.64.0 4684 - '@typescript-eslint/typescript-estree': 8.64.0(supports-color@7.2.0)(typescript@6.0.3) 4947 + '@typescript-eslint/typescript-estree': 8.64.0(supports-color@10.2.2)(typescript@6.0.3) 4685 4948 '@typescript-eslint/visitor-keys': 8.64.0 4686 4949 debug: 4.4.3(supports-color@7.2.0) 4687 4950 eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) ··· 4689 4952 transitivePeerDependencies: 4690 4953 - supports-color 4691 4954 4692 - '@typescript-eslint/project-service@8.64.0(supports-color@7.2.0)(typescript@6.0.3)': 4955 + '@typescript-eslint/project-service@8.64.0(supports-color@10.2.2)(typescript@6.0.3)': 4693 4956 dependencies: 4694 4957 '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@6.0.3) 4695 4958 '@typescript-eslint/types': 8.64.0 4696 - debug: 4.4.3(supports-color@7.2.0) 4959 + debug: 4.4.3(supports-color@10.2.2) 4697 4960 typescript: 6.0.3 4698 4961 transitivePeerDependencies: 4699 4962 - supports-color ··· 4707 4970 dependencies: 4708 4971 typescript: 6.0.3 4709 4972 4710 - '@typescript-eslint/type-utils@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': 4973 + '@typescript-eslint/type-utils@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3)': 4711 4974 dependencies: 4712 4975 '@typescript-eslint/types': 8.64.0 4713 - '@typescript-eslint/typescript-estree': 8.64.0(supports-color@7.2.0)(typescript@6.0.3) 4714 - '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) 4976 + '@typescript-eslint/typescript-estree': 8.64.0(supports-color@10.2.2)(typescript@6.0.3) 4977 + '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) 4715 4978 debug: 4.4.3(supports-color@7.2.0) 4716 4979 eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) 4717 4980 ts-api-utils: 2.5.0(typescript@6.0.3) ··· 4721 4984 4722 4985 '@typescript-eslint/types@8.64.0': {} 4723 4986 4724 - '@typescript-eslint/typescript-estree@8.64.0(supports-color@7.2.0)(typescript@6.0.3)': 4987 + '@typescript-eslint/typescript-estree@8.64.0(supports-color@10.2.2)(typescript@6.0.3)': 4725 4988 dependencies: 4726 - '@typescript-eslint/project-service': 8.64.0(supports-color@7.2.0)(typescript@6.0.3) 4989 + '@typescript-eslint/project-service': 8.64.0(supports-color@10.2.2)(typescript@6.0.3) 4727 4990 '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@6.0.3) 4728 4991 '@typescript-eslint/types': 8.64.0 4729 4992 '@typescript-eslint/visitor-keys': 8.64.0 4730 - debug: 4.4.3(supports-color@7.2.0) 4993 + debug: 4.4.3(supports-color@10.2.2) 4731 4994 minimatch: 10.2.5 4732 4995 semver: 7.8.5 4733 4996 tinyglobby: 0.2.17 ··· 4736 4999 transitivePeerDependencies: 4737 5000 - supports-color 4738 5001 4739 - '@typescript-eslint/utils@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': 5002 + '@typescript-eslint/utils@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3)': 4740 5003 dependencies: 4741 5004 '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0)) 4742 5005 '@typescript-eslint/scope-manager': 8.64.0 4743 5006 '@typescript-eslint/types': 8.64.0 4744 - '@typescript-eslint/typescript-estree': 8.64.0(supports-color@7.2.0)(typescript@6.0.3) 5007 + '@typescript-eslint/typescript-estree': 8.64.0(supports-color@10.2.2)(typescript@6.0.3) 4745 5008 eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) 4746 5009 typescript: 6.0.3 4747 5010 transitivePeerDependencies: ··· 4751 5014 dependencies: 4752 5015 '@typescript-eslint/types': 8.64.0 4753 5016 eslint-visitor-keys: 5.0.1 5017 + 5018 + '@ungap/structured-clone@1.3.3': {} 4754 5019 4755 5020 '@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@6.0.3))': 4756 5021 dependencies: ··· 4928 5193 dependencies: 4929 5194 run-applescript: 7.1.0 4930 5195 5196 + ccount@2.0.1: {} 5197 + 4931 5198 chai@5.3.3: 4932 5199 dependencies: 4933 5200 assertion-error: 2.0.1 ··· 4943 5210 ansi-styles: 4.3.0 4944 5211 supports-color: 7.2.0 4945 5212 5213 + character-entities-html4@2.1.0: {} 5214 + 5215 + character-entities-legacy@3.0.0: {} 5216 + 4946 5217 check-error@2.1.3: {} 4947 5218 4948 5219 chokidar@4.0.3: ··· 4960 5231 color-name: 1.1.4 4961 5232 4962 5233 color-name@1.1.4: {} 5234 + 5235 + comma-separated-tokens@2.0.3: {} 4963 5236 4964 5237 commondir@1.0.1: {} 4965 5238 ··· 4987 5260 4988 5261 dayjs@1.11.21: {} 4989 5262 5263 + debug@4.4.3(supports-color@10.2.2): 5264 + dependencies: 5265 + ms: 2.1.3 5266 + optionalDependencies: 5267 + supports-color: 10.2.2 5268 + 4990 5269 debug@4.4.3(supports-color@7.2.0): 4991 5270 dependencies: 4992 5271 ms: 2.1.3 ··· 5018 5297 5019 5298 devalue@5.8.1: {} 5020 5299 5300 + devlop@1.1.0: 5301 + dependencies: 5302 + dequal: 2.0.3 5303 + 5304 + diff@9.0.0: {} 5305 + 5021 5306 dom-accessibility-api@0.5.16: {} 5022 5307 5023 5308 dom-accessibility-api@0.6.3: {} ··· 5134 5419 - '@eslint/css' 5135 5420 - typescript 5136 5421 5137 - eslint-plugin-storybook@10.5.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(storybook@10.5.0(@types/react@19.2.17)(prettier@3.9.5)(react@19.2.7))(supports-color@7.2.0)(typescript@6.0.3): 5422 + eslint-plugin-storybook@10.5.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(storybook@10.5.0(@types/react@19.2.17)(prettier@3.9.5)(react@19.2.7))(supports-color@10.2.2)(typescript@6.0.3): 5138 5423 dependencies: 5139 5424 '@typescript-eslint/types': 8.64.0 5140 - '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) 5425 + '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) 5141 5426 eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) 5142 5427 storybook: 10.5.0(@types/react@19.2.17)(prettier@3.9.5)(react@19.2.7) 5143 5428 transitivePeerDependencies: ··· 5320 5605 dependencies: 5321 5606 function-bind: 1.1.2 5322 5607 5608 + hast-util-to-html@9.0.5: 5609 + dependencies: 5610 + '@types/hast': 3.0.5 5611 + '@types/unist': 3.0.3 5612 + ccount: 2.0.1 5613 + comma-separated-tokens: 2.0.3 5614 + hast-util-whitespace: 3.0.0 5615 + html-void-elements: 3.0.0 5616 + mdast-util-to-hast: 13.2.1 5617 + property-information: 7.2.0 5618 + space-separated-tokens: 2.0.2 5619 + stringify-entities: 4.0.4 5620 + zwitch: 2.0.4 5621 + 5622 + hast-util-whitespace@3.0.0: 5623 + dependencies: 5624 + '@types/hast': 3.0.5 5625 + 5323 5626 html-escaper@2.0.2: {} 5627 + 5628 + html-void-elements@3.0.0: {} 5324 5629 5325 5630 htmlparser2@10.1.0: 5326 5631 dependencies: ··· 5501 5806 5502 5807 loupe@3.2.1: {} 5503 5808 5809 + lru_map@0.4.1: {} 5810 + 5504 5811 lz-string@1.5.0: {} 5505 5812 5506 5813 magic-string@0.30.21: ··· 5533 5840 punycode.js: 2.3.1 5534 5841 uc.micro: 2.1.0 5535 5842 5843 + mdast-util-to-hast@13.2.1: 5844 + dependencies: 5845 + '@types/hast': 3.0.5 5846 + '@types/mdast': 4.0.4 5847 + '@ungap/structured-clone': 1.3.3 5848 + devlop: 1.1.0 5849 + micromark-util-sanitize-uri: 2.0.1 5850 + trim-lines: 3.0.1 5851 + unist-util-position: 5.0.0 5852 + unist-util-visit: 5.1.0 5853 + vfile: 6.0.3 5854 + 5536 5855 mdn-data@2.29.0: {} 5537 5856 5538 5857 mdurl@2.1.0: {} 5539 5858 5859 + micromark-util-character@2.1.1: 5860 + dependencies: 5861 + micromark-util-symbol: 2.0.1 5862 + micromark-util-types: 2.0.2 5863 + 5864 + micromark-util-encode@2.0.1: {} 5865 + 5866 + micromark-util-sanitize-uri@2.0.1: 5867 + dependencies: 5868 + micromark-util-character: 2.1.1 5869 + micromark-util-encode: 2.0.1 5870 + micromark-util-symbol: 2.0.1 5871 + 5872 + micromark-util-symbol@2.0.1: {} 5873 + 5874 + micromark-util-types@2.0.2: {} 5875 + 5540 5876 min-indent@1.0.1: {} 5541 5877 5542 5878 miniflare@4.20260722.1: ··· 5577 5913 natural-compare@1.4.0: {} 5578 5914 5579 5915 obug@2.1.3: {} 5916 + 5917 + oniguruma-parser@0.12.2: {} 5918 + 5919 + oniguruma-to-es@4.3.6: 5920 + dependencies: 5921 + oniguruma-parser: 0.12.2 5922 + regex: 6.1.0 5923 + regex-recursion: 6.0.2 5580 5924 5581 5925 open@10.2.0: 5582 5926 dependencies: ··· 5738 6082 ansi-styles: 5.2.0 5739 6083 react-is: 17.0.2 5740 6084 6085 + property-information@7.2.0: {} 6086 + 5741 6087 punycode.js@2.3.1: {} 5742 6088 5743 6089 punycode@2.3.1: {} ··· 5768 6114 indent-string: 4.0.0 5769 6115 strip-indent: 3.0.0 5770 6116 6117 + regex-recursion@6.0.2: 6118 + dependencies: 6119 + regex-utilities: 2.3.0 6120 + 6121 + regex-utilities@2.3.0: {} 6122 + 6123 + regex@6.1.0: 6124 + dependencies: 6125 + regex-utilities: 2.3.0 6126 + 5771 6127 regexparam@3.0.0: {} 5772 6128 5773 6129 resolve@1.22.12: ··· 5891 6247 5892 6248 shebang-regex@3.0.0: {} 5893 6249 6250 + shiki@4.3.1: 6251 + dependencies: 6252 + '@shikijs/core': 4.3.1 6253 + '@shikijs/engine-javascript': 4.3.1 6254 + '@shikijs/engine-oniguruma': 4.3.1 6255 + '@shikijs/langs': 4.3.1 6256 + '@shikijs/themes': 4.3.1 6257 + '@shikijs/types': 4.3.1 6258 + '@shikijs/vscode-textmate': 10.0.2 6259 + '@types/hast': 3.0.5 6260 + 5894 6261 siginfo@2.0.0: {} 5895 6262 5896 6263 sirv@3.0.2: ··· 5902 6269 source-map-js@1.2.1: {} 5903 6270 5904 6271 source-map@0.6.1: {} 6272 + 6273 + space-separated-tokens@2.0.2: {} 5905 6274 5906 6275 stackback@0.0.2: {} 5907 6276 ··· 5933 6302 - bufferutil 5934 6303 - react 5935 6304 - utf-8-validate 6305 + 6306 + stringify-entities@4.0.4: 6307 + dependencies: 6308 + character-entities-html4: 2.1.0 6309 + character-entities-legacy: 3.0.0 5936 6310 5937 6311 strip-ansi@7.2.0: 5938 6312 dependencies: ··· 6050 6424 6051 6425 totalist@3.0.1: {} 6052 6426 6427 + trim-lines@3.0.1: {} 6428 + 6053 6429 ts-api-utils@2.5.0(typescript@6.0.3): 6054 6430 dependencies: 6055 6431 typescript: 6.0.3 ··· 6081 6457 dependencies: 6082 6458 tagged-tag: 1.0.0 6083 6459 6084 - typescript-eslint@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): 6460 + typescript-eslint@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3): 6085 6461 dependencies: 6086 - '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) 6087 - '@typescript-eslint/parser': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) 6088 - '@typescript-eslint/typescript-estree': 8.64.0(supports-color@7.2.0)(typescript@6.0.3) 6089 - '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) 6462 + '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) 6463 + '@typescript-eslint/parser': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) 6464 + '@typescript-eslint/typescript-estree': 8.64.0(supports-color@10.2.2)(typescript@6.0.3) 6465 + '@typescript-eslint/utils': 8.64.0(eslint@10.7.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@10.2.2)(typescript@6.0.3) 6090 6466 eslint: 10.7.0(jiti@2.7.0)(supports-color@7.2.0) 6091 6467 typescript: 6.0.3 6092 6468 transitivePeerDependencies: ··· 6110 6486 6111 6487 unicode-segmenter@0.14.5: {} 6112 6488 6489 + unist-util-is@6.0.1: 6490 + dependencies: 6491 + '@types/unist': 3.0.3 6492 + 6493 + unist-util-position@5.0.0: 6494 + dependencies: 6495 + '@types/unist': 3.0.3 6496 + 6497 + unist-util-stringify-position@4.0.0: 6498 + dependencies: 6499 + '@types/unist': 3.0.3 6500 + 6501 + unist-util-visit-parents@6.0.2: 6502 + dependencies: 6503 + '@types/unist': 3.0.3 6504 + unist-util-is: 6.0.1 6505 + 6506 + unist-util-visit@5.1.0: 6507 + dependencies: 6508 + '@types/unist': 3.0.3 6509 + unist-util-is: 6.0.1 6510 + unist-util-visit-parents: 6.0.2 6511 + 6113 6512 universalify@2.0.1: {} 6114 6513 6115 6514 unplugin-icons@23.0.1(svelte@5.56.5(@typescript-eslint/types@8.64.0)): ··· 6142 6541 valibot@1.4.2(typescript@6.0.3): 6143 6542 optionalDependencies: 6144 6543 typescript: 6.0.3 6544 + 6545 + vfile-message@4.0.3: 6546 + dependencies: 6547 + '@types/unist': 3.0.3 6548 + unist-util-stringify-position: 4.0.0 6549 + 6550 + vfile@6.0.3: 6551 + dependencies: 6552 + '@types/unist': 3.0.3 6553 + vfile-message: 4.0.3 6145 6554 6146 6555 vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0): 6147 6556 dependencies: ··· 6263 6672 zimmerframe@1.1.2: {} 6264 6673 6265 6674 zimmerframe@1.1.4: {} 6675 + 6676 + zwitch@2.0.4: {}
+1
web/src/app.css
··· 1 1 @import "tailwindcss"; 2 2 @import "./markup.css"; 3 + @import "./pierre.css"; 3 4 4 5 @theme { 5 6 --font-sans: "InterVariable", "system-ui", sans-serif, ui-sans-serif;
+74
web/src/lib/api/diff.ts
··· 1 + import type { BobbinContext, XrpcRequestInit } from "./client"; 2 + import { diff as knotDiff } from "./knot"; 3 + import type { GitSignature } from "./repo"; 4 + 5 + export interface NiceCommit { 6 + hash?: number[]; 7 + // hex form of the hash bytes above 8 + this?: string; 9 + // hex of the first parent, empty for a root commit 10 + parent?: string; 11 + parent_hashes?: number[][]; 12 + author?: GitSignature; 13 + committer?: GitSignature; 14 + message?: string; 15 + tree?: string; 16 + change_id?: string; 17 + pgp_signature?: string; 18 + } 19 + 20 + export interface DiffStat { 21 + insertions: number; 22 + deletions: number; 23 + files_changed: number; 24 + } 25 + 26 + export const LineOp = { Context: 0, Delete: 1, Add: 2 } as const; 27 + export type LineOp = (typeof LineOp)[keyof typeof LineOp]; 28 + 29 + export interface DiffLine { 30 + Op: LineOp; 31 + Line: string; 32 + } 33 + 34 + export interface TextFragment { 35 + Comment?: string; 36 + OldPosition?: number; 37 + OldLines?: number; 38 + NewPosition?: number; 39 + NewLines?: number; 40 + LinesAdded?: number; 41 + LinesDeleted?: number; 42 + LeadingContext?: number; 43 + TrailingContext?: number; 44 + Lines?: DiffLine[]; 45 + } 46 + 47 + export interface DiffFile { 48 + name: { old: string; new: string }; 49 + text_fragments?: TextFragment[]; 50 + is_binary?: boolean; 51 + is_new?: boolean; 52 + is_delete?: boolean; 53 + is_copy?: boolean; 54 + is_rename?: boolean; 55 + } 56 + 57 + export interface NiceDiff { 58 + commit?: NiceCommit; 59 + stat?: DiffStat; 60 + diff?: DiffFile[]; 61 + } 62 + 63 + // the new path, or the old one for deletions 64 + export const diffFileName = (file: DiffFile): string => file.name.new || file.name.old; 65 + 66 + export interface RepoCommitResponse { 67 + ref?: string; 68 + diff?: NiceDiff; 69 + } 70 + 71 + // the mirror's temp surface has no diff endpoint, diffs go through 72 + // bobbin's knot proxy 73 + export const diffFor = (ctx: BobbinContext, repo: string, ref: string, init?: XrpcRequestInit) => 74 + knotDiff<RepoCommitResponse>(ctx, { repo, ref }, init);
+132
web/src/lib/api/rawdiff.ts
··· 1 + import { diffFileName, type DiffFile, type NiceDiff, type TextFragment } from "./diff"; 2 + import { hexFromBytes } from "./repo"; 3 + 4 + // port of appview/repo/rawdiff.go 5 + 6 + const OP_CHARS = [" ", "-", "+"] as const; 7 + 8 + const opChar = (op: number): string => OP_CHARS[op] ?? "?"; 9 + 10 + const noEol = (line: string): boolean => line.length === 0 || !line.endsWith("\n"); 11 + 12 + const fragmentHeader = (fragment: TextFragment): string => { 13 + const header = `@@ -${fragment.OldPosition ?? 0},${fragment.OldLines ?? 0} +${fragment.NewPosition ?? 0},${fragment.NewLines ?? 0} @@`; 14 + return fragment.Comment ? `${header} ${fragment.Comment}` : header; 15 + }; 16 + 17 + const fragmentString = (fragment: TextFragment): string => { 18 + let out = `${fragmentHeader(fragment)}\n`; 19 + for (const line of fragment.Lines ?? []) { 20 + out += opChar(line.Op) + line.Line; 21 + if (noEol(line.Line)) out += "\n\\ No newline at end of file\n"; 22 + } 23 + return out; 24 + }; 25 + 26 + export const fileStats = (file: DiffFile): { insertions: number; deletions: number } => { 27 + let insertions = 0; 28 + let deletions = 0; 29 + for (const fragment of file.text_fragments ?? []) { 30 + insertions += fragment.LinesAdded ?? 0; 31 + deletions += fragment.LinesDeleted ?? 0; 32 + } 33 + return { insertions, deletions }; 34 + }; 35 + 36 + export const renderUnifiedDiff = (niceDiff: NiceDiff | null | undefined): string => { 37 + if (!niceDiff) return ""; 38 + let out = ""; 39 + for (const file of niceDiff.diff ?? []) { 40 + const oldName = file.name.old || file.name.new; 41 + const newName = file.name.new || file.name.old; 42 + 43 + out += `diff --git a/${oldName} b/${newName}\n`; 44 + if (file.is_new) { 45 + out += "new file mode 100644\n"; 46 + out += "--- /dev/null\n"; 47 + out += `+++ b/${newName}\n`; 48 + } else if (file.is_delete) { 49 + out += "deleted file mode 100644\n"; 50 + out += `--- a/${oldName}\n`; 51 + out += "+++ /dev/null\n"; 52 + } else if (file.is_rename) { 53 + out += `rename from ${oldName}\n`; 54 + out += `rename to ${newName}\n`; 55 + out += `--- a/${oldName}\n`; 56 + out += `+++ b/${newName}\n`; 57 + } else { 58 + out += `--- a/${oldName}\n`; 59 + out += `+++ b/${newName}\n`; 60 + } 61 + 62 + for (const fragment of file.text_fragments ?? []) { 63 + out += fragmentString(fragment); 64 + } 65 + } 66 + return out; 67 + }; 68 + 69 + const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const; 70 + const MONTHS = [ 71 + "Jan", 72 + "Feb", 73 + "Mar", 74 + "Apr", 75 + "May", 76 + "Jun", 77 + "Jul", 78 + "Aug", 79 + "Sep", 80 + "Oct", 81 + "Nov", 82 + "Dec" 83 + ] as const; 84 + 85 + const pad = (value: number): string => String(value).padStart(2, "0"); 86 + 87 + // RFC1123Z, always utc 88 + const formatPatchDate = (input: string): string => { 89 + const date = new Date(input); 90 + // a missing When is go's zero time, formats to this exact string 91 + if (Number.isNaN(date.getTime())) return "Mon, 01 Jan 0001 00:00:00 +0000"; 92 + return ( 93 + `${WEEKDAYS[date.getUTCDay()]}, ${pad(date.getUTCDate())} ${MONTHS[date.getUTCMonth()]} ` + 94 + `${String(date.getUTCFullYear()).padStart(4, "0")} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())} +0000` 95 + ); 96 + }; 97 + 98 + export const renderFormatPatch = (niceDiff: NiceDiff | null | undefined): string => { 99 + if (!niceDiff) return ""; 100 + const commit = niceDiff.commit ?? {}; 101 + const message = commit.message ?? ""; 102 + 103 + // format-patch takes the first LINE as the subject, unlike splitMessage's 104 + // first paragraph 105 + const newline = message.indexOf("\n"); 106 + const subject = newline === -1 ? message : message.slice(0, newline); 107 + 108 + const separator = message.indexOf("\n\n"); 109 + const body = separator === -1 ? "" : message.slice(separator + 2).replace(/\n+$/, ""); 110 + 111 + const hash = commit.this || hexFromBytes(commit.hash ?? []); 112 + 113 + let out = `From ${hash} Mon Sep 17 00:00:00 2001\n`; 114 + out += `From: ${commit.author?.Name ?? ""} <${commit.author?.Email ?? ""}>\n`; 115 + out += `Date: ${formatPatchDate(commit.author?.When ?? "")}\n`; 116 + out += `Subject: [PATCH] ${subject}\n`; 117 + out += "\n"; 118 + if (body !== "") out += `${body}\n`; 119 + out += "---\n"; 120 + 121 + for (const file of niceDiff.diff ?? []) { 122 + const name = diffFileName(file); 123 + const { insertions, deletions } = fileStats(file); 124 + out += ` ${name} | ${insertions + deletions} ${"+".repeat(insertions)}${"-".repeat(deletions)}\n`; 125 + } 126 + const stat = niceDiff.stat ?? { insertions: 0, deletions: 0, files_changed: 0 }; 127 + out += ` ${stat.files_changed} file(s) changed, ${stat.insertions} insertion(s)(+), ${stat.deletions} deletion(s)(-)\n\n`; 128 + 129 + out += renderUnifiedDiff(niceDiff); 130 + out += "\n--\ntangled.sh\n"; 131 + return out; 132 + };
+1 -1
web/src/lib/api/repo.ts
··· 168 168 message?: string; 169 169 } 170 170 171 - const hexFromBytes = (bytes: number[]): string => 171 + export const hexFromBytes = (bytes: number[]): string => 172 172 bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(""); 173 173 174 174 export const toTagSummary = (tag: TagEntry): TagSummary => {
+191
web/src/lib/components/repo/PierreDiff.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta, type StoryContext } from "@storybook/addon-svelte-csf"; 3 + import { expect, waitFor } from "storybook/test"; 4 + import { LineOp, type DiffFile } from "$lib/api/diff"; 5 + import { toFileDiffMetadata } from "./fileDiff"; 6 + import PierreDiff from "./PierreDiff.svelte"; 7 + 8 + const context = (line: string) => ({ Op: LineOp.Context, Line: `${line}\n` }); 9 + const deletion = (line: string) => ({ Op: LineOp.Delete, Line: `${line}\n` }); 10 + const addition = (line: string) => ({ Op: LineOp.Add, Line: `${line}\n` }); 11 + 12 + const files: DiffFile[] = [ 13 + { 14 + name: { old: "src/colors.ts", new: "src/colors.ts" }, 15 + text_fragments: [ 16 + { 17 + OldPosition: 1, 18 + OldLines: 4, 19 + NewPosition: 1, 20 + NewLines: 6, 21 + Lines: [ 22 + context("export const palette = {"), 23 + deletion('\tbackground: "white",'), 24 + deletion('\tforeground: "black",'), 25 + addition('\tbackground: "oklch(0.98 0 0)",'), 26 + addition('\tforeground: "oklch(0.2 0 0)",'), 27 + context("};"), 28 + addition(""), 29 + addition('export const accent = "oklch(0.6 0.2 250)";') 30 + ] 31 + }, 32 + { 33 + OldPosition: 10, 34 + OldLines: 3, 35 + NewPosition: 11, 36 + NewLines: 3, 37 + Lines: [ 38 + context("export const spacing = 4;"), 39 + deletion("export const radius = 0;"), 40 + addition("export const radius = 8;"), 41 + context('export const shadow = "none";') 42 + ] 43 + } 44 + ] 45 + }, 46 + { 47 + name: { old: "src/old.ts", new: "src/renamed.ts" }, 48 + is_rename: true, 49 + text_fragments: [ 50 + { 51 + OldPosition: 1, 52 + OldLines: 2, 53 + NewPosition: 1, 54 + NewLines: 2, 55 + Lines: [ 56 + deletion('export const name = "old";'), 57 + addition('export const name = "renamed";'), 58 + context("export default name;") 59 + ] 60 + } 61 + ] 62 + } 63 + ]; 64 + 65 + // the hunk starts deep in the file, the leading unmodified region collapses into a 66 + // top separator 67 + const offsetFiles: DiffFile[] = [ 68 + { 69 + name: { old: "src/deep.ts", new: "src/deep.ts" }, 70 + text_fragments: [ 71 + { 72 + OldPosition: 20, 73 + OldLines: 3, 74 + NewPosition: 20, 75 + NewLines: 3, 76 + Lines: [ 77 + context("export const spacing = 4;"), 78 + deletion("export const radius = 0;"), 79 + addition("export const radius = 8;"), 80 + context('export const shadow = "none";') 81 + ] 82 + } 83 + ] 84 + } 85 + ]; 86 + 87 + type PlayContext = Pick<StoryContext<Record<string, unknown>>, "canvasElement">; 88 + 89 + const hostsOf = (canvasElement: HTMLElement) => [ 90 + ...canvasElement.querySelectorAll("diffs-container") 91 + ]; 92 + 93 + // pierre renders into shadow roots, testing library cannot see through 94 + const shadowText = (canvasElement: HTMLElement) => 95 + hostsOf(canvasElement) 96 + .map((host) => host.shadowRoot?.textContent ?? "") 97 + .join("\n"); 98 + 99 + const rendersPatch = async (canvasElement: HTMLElement) => { 100 + const hosts = hostsOf(canvasElement); 101 + await expect(hosts.length).toBe(2); 102 + await waitFor(() => expect(shadowText(canvasElement)).toContain("export const palette")); 103 + await waitFor(() => expect(shadowText(canvasElement)).toContain("export const name")); 104 + return hosts; 105 + }; 106 + 107 + const appviewChrome = async ({ canvasElement }: PlayContext) => { 108 + const hosts = await rendersPatch(canvasElement); 109 + for (const host of hosts) { 110 + expect(host.shadowRoot?.querySelector("[data-diffs-header]")).toBeNull(); 111 + expect(host.shadowRoot?.querySelector("[data-indicators='classic']")).not.toBeNull(); 112 + } 113 + // the hunks cover old lines 1-5 and 10-12, 4 collapse between 114 + expect(hosts[0].shadowRoot?.querySelector("[data-separator='line-info']")).not.toBeNull(); 115 + await waitFor(() => expect(shadowText(canvasElement)).toContain("4 unmodified lines")); 116 + }; 117 + 118 + const expectSplitColumns = (present: boolean) => { 119 + return async ({ canvasElement }: PlayContext) => { 120 + const hosts = await rendersPatch(canvasElement); 121 + await waitFor(() => { 122 + const found = hosts.some( 123 + (host) => 124 + host.shadowRoot?.querySelector("[data-deletions]") && 125 + host.shadowRoot?.querySelector("[data-additions]") 126 + ); 127 + expect(found).toBe(present); 128 + }); 129 + }; 130 + }; 131 + 132 + // the separator bar must span the diff instead of clipping in the 44px 133 + // gutter. the additions-side copy hides its text, only measure 134 + // non-zero spans 135 + const separatorNotClipped = async ({ canvasElement }: PlayContext) => { 136 + const hosts = await rendersPatch(canvasElement); 137 + await waitFor(() => { 138 + const spans = hosts.flatMap((host) => [ 139 + ...(host.shadowRoot?.querySelectorAll("[data-unmodified-lines]") ?? []) 140 + ]); 141 + const visible = spans.filter((span) => span.getBoundingClientRect().width > 0); 142 + expect(visible.length).toBeGreaterThan(0); 143 + for (const span of visible) { 144 + const wrapper = span.closest("[data-separator-wrapper]"); 145 + expect(wrapper).not.toBeNull(); 146 + const wrapperRect = wrapper!.getBoundingClientRect(); 147 + const spanRect = span.getBoundingClientRect(); 148 + expect(wrapperRect.width).toBeGreaterThan(200); 149 + expect(spanRect.x).toBeGreaterThanOrEqual(wrapperRect.x - 1); 150 + expect(spanRect.right).toBeLessThanOrEqual(wrapperRect.right + 1); 151 + } 152 + }); 153 + }; 154 + 155 + const { Story } = defineMeta({ 156 + title: "Repo/PierreDiff", 157 + component: PierreDiff, 158 + tags: ["autodocs"], 159 + args: { files: files.map(toFileDiffMetadata) } 160 + }); 161 + </script> 162 + 163 + <Story 164 + name="Unified" 165 + play={async (ctx) => { 166 + await appviewChrome(ctx); 167 + await expectSplitColumns(false)(ctx); 168 + await separatorNotClipped(ctx); 169 + }} 170 + /> 171 + 172 + <Story 173 + name="Split" 174 + args={{ diffStyle: "split" }} 175 + play={async (ctx) => { 176 + await expectSplitColumns(true)(ctx); 177 + await separatorNotClipped(ctx); 178 + }} 179 + /> 180 + 181 + <Story 182 + name="Leading collapse" 183 + args={{ files: offsetFiles.map(toFileDiffMetadata) }} 184 + play={async ({ canvasElement }: PlayContext) => { 185 + await waitFor(() => expect(shadowText(canvasElement)).toContain("19 unmodified lines")); 186 + const host = hostsOf(canvasElement)[0]; 187 + const separator = host.shadowRoot?.querySelector("[data-separator='line-info']"); 188 + expect(separator).not.toBeNull(); 189 + expect(separator?.hasAttribute("data-separator-first")).toBe(true); 190 + }} 191 + />
+74
web/src/lib/components/repo/PierreDiff.svelte
··· 1 + <script lang="ts"> 2 + import { untrack } from "svelte"; 3 + import { FileDiff, type FileDiffMetadata } from "@pierre/diffs"; 4 + import { pierreDiffOptions, type DiffStyle } from "./pierre"; 5 + 6 + interface Props { 7 + files: FileDiffMetadata[]; 8 + // side-by-side vs stacked 9 + diffStyle?: DiffStyle; 10 + // server-prerendered shadow dom, aligned with files. only set for the 11 + // initial ssr, client navigations let pierre paint on its own 12 + prerenderedHTML?: (string | undefined)[]; 13 + } 14 + 15 + let { files, diffStyle = "unified", prerenderedHTML = [] }: Props = $props(); 16 + 17 + let container: HTMLElement | undefined = $state(); 18 + 19 + let mounted: { instance: FileDiff; file: FileDiffMetadata }[] = []; 20 + // the style at hydrate time, the options effect skips right after a 21 + // (re)mount 22 + let mountedStyle: DiffStyle | undefined; 23 + 24 + // pierre does not update on its own. hydrate paints once per file, prop 25 + // changes re-render. diffStyle is read untracked so a toggle re-renders 26 + // through the effect below, a remount would lose expanded hunks 27 + const mountAll = (source: FileDiffMetadata[], html: (string | undefined)[]) => { 28 + const host = container; 29 + if (!host) return; 30 + for (const { instance } of mounted) instance.cleanUp(); 31 + mounted = []; 32 + 33 + mounted = source.map((file, index) => { 34 + const element = host.children[index] as HTMLElement; 35 + // the html prop never reaches the client, pierre marks its ssr 36 + // output with data-dehydrated 37 + if (html[index] === undefined && element.shadowRoot?.querySelector("[data-dehydrated]") == null) 38 + element.shadowRoot?.replaceChildren(); 39 + const instance = new FileDiff(pierreDiffOptions(untrack(() => diffStyle)), undefined, true); 40 + instance.hydrate({ fileDiff: file, fileContainer: element, prerenderedHTML: html[index] }); 41 + return { instance, file }; 42 + }); 43 + mountedStyle = untrack(() => diffStyle); 44 + }; 45 + 46 + $effect(() => { 47 + mountAll(files, prerenderedHTML); 48 + return () => { 49 + for (const { instance } of mounted) instance.cleanUp(); 50 + mounted = []; 51 + }; 52 + }); 53 + 54 + $effect(() => { 55 + const style = diffStyle; 56 + if (style === mountedStyle) return; 57 + mountedStyle = style; 58 + for (const { instance, file } of mounted) { 59 + instance.setOptions(pierreDiffOptions(style)); 60 + instance.render({ fileDiff: file, forceRender: true }); 61 + } 62 + }); 63 + </script> 64 + 65 + <div bind:this={container} class="flex flex-col gap-4"> 66 + {#each files as file, index (index)} 67 + <diffs-container class="diffs-container"> 68 + {#if prerenderedHTML[index]} 69 + <!-- pierre's own renderer output, token text is escaped by its hast builder --> 70 + <template shadowrootmode="open">{@html prerenderedHTML[index]}</template> 71 + {/if} 72 + </diffs-container> 73 + {/each} 74 + </div>
+50
web/src/lib/components/repo/PierreFile.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta, type StoryContext } from "@storybook/addon-svelte-csf"; 3 + import { expect, waitFor } from "storybook/test"; 4 + import PierreFile from "./PierreFile.svelte"; 5 + 6 + const contents = `import { serve } from "./server"; 7 + 8 + // start the knot on the configured port 9 + export const main = () => { 10 + serve({ port: 5555 }); 11 + }; 12 + `; 13 + 14 + type PlayContext = Pick<StoryContext<Record<string, unknown>>, "canvasElement">; 15 + 16 + // pierre renders into a shadow root, testing library cannot see through 17 + const rendersContents = async ({ canvasElement }: PlayContext) => { 18 + const host = canvasElement.querySelector("diffs-container"); 19 + await expect(host?.shadowRoot).toBeTruthy(); 20 + await waitFor(() => expect(host?.shadowRoot?.textContent).toContain("serve({ port: 5555 })")); 21 + await waitFor(() => expect(host?.shadowRoot?.querySelector("[data-diffs-header]")).toBeNull()); 22 + }; 23 + 24 + const { Story } = defineMeta({ 25 + title: "Repo/PierreFile", 26 + component: PierreFile, 27 + tags: ["autodocs"], 28 + args: { name: "main.ts", contents } 29 + }); 30 + </script> 31 + 32 + <Story name="TypeScript file" play={rendersContents} /> 33 + 34 + <Story 35 + name="Wrapped" 36 + args={{ 37 + wrap: true, 38 + contents: `${contents}const url = "https://example.com/" + "a-very-long-path-segment-".repeat(8);\n` 39 + }} 40 + play={async (context) => { 41 + await rendersContents(context); 42 + const host = context.canvasElement.querySelector("diffs-container"); 43 + await waitFor(() => 44 + expect(host?.shadowRoot?.querySelector("[data-overflow='wrap']")).toBeTruthy() 45 + ); 46 + await waitFor(() => 47 + expect(host?.shadowRoot?.querySelector("[data-line] span[style]")).toBeTruthy() 48 + ); 49 + }} 50 + />
+73
web/src/lib/components/repo/PierreFile.svelte
··· 1 + <script lang="ts"> 2 + import { untrack } from "svelte"; 3 + import { File, type FileOptions } from "@pierre/diffs"; 4 + import { pierreFileOptions } from "./pierre"; 5 + 6 + interface Props { 7 + // used for the header and to infer the highlighting language 8 + name: string; 9 + contents: string; 10 + // pierre's native overflow: wrap keeps highlighting instead of scrolling 11 + wrap?: boolean; 12 + // server-prerendered shadow dom, only set for the initial ssr 13 + prerenderedHTML?: string; 14 + // forwarded to pierre, fires after every (async-safe) render pass 15 + onPostRender?: FileOptions<undefined>["onPostRender"]; 16 + } 17 + 18 + let { name, contents, wrap = false, prerenderedHTML, onPostRender }: Props = $props(); 19 + 20 + let element: HTMLElement | undefined = $state(); 21 + 22 + let instance: File | undefined; 23 + 24 + // same manual updates as PierreDiff, this effect owns the instance, 25 + // a contents swap re-renders in place instead of a shadow dom teardown 26 + $effect(() => { 27 + const host = element; 28 + if (!host) return; 29 + // scroll and wrap lay out differently enough in grid to re-hydrate on 30 + // toggle, and the toggle is rare 31 + const options: FileOptions<undefined> = { 32 + ...pierreFileOptions(wrap), 33 + onPostRender 34 + }; 35 + 36 + // sveltekit re-runs the load on hydration, the prop never reaches the 37 + // client. pierre marks its ssr output with data-dehydrated 38 + const dehydrated = host.shadowRoot?.querySelector("[data-dehydrated]") != null; 39 + if (prerenderedHTML === undefined && !dehydrated) host.shadowRoot?.replaceChildren(); 40 + const created = new File(options, undefined, true); 41 + // untracked, the render effect below owns updates 42 + created.hydrate({ 43 + file: untrack(() => ({ name, contents })), 44 + fileContainer: host, 45 + prerenderedHTML 46 + }); 47 + instance = created; 48 + 49 + return () => { 50 + created.cleanUp(); 51 + instance = undefined; 52 + }; 53 + }); 54 + 55 + // hydrate paints the initial file, this pass covers later name/contents 56 + // changes 57 + let firstRender = true; 58 + $effect(() => { 59 + const file = { name, contents }; 60 + if (firstRender) { 61 + firstRender = false; 62 + return; 63 + } 64 + instance?.render({ file, forceRender: true }); 65 + }); 66 + </script> 67 + 68 + <diffs-container bind:this={element} class="diffs-container"> 69 + {#if prerenderedHTML} 70 + <!-- pierre's own renderer output, token text is escaped by its hast builder --> 71 + <template shadowrootmode="open">{@html prerenderedHTML}</template> 72 + {/if} 73 + </diffs-container>
+142
web/src/lib/components/repo/fileDiff.test.ts
··· 1 + import { processFile, type FileDiffMetadata } from "@pierre/diffs"; 2 + import { describe, expect, it } from "vitest"; 3 + import { LineOp, type DiffFile } from "$lib/api/diff"; 4 + import { renderUnifiedDiff } from "$lib/api/rawdiff"; 5 + import { toFileDiffMetadata } from "./fileDiff"; 6 + 7 + // the mapper must produce exactly what pierre's own parser 8 + // produces from the rendered text! 9 + // 10 + // excluded: type/prevName, the parser 11 + // can only get it from git header lines the renderer doesn't emit (mode too), 12 + // cacheKey, only worker pools use it, and hunkSpecs 13 + const matchesParsedPatch = (file: DiffFile) => { 14 + const parsed = processFile(renderUnifiedDiff({ diff: [file] })); 15 + expect(parsed).toBeDefined(); 16 + const stripKeys = (value: object, keys: string[]): Record<string, unknown> => 17 + Object.fromEntries(Object.entries(value).filter(([key]) => !keys.includes(key))); 18 + const strip = (meta: FileDiffMetadata) => ({ 19 + ...stripKeys(meta, ["type", "prevName", "mode", "cacheKey"]), 20 + hunks: meta.hunks.map((hunk) => stripKeys(hunk, ["hunkSpecs"])) 21 + }); 22 + expect(strip(toFileDiffMetadata(file))).toEqual(strip(parsed!)); 23 + }; 24 + 25 + describe("toFileDiffMetadata", () => { 26 + it("maps a modified file", () => { 27 + matchesParsedPatch({ 28 + name: { old: "foo.go", new: "foo.go" }, 29 + text_fragments: [ 30 + { 31 + Comment: "func main()", 32 + OldPosition: 1, 33 + OldLines: 3, 34 + NewPosition: 1, 35 + NewLines: 3, 36 + Lines: [ 37 + { Op: LineOp.Context, Line: "package main\n" }, 38 + { Op: LineOp.Delete, Line: "// old comment\n" }, 39 + { Op: LineOp.Add, Line: "// new comment\n" }, 40 + { Op: LineOp.Context, Line: "func main() {}\n" } 41 + ] 42 + } 43 + ] 44 + }); 45 + }); 46 + 47 + it("maps a new file", () => { 48 + matchesParsedPatch({ 49 + name: { old: "", new: "new.go" }, 50 + is_new: true, 51 + text_fragments: [ 52 + { 53 + OldPosition: 0, 54 + OldLines: 0, 55 + NewPosition: 1, 56 + NewLines: 2, 57 + Lines: [ 58 + { Op: LineOp.Add, Line: "package main\n" }, 59 + { Op: LineOp.Add, Line: "func main() {}\n" } 60 + ] 61 + } 62 + ] 63 + }); 64 + }); 65 + 66 + it("maps a deleted file", () => { 67 + matchesParsedPatch({ 68 + name: { old: "old.go", new: "" }, 69 + is_delete: true, 70 + text_fragments: [ 71 + { 72 + OldPosition: 1, 73 + OldLines: 2, 74 + NewPosition: 0, 75 + NewLines: 0, 76 + Lines: [ 77 + { Op: LineOp.Delete, Line: "package main\n" }, 78 + { Op: LineOp.Delete, Line: "func main() {}\n" } 79 + ] 80 + } 81 + ] 82 + }); 83 + }); 84 + 85 + it("maps a renamed file with multiple fragments", () => { 86 + matchesParsedPatch({ 87 + name: { old: "old.go", new: "renamed.go" }, 88 + is_rename: true, 89 + text_fragments: [ 90 + { 91 + OldPosition: 1, 92 + OldLines: 2, 93 + NewPosition: 1, 94 + NewLines: 2, 95 + Lines: [ 96 + { Op: LineOp.Context, Line: "package main\n" }, 97 + { Op: LineOp.Delete, Line: "func old() {}\n" }, 98 + { Op: LineOp.Add, Line: "func renamed() {}\n" } 99 + ] 100 + }, 101 + { 102 + Comment: "func init()", 103 + OldPosition: 10, 104 + OldLines: 1, 105 + NewPosition: 10, 106 + NewLines: 1, 107 + Lines: [{ Op: LineOp.Context, Line: "var x = 1\n" }] 108 + } 109 + ] 110 + }); 111 + }); 112 + 113 + it("maps a missing trailing newline", () => { 114 + matchesParsedPatch({ 115 + name: { old: "foo.go", new: "foo.go" }, 116 + text_fragments: [ 117 + { 118 + OldPosition: 1, 119 + OldLines: 2, 120 + NewPosition: 1, 121 + NewLines: 2, 122 + Lines: [ 123 + { Op: LineOp.Delete, Line: "old" }, 124 + { Op: LineOp.Add, Line: "new\n" }, 125 + { Op: LineOp.Context, Line: "tail" } 126 + ] 127 + } 128 + ] 129 + }); 130 + }); 131 + 132 + it("marks the file type from the wire flags", () => { 133 + expect(toFileDiffMetadata({ name: { old: "", new: "n.go" }, is_new: true }).type).toBe("new"); 134 + expect(toFileDiffMetadata({ name: { old: "o.go", new: "" }, is_delete: true }).type).toBe( 135 + "deleted" 136 + ); 137 + const renamed = toFileDiffMetadata({ name: { old: "o.go", new: "n.go" }, is_rename: true }); 138 + expect(renamed.type).toBe("rename-changed"); 139 + expect(renamed.prevName).toBe("o.go"); 140 + expect(toFileDiffMetadata({ name: { old: "n.go", new: "n.go" } }).prevName).toBeUndefined(); 141 + }); 142 + });
+146
web/src/lib/components/repo/fileDiff.ts
··· 1 + import type { ChangeContent, ContextContent, FileDiffMetadata, Hunk } from "@pierre/diffs"; 2 + import { LineOp, type DiffFile } from "$lib/api/diff"; 3 + 4 + // old alone collides when one file is deleted and another renamed onto its 5 + // path in the same commit so we need new too 6 + export const diffRowKey = (file: DiffFile): string => `${file.name.old}→${file.name.new}`; 7 + 8 + // pierre parses patch text into FileDiffMetadata, but the wire already hands 9 + // us hunks, so we can skip the text roundtrip by mapping 10 + export const toFileDiffMetadata = (file: DiffFile): FileDiffMetadata => { 11 + const fragments = file.text_fragments ?? []; 12 + 13 + const hunks: Hunk[] = []; 14 + const deletionLines: string[] = []; 15 + const additionLines: string[] = []; 16 + let splitLineCount = 0; 17 + let unifiedLineCount = 0; 18 + let lastHunkEnd = 0; 19 + 20 + for (const fragment of fragments) { 21 + const additionStart = fragment.NewPosition ?? 0; 22 + const additionCount = fragment.NewLines ?? 0; 23 + const deletionStart = fragment.OldPosition ?? 0; 24 + const deletionCount = fragment.OldLines ?? 0; 25 + 26 + const collapsedBefore = Math.max(additionStart - 1 - lastHunkEnd, 0); 27 + lastHunkEnd = additionStart + additionCount - 1; 28 + 29 + const hunk: Hunk = { 30 + collapsedBefore, 31 + additionStart, 32 + additionCount, 33 + additionLines: 0, 34 + additionLineIndex: additionLines.length, 35 + deletionStart, 36 + deletionCount, 37 + deletionLines: 0, 38 + deletionLineIndex: deletionLines.length, 39 + hunkContent: [], 40 + hunkContext: fragment.Comment, 41 + hunkSpecs: `@@ -${deletionStart},${deletionCount} +${additionStart},${additionCount} @@`, 42 + splitLineStart: 0, 43 + splitLineCount: 0, 44 + unifiedLineStart: 0, 45 + unifiedLineCount: 0, 46 + noEOFCRDeletions: false, 47 + noEOFCRAdditions: false 48 + }; 49 + 50 + let current: ContextContent | ChangeContent | null = null; 51 + let lastOp: LineOp | null = null; 52 + for (const { Op: op, Line: raw } of fragment.Lines ?? []) { 53 + if (op === LineOp.Context) { 54 + if (current?.type !== "context") { 55 + current = { 56 + type: "context", 57 + lines: 0, 58 + additionLineIndex: additionLines.length, 59 + deletionLineIndex: deletionLines.length 60 + }; 61 + hunk.hunkContent.push(current); 62 + } 63 + current.lines++; 64 + additionLines.push(raw); 65 + deletionLines.push(raw); 66 + } else { 67 + if (current?.type !== "change") { 68 + current = { 69 + type: "change", 70 + additions: 0, 71 + deletions: 0, 72 + additionLineIndex: additionLines.length, 73 + deletionLineIndex: deletionLines.length 74 + }; 75 + hunk.hunkContent.push(current); 76 + } 77 + if (op === LineOp.Add) { 78 + current.additions++; 79 + hunk.additionLines++; 80 + additionLines.push(raw); 81 + } else { 82 + current.deletions++; 83 + hunk.deletionLines++; 84 + deletionLines.push(raw); 85 + } 86 + } 87 + lastOp = op; 88 + } 89 + 90 + // a line without a trailing newline is the patch's "\ No newline" 91 + // marker, only possible at the end of the file 92 + const last = fragment.Lines?.[fragment.Lines.length - 1]; 93 + if (last && !last.Line.endsWith("\n")) { 94 + if (last.Op === LineOp.Context) { 95 + hunk.noEOFCRAdditions = true; 96 + hunk.noEOFCRDeletions = true; 97 + } else if (last.Op === LineOp.Delete) { 98 + hunk.noEOFCRDeletions = true; 99 + } else { 100 + hunk.noEOFCRAdditions = true; 101 + } 102 + if (lastOp === LineOp.Add || lastOp === LineOp.Context) { 103 + additionLines[additionLines.length - 1] = last.Line; 104 + } 105 + if (lastOp === LineOp.Delete || lastOp === LineOp.Context) { 106 + deletionLines[deletionLines.length - 1] = last.Line; 107 + } 108 + } 109 + 110 + for (const content of hunk.hunkContent) { 111 + if (content.type === "context") { 112 + hunk.splitLineCount += content.lines; 113 + hunk.unifiedLineCount += content.lines; 114 + } else { 115 + hunk.splitLineCount += Math.max(content.additions, content.deletions); 116 + hunk.unifiedLineCount += content.deletions + content.additions; 117 + } 118 + } 119 + hunk.splitLineStart = splitLineCount + collapsedBefore; 120 + hunk.unifiedLineStart = unifiedLineCount + collapsedBefore; 121 + splitLineCount += collapsedBefore + hunk.splitLineCount; 122 + unifiedLineCount += collapsedBefore + hunk.unifiedLineCount; 123 + 124 + hunks.push(hunk); 125 + } 126 + 127 + const type: FileDiffMetadata["type"] = file.is_new 128 + ? "new" 129 + : file.is_delete 130 + ? "deleted" 131 + : file.is_rename 132 + ? "rename-changed" 133 + : "change"; 134 + 135 + return { 136 + name: file.name.new || file.name.old, 137 + prevName: type === "rename-changed" ? file.name.old : undefined, 138 + type, 139 + hunks, 140 + splitLineCount, 141 + unifiedLineCount, 142 + isPartial: true, 143 + deletionLines, 144 + additionLines 145 + }; 146 + };
+100
web/src/lib/components/repo/pierre.ts
··· 1 + // the css side of pierre theming lives in src/pierre.css 2 + 3 + import { 4 + getSharedHighlighter, 5 + registerCustomCSSVariableTheme, 6 + type FileDiffOptions, 7 + type FileOptions 8 + } from "@pierre/diffs"; 9 + import { browser } from "$app/environment"; 10 + 11 + // pierre's two diff layouts, threaded from the ?diff= url param down to the 12 + // FileDiff instances 13 + export type DiffStyle = NonNullable<FileDiffOptions<undefined>["diffStyle"]>; 14 + 15 + // pierre's theming hook is a registered shiki css-variable theme. the token 16 + // colors come from --diffs-token-* properties in pierre.css, they flip 17 + // with the media query. the name is pinned by app.css's @utility or tailwind 18 + // tree-shakes those rules away 19 + export const PIERRE_THEME_NAME = "tangled"; 20 + 21 + registerCustomCSSVariableTheme(PIERRE_THEME_NAME, { 22 + // pierre.css defines the real tokens on diffs-container, these are just 23 + // fallbacks for the generated bindings 24 + background: "#ffffff", 25 + foreground: "#111827", 26 + "token-comment": "#6b7280", 27 + "token-string": "#16a34a", 28 + "token-string-expression": "#16a34a", 29 + "token-keyword": "#c084fc", 30 + "token-function": "#60a5fa", 31 + "token-constant": "#f59e0b", 32 + "token-parameter": "#111827", 33 + "token-punctuation": "#4b5563", 34 + "token-link": "#4338ca", 35 + "token-inserted": "#16a34a", 36 + "token-deleted": "#dc2626", 37 + "token-changed": "#d97706" 38 + }); 39 + 40 + // pierre skips highlighting lines past 1e3 chars by default. 41 + // lockfiles however do not care... 42 + export const PIERRE_TOKENIZE_MAX_LINE_LENGTH = 5000; 43 + 44 + // "line-info" separators carry pierre's "N unmodified lines" text, bare 45 + // "simple" ones get a "···" 46 + export const PIERRE_SEPARATOR_CSS = 47 + // flush against our own file header, pierre's is disabled 48 + ":host{--diffs-gap-block:0}" + 49 + // overflow auto so the bottom scrollbar only exists when a line overflows 50 + ":host{--diffs-overflow-override:auto}" + 51 + "[data-separator]{block-size:21px;font-size:14px;line-height:21px;" + 52 + "color:var(--diffs-fg-number-override,#9ca3af)}" + 53 + // kill pierre's block gap, padding and rounded chip, the separator reads 54 + // as one full-width row 55 + '[data-separator="line-info"]{margin-block:0}' + 56 + '[data-unified] [data-separator="line-info"] [data-separator-wrapper]{padding-inline:0}' + 57 + '[data-gutter] [data-separator="line-info"] [data-separator-wrapper]{padding-left:0}' + 58 + // pierre full-widths the unified separator and the split additions bar but 59 + // not the split deletions one, that one carries the count text and clips in 60 + // the 44px gutter 61 + '[data-deletions] [data-gutter] [data-separator="line-info"] [data-separator-wrapper]{width:100cqi}' + 62 + '[data-separator="line-info"] [data-separator-content]{border-radius:0}' + 63 + '[data-separator="line-info"] [data-expand-button]{border-radius:0}' + 64 + '[data-additions] [data-gutter] [data-separator="line-info"] [data-separator-wrapper]{border-radius:0}' + 65 + "[data-separator-content]{justify-content:center}" + 66 + '[data-separator="simple"]{display:flex;align-items:center;justify-content:center}' + 67 + '[data-separator="simple"]::after{content:"···"}'; 68 + 69 + export const PIERRE_BASE_OPTIONS = { 70 + theme: PIERRE_THEME_NAME, 71 + themeType: "system", 72 + tokenizeMaxLineLength: PIERRE_TOKENIZE_MAX_LINE_LENGTH 73 + } as const; 74 + 75 + // shared by the components and the ssr preloads, the prerendered markup is 76 + // only adoptable when both sides build the same options 77 + export const pierreDiffOptions = (style: DiffStyle): FileDiffOptions<undefined> => ({ 78 + ...PIERRE_BASE_OPTIONS, 79 + diffStyle: style, 80 + diffIndicators: "classic", 81 + lineDiffType: "word-alt", 82 + // "line-info" separators size the bar to span the whole diff. 83 + // "line-info-basic" never gets that, its count text clips in the 44px 84 + // gutter cell 85 + hunkSeparators: "line-info", 86 + disableFileHeader: true, 87 + overflow: "scroll", 88 + unsafeCSS: PIERRE_SEPARATOR_CSS 89 + }); 90 + 91 + export const pierreFileOptions = (wrap: boolean): FileOptions<undefined> => ({ 92 + ...PIERRE_BASE_OPTIONS, 93 + disableFileHeader: true, 94 + overflow: wrap ? "wrap" : "scroll" 95 + }); 96 + 97 + // a renderer with a missing language paints plain text first and swaps in 98 + // the highlight, but only once the shared highlighter is up. before that the 99 + // first render waits on the shiki import and paints nothing, so warm it here 100 + if (browser) void getSharedHighlighter({ themes: [PIERRE_THEME_NAME], langs: [] });
+55
web/src/pierre.css
··· 1 + /* pierre's -override properties must land on the diffs-container element 2 + itself, its :host stylesheet beats inheriting from a wrapper. these rules 3 + must stay UNLAYERED: pierre injects its :host stylesheet unlayered, which 4 + beats anything in @layer. the doubled class lifts specificity above it 5 + regardless of order */ 6 + 7 + diffs-container.diffs-container { 8 + --diffs-font-family: var(--font-mono); 9 + --diffs-font-size: 0.875rem; 10 + --diffs-line-height: 1.5; 11 + /* the "tangled" theme (pierre.ts) binds --diffs-bg/--diffs-fg to these and 12 + forces color-scheme: dark. without them the bg chain goes 13 + guaranteed-invalid and light-dark() is stuck on the dark branch */ 14 + --diffs-background: var(--color-background-default); 15 + --diffs-foreground: var(--color-foreground-default); 16 + color-scheme: light dark; 17 + --diffs-light-bg: var(--color-background-default); 18 + --diffs-dark-bg: var(--color-background-default); 19 + --diffs-bg-separator-override: var(--color-background-inset); 20 + --diffs-fg-number-override: var(--color-foreground-placeholder); 21 + 22 + /* pierre blends line backgrounds 88/12 (light) toward these, so they must 23 + be the saturated tokens. the *-color overrides drive the indicators and 24 + number gutter */ 25 + --diffs-bg-addition-override: var(--color-foreground-success); 26 + --diffs-bg-deletion-override: var(--color-foreground-danger); 27 + --diffs-addition-color-override: var(--color-foreground-success-strong); 28 + --diffs-deletion-color-override: var(--color-foreground-danger-strong); 29 + 30 + /* syntax palette for the "tangled" theme registered in pierre.ts */ 31 + --diffs-token-comment: var(--color-foreground-placeholder); 32 + --diffs-token-string: var(--color-foreground-success); 33 + --diffs-token-string-expression: var(--color-foreground-success); 34 + --diffs-token-keyword: #c084fc; 35 + --diffs-token-function: #60a5fa; 36 + --diffs-token-constant: #f59e0b; 37 + --diffs-token-parameter: var(--color-foreground-default); 38 + --diffs-token-punctuation: var(--color-foreground-muted); 39 + --diffs-token-link: var(--color-foreground-info); 40 + --diffs-token-inserted: var(--color-foreground-success); 41 + --diffs-token-deleted: var(--color-foreground-danger); 42 + --diffs-token-changed: var(--color-foreground-warning); 43 + } 44 + 45 + @media (prefers-color-scheme: dark) { 46 + diffs-container.diffs-container { 47 + --diffs-fg-number-override: var(--color-foreground-subtle); 48 + 49 + /* the dark blend is 80/20, hence the subtle backgrounds */ 50 + --diffs-bg-addition-override: var(--color-background-success-subtle); 51 + --diffs-bg-deletion-override: var(--color-background-danger-subtle); 52 + --diffs-addition-color-override: var(--color-foreground-success); 53 + --diffs-deletion-color-override: var(--color-foreground-danger); 54 + } 55 + }