Skip to content
Merged
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
60 changes: 60 additions & 0 deletions src/components/Head.astro
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,63 @@ const breadcrumbSchema = {
)}

<Fragment set:html={`<script type="application/ld+json">${JSON.stringify(breadcrumbSchema)}</script>`} />

<!-- Custom code block copy button with animated green check -->
<script>
const copyIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
const checkIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="check-icon"><path d="M5 12l5 5L20 7"/></svg>`;

function initCodeBlocks() {
// Custom copy buttons
document.querySelectorAll('.expressive-code .copy button').forEach((button) => {
if (button.dataset.customized) return;
button.dataset.customized = 'true';

const code = button.dataset.code || '';

button.innerHTML = `
<span class="copy-icon-wrapper">${copyIcon}</span>
<span class="check-icon-wrapper">${checkIcon}</span>
`;

button.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();

try {
await navigator.clipboard.writeText(code);
button.classList.add('copied');
setTimeout(() => button.classList.remove('copied'), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
}, { capture: true });
});

// Mobile fade overlay - inject actual DOM element since pseudo-elements don't work with ExpressiveCode
if (window.innerWidth <= 640) {
document.querySelectorAll('.expressive-code .frame').forEach((frame) => {
if (frame.querySelector('.code-fade-overlay')) return;

const overlay = document.createElement('div');
overlay.className = 'code-fade-overlay';
frame.appendChild(overlay);
});
}
}

// Run on initial load and page transitions
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initCodeBlocks);
} else {
initCodeBlocks();
}
document.addEventListener('astro:page-load', initCodeBlocks);

// Re-check on resize for mobile overlay
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(initCodeBlocks, 100);
});
</script>
145 changes: 142 additions & 3 deletions src/styles/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,16 @@ pre[data-bash-prompt] > code {
background: var(--sl-color-bg-sidebar);
}

/* Dark mode - transparent sidebar to show green background */
[data-theme='dark'] .sidebar-pane {
background: transparent;
/* Dark mode - transparent sidebar to show green background (desktop only) */
@media (min-width: 50rem) {
[data-theme='dark'] .sidebar-pane {
background: transparent;
}
}

/* Mobile menu needs solid background when expanded */
[data-theme='dark'] nav.sidebar[data-mobile-menu-expanded] {
background: var(--sl-color-bg);
}

.site-title {
Expand All @@ -311,6 +318,138 @@ pre[data-bash-prompt] > code {
--ec-brdCol: var(--sl-color-hairline);
}

/* Ensure frame has relative positioning for overlays */
.expressive-code .frame {
position: relative;
}

/* Hide terminal header bar for CLI/bash blocks */
.expressive-code .frame.is-terminal .header {
display: none;
}

.expressive-code .frame.is-terminal pre {
border-top: var(--ec-brdWd) solid var(--ec-brdCol) !important;
border-top-left-radius: calc(var(--ec-brdRad) + var(--ec-brdWd));
border-top-right-radius: calc(var(--ec-brdRad) + var(--ec-brdWd));
}

/* Reposition copy button for terminal blocks without header */
.expressive-code .frame.is-terminal .copy {
inset-block-start: calc(var(--ec-brdWd) + 0.4rem);
}

/* Fade overlay - hidden by default, shown on mobile */
.expressive-code .code-fade-overlay {
display: none;
}

/* Mobile: smaller copy button and fade overlay on code */
@media (max-width: 640px) {
/* Always show copy button on mobile (no hover state) */
.expressive-code .copy {
z-index: 2; /* Above fade overlay */
}

.expressive-code .copy button {
width: 2rem;
height: 2rem;
opacity: 1 !important;
}

.expressive-code .copy button svg {
width: 14px;
height: 14px;
}

/* Fade overlay - only first line height */
.expressive-code .code-fade-overlay {
display: block;
position: absolute;
top: 0;
right: 0;
height: 2.5rem; /* Approximately first line height */
width: 8rem;
background: linear-gradient(
to right,
transparent 0px,
color-mix(in srgb, var(--code-background) 40%, transparent) 16px,
color-mix(in srgb, var(--code-background) 70%, transparent) 32px,
color-mix(in srgb, var(--code-background) 90%, transparent) 48px,
var(--code-background) 56px
);
pointer-events: none;
z-index: 1;
}
}

/* Custom copy button with animated green check */
.expressive-code .copy button {
position: relative;
}

/* Hide the default ExpressiveCode copy icon (rendered via ::after) */
.expressive-code .copy button[data-customized]::after {
display: none !important;
}

/* Also hide the inner div that ExpressiveCode uses for hover background */
.expressive-code .copy button[data-customized] > div {
display: none;
}

.expressive-code .copy button .copy-icon-wrapper,
.expressive-code .copy button .check-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.15s ease, transform 0.2s ease;
color: var(--sl-color-text); /* Prevent mobile browsers from applying blue tap color */
}

.expressive-code .copy button .check-icon-wrapper {
position: absolute;
inset: 0;
opacity: 0;
transform: scale(0.5);
color: oklch(0.723 0.191 145.579); /* Sprites green */
}

/* Copied state */
.expressive-code .copy button.copied .copy-icon-wrapper {
opacity: 0;
transform: scale(0.5);
}

.expressive-code .copy button.copied .check-icon-wrapper {
opacity: 1;
transform: scale(1);
}

/* Check icon stroke animation */
.expressive-code .copy button .check-icon path {
stroke-dasharray: 24;
stroke-dashoffset: 24;
transition: stroke-dashoffset 0.3s ease-out 0.1s;
}

.expressive-code .copy button.copied .check-icon path {
stroke-dashoffset: 0;
}

/* Button pop animation */
@keyframes copy-pop {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}

/* Keep button visible during copied state even when not hovering */
.expressive-code .copy button.copied {
animation: copy-pop 0.2s ease-out;
opacity: 1 !important;
}

:not(pre) > code {
background: var(--sl-color-bg-inline-code);
padding: 0.2em 0.4em;
Expand Down
Loading