Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/slides-kit-crossfade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"sideshow": patch
---

Slides kit now grid-stacks its deck so it cross-fades in normal flow. Previously
it swapped slides with `display:none`, which can't fade — so decks were hand-rolled
with `position:absolute` slides over a `min-height` stage, an out-of-flow layout the
surface-page height bridge can't measure (the overlay grows `scrollHeight` but not
the box its ResizeObserver watches), leaving the frame clipped/frozen. The kit now
stacks slides in one grid cell (in flow, sized to the tallest slide) and fades with
opacity/visibility, so the frame follows it. DESIGN_GUIDE documents the out-of-flow
trap and the grid-stack recipe alongside the existing `position: fixed` ban.
18 changes: 14 additions & 4 deletions guide/DESIGN_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,17 @@ is yours.
height automatically.
- `<style>` and `<script>` tags are allowed. Scripts run inside a sandboxed
iframe with no access to the host page.
- **Never use `position: fixed`** — the iframe sizes to content height and
fixed elements break that. Use normal-flow layout.
- **Keep content in normal flow.** The frame measures your content's height from
the document box, so anything taken out of flow is invisible to the sizer and
can leave the surface clipped — or frozen at the wrong height after load.
- Never use `position: fixed`.
- Don't stack `position: absolute` layers over a fixed-`height`/`min-height`
box (the usual cross-fade-deck mistake): the overlay grows `scrollHeight` but
not the measured box, so the frame won't follow it.
- To **overlap** elements (e.g. a cross-fading slide deck), grid-stack them in
normal flow instead — `display: grid` on the container, `grid-area: 1 / 1` on
each child: they overlap, but the container still sizes to the tallest child.
(The `slides` kit does exactly this — reach for it before hand-rolling a deck.)

## Built-in kit — a head start, not a straitjacket

Expand Down Expand Up @@ -266,8 +275,9 @@ theme tokens, so kit output re-themes with the workspace.
and text (`.dim`/`.faint`/`.mono`/`.title`) helpers. Composes an issue/PR/CI
tree — nest a `.tree` inside a `.tree` to indent — or a status board, from
generic primitives.
- **`slides`** — author a `.deck` with `.slide` children; the kit shows one at a
time and injects prev/dots/counter/next controls. Arrow keys and PageUp/Down
- **`slides`** — author a `.deck` with `.slide` children; the kit cross-fades one
at a time (grid-stacked in normal flow, so the frame always sizes to the tallest
slide) and injects prev/dots/counter/next controls. Arrow keys and PageUp/Down
navigate.

```sh
Expand Down
20 changes: 16 additions & 4 deletions server/kits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,22 @@ const ISSUES_CSS = `
// slides: a stepped deck. Author `.deck` with `.slide` children; the JS shows
// one at a time and injects prev/dots/counter/next controls. Arrow keys and
// PageUp/Down navigate (plain keys only — the host owns the meta/alt combos).
//
// The slides are GRID-STACKED (every `.slide` in the same `1/1` cell), not
// swapped with display:none or overlaid with position:absolute. That keeps them
// in normal flow — so the deck (and the document) sizes to the TALLEST slide and
// the height the surface reports stays stable — while still letting them overlap
// so inactive slides can cross-fade under the active one. An absolute overlay
// would grow scrollHeight without growing the measured box, and the frame's
// ResizeObserver (which watches the box, not scrollHeight) would go blind to it
// and clip the deck; grid-stacking avoids that trap. Inactive slides stay laid
// out (so they keep sizing the track) but are visibility:hidden — out of the tab
// order and the a11y tree — until they become `.on`.
const SLIDES_CSS = `
.deck{display:block}
.deck>.slide{display:none}
.deck>.slide.on{display:block;min-height:140px}
.deck{display:grid;min-height:140px}
.deck>.slide{grid-area:1/1;opacity:0;visibility:hidden;pointer-events:none;transition:opacity .3s ease,visibility 0s linear .3s}
.deck>.slide.on{opacity:1;visibility:visible;pointer-events:auto;transition:opacity .3s ease}
@media (prefers-reduced-motion:reduce){.deck>.slide{transition:none}}
.deck>.slide h2{font:500 22px/1.3 var(--font-sans);margin:0 0 14px}
.deck-ctl{display:flex;align-items:center;justify-content:center;gap:14px;margin-top:18px;padding-top:14px;border-top:1px solid var(--color-border-secondary)}
.deck-dots{display:inline-flex;gap:7px}
Expand Down Expand Up @@ -110,7 +122,7 @@ export const KITS: Kit[] = [
{
id: "slides",
label: "Slides",
summary: "a stepped deck with prev/next controls and a counter",
summary: "a stepped, cross-fading deck with prev/next controls and a counter",
classes: "deck · slide (+ injected controls)",
css: SLIDES_CSS,
js: SLIDES_JS,
Expand Down
11 changes: 11 additions & 0 deletions test/kits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ test("only behavior kits ship js", () => {
assert.equal(kitAssets(["issues"]).js, "");
});

test("slides kit grid-stacks in normal flow (measurable height), never an absolute overlay", () => {
// A cross-fade deck must overlap its slides IN FLOW (grid-stack) so the deck
// sizes to the tallest slide and the frame's box-watching ResizeObserver can
// see it. An absolute overlay grows scrollHeight without growing the box, so
// the frame goes blind and clips the deck — the exact regression this guards.
const { css } = kitAssets(["slides"]);
assert.match(css, /\.deck\{[^}]*display:grid/); // container grid-stacks
assert.match(css, /\.deck>\.slide\{[^}]*grid-area:1\/1/); // children share one cell
assert.doesNotMatch(css, /\.deck>\.slide[^}]*position:absolute/); // never out of flow
});

test("isKnownKit gates on the registry", () => {
assert.ok(isKnownKit("issues"));
assert.ok(!isKnownKit("issue"));
Expand Down
Loading