import { type LocaleOptions, type Localized, type PartialMessages } from "./i18n.js"; import { type BillingInterval, type Money, type PlanCatalog, type Sale } from "./plan-model.js"; export { definePlans, normalizePlan, normalizePlans, planModel, plansWhere, selfServePlans, defaultBasket, validateBasket, describeBasketProblem, priceBasket, basketBounds, poolSizeOf, packSizeOf, } from "./plan-model.js"; export { resolveLocalized, resolveLocalizedList, resolveMessages, formatMessage, formatMinor, describeReason, DEFAULT_MESSAGES, } from "./i18n.js"; export type { Localized, LocalizedList, LocaleOptions, Messages, PartialMessages, } from "./i18n.js"; export type { BillingInterval, Money, IntervalPrice, PlanCatalog, PlanSpec, PlanModel, PlanDisplay, SeatTypeSpec, SeatTypeDisplay, Sells, Grant, Cap, CapWindow, CapCovers, Replenish, Sale, Quantities, BasketProblem, } from "./plan-model.js"; export interface MoneyView { /** Minor units, for arithmetic. */ minor: Money; /** Formatted for display. */ text: string; } export interface SeatRowView { key: string; label: string; usage: string | null; /** Drawn by agents/API keys rather than a person — normally not a card row. */ shared: boolean; /** Priced at zero (a free plan's single seat). */ free: boolean; min: number; max: number | null; /** Per-MONTH figure for each interval (yearly ÷ 12) — how seats are compared. */ perMonth: Record; /** What is actually charged for that interval. */ total: Record; includedCredits: number; } export interface PlanPriceView { kind: "free" | "seats" | "flat" | "quote"; /** Headline for the selected interval. Null when the price is quoted. */ headline: MoneyView | null; /** A key, not prose: the app supplies "/month" or "per seat / month". */ unit: "month" | "year" | "seat_month" | "seat_year" | null; /** The other interval, for a muted "billed annually" line. */ alternate: { interval: BillingInterval; perMonth: MoneyView; total: MoneyView; } | null; /** * What the DEFAULT basket actually costs, per interval — the charge, not the * comparison figure. * * `headline` is deliberately a per-MONTH number (that is how plans are * compared), so a surface that needs the real annual amount has to read it * here. Rendering the headline in a "Yearly" column shows a twelfth of the * price, which is exactly the mistake this field exists to prevent. */ totals: Record; rows: readonly SeatRowView[]; /** * The name of the seat a plan that sells NONE gives you (`seat.display.label`), * else null. * * A card still has to label that plan's single seat segment, and with nothing * here the app typed the words in — a string that then had to agree with what * the meter and the usage screen call the same seat. `rows` stays empty: this * seat is not purchasable and must never appear in a basket. */ seatLabel: string | null; /** Total seats a basket must stay within, across every type. `minSeats: 2` is * "a team of one is Hobby". Null max = unlimited. A seat stepper needs both, * and deriving them from the rows' own min/max gets the plan-level total * wrong. */ minSeats: number; maxSeats: number | null; /** For a plan with no per-seat figure to show (a committed package). */ pooled: { title: string; note: string | null; } | null; } export interface CtaView { kind: "signup" | "checkout" | "contact" | "current" | "unavailable"; label: string; href: string | null; disabledReason: string | null; } export interface PlanView { key: string; name: string; tagline: string | null; badge: string | null; featured: boolean; order: number; featuresIntro: string | null; features: readonly string[]; price: PlanPriceView; cta: CtaView; /** Whole percent saved by paying yearly, FLOORED so it never overstates. * Null when the plan has no monthly/yearly pair. */ annualSaving: number | null; /** WHICH basket that percentage came from. Naming it is what stops one surface * advertising a saving another surface doesn't charge. */ annualSavingBasis: "flat" | "basket" | null; members: { max: number | null; }; /** Included usage per cycle for the default basket, and where it pools. */ included: { credits: number; scope: "per_seat" | "pool" | "none"; }; /** Intervals the plan is actually SOLD on — `["yearly"]` for an annual-only * commitment. Not derivable from the rendered price: a quoted plan shows no * price at all yet still has a billing cycle. */ intervals: readonly BillingInterval[]; sale: Sale; interval: BillingInterval; } export interface DerivePlanViewsOptions extends LocaleOptions { /** Which interval the headline shows. Default "yearly". */ interval?: BillingInterval; currency?: string; /** * Override the handful of words the library supplies itself ("Unlimited", * "Monthly", "Contact us", the refusal messages). Anything not overridden stays * ENGLISH — see DEFAULT_MESSAGES. */ messages?: PartialMessages; /** Override the formatter. Intl renders "18,00 €" for de-DE; a house style may * want "€18". */ formatMoney?: (minor: Money, currency: string, locale: string) => string; /** Its card becomes `cta.kind: "current"`. */ currentPlan?: string | null; /** false → every CTA disabled with a reason (e.g. a non-admin viewer). */ canManage?: boolean | { reason: string; }; hrefs?: { signup?: string; contact?: string; checkout?: (plan: string) => string; }; /** Also include `display.hidden` plans and `sale: "legacy"`. Default false. */ includeHidden?: boolean; /** * Drop the tiers BELOW `currentPlan` — a grid of moves up, plus where you are. * * A pricing grid shown to an existing customer is a place to move UP; a card * headed "Get the Hobby plan" offered to somebody on Pro is a downgrade dressed * as an upgrade, and it is not where cancelling belongs (that is the billing * page, which says so in those words and schedules it for the period end). * * Ranked by `planRank`, the SAME arithmetic `isDowngrade` uses, so the grid and * the refusal cannot disagree about which way a move goes. Ignored without a * `currentPlan` — a visitor has no tier to be above — which is what lets one * public page serve a signed-out reader and a signed-in customer. */ upgradesOnly?: boolean; } export declare function derivePlanViews(plans: PlanCatalog, opts?: DerivePlanViewsOptions): readonly PlanView[]; export declare function derivePlanView(plans: PlanCatalog, key: string, opts?: DerivePlanViewsOptions): PlanView | null; export interface MarkdownOptions { columns?: readonly ("name" | "members" | "included" | "monthly" | "yearly" | "seats")[]; /** * Escape `$` so MDX doesn't read it as an expression. Default true. * * BODY only. `\$` is a character escape in MDX and an INVALID one in YAML, so carrying * this into a page's frontmatter breaks the whole build rather than one line: Fern * refused a consumer's docs deploy for four days with "unknown escape sequence * (4:102)", pointing at a `description:` scalar somebody had escaped by the same * convention. Nothing these renderers return belongs in frontmatter. */ mdx?: boolean; /** Header for the `included` column. Overrides `messages.columnIncluded`. */ includedLabel?: string; /** Column headers and the "Contact us" / "Unlimited" / "Free" cells. English * unless overridden. */ messages?: PartialMessages; } /** A plan table. Quoted plans show "Contact us" rather than a fabricated price. */ export declare function renderPlansMarkdown(views: readonly PlanView[], opts?: MarkdownOptions): string; /** A cost-per-action table, optionally grouped. Replaces a hand-maintained one. */ export declare function renderRateCardMarkdown(rateCard: Record, opts?: { groups?: Record; unit?: string; mdx?: boolean; /** Heading level for group titles. Default 3. */ headingLevel?: number; messages?: PartialMessages; }): string; /** What a cell can say when it is authored by hand. A text value is * {@link Localized}, so a matrix can serve several languages. */ export type CompareValue = boolean | number | Localized; /** Fill a row from the plan model instead of by hand. */ export type CompareSource = /** `limits.members` — a count, or the unlimited label. */ "members" /** `included.credits` per cycle. */ | "included" /** The plan's headline price for the selected interval. */ | "price" /** The intervals the plan is sold on. */ | "intervals" /** The names of its purchasable seat types. */ | "seatTypes"; export interface CompareRow { label: Localized; /** One muted line under the label, when the label alone isn't enough. */ hint?: Localized; /** Boolean shorthand: these plans get a tick, everything else a dash. */ in?: readonly string[]; /** Explicit per-plan values. A plan with no entry reads as "not included". */ values?: Record; /** Derive the value from each plan. Beats `values` when both are present, * because the config is the thing that can't be wrong. */ from?: CompareSource; } export interface CompareGroup { /** The sub-heading inside a section. */ label: Localized; rows: readonly CompareRow[]; } export interface CompareSection { title: Localized; description?: Localized; /** Icon NAME, resolved by the app — the library ships no components. */ icon?: string; groups: readonly CompareGroup[]; } export type CompareConfig = readonly CompareSection[]; /** Identity helper, for literal types and autocomplete on a compare config. */ export declare function defineCompare(compare: T): T; /** A resolved cell: the component renders a tick, a dash, or text — it never has * to interpret a value. */ export type CompareCell = { kind: "yes"; } | { kind: "no"; } | { kind: "text"; text: string; }; export interface CompareRowView { label: string; hint: string | null; /** Keyed by plan, and also ordered to match `columns`. */ cells: Record; } export interface CompareGroupView { label: string; rows: readonly CompareRowView[]; } export interface CompareSectionView { title: string; description: string | null; icon: string | null; groups: readonly CompareGroupView[]; } export interface CompareTableView { /** The plans, in the same order as the pricing cards. */ columns: readonly { key: string; name: string; featured: boolean; }[]; sections: readonly CompareSectionView[]; } export type DeriveCompareOptions = DerivePlanViewsOptions; /** * Resolve a compare config against the plans into something a table can render. * * Columns come from the same derivation as the pricing cards, so the table's * plans are in the same order, with the same names, and a hidden or legacy plan * is absent from both. */ export declare function deriveCompareTable(plans: PlanCatalog, compare: CompareConfig, opts?: DeriveCompareOptions): CompareTableView; /** Every row label in a compare config, in one locale — for a search index, or to * check that a row hasn't been written twice. */ export declare function compareRowLabels(compare: CompareConfig, opts?: LocaleOptions): string[]; //# sourceMappingURL=pricing.d.ts.map