import type { IndexRecord } from "./schema.js"; /** * URL-driven filter state for the /items discovery page. * * Multi-value fields use repeated keys in the URL: `?stack=Flutter&stack=React+Native`. * Empty / undefined fields mean "no filter on this dimension". */ export type IndexFilters = { q?: string; stacks?: string[]; platforms?: string[]; categories?: string[]; tags?: string[]; labels?: string[]; licenses?: string[]; statuses?: string[]; /** A curated lens (e.g. "good-to-learn", "production-like"). Single value. */ lens?: string; /** Sort order. Single value, defaults to "recently-updated". */ sort?: IndexSort | undefined; /** 1-based page number. */ page?: number | undefined; }; /** Available sort orders, in UI order. */ export declare const SORT_OPTIONS: readonly [{ readonly value: "recently-updated"; readonly label: "Recently updated"; }, { readonly value: "most-starred"; readonly label: "Most starred"; }, { readonly value: "recently-added"; readonly label: "Recently added"; }, { readonly value: "best-overall"; readonly label: "Best overall"; }, { readonly value: "alphabetical"; readonly label: "Alphabetical"; }]; export type IndexSort = (typeof SORT_OPTIONS)[number]["value"]; /** * Read filters from a URLSearchParams (or anything URLSearchParams-shaped, * e.g. the result of `new URL(req.url).searchParams`). */ export declare function filtersFromSearchParams(sp: URLSearchParams): IndexFilters; /** * Serialize a filters object back to URLSearchParams. * Undefined / empty values are dropped. Defaults (sort, page 1, comfortable * density) are also dropped to keep URLs short and canonical. */ export declare function searchParamsFromFilters(f: IndexFilters): URLSearchParams; /** True if any filter is active (i.e. the result list isn't "all items"). */ export declare function hasAnyFilter(f: IndexFilters): boolean; /** How many items per page. Constant for now; future versions may vary. */ export declare const PAGE_SIZE = 20; export declare function applySort(items: IndexRecord[], sort: IndexSort): IndexRecord[]; /** Slice an array for a given page (1-based). */ export declare function paginate(items: T[], page: number, pageSize?: number): T[]; /** Total number of pages given an item count. Always >= 1. */ export declare function totalPages(itemCount: number, pageSize?: number): number; /** * Windowed page list for pagination controls: all pages when 7 or * fewer, otherwise `1 … active−1 active active+1 … count` with * "ellipsis" markers. Shared by the server-rendered `Pagination` * component and the client-side rebuild in `DirectoryIndexClient` * so the two never drift. */ export declare function paginationPageList(active: number, count: number): Array; /** Resolve the effective sort from a filters object, applying default. */ export declare function effectiveSort(f: IndexFilters): IndexSort; /** Resolve the effective page from a filters object, applying default. */ export declare function effectivePage(f: IndexFilters): number; /** * Return the subset of items that match all active filters (AND across * dimensions, OR within a dimension). * * `q` is a case-insensitive substring search across name, owner, * description, and category. Other dimensions are exact match. */ export declare function filterRecords(items: IndexRecord[], f: IndexFilters): IndexRecord[]; /** Serialize filters back to a browsable URL under `pathPrefix`. */ export declare function hrefForFilters(f: IndexFilters, pathPrefix?: string): string; /** * URL for a given result page, keeping every other filter. One * definition for the server render and the client rebuild — they had * drifted as two hand-rolled copies. */ export declare function hrefForPage(f: IndexFilters, page: number, pathPrefix?: string): string; /** URL that drops every filter but keeps the chosen sort. */ export declare function hrefForClearedFilters(f: IndexFilters, pathPrefix?: string): string; /** * Path for a prerendered result page — `/projects/`, `/projects/page/2/`. * * The unfiltered pages of a directory are real, crawlable, no-JS pages; * `?page=N` is reserved for filtered views, which only exist on the * client. A directory whose pages 2..n are unreachable to a crawler is * a directory whose records mostly do not exist as far as search is * concerned. * * The `/page/` segment is load-bearing: record detail pages live at * `/{slug}/{recordSlug}`, so `/projects/2/` would collide with a record * whose slug is "2". */ export declare function pagePathHref(pathPrefix: string, page: number): string; /** Minimal taxonomy shape needed to turn ids into display names. */ export type TaxonomyNames = Record; export interface FilterChip { key: keyof IndexFilters; value: string; label: string; /** URL that removes this one filter. */ href: string; } /** * Build a flat list of "active filter" chips for display + removal. * * Chips carry their own remove `href` and a taxonomy-resolved label. * Both used to be rebuilt by every caller, and the server and the * client resolved labels differently — the server showed * `Stack: react-native`, the client `Stack: React Native`. */ export declare function activeFilterChips(f: IndexFilters, context?: { taxonomy?: TaxonomyNames; pathPrefix?: string; }): FilterChip[]; /** * Return a new filters object with one (key, value) removed. * - scalar keys (`q`, `lens`) → clears the value * - multi-value keys → removes the single matching value * Used by the "x" button on each active filter chip. * Also resets page to 1 so the user doesn't get stranded on a high page * that no longer has results. */ export declare function removeFilter(f: IndexFilters, key: keyof IndexFilters, value: string): IndexFilters; /** * Build the full list of facet values from the items array, preserving * a sensible order (frequency desc, then alphabetical for ties). */ export declare function buildFacets(items: IndexRecord[], options?: { curatedTagIds?: string[]; /** * When set, facet counts reflect records that satisfy all * current filters *except* the facet being computed. This is the * "intersection count" UX — Platform after Flutter should show * Flutter+Platform counts, not the global Platform count. * * Set to `null` (or omit) to keep the legacy global-count * behavior. The browse page opts in by passing the current * `IndexFilters` minus each facet's filter. */ filters?: IndexFilters | null; /** * Taxonomy-owned display order per dimension: an array of ids in * the order the taxonomy YAML declares them. Known ids sort by * that position; ids that exist only in record data append after, * in the default count-desc-then-alpha order. Curated tag ids * (topics.yml) flow through the `tags` key the same way. */ order?: Partial>; }): { stacks: { value: string; count: number; }[]; platforms: { value: string; count: number; }[]; categories: { value: string; count: number; }[]; tags: { value: string; count: number; }[]; labels: { value: string; count: number; }[]; licenses: { value: string; count: number; }[]; }; //# sourceMappingURL=directory-search.d.ts.map