import type { ReactElement, ReactNode } from "react"; import type { Category } from "../api/types.js"; import type { BlockRhythm } from "./blockRhythm.js"; import type { CarouselEntry, VirtualChildHref } from "../headless/CategoryCarousel.js"; import type { CategoryCrumbInput } from "../headless/CategoryBreadcrumbs.js"; import type { CategoryIconResolver, TileDensity, TileLayout, TileOverflow, TileSize, TileSurface } from "./CategoryTileGrid.js"; import type { LinkComponentProp } from "./CategoryLink.js"; import type { ThemeModeProp } from "./types.js"; /** * How wide a catalogue screen may get, by default. Without one, a 1280px * window put the "2 subcategories" chip ~2,300px from the label it counts and * left more than 90% of the page empty. A host that wants a different measure * passes {@link CategoryPageProps.measure} rather than overriding this value's * `max-width` from outside with `!important`. */ export declare const CATEGORY_MEASURE = "64rem"; /** What a heading callback is told about the page it is titling. */ export interface CategoryHeadingContext { /** The resolved category — the row the page drew its own title from. */ readonly category: Category; /** * How many SUB-CATEGORIES this page has in hand, which is the only count * this pair owns. A results count belongs to the listings pair and is * already in the host's own state, which is the reason this slot takes a * node at all. */ readonly count?: number; } /** * The page's title, host-supplied. * * A node, or a function of {@link CategoryHeadingContext} for a host that * wants the category and the page's own count without resolving the slug a * second time. Either way it replaces the CONTENT of the page's heading and * not the heading itself: one `` in the document, in the same place, with * the host's sentence inside it — a storefront that needs "Buy a car in Sochi * · 54 364" gets it without drawing a second heading above the page's and * leaving two in the outline. */ export type CategoryHeading = ReactNode | ((ctx: CategoryHeadingContext) => ReactNode); export interface CategoryPageProps extends ThemeModeProp, LinkComponentProp { /** * Replace the page's heading text — see {@link CategoryHeading}. Omitted, * the page renders the category's own translated name, which is what it has * always rendered. */ readonly heading?: CategoryHeading; /** * WHICH HEADING LEVEL the page's title takes. Default `3`, what this page * has always rendered, so no existing host changes shape. * * The level is a fact about the DOCUMENT, not about this page: a page * mounted straight into a router owns the outline and its title is the * `

`; the same page mounted under a shell that already prints the * storefront's `

` must not print a second one, and a page dropped into * a section wants an `

`. Only the composing surface knows which of the * three it is, and until this prop existed a host that knew had no way to * say it — a storefront put its own heading above the page and left two * competing headings in the outline, which is the accessibility twin of the * two-lists defect {@link CategoryPageProps.subcategories} closed. * * Content is unchanged either way — this moves the tag, never the words; * {@link CategoryPageProps.heading} is the slot for those. */ readonly headingLevel?: 1 | 2 | 3; /** * The category, by id — the address that costs two small requests. Every * in-app navigation has it, because it drew the link that got here. */ readonly categoryId?: number | null; /** * The `:slug` segment of `/c/:slug` — the SEO address, resolved against the * synced catalogue because the server offers no slug lookup. Ignored when * `categoryId` is given. See this file's header. */ readonly slug?: string; readonly basePath?: string; /** The listings half of the screen — supplied by the container, because it * belongs to another pair. Unfilled, the gap NAMES itself in development * (see this file's header). */ readonly renderListings?: (category: Category) => ReactNode; /** Show the category's feature schema. Off by default: on a storefront the * schema is the facet panel's input, not a visitor-facing table. */ readonly showFeatures?: boolean; /** * Draw the trail above the title. Default `true` — what this page has always * rendered, so no existing host changes behaviour. * * NOT a taste knob, and the reason matters because the next reader will * otherwise assume it is one. Which chrome carries "where am I" is a * DEPLOYMENT'S navigation decision, and the two answers are both correct: * * - on a desktop the trail IS the catalogue's navigation — it is how a * visitor moves back up a tree that has no other affordance on screen; * - on a phone the reference design gives that job to the back arrow in * the app bar, above a landing that is search field and tiles. A crumb * row there repeats the back arrow in a second visual language and * spends a line of a fold that has four of them. * * `false` mounts NOTHING — the bar is absent from the document rather than * covered — which is the same rule the sub-category arms follow and the only * version of this a test can check. * * An object form mounts the bar (like `true`) and forwards `unlink` to * `` — a host that holds ancestry knowledge this * page does not (a wrapper the catalogue itself cannot detect from * `children_pks`, or `tn_children_pks` on an older server, alone) states it * here instead of overriding the bar's own default from outside. Supplied, * the host's predicate REPLACES the * bar's automatic transparent-wrapper check for every crumb; see * `CategoryBreadcrumbsBarProps.unlink`. */ readonly breadcrumbs?: boolean | { readonly unlink?: (crumb: CategoryCrumbInput) => boolean; }; /** * Which FORM the sub-categories take. Default `"pane"` — what this page has * always rendered, so no existing host changes behaviour. * * Exactly one arm is mounted; see this file's header for the defect that * made this a prop rather than a stylesheet. */ readonly subcategories?: SubcategoryForm; /** * Tile geometry for the `"tiles"` arm — ``, * passed through verbatim. Ignored by the `"pane"` arm, which has no tiles * to size. Default is the grid's own (`"cozy"`). */ readonly subcategoryTileDensity?: TileDensity; /** * How the `"tiles"` arm fills its container — ``, * passed through verbatim. Ignored by every other {@link SubcategoryForm}. * * Default unchanged: the grid's own `"scroll"`, so no existing host * changes shape. A desktop category landing with a handful of children * (5 tiles in a 1232px column, say) reads as an empty corner under * `"scroll"`'s phone-width scroller — `"wrap"` is the fix, the same tiles * stretched to fill the row like the reference design. */ readonly subcategoryLayout?: TileLayout; /** * The `"tiles"` arm's `minTileWidth` under `subcategoryLayout="wrap"` — * ``, passed through verbatim. Ignored by * `"scroll"` and by every other {@link SubcategoryForm}. */ readonly subcategoryMinTileWidth?: number; /** * The `"tiles"` arm's own anatomy — ``, passed * through verbatim. Ignored by every other {@link SubcategoryForm}. Default * `"regular"`, the grid's own default — a root's tiles keep their reference * anatomy. A category landing (below the home) is where `"compact"` * belongs — see `CategoryTileGrid.tsx`'s {@link TileSize}. */ readonly subcategoryTileSize?: TileSize; /** * How many lines a tile's LABEL may take in the `"tiles"` arm — * ``, passed through verbatim. Default: the * tile anatomy's own count, so no existing page changes shape. * * It travels with {@link CategoryPageProps.subcategoryTileSize} rather than * being implied by it: `"compact"` clamps at two lines because a * half-height row has no third, and a catalogue whose section names are * longer than the ones that anatomy was measured on needs the third line * without giving up the anatomy. */ readonly subcategoryLabelLines?: number; /** * The `"tiles"` arm's own row cap — ``, * passed through verbatim. Meaningful only together with * {@link CategoryPageProps.subcategoryOverflow} `"modal"`. */ readonly subcategoryMaxVisible?: number; /** * What the `"tiles"` arm does past * {@link CategoryPageProps.subcategoryMaxVisible} — * ``, passed through verbatim. Default `"none"`. */ readonly subcategoryOverflow?: TileOverflow; /** * WHAT URL A VIRTUAL CHILD'S `filter` BECOMES — see * {@link VirtualChildHref}. * * A category that expands a feature into its children * (`children_expand_by`, stapel-categories 0.22.0) has children with no row * and no slug: their address is a filter on THIS category, spelled the way * the storefront spells filters, which this page cannot know. Without it * such a child is dropped from the `"tiles"` arm and a development build * says so; every other kind of child is unaffected. */ readonly hrefForVirtual?: VirtualChildHref; /** * A narrowing made in the cascade below the tiles. * * The page does not own where that goes — the results are another pair's, * and only the container knows whether a narrowing belongs in the URL, in a * search state or in both. So it is reported, not applied. `null` is the * cascade being cleared back to this landing. */ readonly onNarrow?: (category: Category | null) => void; /** The narrowing the host currently holds, for a controlled cascade. */ readonly narrowValue?: number | null; /** * Turn a tile's opaque icon reference into something renderable — the same * contract `` and `` take, and * meaningful only on the `"tiles"` arm. Absent, a tile draws the category's * own initial; this page never builds a URL out of a reference. */ readonly renderIcon?: (reference: string, entry: CarouselEntry) => ReactNode; /** * Address a tile's opaque reference without projecting the rows — forwarded * verbatim to ``, and meaningful only on the `"tiles"` * arm. See `CategoryIconResolver`. */ readonly resolveIconSrc?: CategoryIconResolver; /** * How wide the page's own content column may get — the `max-width` this * file's header explains ({@link CATEGORY_MEASURE}'s own default). * * Without a prop a host that wanted a different measure had to override it * with `!important` from outside, against a value it could not read back — * this is that knob, taking anything CSS `max-width` takes (`"72rem"`, * `960`, `"100%"`). Default {@link CATEGORY_MEASURE}, so no existing host * changes shape. * * `"none"` is the answer for the OTHER case, and it is a real one rather * than a very large number: a page mounted inside a shell whose content * column already holds the measure every page of the site shares. A second, * lower cap there is not a narrower page, it is a second answer to a * question already answered — and the 64rem one wins, so the host's own * column silently stops applying. Removing this page's cap is what says * "the measure is not mine", and it is the same sentence `gutter={false}` * makes about the indent. */ readonly measure?: number | string; /** * WHERE the space between this page's blocks comes from. Default `"token"`. * * - `"token"` — one gap for every block, read from * `var(--stapel-block-gap)` (and `var(--stapel-block-gap-compact)` on a * coarse pointer or a narrow window), defaulting to the design system's * own spacing steps, with every direct block's outer margin reset so the * distance between two of them is ONE number a host can retune by * declaring the property once — see {@link blockRhythmCss}; * - `"legacy"` — the flat `spacing[4]` this page wrote inline, for a host * whose own layout was measured against it. * * The default is the NEW behaviour: 16px between a page's sections was the * value a vertical `` was given when the page was first assembled, not * a rhythm anybody chose, and it is what made the blocks read as one column * with no seams. `@stapel/search-react` declares the same two properties, so * a storefront tunes both halves of a category screen with one declaration. */ readonly blockRhythm?: BlockRhythm; /** * The SURFACE the subcategory tiles draw — forwarded to * {@link CategoryTileGridProps.tileSurface}. Only the `"tiles"` arm of * {@link subcategories} reads it; default `"flat"`, like the grid's own. */ readonly subcategoryTileSurface?: TileSurface; /** * The page's own side padding — its distance from whatever is to its left * and right. * * Default `true`, which is what this page has always drawn: `spacing[4]` on * all four sides, correct for a page mounted straight into a router with * nothing around it. * * `false` removes the INLINE half of it, for the far more common case — the * page mounted inside a shell whose `` already holds the * page gutter (`@stapel/shell-react`, `--stapel-page-gutter`). Two boxes * each adding their own padding is a page indented twice: the heading sits * further in than the header above it and the footer below it, which is the * three-left-edges defect the shared token role exists to end. The block * padding stays either way — vertical rhythm between a header and a page's * first line is this page's own business. * * A prop rather than a context read on purpose: whether there is a gutter * outside this component is a fact about the COMPOSITION, and the composing * surface is the only party that knows it. Defaulting to `true` keeps every * existing host exactly where it is. */ readonly gutter?: boolean; /** * Hold the page that is on the glass while the NEXT category's reads are in * flight, instead of swapping it for the skeleton. Default `true`. * * A partition press moves to a sibling: the heading changes, the rows * change, and everything between them — the trail, the sub-category chrome, * and whatever the host rendered into * {@link CategoryPageProps.renderListings} — is the same page. Redrawing * the boundary's loading arm there unmounts all of it; see * `useCategoryPageSource` for the defect (D454) and what a storefront had * to write to work around it. * * While a new frame is in flight the ready arm carries * `data-stapel-load-refreshing="true"` (``), so a host that * wants to dim or announce the wait can, without owning the state. * * The breadcrumb bar sits ABOVE that boundary and is held with it: it reads * the row the frame holds rather than the address, so the one line the * boundary does not cover cannot blank on its own — see * `CategoryBreadcrumbsBarProps.keepPrevious`. * * `false` restores the old behaviour exactly: every change of address goes * through `loading` first. For a host that draws its OWN held frame and * would otherwise hold one on top of another. * * A FAILED read is never held either way — the error arm and its retry * replace the page, because a dead category wearing the previous live one * is a dead link that looks alive. */ readonly keepPrevious?: boolean; } /** * How a category page offers what is inside it. * * `"pane"` a titled LIST of links — the default, and the only shape this * page had. Drawn from the level the id path already holds, or * by `` for a slug-only host; one shared row * markup either way (see this file's header). * `"tiles"` the reference design's tile grid within the catalogue's depth * cap (`catalog/tiles.ts`), and past it the CASCADE the cap hands * over to. One arm, two halves of one navigation model. * `"cascade"` the cascading child selectors at every depth, tiles never. For * a host that draws its own tiles above this page. * `"none"` neither. For a host that draws its own sub-category chrome and * wants the rest of this page. */ export type SubcategoryForm = "pane" | "tiles" | "cascade" | "none"; export declare function CategoryPage(props: CategoryPageProps): ReactElement; //# sourceMappingURL=CategoryPage.d.ts.map