Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 5 additions & 12 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -38,6 +39,9 @@ const config = defineConfig({
integrations: [
starlight({
title: 'Crowdin Docs',
markdown: {
headingLinks: false
},
logo: {
replacesTitle: true,
light: './src/assets/logo/dark.svg',
Expand Down Expand Up @@ -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: {
Expand Down
7 changes: 7 additions & 0 deletions src/assets/images/link.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 69 additions & 0 deletions src/scripts/rehypeButtonHeadings.mjs
Original file line number Diff line number Diff line change
@@ -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);
};
}

104 changes: 104 additions & 0 deletions src/style/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading