import { type VariantProps } from 'class-variance-authority'; import * as React from 'react'; import { type RenderProp } from '../../internal/render-seam'; /** * A setup-page scenario card: an image, a title, a description and an arrow affordance. * * Ported from two Figma components in the Multi product demo file (`85sD2HI8wksP9Y7uDmJ9DK`), which * GTM-870 names as the design authority for this work: * * - `Scenario card` 816:14901 → `variant="detail"`, whose image is a fixed header band and whose * title and description are always visible. Figma's `Device=Desktop|Mobile` axis is `orientation`. * - `Scenario card opt2` 672:8354 → `variant="reveal"`, which is image-led: the image occupies the * card at rest and shrinks to let the title in. * * ONE COMPONENT, TWO VARIANTS, NAMED FOR BEHAVIOUR rather than for which Figma node they came from — * Decision 2 of docs/superpowers/plans/2026-07-31-0.1.2-gtm-990-plan.md. Both share `radius: 10`, the * same 12px root gap and the same arrow affordance (`radius: 10`, `paddingHorizontal: 16`, `gap: 8`, * 24px right-arrow icon), and they differ only in whether the title is always shown. * * THIS IS A NAVIGATION CARD, NOT A STATIC ONE. It has a hover state and an arrow, so it is a control * and it renders as one. Neither existing component fits: `Card` has no image, no hover and no * interaction, and `SelectableTile` is a toggle that reports `aria-pressed`, which is the wrong * semantics for navigation — it would announce a selection state that never changes. * * `Card image` ({CREDIT_DEBIT_CARD_NUMBER}) has 18 variants over `Image=Hotel|Order|Car|Insurance|Customer|Loan` x * `State=Default|RCS|Mobile`. That is an ASSET SET, not a component, so the image arrives as a * ReactNode slot and its intrinsic height belongs to the asset rather than to this component. */ declare const scenarioCardStyles: (props?: ({ variant?: "detail" | "reveal" | null | undefined; orientation?: "horizontal" | "vertical" | "responsive" | null | undefined; } & import("class-variance-authority/types").ClassProp) | undefined) => string; /** * The focus ring, in its own style function rather than in the root's list. * * Not a stylistic separation: `colorTokensIn` buckets `outline-*` with `border-*`, so leaving it in * the root would put two boundary tokens in one list and force the contrast test to tell them apart * by NAME. That is the split that made `InfoSection`'s first draft vacuous. Each boundary now comes * from its own function and its role is known from which function produced it. * * `--color-border-focus` is 6.98:1 light / 6.11:1 dark against the page and 6.39:1 / 4.63:1 against * the card's own fill, so the ring is visible on either side of the card's edge. */ declare const scenarioCardFocusStyles: (props?: import("class-variance-authority/types").ClassProp | undefined) => string; /** The image slot's frame. No colour of its own: the asset paints itself. */ declare const scenarioCardImageStyles: (props?: ({ variant?: "detail" | "reveal" | null | undefined; } & import("class-variance-authority/types").ClassProp) | undefined) => string; /** * The content column/row. Figma's `Content` gap is 8 in vertical cards; its 12px horizontal padding comes * from the nested `Container` and `Arrow container`, which share the same value, so it is stated once * here instead of twice. * * In horizontal orientation, `Content` lays out as a row: title and description on the left, arrow at the * trailing edge, vertically centred. */ declare const scenarioCardContentStyles: (props?: ({ orientation?: "horizontal" | "vertical" | "responsive" | null | undefined; } & import("class-variance-authority/types").ClassProp) | undefined) => string; /** Title over description, Figma's `Container`, `gap: 4`. */ declare const scenarioCardContainerStyles: (props?: import("class-variance-authority/types").ClassProp | undefined) => string; /** * The reveal block: the wrapper `variant="reveal"` puts the title inside. * * THE TITLE IS NEVER ONLY IN A HOVER STATE. Figma hides it in `Property 1=Default` and shows it in * `Property 1=hover`, and a hover-only reveal is invisible to the keyboard, unreliable on touch and * — if implemented with `hidden` or a conditional render — absent from the accessibility tree, which * would leave the card's only specific label unreachable. Four things together prevent that: * * 1. the title is always rendered. The collapse is `max-h-0` plus `opacity-0`, which keep the node * in the DOM and in the accessibility tree, so it is always part of the control's accessible * name. Never `hidden`, never `display: none`, never a conditional render. * 2. focus reveals it as well as hover — `group-focus-visible:` for a keyboard and * `group-focus-within:` for anything that moves focus into the card, including a tap. * 3. the description and the arrow stay visible at rest, exactly as Figma's `Property 1=Default` * draws them, so the collapsed card already says what it is for. * 4. `revealed` pins it open, for a touch device with no hover at all. * * Every trigger is a literal class. A name assembled at runtime generates no CSS at all, because * Tailwind extracts class names by scanning source text — so a helper here would leave the block * collapsed for everyone while looking correct in the rendered `className`. */ declare const scenarioCardRevealStyles: (props?: ({ revealed?: boolean | null | undefined; } & import("class-variance-authority/types").ClassProp) | undefined) => string; /** * 16/24 Semibold, Figma's `font/size/small` over `font/line-height/small` at `font/weight/Semibold`. * * The weight comes from the variable NAME: Semibold → 600. Figma reports 375, which is Whitney SSm's * embedded value and provenance only — `font-weight: 375` is not Semibold to a browser. * * Figma inks it `color/brand/ink` #000d25 on the white card: **19.37:1**, and the best ratio any title * in the file gets. It was `--color-text-primary`, refused because #000d25 "is 1.00:1 as soon as the * page is light" — a light page rule 3 removed. * * The type utilities are written out here rather than reached through `` for the * same reason `ToolCall` does it: the title's element is chosen by `titleAs`, so it is sometimes a * heading and sometimes a span, and `Typography` would add a second box to say one thing. Every * utility is a literal. */ declare const scenarioCardTitleStyles: (props?: ({ orientation?: "horizontal" | "vertical" | "responsive" | null | undefined; } & import("class-variance-authority/types").ClassProp) | undefined) => string; /** * 12/18 Book, Figma's `font/size/2x-small` over `font/line-height/2x-small` at `font/weight/Book` * (→ 400). * * The `--text-size-figma-12` / `--leading-height-figma-12` pair rather than the `body-xs` role: that * role's leading is 16px, and Figma's description layer is exactly 18px tall — one line at 18. * `Badge`'s `small` size and `ToolCall`'s pill use the same pair for the same reason. * * Figma inks it `color/gray/gray-700` #38425e, which on the white card is **9.96:1**. The refusal * measured it against the wrong surface: "1.79:1 against Strix's dark surface" is what it would be on * the SUBSTITUTED dark fill, and the card is white. Name the surface, always — a token has no * contrast; a token on a named surface does. */ declare const scenarioCardDescriptionStyles: (props?: import("class-variance-authority/types").ClassProp | undefined) => string; /** * The arrow affordance, at rest. `radius: 10`, `paddingHorizontal: 16`, `paddingVertical: 8`, * `gap: 8` and a 24px icon (`size="lg"`), identical across both Figma cards. * * Figma's resting fill is `none` and its glyph is `color/brand/ink`, so no surface is painted here. * The glyph is `--color-icon-on-card-inverted`, Figma's own literal at **19.37:1** on the white card * against the 3:1 WCAG 1.4.11 asks of a non-text glyph. It was `--color-icon-primary`, which was the * right idea for a dark card and is white — invisible on this one. */ declare const scenarioCardArrowStyles: (props?: import("class-variance-authority/types").ClassProp | undefined) => string; /** * The arrow's hovered and focused treatment, kept in its own style function so its glyph is measured * against the fill it is actually seen on rather than against the card's. * * FIGMA'S COLOURS, AND THE RATIO IN THE REFUSAL WAS WRONG. Figma fills the hovered arrow * `color/brand/accesible-red-dark-theme` #ef223a under a `color/brand/white` glyph. This was declined * as "**3.30:1**, which passes as a glyph and would fail as text", and substituted with * `--color-surface-accent-extra-strong` — a BLUE. Recomputed: white on #ef223a is **4.23:1**. So the * number that justified the substitution was not the number, and 1.4.3's 4.5:1 is not the floor for a * glyph in any case. * * This is the failure mode CLAUDE.md names: an invented measurement becoming cited evidence. Recompute * before citing. The glyph needs no new role — `--color-icon-inverse` is already #ffffff, Figma's * value exactly. * * Focus is stated alongside hover: a keyboard user gets the same affordance, and this is also the * pair that reveals the title in `variant="reveal"`. */ declare const scenarioCardArrowActiveStyles: (props?: import("class-variance-authority/types").ClassProp | undefined) => string; export { scenarioCardStyles, scenarioCardFocusStyles, scenarioCardImageStyles, scenarioCardContentStyles, scenarioCardContainerStyles, scenarioCardRevealStyles, scenarioCardTitleStyles, scenarioCardDescriptionStyles, scenarioCardArrowStyles, scenarioCardArrowActiveStyles, }; /** Which Figma card, named for what it does. */ export type ScenarioCardVariant = NonNullable['variant']>; /** * Figma's `Device` axis on `Scenario card`, named for the layout rather than the device: it is a * flex-direction switch and touches no colour. `Scenario card opt2` has no horizontal form. */ export type ScenarioCardOrientation = NonNullable['orientation']>; /** What the `render` function form is told about the card. */ export interface ScenarioCardRenderState { variant: ScenarioCardProps['variant']; orientation: ScenarioCardProps['orientation']; } export interface ScenarioCardProps extends Omit, 'title'> { /** The scenario's name. Always in the accessible name, in both variants. */ title: React.ReactNode; /** * Render the title as a real heading. Omitted by default, because guessing a level produces a * wrong document outline that no test in the consumer's app will catch — the same reasoning as * `InfoSection` and `CollapsiblePanel`. */ titleAs?: 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; /** The supporting line. Omit it and no element is rendered, rather than an empty 18px line. */ description?: React.ReactNode; /** * The card image. A slot, because Figma's `Card image` is an 18-variant asset set rather than a * component; its intrinsic height is the asset's. */ image?: React.ReactNode; /** * `detail` shows the title always; `reveal` is image-led and expands the title on hover or focus. * @default 'detail' */ variant?: ScenarioCardVariant; /** * @default 'vertical' */ orientation?: ScenarioCardOrientation; /** * Pin `variant="reveal"` open. The escape hatch for a touch device, which has no hover at all. * Ignored by `detail`, whose title is never collapsed. * @default false */ revealed?: boolean; /** * Render as a different element, keeping the styling and behaviour. This is Base UI's `render` * seam and the house convention — never `asChild` or `as`. Takes an element or a function: * `render={}` or `render={(props) => }`. * * `jsx-a11y/anchor-has-content` fires on an `` element literal with no children, which is the * shape the element form requires; see docs/CONSUMING.md. The function form sidesteps that. */ render?: RenderProp; } /** * @example } /> * @example } /> * @example */ export declare const ScenarioCard: React.ForwardRefExoticComponent>; //# sourceMappingURL=scenario-card.d.ts.map