diff --git a/astro.config.mjs b/astro.config.mjs index 2b65a57d..ecaf2177 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -20,6 +20,7 @@ import developerSidebar from './src/content/sidebars/developer.ts'; import customConsentScript from './src/scripts/custom-consent-mode.js?raw'; import postHogScript from './src/scripts/posthog.js?raw'; import gtmScript from './src/scripts/gtm.js?raw'; +import { rehypeButtonHeadings } from "./src/scripts/rehypeButtonHeadings.mjs"; let site; @@ -38,6 +39,9 @@ const config = defineConfig({ integrations: [ starlight({ title: 'Crowdin Docs', + markdown: { + headingLinks: false + }, logo: { replacesTitle: true, light: './src/assets/logo/dark.svg', @@ -231,18 +235,7 @@ const config = defineConfig({ ], rehypePlugins: [ rehypeHeadingIds, - [ - rehypeAutolinkHeadings, - { - behavior: 'wrap', // Wrap the heading text in a link. - }, - ], - [ - rehypeExternalLinks, - { - target: '_blank', // Open external links in a new tab. - } - ] + rehypeButtonHeadings, ], }, vite: { diff --git a/src/assets/images/link.svg b/src/assets/images/link.svg new file mode 100644 index 00000000..d3ad5d15 --- /dev/null +++ b/src/assets/images/link.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/scripts/rehypeButtonHeadings.mjs b/src/scripts/rehypeButtonHeadings.mjs new file mode 100644 index 00000000..9aa5c6ad --- /dev/null +++ b/src/scripts/rehypeButtonHeadings.mjs @@ -0,0 +1,69 @@ +/** + * Custom rehype plugin to add button elements with heading icons to headings + * The button is used onClick handlers for copy-link functionality + */ +export function rehypeButtonHeadings() { + return (tree) => { + function traverse(node) { + // Check if it's a heading element (h1-h6) with an id + if ( + node.type === 'element' && + /^h[1-6]$/.test(node.tagName) && + node.properties && + node.properties.id + ) { + const headingId = node.properties.id; + + const onClickHandler = `(function(btn) { + const url = window.location.origin + window.location.pathname + '#${headingId}'; + if (navigator.clipboard) { + navigator.clipboard.writeText(url) + .then(() => { + const originalTitle = btn.title; + btn.setAttribute('data-copied', 'true'); + + // Reset after 2 seconds + setTimeout(() => { + btn.removeAttribute('data-copied'); + }, 2000); + }) + .catch(err => console.error('Failed to copy:', err)); + } else { + console.error('Clipboard API not available. Requires HTTPS or localhost.'); + } + })(this)`; + + const button = { + type: 'element', + tagName: 'button', + properties: { + type: 'button', + title: 'Click to copy link', + onClick: onClickHandler, + 'data-tooltip': 'Copy link', + class: 'tooltip-container' + }, + children: [ + { + type: 'element', + tagName: 'span', + properties: { + className: ['heading-icon'] + }, + children: [] + } + ] + }; + + node.children.push(button); + } + + if (node.children && Array.isArray(node.children)) { + node.children.forEach(child => traverse(child)); + } + } + + traverse(tree); + }; +} + diff --git a/src/style/global.css b/src/style/global.css index e2080bab..f6ef0fe1 100644 --- a/src/style/global.css +++ b/src/style/global.css @@ -101,6 +101,110 @@ h1, h2, h3, h4, h5, h6 { } } +/* Heading button with tooltip */ + +.sl-markdown-content :is(h1, h2, h3, h4, h5, h6) { + button.tooltip-container { + position: relative; + background-color: transparent !important; + border: none; + cursor: pointer; + padding: 0; + + /* Tooltip text */ + &::before { + content: attr(data-tooltip); + position: absolute; + bottom: calc(100% + 0.5rem); + left: 50%; + transform: translateX(-50%); + padding: 0.375rem 0.75rem; + background-color: var(--color-gray-800); + color: white; + font-size: 0.875rem; + white-space: nowrap; + border-radius: 0.375rem; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease-in-out; + z-index: 1000; + } + + /* Tooltip arrow */ + &::after { + content: ''; + position: absolute; + bottom: calc(100% + 0.125rem); + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 0.375rem solid transparent; + border-right: 0.375rem solid transparent; + border-top: 0.375rem solid var(--color-gray-800); + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease-in-out; + z-index: 1000; + } + + /* Only show tooltip when link is copied */ + &[data-copied="true"]::before { + content: "Link copied!"; + background-color: var(--color-accent-600); + opacity: 1; + } + + &[data-copied="true"]::after { + border-top-color: var(--color-accent-600); + opacity: 1; + } + } + + .heading-icon { + margin-left: 0.25rem; + background-image: url("/src/assets/images/link.svg"); + background-repeat: no-repeat; + background-position: center; + background-size: contain; + width: 1.25rem; + height: 1.25rem; + display: inline-block; + visibility: hidden; + } + + &:hover { + .heading-icon { + visibility: visible; + } + } +} + +/* Light mode adjustments */ +:root[data-theme='light'] .sl-markdown-content :is(h1, h2, h3, h4, h5, h6) { + button.tooltip-container::before { + background-color: var(--color-gray-800); + color: white; + } + + button.tooltip-container::after { + border-top-color: var(--color-gray-800); + } + + button.tooltip-container[data-copied="true"]::before { + background-color: var(--color-accent-600); + } + + button.tooltip-container[data-copied="true"]::after { + border-top-color: var(--color-accent-600); + } +} + +/* Dark mode: invert the link icon */ +:root:not([data-theme='light']) .sl-markdown-content :is(h1, h2, h3, h4, h5, h6) .heading-icon { + filter: brightness(0) saturate(100%) invert(100%); +} + /* Markdown content */ .sl-markdown-content {