import type { ReactNode } from "react"; import type { LoadState } from "@stapel/core"; import type { Category } from "../api/types.js"; import type { CategoryLabel } from "../catalog/labels.js"; import type { CategoryBrowseOptions, UseCategoryCatalogOptions } from "../model/queries.js"; /** One option of one level, with everything a skin needs to draw a row. */ export interface CategoryCascadeOption { readonly category: Category; readonly label: CategoryLabel; /** * `categoryChildIds` (this row's own `children_pks`, falling back to * `tn_children_pks` on an older server) is empty — nothing at all lives * under this row, so choosing it ends the ladder. * * A HINT, and the one place this hook reports something it has not verified. * The fallback column is maintained by django-treenode, which knows nothing * about `active` or `deleted`, so a fallback-derived `false` can still lead * to a rung with no browsable options. The authoritative verdict is * {@link CategoryCascadeBag.atLeaf}, which is the absence of a further rung * — i.e. the server's own empty answer. This is here so a skin can mark an * option as terminal before it is chosen, never so it can skip a request. */ readonly isLeaf: boolean; /** * Live results under this option, or `null` when nobody counted. * * `null` and `0` are different sentences and both reach the screen as * themselves: "we did not count this" is not "there are none". See this * file's header for why the default is `null` everywhere. */ readonly count: number | null; } /** One rung, resolved for a skin. */ export interface CategoryCascadeStep { /** 0 for the children of the cascade's root. */ readonly depth: number; /** Whose children these are — `null` at the top of a rootless cascade. */ readonly parent: Category | null; /** The parent's name, for a skin that labels each select. `null` at the * top of a rootless cascade, where the label belongs to the host's form. */ readonly parentLabel: CategoryLabel | null; readonly options: readonly CategoryCascadeOption[]; readonly chosen: Category | null; readonly chosenLabel: CategoryLabel | null; } export interface CategoryCascadeBag { /** * The ladder. `empty` is a real answer and means the cascade's root has no * children — the tiles arrived at a leaf and there is nothing to narrow. * * `ready` as soon as the TOP rung is known. A rung still in flight below it * is simply not built yet, so the ladder grows downward instead of blanking * — which is the whole reason a request per rung is affordable. */ readonly state: LoadState; /** The deepest answered row, or `null`. */ readonly selected: Category | null; /** Root -> selected, for the poppable trail. Excludes the cascade's own * root, which is where the person already was. */ readonly trail: readonly Category[]; /** The ladder finished on a category nothing lives under. */ readonly atLeaf: boolean; /** The host's {@link UseCategoryCascadeOptions.partitionChild}, echoed back * — see there. */ readonly partitionChild: Category | null; /** A rung below the ones on screen is still in flight. */ readonly isFetching: boolean; /** Why the cascade will not hand a value back yet, or `null`. */ readonly blockedReason: CategoryCascadeBlockedReason | null; /** Answer one level. `null` un-answers it, dropping every level below. */ choose(depth: number, category: Category | null): void; /** Un-answer everything from this depth down — what popping a trail chip * does. `clearFrom(0)` empties the cascade. */ clearFrom(depth: number): void; refetch(): void; } /** * Why the cascade has no value for its host. * * `not_a_leaf` is the `commit: "leaf"` refusal — the ladder has not reached a * category nothing lives under. `has_subcategories` is the `commit: "stage"` * one: the chosen category still divides into a LEVEL of subcategories, which * is a rung the person has not answered rather than a partition the host * offers. Both say the same thing to a person ("this one has subcategories"), * and they are separate values because the rules that produced them differ — * a `chips` parent is refused by the first and accepted by the second. */ export type CategoryCascadeBlockedReason = "nothing_selected" | "not_a_leaf" | "has_subcategories"; /** * What a cascade reports to its host. * * `"any"` — every choice, leaf or not. The FILTER's rule: "show me everything * under Cars" is the commonest narrowing there is, and the search index * matches a category path as a prefix precisely so a parent finds its * descendants. * * `"leaf"` — only a leaf, and `null` for every step on the way. The * COMPOSER's rule as the tree alone states it: a listing lives in exactly one * category, and a non-leaf inherits the wrong feature set. The intermediate * steps still show as answered — they are the cursor, not a value. * * `"stage"` — the COMPOSER's rule as the BROWSE CONTRACT states it, and the * one a composer should use where the server resolves `children_as`. The * ladder ends where {@link childControl} would put no LIST filter on a feed * page — i.e. a leaf (`"none"`) OR a `chips` parent (`"segmented"`) — and * offers no rung below it. (Not {@link browseStage}: that answers a different * question now — whether a ROOT's page is tiles — and a non-root `chips` * parent three levels down is `"feed"` under it, same as everything else.) * * The difference is a partition. Under `"leaf"` a `chips` parent (`Cars`, with * `New` and `Used` under it) is refused and the cascade goes on asking for one * of the two, which files the person's listing through a control that presents * a FILTER as a level of the tree — and the two halves of the site then * disagree about what a category is, which is exactly what the browse contract * settled. Under `"stage"` the cascade commits `Cars` and stops; the partition * child is a required select the host draws beside it, out of the same rows, * where it reads as the choice it is. */ export type CategoryCascadeCommit = "any" | "leaf" | "stage"; export interface UseCategoryCascadeOptions extends CategoryBrowseOptions, UseCategoryCatalogOptions { /** * Where the ladder starts. Absent/`null` starts at the catalogue's roots, * which is the one rung that still costs a catalogue sync — see this file's * header. * * On a category landing, pass the category the TILES arrived at: the two * mechanisms then meet at exactly one boundary instead of both offering the * same level, and the whole control becomes one `children/` per rung. */ readonly rootId?: number | null; /** The host's current answer. */ readonly value?: number | null; readonly onChange?: (id: number | null, category: Category | null) => void; /** Default `"any"`. See {@link CategoryCascadeCommit}. */ readonly commit?: CategoryCascadeCommit; /** * The top rung of a ROOTLESS ladder, supplied by the host. * * The escape hatch for the one question the server cannot answer. A host * that already knows its top-level categories — a small catalogue, a * navigation config, a carousel it has just drawn — hands them over and the * catalogue sync is never mounted. Ignored when `rootId` is given, because * then there is no rootless rung to fill. */ readonly roots?: readonly Category[]; /** * Live results per category id. Unfilled by default and unfillable by this * package — see this file's header. */ readonly counts?: ReadonlyMap; /** * The chip picked on a `commit: "stage"` stop's OWN partition select — the * one the host draws beside this cascade, out of the stopped category's own * children (see this file's header: it is a required select, not a rung of * the ladder, so this hook never fetches or chooses it itself). * * Echoed back unchanged as {@link CategoryCascadeBag.partitionChild}, so one * bag tells a composer or a facet rail whether a chip is picked — which is * exactly what `visibleFeatures`'s `chipPicked` needs * (`bag.partitionChild !== null`) — instead of the host keeping that fact in * a second piece of state that can drift from this one. `null`/absent: no * chip picked yet (or the ladder is not stopped on a partition at all). */ readonly partitionChild?: Category | null; } /** * The ladder, its cursor and its commit rule. * * Renderless callers use {@link CategoryCascade}; the antd control over this * is `` in the `/default` entry. */ export declare function useCategoryCascade(options?: UseCategoryCascadeOptions): CategoryCascadeBag; export interface CategoryCascadeProps extends UseCategoryCascadeOptions { children: (bag: CategoryCascadeBag) => ReactNode; } /** {@link useCategoryCascade} as a renderless component. */ export declare function CategoryCascade(props: CategoryCascadeProps): ReactNode; //# sourceMappingURL=CategoryCascade.d.ts.map