/** * `` — the landing page's row of category tiles. * * The icon references (`carousel_icon` / `catalog_icon`) are OPAQUE STRINGS * that the backend deliberately does not resolve ("Decoupled from stapel-cdn: * an opaque string, resolved by the host if at all"). This skin therefore * renders no `` and builds no URL: it hands the reference to the host * through `renderIcon`, and draws nothing when the host does not supply one. A * guessed CDN path would be a broken image on every deployment that guessed * differently, and a broken image is worse than no image. */ import { cssVar, radii, spacing } from "@stapel/tokens-antd"; import type { CSSProperties, ReactElement, ReactNode } from "react"; import { Card, Flex, Skeleton, Typography } from "antd"; import { useT } from "@stapel/core"; import { renderCategoryLabel } from "../catalog/labels.js"; import { CategoryCarousel } from "../headless/CategoryCarousel.js"; import type { CarouselEntry } from "../headless/CategoryCarousel.js"; import { CATEGORIES_I18N_KEYS } from "../i18n/keys.js"; import { CategoryLink } from "./CategoryLink.js"; import type { LinkComponentProp } from "./CategoryLink.js"; import { CATEGORY_TILE_CLASS, CATEGORY_TILE_FLAT_CLASS, CATEGORY_TILE_STYLE_HREF, categoryTileCss, } from "./CategoryTileGrid.js"; import type { TileSurface } from "./CategoryTileGrid.js"; import { EmptyState, ErrorAlert, LoadList, SkinTheme, } from "@stapel/tokens-antd/skin"; import type { ThemeModeProp } from "./types.js"; export interface CategoryCarouselStripProps extends ThemeModeProp, LinkComponentProp { readonly basePath?: string; /** Turn an opaque icon reference into something renderable. Absent means no * icon is drawn — see this file's header. */ readonly renderIcon?: (reference: string, entry: CarouselEntry) => ReactNode; /** * Whether this strip and its tiles draw a SURFACE of their own. Default * `"flat"` — the same word, the same default and the same rule * `` takes, because a landing that draws both * must not be flat in one row and panelled in the next. * * - `"flat"` — no fill and no border, on the strip's own wrapper AND on * every tile in it: the row sits on the page, and a tile takes the * token fill only under the pointer or a keyboard focus, from the one * rule set `` publishes (`categoryTileCss`). Two * components, one hover, no second definition to drift; * - `"card"` — today's strip: an antd `Card` per entry inside a raised * wrapper. * * The flat default is the owner's ruling for every tile in the catalogue * (0.27.0) and its container (0.29.0): the fills carry no information and a * row of them outweighs the art inside them. */ readonly tileSurface?: TileSurface; } /** * The flat tile's own box — what the `Card` was giving it, minus the fill and * the border. * * `inline-flex` and the padding keep the row's rhythm and the pointer target * the card had, and the radius is the one `categoryTileCss`'s hover fill is * drawn at, so the highlight is the shape the tile already occupies. */ const FLAT_TILE: CSSProperties = { display: "inline-flex", alignItems: "center", gap: spacing[2], paddingBlock: spacing[2], paddingInline: spacing[3], borderRadius: radii.lg, color: cssVar("text"), }; export function CategoryCarouselStrip( props: CategoryCarouselStripProps ): ReactElement { const t = useT(); const surface: TileSurface = props.tileSurface ?? "flat"; const flat = surface === "flat"; return ( /* THE STRIP TAKES THE SAME SURFACE AS ITS TILES — and, flat, paints * nothing at all. `SkinTheme` defaults to `surface="raised"`, which puts * `colorBgContainer` behind the whole row: a panel under a row of tiles * that have just stopped being panels themselves. `bare` paints NOTHING, * including the text colour `raised` would have written, so the flat arm * states it from the token — `--stapel-text` resolves per theme at paint * time where `SkinTheme` freezes whichever side mounted first. Same gate, * same reasoning and the same two lines as ``. */ {/* The tiles' hover and focus fill — ``'s own rule set, not a second copy of it. Hoisted and deduped by the same `href`, so a landing drawing both a strip and a grid ships it once. */} {flat && ( )} {(bag) => ( {t(CATEGORIES_I18N_KEYS.carouselTitle)} } failed={(error) => ( )} empty={ } > {(entries) => ( {entries.map((entry) => { const link = ( {entry.icon !== null && props.renderIcon !== undefined ? props.renderIcon(entry.icon, entry) : null} {renderCategoryLabel(entry.label, t)} ); return flat ? ( link ) : ( {link} ); })} )} )} ); }