← All changelog entries
August 17, 2026 · Components

Image carousels become a first-class LFM directive — four variants, and a timestamp convention that turns out to be a run stamp

Screenshots of a setup flow are a sequence, and we had no way to say so in markdown. `:::image-carousel` now does, in four variants split as ImageCarousel--{Variant} siblings. Two findings fell out of building it: the image-prep timestamp records when a file was processed rather than when the screen was captured, and our prose reset out-specifies any component that renders a list inside it.

Authors
Michael Staton
Augmented with
Claude Code on Opus 5
Tags
#LFM#Design-System#Markdown-Rendering#Image-Carousel#Recipes#Naming-Conventions#CSS-Specificity

Image carousels become a first-class LFM directive

Why Care?

Four screenshots of a setup flow are not four images — they’re one sequence, and the order carries the meaning. Markdown had no way to say that. You could stack four ::image directives and hope the reader inferred the arrow between them.

:::image-carousel says it explicitly. The immediate consumer is the agent-native-browsers Recipe, where Aside’s four-screen onboarding now reads as the walkthrough it is instead of four unrelated figures interrupting the prose.

What’s New?

  • :::image-carousel / :::img-carousel container directive, wired into AstroMarkdown’s dispatch. img-carousel is a syntactic alias that collapses to the same component, per LFM’s polyglot rule.
  • Four variants as -- modifier siblingsImageCarousel--Filmstrip, --Stepper, --Peek, --ContactSheet — with ImageCarousel.astro as a block-level router that holds no markup of its own.
  • A three-file split in the Callout mould: image-carousel-types.ts (registry + slide extraction), image-carousel.css (block + element styles), image-carousel.client.ts (progressive enhancement), so the authoring contract is defined once.
  • Demo at /design-system/markdown/image-carousel — all four variants rendering the same four images, plus an alias check proving img-carousel renders identically.
  • The Recipe now opens its field section with the peek carousel, replacing two standalone onboarding figures.

Every variant works with JavaScript off

scroll-snap is the mechanism and the numbered dots are ordinary anchor links, so all four variants function unenhanced. The client script only adds the live “Step N of M” readout, active-slide state, and arrow-key navigation — and only --Stepper and --Peek import it. --Filmstrip and --ContactSheet ship no JavaScript at all.

The naming, corrected twice

Two conventions got applied the wrong way round on the first pass, both caught in review:

  1. The demo page was pluralised (image-carousels.astro). Design-system pages are kebab-case of the component name, singularalert-display, markdown-reader, project-gallery. The one plural in the directory is callouts.astro, and pattern-matching on the exception rather than the rule is how it happened.
  2. The variants were a variant prop on one component. They are now ImageCarousel--{Variant}.astro siblings, per the documented rule already sitting in the design-system index: “Naming: PersonCard--{variant}.astro … future variants live as siblings.”

The timestamp finding

The chronological-ordering default was supposed to lean on the ISO 8601 stamp the image-prep convention appends to every filename (Aside__Welcome-Screen_20260817T164659Z.jpg). Reading the script closes the case: const STAMP = isoStamp() runs once per invocation, so the stamp records when a batch was processed, not when each screen was captured. Every image in one run shares a stamp.

That is survivable, because the sort is stable and ties fall back to authored order — so capture-then-upload-in-one-batch behaves correctly. It breaks in exactly one case, which is the case we hit: an image belonging mid-sequence, uploaded later. The Aside recovery-key screen was re-uploaded after redaction and sorts to the end.

Verified against the real filenames:

authored        Welcome > Recovery key > The agent > Done
chronological   Welcome > The agent > Done > Recovery key    ← the failure
one batch       Welcome > Recovery key > The agent > Done

The real fix belongs upstream in the image-prep script — stamp per-image capture time from EXIF or mtime rather than per-run wall clock. Logged, not done.

The prose reset collides with any component that uses a list

First render inside the Recipe came out visibly wrong: the dot nav showed 1. 12. 23. 34. 4 — decimal list markers sitting outside the dot circles, colliding with each dot’s own number, every numeral underlined and primary-colored.

The instinct was that local development had linked a local build of @lossless-group/lfm and the two were fighting. It hadn’t: node_modules/@lossless-group/lfm resolves to .pnpm/@jsr+lossless-group__lfm@0.3.0, the published package, and none of this styling comes from LFM in any case.

It was ordinary CSS specificity, in our own stylesheet. The dot nav is an <ol> of <a>, which puts it squarely in the path of the prose reset:

docs-prose.css Specificity Symptom
.docs-prose ol { list-style: decimal; padding-left: 1.5rem } (0,1,1) Markers rendered, pushed outside the circles by the padding
.docs-prose li { margin-bottom: 0.4rem } (0,1,1) Vertical drift across the dot row
.docs-prose a { text-decoration: underline } (0,1,1) Underlined, primary-colored numerals

A bare .lfm-carousel__dots is (0,1,0) and loses to all three. Every dot rule is now scoped under the block for (0,2,0) — the class count is compared before the element count, so it wins regardless of source order, with no !important and without docs-prose.css needing to know this component exists.

The general lesson is the reusable part: any LFM component that renders a list or a link inside prose will hit this. Raising the component’s own specificity floor is the fix that scales; teaching the prose stylesheet about each new component is the one that doesn’t.

What’s Next?

  • Upstream the per-image capture stamp in the image-prep script.
  • Consume the LFM-side lfm-image-carousel plugin once it publishes, and drop the site-local extraction copy.