import { type TVConfig } from '../utils/variants.js'; import type { SlotOf } from './component-slots.js'; /** * A prop-conditional style rule. Its non-`class` keys are matched against the * component's **effective** variant props: every axis that component's own * condition object *names*, at its value, or at the config's `defaultVariants` * value where it named the axis but wrote `undefined` (see * {@link effectiveVariants}). Naming is the boundary, not declaring — a rule * can only address an axis the component speaks for. * * So `{ disabled: false }` fires on every component that names a `disabled` * **key**, and on no others — declaring the axis is neither necessary nor * sufficient. `datePickerVariants` declares no axes at all and its rules fire, * because DatePicker names the keys of the Input it wraps. Three kinds fall * outside, all pinned in `provider/boolean-conditions.svelte.test.ts`: a * component with no such axis, which reaches the state some other way — the * `disabled:` CSS variant, forwarding to a component it wraps, another axis * carrying it, a predicate writing classes in the markup, or suppressing the * element rather than styling it; one whose config declares the axis while the * component hands it to a slot function per element rather than carrying it for * itself (`Menu`, whose rows each get their own `disabled`); and one the * provider cannot address at all, having no name registered. * * Matching works like a `tv()` compoundVariant: `string` = equality, * `string[]` = "one of", `boolean` for a boolean axis such as the table's * `contained`; the comparison runs on the stringified value, so `true` and * `'true'` are the same condition. On a match, the `class` record (slot → * classes) is merged into the slot-class cascade. Additive: every matching * rule contributes; later sources win per Tailwind bucket. * * The keys are **variant axis names**, which are the component's public prop * names wherever it has one for the axis, and internal where the axis is * computed rather than received (`hasRightIcon`, `messageType`, `open`). * * The index signature admits any string, so a mistyped key type-checks and * paints nothing. A development build reports a key the component neither * passes nor declares — once, naming the keys that *can* match, which is the * passed half alone: an axis the config declares but the component hands to a * slot function per element can never match either, so the message keeps it out * of that list. A rule that is merely unmatched stays silent, being the normal * case. Only a rendered component is checked: a rule under a component name * that never mounts reaches nothing and is reported by nothing. * * @example * { variant: 'outlined', class: { base: 'border' } } // 1px border only on outlined */ export interface ConditionalOverride { /** Per-slot classes applied when the prop conditions match. */ class: Partial>; /** Prop conditions: prop name → required value (or one of several). */ [propCondition: string]: string | string[] | boolean | Partial> | undefined; } export interface ComponentDefaults { slotClasses?: Partial>; /** * Prop-conditional style rules, applied after unconditional `slotClasses` * (so they win per bucket) but before instance-level `slotClasses` / `class`. * Use for surgical per-variant tweaks the unconditional `slotClasses` cannot * express, e.g. `overrides: [{ variant: 'outlined', class: { base: 'border' } }]`. */ overrides?: ConditionalOverride[]; } /** * A preset is a named, project-defined visual style for a component. * It provides `slotClasses` that are merged *after* provider defaults * but *before* instance-level `slotClasses` / `class`. * * Presets are the recommended way to introduce custom looks that fall * outside the semantic intent palette — instead of overriding styles * with `class="bg-…!"` at each usage site. */ export interface ComponentPreset { slotClasses?: Partial>; /** Prop-conditional rules scoped to this preset (see {@link ConditionalOverride}). */ overrides?: ConditionalOverride[]; } /** Map of component name → preset name → preset definition. */ export type PresetMap = Record>; /** * A provider `defaults` object, with each entry checked against the slot names * of the component its key names. * * Generic in the object that is *written*, not in a fixed key set: `K` is the * literal the consumer typed, so {@link SlotOf} can answer per key — a known * component gets its own slots, any other name keeps `string`. A non-generic * `Record` would be the alternative and it would * close the map, which breaks the consumer wrapper that * COMPONENT-API-CONVENTIONS.md documents. * * **Sharp for the object literal that reaches the attribute.** These records are * weak types (every property optional), so the base rule rejects only an object * with *no* key in common; catching a wrong key *beside* a right one is * excess-property checking, which needs a fresh literal. So the prop must not be * the bare type parameter — inferring the literal *into* it makes the literal * its own target, freshness is gone and only the weak-type rule is left. It is * declared `BlocksDefaults>` instead: the type * parameter carries the **keys** alone, and this mapped type builds the target * from them. * * **The properties are optional for a reason a required version cannot serve.** * Inferring keys from `cond ? themeA : themeB` takes them from one branch, and a * required property the other branch lacks is then missing — so a config * switched on at runtime, the ordinary shape, fails to compile with no wrong * slot name anywhere. Both halves are needed: optional alone still fails the * ternary, key-inference alone still fails it. * * Every way a config can reach the attribute is tabulated in `docs/MIGRATION.md`; * the quietest is an annotated `Record`, which reports * nothing at all. The rejected half is asserted in * `component-slots.types.test.ts` by calling the component, so it reads this * declaration rather than a copy of it. */ export type BlocksDefaults = { [K in keyof T]?: ComponentDefaults>; }; /** A provider `presets` object, checked per component key like {@link BlocksDefaults}. */ export type BlocksPresets = { [K in keyof T]?: Record>>; }; export interface BlocksConfig { readonly unstyled: boolean; readonly defaults: Readonly>; readonly presets: Readonly; } declare const getBlocksConfig: () => BlocksConfig | undefined, setBlocksConfig: (value: BlocksConfig | undefined) => BlocksConfig | undefined; export { getBlocksConfig, setBlocksConfig }; /** * Merge slot-class records, later sources appending to earlier ones. * * Takes `string | undefined` values because that is what every source in this * file already is — a `Partial>` leaves out the slots the * caller did not write — and because the loop below has always skipped a falsy * value. The parameter says what the body does, which is also what lets the two * exported helpers compose: `mergeSlotClasses(resolvePresetSlotClasses(…))`. */ export declare function mergeSlotClasses(...sources: (Record | undefined)[]): Record; /** * Look up a preset's `slotClasses` for a given component + preset name. * Returns `undefined` if no preset is registered (caller falls back to * provider defaults only). Emits a dev-only warning when a preset name * is used but not registered, so missing presets are discoverable. */ export declare function resolvePresetSlotClasses(presets: PresetMap | undefined, component: string, presetName: string | undefined): Record | undefined; /** * Collect the per-slot classes of every `overrides` entry whose prop * conditions match `activeProps`. Returns `undefined` when there are no * overrides or none match. Multiple matches merge additively, in order. */ export declare function resolveOverrideSlotClasses(overrides: ConditionalOverride[] | undefined, activeProps: Record): Record | undefined; /** * Resolve the full slot-class cascade for a component instance, honoring * conditional `overrides` from both provider defaults and the active preset. * * Precedence (weak → strong), conflict-resolved per slot so a later source * wins within the same Tailwind bucket: * * defaults.slotClasses → defaults.overrides[match] * → preset.slotClasses → preset.overrides[match] → instance.slotClasses * * The result is handed to the component's `tv()` slot fn as the `class` * override, where it additionally strips conflicting library classes. * * `instanceSlotClasses` admits `undefined` values because that is what a * `Partial` prop is: a slot the caller left out. Such a slot is * skipped, exactly like an empty string, and never reaches the result. * * `variantConfig` is the component's own `tv()` config (`xVariants.config`), * and it is required rather than optional on purpose: it is what supplies the * default for an axis the component named but left `undefined` * (see {@link effectiveVariants}). A call site that could omit it would * silently lose those, which is the defect this parameter exists to remove. * * **Required buys the presence of an argument, not the right one.** `TVConfig` * is optional in every field, so *any* config satisfies the parameter and the * compiler cannot tell `cardVariants.config` from `badgeVariants.config` here * — measured: wiring Badge's config into Card's call leaves every suite green * while four rules on axes Card does not have start matching every Card. * * Typing the pair together (`activeProps` as the props of *this* config) is not * the way out, and not for the reason this comment first gave. Two `tsc * --strict` reproductions of that signature disagree with each other depending * only on how inference is arranged, and in one of them the mismatch passes * while an inline literal of the same object is rejected — the weak-type * asymmetry `blocks/docs/MIGRATION.md` already records for slot keys: a target * whose properties are all optional rejects only an object with *no* key in * common, and a condition object held in a variable (which is how every call * site writes it) skips excess-property checking altogether. * * Measured across three candidate signatures, the only form that catches a * mispairing is a non-`Partial` `Record` — and it demands that every call site * name **every** axis its config declares. `Input` deliberately omitting * `iconPosition`, which it passes per slot call, becomes a compile error. So a * type tie is not merely unmeasured; it forbids the deliberate omission this * fold exists to permit. * * The binding does not have to be on the type level, though. A closure would * make the mispairing unrepresentable — one identifier instead of two, e.g. * `cardVariants.resolveSlots(config, 'Card', preset, variantProps, slotClasses)` * — and it leaves `DatePicker` alone, whose props parameter stays * `Record` (its config declares no axes and it hands over five * keys belonging to the Input and Calendar it wraps). What blocks it today is * **module layering, not typing**: this file imports `variants.ts` and not the * other way round, so the binder would have to live on the provider side. * Until then the pairing rests on the call site naming one identifier twice. */ export declare function resolveSlotClasses(config: BlocksConfig | undefined, component: string, preset: string | undefined, activeProps: Record, instanceSlotClasses: Record | undefined, variantConfig: TVConfig): Record;