theme-grid.astro · 5703 B · ext
.astro · seen 81× in SWHvia unique-primary
swh:1:cnt:64f4f9d3387aa14cfcce0689494ef1b5d67d63b2;origin=https://github.com/withastro/starlight;anchor=swh:1:rev:8a2c522785402fffd32a8b7db51d818e93523115;path=/docs/src/components/theme-grid.astro
Show source
---
import { Icon } from '@astrojs/starlight/components';
import FluidGrid from './fluid-grid.astro';
import MediaCard from './media-card.astro';
import ThemeImage from './theme-image.astro';
interface Props {
labels: {
/** Accessible label for the theme toggle. */
legend: string;
/** Accessible label for the dark color scheme variant. */
dark: string;
/** Accessible label for the light color scheme variant. */
light: string;
};
themes: Array<{
/** The name of this theme. */
title: string;
/** A short description of this theme. */
description: string;
/** URL for this theme’s website. Ideally should link to a demo site showing off the theme. */
href: string;
previews: {
/** Image filename as found in `src/assets/themes/`, e.g. `ion-light.png`. */
light: string;
/** Image filename as found in `src/assets/themes/`, e.g. `ion-dark.png`. */
dark: string;
};
}>;
}
const { labels, themes } = Astro.props;
---
<starlight-theme-preview data-theme-preview="dark">
<fieldset class="not-content sl-flex">
{
/* `<legend>` is a pain to position, so we visually hide it and use an `aria-hidden` duplicate
for the visual representation. See https://adrianroselli.com/2022/07/use-legend-and-fieldset.html */
}
<legend class="sr-only">{labels.legend}</legend>
<strong aria-hidden="true">{labels.legend}</strong>
{
(['dark', 'light'] as const).map((variant) => (
<label class:list={['sl-flex', variant]} for={`${variant}-input`}>
<span class="radio">
<Icon name="approve-check" />
</span>
<span>{labels[variant]}</span>
<input
id={`${variant}-input`}
class="sr-only"
name="preview-theme"
value={variant}
type="radio"
checked={variant === 'dark'}
/>
</label>
))
}
{/* If the currently active theme is light mode, default to showing the light mode previews. */}
<script is:inline>
// @ts-nocheck
const theme = document.documentElement.dataset.theme || 'dark';
const input = document.querySelector(`input[value="${theme}"]`);
if (input) input.checked = true;
const customEl = document.querySelector('starlight-theme-preview');
if (customEl) customEl.dataset.themePreview = theme;
</script>
</fieldset>
<FluidGrid minColumnWidth="25rem" gap="clamp(1rem, calc(0.75rem + 1vw), 1.5rem)">
{
themes.map(
async ({ title, description, href, previews }) =>
previews && (
<MediaCard {href}>
<div class="preview-images" slot="media" aria-hidden="true">
<ThemeImage src={previews.light} class="light" />
<ThemeImage src={previews.dark} class="dark" />
</div>
<div class="meta sl-flex">
<p class="title">{title}</p>
<p class="description" set:html={description} />
</div>
</MediaCard>
)
)
}
</FluidGrid>
</starlight-theme-preview>
<script>
customElements.define(
'starlight-theme-preview',
class ThemePreview extends HTMLElement {
constructor() {
super();
this.querySelector('fieldset')?.addEventListener('change', ({ target }) => {
if (target instanceof HTMLInputElement) {
this.dataset.themePreview = target.value;
}
});
}
}
);
</script>
<style>
starlight-theme-preview {
--transition-time: 0.25s;
display: block;
}
fieldset {
/* Snappier transitions for the form controls. */
--transition-time: 0.15s;
/* Stick the control bar to bottom of the nav bar on scroll. */
position: sticky;
top: var(--sl-nav-height);
z-index: var(--sl-z-index-toc);
/* Pull the bar out so it is full width on small screens. */
margin-inline: calc(-1 * var(--sl-nav-pad-x));
padding: var(--sl-nav-pad-y) var(--sl-nav-pad-x);
/* Internal layout and styles. */
gap: 1rem;
align-items: center;
justify-content: end;
border: 1px solid var(--sl-color-hairline);
font-size: var(--sl-text-sm);
background-color: var(--sl-color-bg-nav);
box-shadow: var(--sl-shadow-md);
}
@media (min-width: 50rem) and (max-width: 72rem) {
/* Medium-width screens need a custom value to avoid x-axis overflow. */
fieldset {
margin-inline: -1rem;
}
}
label {
align-items: center;
gap: 0.375rem;
color: var(--sl-color-gray-3);
transition: color var(--transition-time) ease-in-out;
}
label:hover,
label:has(:checked) {
color: var(--sl-color-white);
}
.radio {
display: grid;
place-content: center;
border: 1px solid var(--sl-color-gray-5);
border-radius: 100%;
width: 1.5rem;
height: 1.5rem;
background-color: #000;
color: #fff;
transition: transform var(--transition-time) ease-in-out;
}
label:not(:has(:checked)):hover .radio {
transform: scale(1.15);
}
.light .radio {
background-color: #fff;
color: #000;
}
/* Show the input’s focus ring on the label. */
label:has(:focus-visible) {
outline: auto;
outline-offset: 2px;
}
/* Show check mark icon only when input is checked. */
label svg {
opacity: 0;
transition: opacity var(--transition-time) ease-in-out;
}
label:has(:checked) svg {
opacity: 1;
}
/* Stack light/dark images in the same grid row and column. */
.preview-images {
display: grid;
grid-template-columns: 1fr;
}
.preview-images > * {
grid-area: 1 / 1;
}
/* Fade out the preview image that isn’t currently selected. */
img {
transition: opacity var(--transition-time) ease-in-out;
}
[data-theme-preview='dark'] img.light {
opacity: 0;
}
[data-theme-preview='light'] img.dark {
opacity: 0;
}
.meta {
padding: 1rem;
flex-direction: column;
gap: 0.5rem;
}
.title {
font-size: var(--sl-text-lg);
}
.description {
color: var(--sl-color-gray-3);
line-height: 1.5;
}
</style>