Skip to content
codefastlabs

Command Palette

Search for a command to run...

v0.7.0· Changelog

@codefast/tailwind-variants

0.7.0

0.6.2

0.6.1

0.6.0

Minor Changes

  • #700 ea48ae2 Thanks @thevuong! - Authoring a configuration is now type-checked as strictly as calling one. Three typos used to compile and then do nothing: a defaultVariants key naming no variant, a compoundVariants key naming no variant, and a variant class map naming no slot. Each failed the same silent way — nothing read the stray key, the compound never matched, the slot map was dropped — so a mistyped character removed a style with no error anywhere.

    The root cause was one overload. extend was optional on ExtendedVariantConfig, which made the last overload a catch-all: a configuration the earlier three correctly rejected still matched it, and TBase with nothing to infer from widened to VariantSchema, whose key is string — so every mistyped name became legal again. extend is now required there, which is what that overload was always for. Alongside it, defaultVariants and compoundVariants no longer act as inference sites for the variant schema (NoInfer), so a stray key is rejected instead of quietly widening the schema to include it.

    Two smaller corrections fall out. defaultVariants is typed by the new VariantValues<T> rather than the call-site VariantSelection<T>, so it no longer accepts a className a configuration has no use for. And in a slot configuration a variant's object value is now held to the declared slots — SlotClassValue<S> — because resolution has always read an object there as slot names rather than clsx conditions; base stays admissible whether or not it is declared, matching the plan that synthesises it.

    A configuration without slots keeps its clsx object values, and a compound condition naming an undeclared variant still resolves at runtime for JavaScript callers and merged configurations — it is only the typed authoring path that now rejects it, since no typed call could ever satisfy such a condition.

    tests/types/common/config-authoring.test.ts holds all of this with @ts-expect-error, the first negative type tests in the package. The 110 existing assertions only ever proved what compiles, which is exactly how three gaps survived.

    One configuration shape stops compiling. Requiring extend closes the overload that used to accept anything, and that overload was also what accepted a configuration whose literal types had widened — a hoisted const defaultVariants = { size: "sm" }, a hoisted compoundVariants array, a spread of a shared partial. Those have type { size: string }, which was never assignable to { size?: "sm" }; they compiled only because the catch-all widened the schema to swallow them. Add as const to the hoisted value, or inline it.

    The error TypeScript reports for this is Property 'extend' is missing, which names the last overload tried rather than the real mismatch. It is the same message a plain variant typo now produces. Nothing in this repository hit either case, but a consumer with a shared configuration fragment will.

  • #700 93b18ac Thanks @thevuong! - A variant function now remembers what each selection resolved to, so a repeated selection skips both the plan walk and tailwind-merge. Against the previous build the resolution rows measure 1.08× to 11.9×, and against tailwind-variants the suite geomean moves from 6.18× to 19.8×. The motivating measurement: in the merged lane most of the cost was never the merge algorithm — tailwind-merge caches — but building and hashing the joined class string to look that cache up, which a key built from the selection avoids entirely.

    The key is a mixed-radix number, one digit per variant. A variant no compound tests is keyed by its group key, since two values sharing a key select the same classes; a variant a compound tests is keyed by the raw value, because a compound compares against what the caller passed and true and "true" share a group key while comparing differently. A call the key cannot represent — an axis past its capacity, a configuration too large to address in one safe integer, a clsx-shaped className — resolves the long way as before.

    Two consequences are worth knowing. A slot component called twice with the same selection gets back the same object of slot functions: stable enough for a React dependency array, and shared, so nothing may mutate it. And the store is bounded and keyed by the selection, so a component whose variant values are effectively unique per call fills it with entries nothing reads again — the new cacheResolutions: false option turns it off for that component.

    Alongside it: extendTailwindMerge is now memoised by twMergeConfig identity, so a design system handing one config to a hundred components builds one merge function instead of a hundred, each with its own cache; a slot resolution keeps only the props a compound can read rather than the caller's whole props object, which would otherwise pin children for as long as the entry lives; and each slot's merged text is memoised, so re-reading a slot no longer re-runs the merge.

    tv itself costs about a quarter of a microsecond more per component definition. The encoder is compiled on first resolution rather than in tv, so a component that is defined and never rendered pays nothing for it.

  • #701 710d533 Thanks @thevuong! - @codefast/tailwind-variants now ships with no runtime dependencies of its own, matching what tailwind-variants has always done. Two changes get it there.

    clsx is gone, replaced by a flattener inside the package. It is deliberately identical, corners included: a bigint contributes nothing despite being a ClassValue, and an object's keys are read with for…in, so an inherited enumerable one counts. Verified by running the behaviour sweep — every configuration, every variant value, every slot, with and without merging — against main and diffing: zero difference across 118,505 outcomes. The dependency was only ever reached at compile time anyway, since resolution stopped calling it when the plan was introduced.

    ClassValue is now declared here rather than re-exported from clsx, so the type survives the dependency leaving. Its shape is unchanged except that ClassDictionary is Record<string, unknown> rather than clsx's Record<string, any> — which means passing a function where a class value is expected is now a type error. It contributed nothing at runtime either way.

    tailwind-merge moves from a dependency to a peer (>=3.0.0). A consumer who pins their own version now gets one copy at the version they chose, instead of a second one arriving underneath this package. Install it alongside — @codefast/ui already does on your behalf.

  • #700 d0dd326 Thanks @thevuong! - Compile the configuration once, in tv, instead of re-deriving it on every resolver call.

    tv now builds a plan. Variant groups become an array of entries whose default classes are already looked up, compound variants become flat condition lists whose fallbacks are already resolved, and every class value is flattened to a string. Resolution reads monomorphic fields and concatenates strings, so the per-call work that used to happen — Object.keys per compound variant, a dictionary lookup per variant per slot, an intermediate array plus a spread into clsx — is gone.

    Slot resolution is also inverted. A slot map names only a few slots, so having each of N slots scan every variant meant mostly-missing lookups. Each variant value now carries the slot positions it targets, and one pass distributes classes into a per-slot buffer that every resolver of that call shares; a slot called without its own props just reads its entry. Compound slots ride the same pass.

    Measured against the previous release on benchmarks/tailwind-variants (paired A/B, one subprocess per side per scenario, three passes with the order alternated, median of per-pass ratios): every one of the sixteen scenarios is faster, geometric mean 2.86×.

    shape without tailwind-merge with
    extreme-slots 9.64× 5.99×
    slots 5.55× 3.48×
    compound-slots 4.94× 3.21×
    complex 2.59× 2.00×
    simple / extends / create-tv / extreme 2.22×–2.33× 1.56×–1.89×

    The merge-enabled rows gain least because merging, not resolution, is what is left in them. The suite's A/A noise floor on the same machine is ±1%.

    The trade is a slower tv call: flattening every class value and precomputing slot positions costs roughly 400ns more for a simple config and ~3.4µs for a ten-slot one. That is once per component definition, at module load, and against a slot resolution that got ~1.6µs cheaper it pays for itself after about two renders.

    One behavioural change, in a corner: a configuration whose classes are all truthy but render to nothing — tv({ base: {} }) — now returns undefined with twMerge enabled, where it previously returned "". It already returned undefined with twMerge disabled, so undefined is now what "no classes" means either way.

Patch Changes

  • #700 fe7e9e4 Thanks @thevuong! - A variant value naming an Object.prototype member no longer reaches it. A compiled variant group is indexed by whatever a caller passes, and it was a plain object — so group["toString"] answered with a function instead of undefined. The flat lane concatenated that function's source text into the class string, and the slot lane read slot positions off Object.prototype and threw:

    tv({ base: "block", variants: { size: { sm: "p-2" } } })({ size: "toString" })
      →  "block function toString() { [native code] }"
    
    tv({ slots: { base: "rounded" }, … })({ size: "__proto__" }).base()
      →  TypeError: Cannot read properties of undefined (reading 'length')

    Groups and the slot index map are now compiled onto prototype-less objects, which closes both resolvers and the selection encoder at once since all three read the same object. It costs roughly twice what reusing the source object did, once per component definition.

    Two smaller things fall out of the same reading. A value the group does not answer is no longer memoised into the id table, which a long-lived server rendering user-supplied values would otherwise grow without bound; and inherited keys no longer consume the ids a group's declared values need, which used to disable a resolver's cache permanently after a few junk values.

0.5.0

Patch Changes

  • #676 641e233 Thanks @thevuong! - Collapse the types and default lanes of package.json#imports from fallback arrays to single strings.

    Node resolves an imports array by taking the first candidate it can parse, without checking that the file exists and without falling through — a specifier whose first candidate is missing throws ERR_MODULE_NOT_FOUND rather than trying the second. ./dist/*/index.js and ./dist/*/index.d.ts could therefore never be reached, so they read as a safety net that does not exist. The source lane keeps its extension candidates, which only tsc and Vite read and both probe.

0.5.0-canary.9

Patch Changes

  • #676 641e233 Thanks @thevuong! - Collapse the types and default lanes of package.json#imports from fallback arrays to single strings.

    Node resolves an imports array by taking the first candidate it can parse, without checking that the file exists and without falling through — a specifier whose first candidate is missing throws ERR_MODULE_NOT_FOUND rather than trying the second. ./dist/*/index.js and ./dist/*/index.d.ts could therefore never be reached, so they read as a safety net that does not exist. The source lane keeps its extension candidates, which only tsc and Vite read and both probe.

0.5.0-canary.8

0.5.0-canary.7

0.5.0-canary.6

1.0.0-canary.7

1.0.0-canary.6

0.5.0-canary.5

0.5.0-canary.4

0.5.0-canary.3

0.5.0-canary.2

0.5.0-canary.1

0.5.0-canary.0

0.4.0

Patch Changes

  • 2397801 Thanks @thevuong! - chore: align package config globs

  • 6350584 Thanks @thevuong! - refactor(tailwind-variants): clarify variant API names

  • f79b333 Thanks @thevuong! - feat(dev): enable source condition for zero-rebuild HMR in apps/docs

  • 8432414 Thanks @thevuong! - refactor(tailwind-variants): streamline variant resolution

  • 6c3ac44 Thanks @thevuong! - Normalize import statement order and package.json key order repo-wide via the new oxfmt sortImports/sortPackageJson settings — purely mechanical, no runtime behavior change.

  • d07b567 Thanks @thevuong! - Simplify the extend resolver type by dropping a redundant generic parameter — public behavior and inference are unchanged.

  • 649cf5a Thanks @thevuong! - fix(types): default the variant schema to a keyless record when tv is called without variants, so className/class accept full ClassValue inputs (arrays, objects) instead of being narrowed by a string index signature

0.4.0-canary.6

0.4.0-canary.5


Showing the latest 20 releases; the full changelog on GitHub has 40 more releases.