// Presentations (ADR 0027): SlideFrame, the slide layouts and Deck. // // What is pinned here is the markup contract jsdom can see: the frame is a // named "slide" group carrying `data-slide-frame` (the attribute the Theme's // print rules key on), it scales a fixed canvas with `zoom` rather than a // transform, it pins the layout and section tiers to the neutral stop's Brand // steps without selecting an axis, and every layout takes its type from the // Theme's roles. Geometry — the 16:9 ratio, scaling, overflow — is measured in // a real browser by the showcase (`slide-frame.browser.test.ts`). import { readFileSync } from "node:fs"; import { join } from "node:path"; import { createRawSnippet, type Component } from "svelte"; import { describe, expect, it } from "vitest"; import { BulletsSlide, Deck, FigureSlide, QuoteSlide, SLIDE_SURFACES, SectionSlide, SlideFrame, TitleSlide, TwoColumnSlide, slideFrame, } from "@reddb-io/design-system/base"; import { classes, classesOf, render, rendered } from "./mount"; const words = (content: string) => createRawSnippet(() => ({ render: () => `${content}` })); const DENSITY_DIR = join(import.meta.dirname, "..", "..", "..", "packages", "tokens", "dist"); function expectNoAxis(target: HTMLElement) { for (const attribute of ["data-theme", "data-color-scheme", "data-density"]) { expect(target.querySelector(`[${attribute}]`), attribute).toBeNull(); } } /** Every layout, with the props it needs to render. */ const LAYOUTS: readonly { name: string; component: Component; props: Record; titleRole?: string }[] = [ { name: "TitleSlide", component: TitleSlide as Component, props: { title: "One system", eyebrow: "reddb.io", subtitle: "Every medium", presenter: "The DS team" }, titleRole: "display", }, { name: "SectionSlide", component: SectionSlide as Component, props: { title: "Part two", summary: "Media" }, titleRole: "display" }, { name: "TwoColumnSlide", component: TwoColumnSlide as Component, props: { title: "Before and after", start: words("before"), end: words("after") }, titleRole: "title", }, { name: "FigureSlide", component: FigureSlide as Component, props: { title: "Readiness by medium", figure: words("chart"), caption: "Audit, 2026-10-05" }, titleRole: "title", }, { name: "QuoteSlide", component: QuoteSlide as Component, props: { quote: "Tokens only.", attribution: "AGENTS.md" } }, { name: "BulletsSlide", component: BulletsSlide as Component, props: { title: "Non-negotiables", items: ["Tokens only", "Density owns spatial values"] }, titleRole: "title", }, ]; describe("the Base SlideFrame", () => { it("is a named slide group carrying the print hook", () => { const frame = rendered(render(SlideFrame, { label: "Opening", children: words("Hello") })); expect(frame.getAttribute("role")).toBe("group"); expect(frame.getAttribute("aria-roledescription")).toBe("slide"); expect(frame.getAttribute("aria-label")).toBe("Opening"); expect(frame.hasAttribute("data-slide-frame")).toBe(true); expect(frame.querySelector("[data-slide-safe-area]")?.textContent).toBe("Hello"); }); it("is a 16:9 size container that zooms a fixed canvas instead of transforming it", () => { const frame = rendered(render(SlideFrame, { label: "Canvas" })); const stage = frame.querySelector("[data-slide-stage]")!; for (const name of ["@container/slide-frame", "aspect-video", "w-full", "overflow-hidden", "break-inside-avoid"]) { expect(classes(frame).has(name), name).toBe(true); } for (const name of ["w-[60rem]", "aspect-video", "[zoom:tan(atan2(100cqi,60rem))]", "@container/slide"]) { expect(classes(stage).has(name), name).toBe(true); } const all = [...classes(frame), ...classes(stage)]; expect(all.filter((name) => /^(?:scale-|transform|\[transform)/.test(name))).toEqual([]); }); it("pins the layout and section tiers to the comfortable stop's Brand steps", () => { const comfortable = readFileSync(join(DENSITY_DIR, "density-comfortable.css"), "utf8"); const stage = rendered(render(SlideFrame, { label: "Rhythm" })).querySelector("[data-slide-stage]")!; const pinned = [...classes(stage)] .map((name) => /^\[(--reddb-spatial-[a-z-]+):(.+)\]$/.exec(name)) .filter((match): match is RegExpExecArray => match !== null); expect(pinned.map((match) => match[1]).sort()).toEqual([ "--reddb-spatial-layout-gap-lg", "--reddb-spatial-layout-gap-md", "--reddb-spatial-layout-gap-sm", "--reddb-spatial-section-gap", "--reddb-spatial-section-inset", ]); for (const [, property, value] of pinned) { expect(comfortable, property).toContain(`${property}: ${value};`); } }); it("shows its position in the deck and announces it in words", () => { const frame = rendered(render(SlideFrame, { label: "Third", number: 3, total: 8 })); const number = frame.querySelector("[data-slide-number-label]")!; expect(frame.dataset.slideNumber).toBe("3"); expect(frame.dataset.slideTotal).toBe("8"); expect(number.querySelector(".sr-only")?.textContent).toBe("Slide 3 of 8"); expect(number.querySelector('[aria-hidden="true"]')?.textContent).toBe("3 / 8"); expect(rendered(render(SlideFrame, { label: "Unnumbered" })).querySelector("[data-slide-number-label]")).toBeNull(); }); it("sets a slide on the background or the raised ground without selecting an axis", () => { expect([...SLIDE_SURFACES]).toEqual(["background", "raised"]); for (const surface of SLIDE_SURFACES) { const target = render(SlideFrame, { label: surface, surface }); const frame = rendered(target); expect(frame.dataset.slideSurface).toBe(surface); expect(classes(frame)).toEqual(classesOf(slideFrame({ surface }).root())); expectNoAxis(target); } expect(classes(rendered(render(SlideFrame, { label: "Default" }))).has("bg-background")).toBe(true); }); }); describe("the Base slide layouts", () => { for (const { name, component, props, titleRole } of LAYOUTS) { it(`${name} is a SlideFrame whose type comes only from the Theme's roles`, () => { const target = render(component, { ...props, number: 2, total: 6 } as never); const frame = rendered(target); expect(frame.hasAttribute("data-slide-frame")).toBe(true); expect(frame.getAttribute("aria-roledescription")).toBe("slide"); expect(frame.getAttribute("aria-label")).toBeTruthy(); expect(frame.dataset.slideNumber).toBe("2"); expectNoAxis(target); for (const element of [frame, ...frame.querySelectorAll("*")]) { const raw = [...classes(element)].filter((cls) => /^(?:text-(?:xs|sm|base|lg|\d*xl|display-(?:sm|md|lg)|\[.*\])|font-(?:semibold|light|black|\[.*\])|tracking-.+|leading-.+)$/.test(cls), ); expect(raw, `${name}: ${element.tagName}`).toEqual([]); } if (titleRole) { const heading = frame.querySelector("[data-heading]") as HTMLElement; expect(heading.tagName).toBe("H2"); expect(heading.dataset.typeRole).toBe(titleRole); expect(frame.getAttribute("aria-label")).toBe(props.title); } }); } it("lets a caller name the slide and choose the title's outline level", () => { const frame = rendered(render(TitleSlide, { title: "Deck", level: 1, label: "Opening slide" })); expect(frame.getAttribute("aria-label")).toBe("Opening slide"); expect(frame.querySelector("h1")?.textContent).toBe("Deck"); }); it("places TwoColumnSlide's columns in reading order", () => { const frame = rendered(render(TwoColumnSlide, { title: "Columns", start: words("first"), end: words("second") })); expect([...frame.querySelectorAll("[data-slide-column]")].map((column) => [column.dataset.slideColumn, column.textContent])).toEqual([ ["start", "first"], ["end", "second"], ]); }); it("gives FigureSlide's figure the remaining height and a native caption", () => { const frame = rendered(render(FigureSlide, { title: "Figure", figure: words("chart"), caption: "Source: audit" })); const figure = frame.querySelector("figure[data-slide-figure]")!; expect(figure.querySelector("[data-slide-figure-body]")?.textContent).toBe("chart"); expect(classes(figure.querySelector("[data-slide-figure-body]")!).has("flex-1")).toBe(true); expect(figure.querySelector("figcaption")?.textContent).toBe("Source: audit"); }); it("sets QuoteSlide as a blockquote with its attribution, in the title role", () => { const frame = rendered(render(QuoteSlide, { quote: "Tokens only.", attribution: "AGENTS.md", source: "Non-negotiables" })); expect(frame.getAttribute("aria-label")).toBe("Quote from AGENTS.md"); const quote = frame.querySelector("blockquote p")!; expect(quote.textContent).toBe("“Tokens only.”"); expect(classes(quote).has("text-title")).toBe(true); expect(frame.querySelector("figcaption")?.textContent?.replace(/\s+/g, " ").trim()).toBe("AGENTS.md, Non-negotiables"); }); it("lists BulletsSlide's points natively, in the heading role", () => { const frame = rendered(render(BulletsSlide, { title: "Points", items: ["One", "Two", "Three"] })); const list = frame.querySelector("ul[data-slide-points]")!; expect([...list.querySelectorAll("li")].map((item) => item.textContent)).toEqual(["One", "Two", "Three"]); expect(classes(list).has("text-heading")).toBe(true); }); }); describe("the Base Deck", () => { it("is a named region stacking its slides with a Density-owned layout gap", () => { const deck = rendered(render(Deck, { label: "About the DS", children: words("slides") })); expect(deck.tagName).toBe("SECTION"); expect(deck.getAttribute("aria-label")).toBe("About the DS"); expect(deck.hasAttribute("data-deck")).toBe(true); expect(classes(deck).has("gap-[var(--reddb-spatial-layout-gap-lg)]")).toBe(true); }); });