---
/**
* ExpandingPanels — a row of panels where the one pointed at or focused widens and shows its words, and the others
* narrow to a strip with their title turned up the side. Each panel is a link (or a plain panel), its picture filling it:
* a slot of its own (`panel-1` … `panel-8`) or the colour block it names. With nothing pointed at, the `open` panel is
* the wide one. Below 40 rem of width the panels stack, each one whole. The widening is a transition of flex-grow, a
* number, so it runs on any engine. Zero JavaScript.
*/
import type { HTMLAttributes } from 'astro/types';
interface Panel {
title: string;
/** A line or two, shown when the panel is wide. */
text?: string;
/** A short line over the title: a figure, a tag. */
meta?: string;
/** The panel is a link to this page. */
href?: string;
/** The colour block the panel paints (1–5) when it has no picture of its own. */
block?: 1 | 2 | 3 | 4 | 5;
}
interface Props extends HTMLAttributes<'div'> {
panels: Panel[];
/** The panel that is wide when nothing is pointed at, from 1. */
open?: number;
/** The row's height on a wide screen, any CSS length. */
height?: string;
/** What the list is, for a screen reader. */
label?: string;
}
const { panels, open = 1, height = '26rem', label, class: className, style, ...rest } = Astro.props;
if (panels.length < 2 || panels.length > 8) throw new Error(`ExpandingPanels: panels holds ${panels.length}; it lays out two to eight.`);
const list = panels;
const slots = Astro.slots;
// slot names are static (a name built in a map is not a slot Astro can fill), so each is rendered here, in order
const art = await Promise.all(list.map((_, i) => (slots.has(`panel-${i + 1}`) ? slots.render(`panel-${i + 1}`) : Promise.resolve(''))));
const vars = [`--ma-xp-height:${height}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
---
<div class:list={['ma-xpanels', className]} style={vars} {...rest}>
<ul class="ma-xpanels__list" aria-label={label}>
{list.map((p, i) => {
const Tag = p.href ? 'a' : 'div';
return (
<li class="ma-xpanels__item" data-open={i + 1 === open ? '' : undefined} data-ma-block={art[i] ? undefined : (p.block ?? ((i % 5) + 1))}>
<Tag class="ma-xpanels__panel" href={p.href} tabindex={p.href ? undefined : 0}>
{art[i] && <span class="ma-xpanels__art" aria-hidden="true" set:html={art[i]} />}
<span class="ma-xpanels__strip" aria-hidden="true">{p.title}</span>
<span class="ma-xpanels__words">
{p.meta && <><span class="ma-xpanels__meta">{p.meta}</span>{' '}</>}
<span class="ma-xpanels__title">{p.title}</span>
{p.text && <>{' '}<span class="ma-xpanels__text">{p.text}</span></>}
</span>
</Tag>
</li>
);
})}
</ul>
</div>
<style is:global>
@layer components {
/* the root is the container and the list its child: an element never answers its own container query */
:where(.ma-xpanels) { container-type: inline-size; }
:where(.ma-xpanels__list) {
display: grid;
gap: 0.75rem;
margin: 0;
padding: 0;
list-style: none;
}
:where(.ma-xpanels__item) { min-inline-size: 0; }
:where(.ma-xpanels__strip) { display: none; }
:where(.ma-xpanels__panel) {
position: relative;
display: grid;
align-content: end;
block-size: 100%;
min-block-size: 15rem;
overflow: hidden;
border-radius: var(--ma-radius-xl, 1.25rem);
background: var(--ma-block-ground, var(--card, var(--ma-panel)));
color: var(--ma-block-text, var(--card-foreground, currentColor));
text-decoration: none;
isolation: isolate;
}
:where(.ma-xpanels__panel:focus-visible) { outline: 2px solid var(--ring, currentColor); outline-offset: 3px; }
:where(.ma-xpanels__art) { position: absolute; inset: 0; z-index: -1; display: grid; }
:where(.ma-xpanels__art > *) { inline-size: 100%; block-size: 100%; }
/* a picture under the words gets a scrim so they read on it: dark enough under every line for white text on the
palest sky (4.5 : 1 at 0.84), fading out over the 3 rem above the first line */
:where(.ma-xpanels__art ~ .ma-xpanels__words) { padding-block-start: 3rem; background: linear-gradient(to top, oklch(0.16 0 0 / 0.88), oklch(0.16 0 0 / 0.84) calc(100% - 3rem), oklch(0.16 0 0 / 0)); color: #fff; }
:where(.ma-xpanels__words) { display: grid; gap: 0.35rem; padding: 1.25rem 1.35rem 1.35rem; }
:where(.ma-xpanels__meta) { font-size: 0.8125rem; font-weight: 600; }
:where(.ma-xpanels__title) { font-family: var(--ma-font-display, inherit); font-size: 1.35rem; font-weight: var(--ma-display-weight, 700); letter-spacing: -0.015em; line-height: 1.15; }
:where(.ma-xpanels__text) { max-inline-size: 30ch; font-size: 0.9375rem; line-height: 1.5; }
/* a wide box: one row, the open panel wide and the rest strips, the one pointed at or focused taking the width */
@container (min-width: 40rem) {
:where(.ma-xpanels__list) { display: flex; block-size: var(--ma-xp-height, 26rem); }
:where(.ma-xpanels__item) { flex: 1 1 0; transition: flex-grow var(--ma-duration-slow, 500ms) var(--ma-ease-out, ease-out); }
:where(.ma-xpanels__item[data-open]) { flex-grow: 4; }
:where(.ma-xpanels__list:is(:hover, :focus-within) .ma-xpanels__item) { flex-grow: 1; }
:where(.ma-xpanels__list:is(:hover, :focus-within) .ma-xpanels__item:is(:hover, :focus-within)) { flex-grow: 4; }
:where(.ma-xpanels__words) { min-inline-size: 16rem; }
/* a narrow panel shows its title only, turned to read up the strip */
:where(.ma-xpanels__item:not([data-open]) .ma-xpanels__words),
:where(.ma-xpanels__list:is(:hover, :focus-within) .ma-xpanels__item .ma-xpanels__words) { opacity: 0; transition: opacity var(--ma-duration-fast, 150ms) var(--ma-ease-out, ease-out); }
:where(.ma-xpanels__list:not(:hover, :focus-within) .ma-xpanels__item[data-open] .ma-xpanels__words),
:where(.ma-xpanels__list .ma-xpanels__item:is(:hover, :focus-within) .ma-xpanels__words) { opacity: 1; transition-delay: 180ms; }
/* physical insets: a logical one would resolve in the strip's own vertical writing mode, block-end being the left */
:where(.ma-xpanels__strip) {
position: absolute;
bottom: 1rem;
left: 50%;
display: block;
max-height: calc(100% - 2rem);
overflow: hidden;
translate: -50% 0;
writing-mode: vertical-rl;
rotate: 180deg;
white-space: nowrap;
text-overflow: ellipsis;
font-family: var(--ma-font-display, inherit);
font-size: 1.05rem;
font-weight: var(--ma-display-weight, 700);
transition: opacity var(--ma-duration-fast, 150ms) var(--ma-ease-out, ease-out);
}
/* over a picture the title sits on a dark pill of its own, so it reads on the lightest sky */
:where(.ma-xpanels__art ~ .ma-xpanels__strip) { padding: 0.7rem 0.35rem; border-radius: 999px; background: oklch(0.15 0 0 / 0.62); color: #fff; }
:where(.ma-xpanels__list:not(:hover, :focus-within) .ma-xpanels__item[data-open] .ma-xpanels__strip),
:where(.ma-xpanels__list .ma-xpanels__item:is(:hover, :focus-within) .ma-xpanels__strip) { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
:where(.ma-xpanels__item), :where(.ma-xpanels__words), :where(.ma-xpanels__strip) { transition: none; }
}
}
</style>