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.
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-carouselcontainer directive, wired intoAstroMarkdown’s dispatch.img-carouselis a syntactic alias that collapses to the same component, per LFM’s polyglot rule.- Four variants as
--modifier siblings —ImageCarousel--Filmstrip,--Stepper,--Peek,--ContactSheet— withImageCarousel.astroas 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 provingimg-carouselrenders 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:
- The demo page was pluralised (
image-carousels.astro). Design-system pages are kebab-case of the component name, singular —alert-display,markdown-reader,project-gallery. The one plural in the directory iscallouts.astro, and pattern-matching on the exception rather than the rule is how it happened. - The variants were a
variantprop on one component. They are nowImageCarousel--{Variant}.astrosiblings, 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-carouselplugin once it publishes, and drop the site-local extraction copy.