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
1 change: 1 addition & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default defineConfig({
},
}),
starlight({
components: { Footer: './src/components/Footer.astro' },
title: "Fancy Mumble",
description:
"Documentation for the Fancy Mumble app and Starling server. Voice chat reimagined for 2026.",
Expand Down
7 changes: 7 additions & 0 deletions src/components/Footer.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
import Footer from '@astrojs/starlight/components/Footer.astro';
import ImageLightbox from './ImageLightbox.astro';
---

<Footer />
<ImageLightbox />
86 changes: 86 additions & 0 deletions src/components/ImageLightbox.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<dialog id="doc-image-lightbox" aria-label="Image preview">
<div class="image-lightbox-toolbar">
<button type="button" data-image-zoom aria-pressed="false">Actual size</button>
<button type="button" data-image-close aria-label="Close image preview">Close ×</button>
</div>
<div class="image-lightbox-view"><img alt="" /></div>
<p class="image-lightbox-caption"></p>
</dialog>

<script>
function setupLightbox() {
const dialog = document.querySelector<HTMLDialogElement>('#doc-image-lightbox');
if (!dialog || dialog.dataset.ready) return;
dialog.dataset.ready = 'true';
const preview = dialog.querySelector<HTMLImageElement>('img')!;
const caption = dialog.querySelector<HTMLElement>('.image-lightbox-caption')!;
const zoom = dialog.querySelector<HTMLButtonElement>('[data-image-zoom]')!;
let opener: HTMLElement | null = null;
let source: HTMLImageElement | null = null;

function updateImage() {
if (!source) return;
const pair = source.closest('.theme-screenshot');
const visible = pair
? Array.from(pair.querySelectorAll('img')).find(image => getComputedStyle(image).display !== 'none')
: source;
if (!visible) return;
preview.src = visible.currentSrc || visible.src;
preview.alt = visible.alt;
caption.textContent = visible.alt;
}

document.querySelectorAll<HTMLImageElement>('.sl-markdown-content img, .hero img').forEach(image => {
if (image.closest('a, button') || image.dataset.noLightbox !== undefined) return;
// Profile cards position their images independently; preserve that layout.
const positioned = image.closest('[data-profile-example]') !== null;
const trigger = positioned ? image : document.createElement('button');
if (trigger instanceof HTMLButtonElement) trigger.type = 'button';
else { trigger.setAttribute('role', 'button'); trigger.tabIndex = 0; }
trigger.classList.add(positioned ? 'doc-image-target' : 'doc-image-trigger');
trigger.setAttribute('aria-label', `Enlarge image${image.alt ? ': ' + image.alt : ''}`);
trigger.setAttribute('aria-haspopup', 'dialog');
// Keep the trigger hidden whenever its theme variant is hidden.
if (image.classList.contains('screenshot-light')) trigger.classList.add('screenshot-light');
if (image.classList.contains('screenshot-dark')) trigger.classList.add('screenshot-dark');
if (!positioned) { image.before(trigger); trigger.append(image); }
const open = () => {
opener = trigger;
source = image;
dialog.classList.remove('actual-size');
zoom.setAttribute('aria-pressed', 'false');
zoom.textContent = 'Actual size';
updateImage();
dialog.showModal();
};
trigger.addEventListener('click', open);
if (positioned) trigger.addEventListener('keydown', event => {
if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); open(); }
});
});

dialog.querySelector('[data-image-close]')!.addEventListener('click', () => dialog.close());
dialog.addEventListener('click', event => {
if (event.target === dialog) dialog.close();
});
dialog.addEventListener('close', () => {
const pair = source?.closest('.theme-screenshot');
if (pair && opener && !opener.getClientRects().length) {
opener = Array.from(pair.querySelectorAll<HTMLButtonElement>('.doc-image-trigger'))
.find(button => button.getClientRects().length > 0) ?? opener;
}
preview.removeAttribute('src');
source = null;
opener?.focus();
});
zoom.addEventListener('click', () => {
const actual = dialog.classList.toggle('actual-size');
zoom.setAttribute('aria-pressed', String(actual));
zoom.textContent = actual ? 'Fit to window' : 'Actual size';
});
new MutationObserver(() => { if (dialog.open) updateImage(); })
.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
}
setupLightbox();
document.addEventListener('astro:page-load', setupLightbox);
</script>
42 changes: 42 additions & 0 deletions src/styles/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,45 @@ div.header {
.theme-screenshot .screenshot-dark { display: none; }
:root[data-theme="dark"] .theme-screenshot .screenshot-light { display: none; }
:root[data-theme="dark"] .theme-screenshot .screenshot-dark { display: block; }

.doc-image-trigger {
display: block;
max-width: 100%;
padding: 0;
border: 0;
background: none;
cursor: zoom-in;
}
.doc-image-trigger img { display: block; margin: 0; }
.doc-image-target { cursor: zoom-in; }
.doc-image-trigger:focus-visible, .doc-image-target:focus-visible { outline: 3px solid var(--sl-color-accent-high); outline-offset: 4px; }
#doc-image-lightbox {
width: 96vw;
max-width: 1800px;
height: 94dvh;
max-height: 94dvh;
padding: 1rem;
border: 1px solid var(--sl-color-gray-4);
border-radius: .75rem;
background: var(--sl-color-bg);
color: var(--sl-color-white);
overflow: hidden;
}
#doc-image-lightbox[open] { display: flex; flex-direction: column; gap: .75rem; }
#doc-image-lightbox::backdrop { background: rgb(0 0 0 / 80%); }
.image-lightbox-toolbar { display: flex; justify-content: flex-end; gap: .75rem; flex-shrink: 0; }
.image-lightbox-toolbar button {
padding: .5rem .75rem;
border: 1px solid var(--sl-color-gray-4);
border-radius: .4rem;
color: inherit;
background: var(--sl-color-gray-6);
cursor: pointer;
font: inherit;
}
.image-lightbox-view { flex: 1; min-height: 0; overflow: auto; display: grid; grid-template: minmax(0, 1fr) / minmax(0, 1fr); place-items: center; }
.image-lightbox-view img { min-width: 0; min-height: 0; max-width: 100%; max-height: 100%; width: 100%; height: 100%; object-fit: contain; }
#doc-image-lightbox.actual-size .image-lightbox-view { display: block; }
#doc-image-lightbox.actual-size img { max-width: none; max-height: none; width: auto; height: auto; }
.image-lightbox-caption { margin: 0; flex-shrink: 0; font-size: .85rem; color: var(--sl-color-gray-2); }
html:has(#doc-image-lightbox[open]) { overflow: hidden; }
Loading