import { EmblaCarouselType } from "embla-carousel"; /** * Framework-agnostic Embla controller shared by the React carousel and the AEM * clientlib runtime. Keep behaviour here so Storybook and server-rendered HTL * use exactly the same navigation and selected-state logic. * * The carousel drives TWO Embla instances from a single index: * * media - the peek / three-up track. Owns drag, arrows and dots. * content - a 100%-wide crossfade track holding one text block per item. * Index-driven only; never a source of truth. * * Both tracks are generated from the same authored item list with no filtering, * so index i always means the same item in both. See `syncSelectedState`. */ export interface LsgCarouselControllerElements { prevBtn?: HTMLElement | null; nextBtn?: HTMLElement | null; paginationBtns?: Iterable | null; /** * Root of the crossfade text track. Omit for a media-only carousel; the * controller then behaves exactly as it did before the text track existed. */ contentViewport?: HTMLElement | null; } export type LsgCarouselDirection = "ltr" | "rtl"; /** * `center` is the full-width layout's focus mode: the selected slide sits in the * middle with a neighbour peeking in from each edge. It also enables * click-to-centre on the neighbours. Every other layout anchors at `start`. */ export type LsgCarouselAlign = "start" | "center"; export declare const alignForLayout: (layout: string | undefined) => LsgCarouselAlign; export interface LsgCarouselControllerOptions { loop?: boolean; /** Defaults to `start`. See `LsgCarouselAlign`. */ align?: LsgCarouselAlign; /** * Embla content direction. Media-left variants use RTL so the active media is * anchored on the right edge of the viewport and the adjacent slide peeks in * from the left, matching the mirrored Figma compositions. * * Embla requires the viewport/track CSS direction to match this value. The * React component sets `dir` on the viewport and the AEM boot path mirrors it. * * Only the media track is mirrored. The text track crossfades in place, so it * has no spatial direction and always stays LTR. */ direction?: LsgCarouselDirection; } export declare class LsgCarouselController { private media; private content; private syncing; private readonly mediaViewport; private readonly mediaSlides; private readonly contentSlides; private readonly els; private readonly paginationBtns; private readonly paginationHandlers; private readonly slideClickHandlers; private readonly root; private readonly mediaColumn; /** Last head element per text slide, index-aligned; null for a headless slide. */ private readonly headEnds; private headObserver; constructor(viewport: HTMLElement, elements?: LsgCarouselControllerElements, options?: LsgCarouselControllerOptions); /** * Embla automatically falls back to non-looping mode when there is not enough * slide content to create a safe loop (most noticeable with two-up layouts and * only 2–3 authored items). Keep the arrow contract cyclic in that fallback: * when there is no native previous snap, jump to the final authored snap. */ readonly scrollPrev: () => void; /** See `scrollPrev` for why the explicit wrap fallback is required. */ readonly scrollNext: () => void; /** * Drive the text track from the media index. * * `jump` skips the crossfade (Embla sets duration 0, and the fade plugin then * snaps opacity instantly). Use it only for re-measure events, never for * reader-initiated navigation, or the text hard-cuts instead of fading. */ private pushToContent; /** * The text track never initiates navigation, but it owns its own resize and * mutation observers. If one instance re-measures without the other, this pulls * the media track back onto the text track's index rather than letting them drift. */ private readonly handleContentSelect; /** Reader-initiated navigation: repaint state, then crossfade the text. */ private readonly handleMediaSelect; /** * Re-measure events (breakpoint flip, resize, authored items added/removed). * The text is realigned with a hard cut because nothing moved for the reader. */ private readonly handleResync; /** Repaints dot/arrow/a11y state from the media index. Never navigates. */ private readonly syncSelectedState; /** * Publishes the geometry a theme needs to place the media column between each * slide's head (pre-title + heading) and its body: * * root --lsg-carousel-head-max tallest head across all slides * --lsg-carousel-media-block media column height (cards + dots) * slide --lsg-carousel-head-height this slide's own head height * * Every slide pads its head up to the tallest one, so the media never moves * as the text crossfades between headings of different lengths. Themes that * do not opt in ignore these. MEASURED_ATTR is only set when at least one * slide has a head, so a carousel without titles keeps the plain stack. */ private readonly measureSplit; get emblaApi(): EmblaCarouselType | null; /** Text track handle. Exposed for the index-parity checks in the test plan. */ get contentApi(): EmblaCarouselType | null; destroy(): void; }