diff --git a/package.json b/package.json index 7ecef9a..166baf7 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,9 @@ "postcss": "^8.4.49", "prettier": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.8", + "rehype-autolink-headings": "^7.1.0", + "rehype-class-names": "^2.0.0", + "rehype-slug": "^6.0.0", "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "remark-mdx-frontmatter": "^5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0beaa5..47ca5df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,6 +87,15 @@ importers: prettier-plugin-tailwindcss: specifier: ^0.6.8 version: 0.6.8(prettier@3.3.3) + rehype-autolink-headings: + specifier: ^7.1.0 + version: 7.1.0 + rehype-class-names: + specifier: ^2.0.0 + version: 2.0.0 + rehype-slug: + specifier: ^6.0.0 + version: 6.0.0 remark-frontmatter: specifier: ^5.0.0 version: 5.0.0 @@ -815,10 +824,16 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bcp-47-match@2.0.3: + resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -923,6 +938,9 @@ packages: resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==} engines: {node: '>= 8'} + css-selector-parser@3.0.5: + resolution: {integrity: sha512-3itoDFbKUNx1eKmVpYMFyqKX04Ww9osZ+dLgrk6GEv6KMVeXUhUnp4I5X+evw+u3ZxVU6RFXSSRxlTeMh8bA+g==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -980,6 +998,10 @@ packages: resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} engines: {node: '>=0.3.1'} + direction@2.0.1: + resolution: {integrity: sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==} + hasBin: true + dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} @@ -1266,6 +1288,9 @@ packages: resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} engines: {node: '>= 0.4'} + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1326,12 +1351,30 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-classnames@3.0.0: + resolution: {integrity: sha512-tI3JjoGDEBVorMAWK4jNRsfLMYmih1BUOG3VV36pH36njs1IEl7xkNrVTD2mD2yYHmQCa5R/fj61a8IAF4bRaQ==} + + hast-util-has-property@3.0.0: + resolution: {integrity: sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==} + + hast-util-heading-rank@3.0.0: + resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-select@6.0.3: + resolution: {integrity: sha512-OVRQlQ1XuuLP8aFVLYmC2atrfWHS5UD3shonxpnyrjcCkwtvmt/+N6kYJdcY4mkMJhxp4kj2EFIxQ9kvkkt/eQ==} + hast-util-to-estree@3.1.0: resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==} hast-util-to-jsx-runtime@2.3.2: resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==} + hast-util-to-string@3.0.1: + resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} @@ -1886,6 +1929,9 @@ packages: resolution: {integrity: sha512-nkc+3pIIhqHVQr085X9d2JzPzLyjzQS96zbruppqC9aZRm/x8xx6xhI98gHtsfELP2bE+loHq8ZaHFHhe+NauA==} engines: {node: ^16.14.0 || >=18.0.0} + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2191,9 +2237,18 @@ packages: resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} engines: {node: '>= 0.4'} + rehype-autolink-headings@7.1.0: + resolution: {integrity: sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==} + + rehype-class-names@2.0.0: + resolution: {integrity: sha512-jldCIiAEvXKdq8hqr5f5PzNdIDkvHC6zfKhwta9oRoMu7bn0W7qLES/JrrjBvr9rKz3nJ8x4vY1EWI+dhjHVZQ==} + rehype-recma@1.0.0: resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + rehype-slug@6.0.0: + resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} + remark-frontmatter@5.0.0: resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} @@ -3386,8 +3441,12 @@ snapshots: balanced-match@1.0.2: {} + bcp-47-match@2.0.3: {} + binary-extensions@2.3.0: {} + boolbase@1.0.0: {} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -3490,6 +3549,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-selector-parser@3.0.5: {} + cssesc@3.0.0: {} csstype@3.1.3: {} @@ -3544,6 +3605,8 @@ snapshots: diff@5.2.0: {} + direction@2.0.1: {} + dlv@1.1.3: {} doctrine@2.1.0: @@ -3986,6 +4049,8 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.2.4 + github-slugger@2.0.0: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -4040,6 +4105,41 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-classnames@3.0.0: + dependencies: + '@types/hast': 3.0.4 + space-separated-tokens: 2.0.2 + + hast-util-has-property@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-heading-rank@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-select@6.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + bcp-47-match: 2.0.3 + comma-separated-tokens: 2.0.3 + css-selector-parser: 3.0.5 + devlop: 1.1.0 + direction: 2.0.1 + hast-util-has-property: 3.0.0 + hast-util-to-string: 3.0.1 + hast-util-whitespace: 3.0.0 + nth-check: 2.1.1 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + hast-util-to-estree@3.1.0: dependencies: '@types/estree': 1.0.6 @@ -4081,6 +4181,10 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-string@3.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -4880,6 +4984,10 @@ snapshots: npm-package-arg: 11.0.3 semver: 7.6.3 + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -5153,6 +5261,22 @@ snapshots: es-errors: 1.3.0 set-function-name: 2.0.2 + rehype-autolink-headings@7.1.0: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.2.0 + hast-util-heading-rank: 3.0.0 + hast-util-is-element: 3.0.0 + unified: 11.0.5 + unist-util-visit: 5.0.0 + + rehype-class-names@2.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-classnames: 3.0.0 + hast-util-select: 6.0.3 + unified: 11.0.5 + rehype-recma@1.0.0: dependencies: '@types/estree': 1.0.6 @@ -5161,6 +5285,14 @@ snapshots: transitivePeerDependencies: - supports-color + rehype-slug@6.0.0: + dependencies: + '@types/hast': 3.0.4 + github-slugger: 2.0.0 + hast-util-heading-rank: 3.0.0 + hast-util-to-string: 3.0.1 + unist-util-visit: 5.0.0 + remark-frontmatter@5.0.0: dependencies: '@types/mdast': 4.0.4 diff --git a/src/index.css b/src/index.css index 37f741a..c55d386 100644 --- a/src/index.css +++ b/src/index.css @@ -31,3 +31,24 @@ ::-webkit-scrollbar-track { @apply bg-transparent; } + +.heading { + @apply relative text-nowrap; +} + +.heading-link { + @apply absolute -left-[40px] top-1/2 -translate-y-1/2 pr-4 opacity-0 transition-opacity duration-300 ease-in-out; +} + +.heading-icon { + @apply flex h-6 w-6 items-center justify-center rounded-md ring-1 ring-black dark:ring-white; +} + +h1:hover .heading-link, +h2:hover .heading-link, +h3:hover .heading-link, +h4:hover .heading-link, +h5:hover .heading-link, +h6:hover .heading-link { + opacity: 1; +} diff --git a/vite.config.ts b/vite.config.ts index e3d8b23..109cec2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,6 +4,9 @@ import mdx from '@mdx-js/rollup'; import remarkGfm from 'remark-gfm'; import remarkFrontmatter from 'remark-frontmatter'; import remarkMdxFrontmatter from 'remark-mdx-frontmatter'; +import rehypeSlug from 'rehype-slug'; +import rehypeAutolinkHeadings from 'rehype-autolink-headings'; +import rehypeClassNames from 'rehype-class-names'; /** @type {import('vite').UserConfig} */ export default defineConfig({ @@ -16,6 +19,60 @@ export default defineConfig({ remarkFrontmatter, [remarkMdxFrontmatter, { name: 'metadata' }], ], + rehypePlugins: [ + rehypeSlug, + [ + rehypeClassNames, + { + h1: 'heading', + h2: 'heading', + h3: 'heading', + h4: 'heading', + h5: 'heading', + h6: 'heading', + }, + ], + [ + rehypeAutolinkHeadings, + { + properties: { + className: ['heading-link'], + ariaLabel: 'Link to this heading', + }, + content: { + type: 'element', + tagName: 'span', + properties: { + className: ['heading-icon'], + }, + children: [ + { + type: 'element', + tagName: 'svg', + properties: { + width: '12', + height: '12', + fill: 'none', + 'aria-hidden': 'true', + }, + children: [ + { + type: 'element', + tagName: 'path', + properties: { + d: 'M3.75 1v10M8.25 1v10M1 3.75h10M1 8.25h10', + stroke: 'currentColor', + 'stroke-width': '1.5', + 'stroke-linecap': 'round', + }, + }, + ], + }, + ], + }, + }, + ], + ], }), }, react({ include: /\.(mdx|js|jsx|ts|tsx)$/ }),