Skip to content

Components / Sections

Piece List

The issues, episodes, lessons or videos of one person's work, as a list of rows in the order you give them (newest first). Each row has its number in the mono face, a picture, the title as the row's link, the date and the length. The picture is the piece's own: a waveform drawn from its title (look="wave", for audio), a drawing on a colour block (look="tile") or a frame with a play mark and the running time (look="frame", for video). A pointer on a row tints it, sets its waveform or its drawing moving and swells a frame's play mark, and with detail each row also carries its summary. A piece with audio gets a play button: one small script plays the file in the page, one piece at a time, and shows how much is played on its waveform. Without audio the script has nothing to bind and every row is a link.

Live demo

Episodes

The latest conversations

All episodes
  1. 29 September 202652 min

  2. 15 September 202647 min

  3. 1 September 202658 min

  4. 18 August 202644 min

Live, from the library itself, at full size: scroll, hover, navigate away and back. The demo is never gated.

Variants · each row writes the attribute the prop writes; the section's own CSS answers

tone
density

Measured

JavaScript of its own

893 B raw

510 B gzip.

Uses the shared runtime (1.9 kB raw, once per site). With those included: 2.6 kB raw.

CSS 19.5 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.

Browser support

Baseline · widely available

Chrome 105 · Firefox 121 · Safari 16; needs container-units, has. Elsewhere: without :has() the row's link shows no focus ring; without container units the titles keep the page's text size and every row stays in its narrow layout, the date under the words.

Install Pro

The demo above is never gated; the source is. With a key in components.json (see account), the registry serves it like any other item:

terminal
npx shadcn@latest add @moonarc-pro/piece-list
src/pages/index.astro
---
import PieceList from '@/components/moonarc/pro/PieceList.astro';
---
<PieceList
  look="wave"
  eyebrow="Episodes"
  detail
  more={{ href: '/episodes/', label: 'All episodes' }}
  pieces={[
    { number: 'Episode 18', title: 'Pricing a first course', date: '3 October 2026', length: '48 min', summary: 'What to charge before anyone has bought.', href: '/episodes/18/', audio: '/audio/episode-18.mp3' },
    { number: 'Episode 17', title: 'The newsletter that paid for itself', date: '19 September 2026', length: '41 min', summary: 'Six months of numbers, in the order they happened.', href: '/episodes/17/' },
  ]}
>
  <span slot="headline">Every episode, <em>newest first</em></span>
</PieceList>

Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: motif, waveform.

Props

PropTypeDefaultDescription
pieces{ title, href, number?, date?, length?, summary?, id?, art?, block?, audio? }[]noneThe pieces, in the order shown. href is where it is read, heard or watched. number is set in the mono face before the title ("No. 42", "Episode 18"); length is "8 min read" or "48 min". id is an anchor a link elsewhere can point at, and the row is tinted when it is the target. art is the drawing for look="tile" and "frame", picked from the title when unset; block is the picture's colour (the first four in turn when unset). audio is a file to play in the page, for look="wave".
look'wave' | 'tile' | 'frame''tile'The picture of each row: wave draws a waveform from the title, tile a Motif drawing on a colour block, frame a 16:10 video frame with a play mark and the running time.
eyebrowstringnoneA short line above the heading. Slot "eyebrow" wins.
headlinestringnoneThe heading. Slot "headline" wins and may hold an <em>, set in the emphasis face and colour.
copystringnoneA line under the heading.
more{ href: string; label: string }noneA link beside the heading, e.g. to the whole archive.
detailbooleanfalseShow each piece's summary under its title.
playLabelstring'Play {title}'The play button's accessible name; {title} is the piece's title.
level1 | 2 | 32Heading level of the section's heading; the pieces' titles are one below.
tone'surface' | 'muted' | 'ink' | 'brand'unset (paints nothing)The section's ground; the picture tiles keep their blocks on every tone.
density'compact' | 'cozy' | 'roomy''cozy'Block padding ×0.8 / ×1 / ×1.25 through --ma-density.

Theming and overrides

The section reads the theme contract (shadcn's variable names with the base tokens as fallback), so a pasted theme or one --ma-brand recolours it. Its class names are the override surface and stay stable; a named slot replaces the matching prop with your own markup.

Classes

.ma-pieces.ma-pieces__body.ma-pieces__copy.ma-pieces__eyebrow.ma-pieces__head.ma-pieces__headline.ma-pieces__icon-pause.ma-pieces__icon-play.ma-pieces__link.ma-pieces__list.ma-pieces__mark.ma-pieces__media.ma-pieces__media--frame.ma-pieces__media--tile.ma-pieces__media--wave.ma-pieces__meta.ma-pieces__more.ma-pieces__number.ma-pieces__play.ma-pieces__row.ma-pieces__summary.ma-pieces__time.ma-pieces__title.ma-pieces__titles.ma-wave

Slots

slot="eyebrow"slot="headline"

Reduced motion

The waveforms and drawings stay at rest on hover and while playing. A row changes its background and lifts, and a frame's play mark swells, at once, without a transition. Playing and the played share still work.

With ClientRouter

The script binds the play buttons once per swap through the runtime's onMount; the audio stops when the page is swapped away. The rest is CSS.

Why it is built this way

Replaces: Podcast site episode lists (Transistor, Buzzsprout) · Newsletter archive pages (Substack, Ghost themes). See the migration table.

Motif

Backgrounds

A drawn picture for a slot that has no photograph yet: a sun over the sea, a split gate, a flower, a grid of tiles, a phone going live, a plate set for dinner, waves under a moon or three shapes.

every browserhover
Ambient loopthe same element rides into the next page

Press play, then navigate: position, volume and playback carry over.

Persist Player

Page transitions

An audio player that keeps playing while you navigate: the same <audio> element is carried into the next page by Astro's router, so position, volume and playback never restart.

every browserclick

Writing as a list: the day, the title at display size and the reading time on one line per post, the whole line a link, and the post's cover floating by the pointer while the line is hovered.

every browserhoverpointer

The first screen of one person's work, as a bento.

newly · Chrome 105 · Firefox 128 · Safari 17pointerscrollalways

Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown