@use-pico/cls
Version:
Type-safe, composable styling system for React, Vue, Svelte, and vanilla JS
1,191 lines (1,169 loc) • 41.2 kB
JavaScript
import { twMerge } from 'tailwind-merge';
import { jsx } from 'react/jsx-runtime';
import { createContext, useContext, useMemo } from 'react';
/**
* Runtime deduplication utility that maintains type compatibility
* with the type-level deduplication
*/
function dedupe(arr) {
const seen = new Set();
const result = [];
for (const item of arr) {
if (!seen.has(item)) {
seen.add(item);
result.push(item);
}
}
return result;
}
/**
* Concatenates and deduplicates two arrays
*/
function dedupeConcat(a, b) {
const combined = [
...a,
...b,
];
const seen = new Set();
const result = [];
for (const item of combined) {
if (!seen.has(item)) {
seen.add(item);
result.push(item);
}
}
return result;
}
/**
* Merges two variant objects, combining arrays for duplicate keys with deduplication.
* This follows the same merging logic used throughout the CLS system
* where variant values are accumulated rather than overridden, but duplicates are removed.
*
* @template TVariantsA - The first variant object type
* @template TVariantsB - The second variant object type
* @param variantsA - The base variant object
* @param variantsB - The variant object to merge in
* @returns A merged variant object with combined and deduplicated arrays for duplicate keys
*
* @example
* ```typescript
* const merged = mergeVariants(
* { size: ["sm", "md"] },
* { size: ["lg", "md"], tone: ["light", "dark"] }
* );
* // Result: { size: ["sm", "md", "lg"], tone: ["light", "dark"] }
* ```
*/
function mergeVariants(variantsA, variantsB) {
const result = {
...variantsA,
};
for (const [key, values] of Object.entries(variantsB)) {
if (key in result && result[key]) {
result[key] = [
...new Set([
...result[key],
...values,
]),
];
continue;
}
// Add new keys (already deduplicated if they come from another merge)
result[key] = [
...new Set(values),
];
}
return result;
}
/**
* Filters out undefined values from an object
*/
function filter(input) {
if (!input) {
return {};
}
const result = {};
for (const [key, value] of Object.entries(input)) {
if (value !== undefined) {
result[key] = value;
}
}
return result;
}
const cleanup = (tweak) => {
return {
...tweak,
token: filter(tweak.token),
slot: filter(tweak.slot),
variant: filter(tweak.variant),
};
};
const tvc = (...classLists) => {
const merged = twMerge(...classLists);
if (!merged) {
return "";
}
const tokens = merged.split(/\s+/).filter(Boolean);
const seen = new Set();
const outRev = [];
for (let i = tokens.length - 1; i >= 0; i--) {
const token = tokens[i];
if (token === undefined) {
continue;
}
if (!seen.has(token)) {
seen.add(token);
outRev.push(token);
}
}
return outRev.reverse().join(" ");
};
/**
* merge(tweak1, tweak2, ...)
*
* Merges multiple tweak objects with specific behavior:
* - Variants are replaced by default (regardless of "override" flag)
* - Slots are merged by default until override is explicitly set
* - Tokens are merged until some tweak has override: true, then that tweak resets and starts over
* - Override flag only affects slots/tokens explicitly set, not the whole tweak
*/
function tweaks(...tweaks) {
if (!tweaks || tweaks.length === 0) {
return {};
}
const list = tweaks
.flat(10)
.filter((tweak) => tweak !== undefined);
if (list.length === 0) {
return {};
}
const [root, ...rest] = list;
return rest.reduce((acc, current) => {
const override = current.override ?? false;
const clear = current.clear ?? false;
if (clear) {
return {
token: filter(current.token),
slot: filter(current.slot),
variant: filter(current.variant),
};
}
return {
token: merge(acc.token ?? {}, current.token ?? {}, override),
slot: merge(acc.slot ?? {}, current.slot ?? {}, override),
variant: {
...filter(acc.variant),
...filter(current.variant),
},
};
}, root);
}
const merge = (acc, current, override) => {
const result = filter({
...acc,
});
Object.entries(filter(current)).forEach(([key, value]) => {
if (!value) {
return;
}
// Check both top-level override and What-level override
const hasOverride = override || value.override === true;
if (hasOverride) {
result[key] = value;
return;
}
if (!result[key]) {
result[key] = value;
return;
}
if ("class" in result[key] && "class" in value) {
result[key].class = [
result[key].class,
value.class,
];
}
if ("token" in result[key] && "token" in value) {
result[key].token = dedupe([
...result[key].token,
...value.token,
]);
}
});
return result;
};
// Standalone function to compute variants
function withVariants(tweak, { contract, definition, }) {
// Build inheritance chain (base -> child order)
const layers = [];
let current = contract;
let currentDef = definition;
while (current && currentDef) {
layers.unshift({
contract: current,
definition: currentDef,
});
current = current["~use"];
currentDef = current?.["~definition"];
}
// Merge defaults from ALL layers in inheritance order
const defaultVariant = {};
// Process layers in inheritance order (base first, child last)
for (const { definition } of layers) {
// Merge defaults (child overrides base)
Object.assign(defaultVariant, definition.defaults);
}
return {
...defaultVariant,
...tweak?.variant,
};
}
// -----------------------------------------------------------------------------
// Compile-time utilities (pure)
// -----------------------------------------------------------------------------
const alwaysTrue = () => {
return true;
};
function compilePredicate(match) {
if (!match) {
return alwaysTrue;
}
const keys = Object.keys(match);
return function pred(variant) {
// Using loose index access intentionally to avoid narrowing gymnastics
// Keys come directly from provided match object
const variantAny = variant;
return !keys.some((key) => {
return variantAny[key] !== match[key];
});
};
}
function buildLayers(contract, definition) {
const layers = [];
let currentContract = contract;
let currentDefinition = definition;
while (currentContract && currentDefinition) {
layers.unshift({
contract: currentContract,
definition: currentDefinition,
});
currentContract = currentContract["~use"];
currentDefinition = currentContract
? currentContract["~definition"]
: undefined;
}
return layers;
}
function collectSlotKeys(layers) {
return Array.from(new Set(layers.flatMap(({ contract }) => contract.slot)));
}
function indexRulesBySlot(layers) {
const rules = layers.flatMap(({ definition }) => {
return definition.rules;
});
const pairs = rules.flatMap((rule) => {
const predicate = compilePredicate(rule.match);
const isOverride = rule.override === true;
const slotMap = rule.slot ?? {};
return Object.entries(slotMap)
.filter((entry) => {
return entry[1] !== undefined;
})
.map(([slotKey, whatValue]) => {
const compiledRule = {
predicate,
what: whatValue,
override: isOverride,
};
return {
slotKey,
compiledRule,
};
});
});
return pairs.reduce((accumulator, pair) => {
const list = accumulator[pair.slotKey] ?? [];
list.push(pair.compiledRule);
accumulator[pair.slotKey] = list;
return accumulator;
}, Object.create(null));
}
function indexRulesByToken(layers) {
const rules = layers.flatMap(({ definition }) => {
return definition.rules;
});
const pairs = rules.flatMap((rule) => {
const predicate = compilePredicate(rule.match);
const isOverride = rule.override === true;
const tokenMap = rule.token ?? {};
return Object.entries(tokenMap)
.filter((entry) => {
return entry[1] !== undefined;
})
.map(([tokenKey, whatValue]) => {
const compiledRule = {
predicate,
what: whatValue,
override: isOverride,
};
return {
tokenKey,
compiledRule,
};
});
});
return pairs.reduce((accumulator, pair) => {
const list = accumulator[pair.tokenKey] ?? [];
list.push(pair.compiledRule);
accumulator[pair.tokenKey] = list;
return accumulator;
}, Object.create(null));
}
function buildTokenTable(layers) {
return layers.reduce((accumulator, item) => {
const layerTokensEntries = Object.entries(item.definition.token ?? {}).filter(([, value]) => {
return value !== undefined;
});
const layerTokens = Object.fromEntries(layerTokensEntries);
return Object.assign(Object.create(accumulator), layerTokens);
}, Object.create(null));
}
function compileContract(contract, definition) {
const layers = buildLayers(contract, definition);
const slotKeys = collectSlotKeys(layers);
const rulesBySlot = indexRulesBySlot(layers);
const rulesByToken = indexRulesByToken(layers);
const tokensProto = buildTokenTable(layers);
return {
layers,
slotKeys,
rulesBySlot,
rulesByToken,
tokensProto,
};
}
// -----------------------------------------------------------------------------
// Resolver (factory) with cycle detection and caching
// -----------------------------------------------------------------------------
function createResolver(baseTokenTable) {
const baseResolvedTokenCache = Object.create(null);
/**
* Push class names into the accumulator, supporting string | string[] | nested arrays.
* Order is preserved; falsy entries are ignored.
*/
function pushClassNames(accumulator, classInput) {
if (Array.isArray(classInput)) {
classInput.forEach((value) => {
pushClassNames(accumulator, value);
});
return;
}
if (typeof classInput === "string" && classInput) {
accumulator.push(classInput);
}
}
function resolve(whatValue, tokenTable = baseTokenTable, visiting = new Set(), localCache) {
const out = [];
const cacheRef = tokenTable === baseTokenTable ? baseResolvedTokenCache : localCache;
if ("token" in whatValue && whatValue.token) {
whatValue.token.forEach((tokenKey) => {
if (visiting.has(tokenKey)) {
throw new Error(`Circular dependency detected in token references: ${Array.from(visiting).join(" -> ")} -> ${tokenKey}`);
}
if (cacheRef?.[tokenKey]) {
out.push(...cacheRef[tokenKey]);
return;
}
const tokenDefinition = tokenTable[tokenKey];
if (!tokenDefinition) {
return;
}
visiting.add(tokenKey);
const resolved = resolve(tokenDefinition, tokenTable, visiting, localCache);
visiting.delete(tokenKey);
if (cacheRef) {
cacheRef[tokenKey] = resolved.classes;
}
out.push(...resolved.classes);
});
}
if ("class" in whatValue && whatValue.class !== undefined) {
pushClassNames(out, whatValue.class);
}
return {
classes: out,
override: whatValue.override === true,
};
}
return {
resolve,
baseResolvedTokenCache,
};
}
// -----------------------------------------------------------------------------
// Public API – cls
// -----------------------------------------------------------------------------
function cls(contract, definition) {
contract["~definition"] = definition;
// Precompile layers, slots, rules and base token table
const { slotKeys, rulesBySlot, rulesByToken, tokensProto } = compileContract(contract, definition);
return {
create(...tweak) {
const $tweak = cleanup(tweaks(...tweak));
const variant = withVariants($tweak, {
contract,
definition,
});
// Global token table with overrides via prototype chain
const tokenTable = $tweak?.token
? Object.assign(Object.create(tokensProto), $tweak.token)
: tokensProto;
// Resolver bound to the token table after global overrides
const { resolve } = createResolver(tokenTable);
// Cache of built slot functions and result strings per local config
const slotFunctionCache = Object.create(null);
const resultCache = new Map();
// --- tweak key helpers (identity + stable stringify) ---
const tweakIdentityIds = new WeakMap();
let tweakIdentitySeq = 0;
/**
* Recursively stringify an arbitrary POJO with sorted keys for determinism.
* Mirrors JSON semantics for primitives and arrays; skips undefined values.
*
* TODO Revisit this - we know what the input is
*/
function stableStringifySorted(value) {
if (value === null || typeof value !== "object") {
return JSON.stringify(value);
}
if (Array.isArray(value)) {
const items = value.map((item) => stableStringifySorted(item));
return `[${items.join(",")}]`;
}
const record = value;
const keys = Object.keys(record).sort();
const parts = keys
.filter((key) => record[key] !== undefined)
.map((key) => {
return `${JSON.stringify(key)}:${stableStringifySorted(record[key])}`;
});
return `{${parts.join(",")}}`;
}
/**
* Stable, slot-scoped cache key for local tweak.
* Strategy:
* 1) Identity-based key via WeakMap for reused object instances (fast path).
* 2) Deterministic structural part that includes only fields that affect this slot.
*/
function computeKeyFromLocal(slotName, local) {
if (!local) {
return `${slotName}|__no_config__`;
}
// Identity key
const localObject = local;
let identityId = tweakIdentityIds.get(localObject);
if (identityId === undefined) {
tweakIdentitySeq = tweakIdentitySeq + 1;
identityId = tweakIdentitySeq;
tweakIdentityIds.set(localObject, identityId);
}
// Slot-scoped normalized view
const normalized = {};
if (local.variant) {
normalized.variant = local.variant;
}
const localSlotTable = local.slot;
if (localSlotTable && Object.hasOwn(localSlotTable, slotName)) {
normalized.slot = {
[slotName]: localSlotTable[slotName],
};
}
if (local.token) {
normalized.token = local.token;
}
try {
const structural = stableStringifySorted(normalized);
return `${slotName}|id:${identityId}|${structural}`;
}
catch {
return null;
}
}
const handler = {
get(_, slotName) {
if (slotName in slotFunctionCache) {
return slotFunctionCache[slotName];
}
const slotKeyStr = String(slotName);
const slotFunction = (...local) => {
const $local = cleanup(tweaks(...local));
// Merge variants (shallow, only defined values)
const localEffective = {
...variant,
};
if ($local?.variant) {
Object.entries($local.variant)
.filter(([, value]) => value !== undefined)
.forEach(([key, value]) => {
localEffective[key] = value;
});
}
const key = computeKeyFromLocal(slotKeyStr, $local);
if (key !== null) {
const cached = resultCache.get(key);
if (cached !== undefined) {
return cached;
}
}
// --- Apply token rules to produce a per-call token overlay ---
const tokenRulesOverlayEntries = Object.entries(rulesByToken)
.map(([tokenKey, compiledRules]) => {
const matches = compiledRules.map((rule) => {
return rule.predicate(localEffective);
});
const anyMatch = matches.some(Boolean);
if (!anyMatch) {
return null;
}
const lastOverrideIdx = compiledRules.reduce((acc, rule, index) => {
if (matches[index] && rule.override) {
return index;
}
return acc;
}, -1);
const startIndex = lastOverrideIdx >= 0 ? lastOverrideIdx : 0;
const accumulated = compiledRules
.slice(startIndex)
.reduce((acc, rule, idx) => {
if (!matches[startIndex + idx]) {
return acc;
}
const resolved = resolve(rule.what);
if (resolved.override) {
acc.classes =
resolved.classes.slice();
acc.tokens = [];
}
else {
acc.classes =
acc.classes.concat(resolved.classes);
}
if (rule.what.token) {
acc.tokens = acc.tokens.concat(rule.what
.token.filter(Boolean));
}
return acc;
}, {
classes: [],
tokens: [],
});
const composed = {};
if (accumulated.classes.length > 0) {
composed.class =
accumulated.classes;
}
if (accumulated.tokens.length > 0) {
composed.token =
accumulated.tokens;
}
return [
tokenKey,
composed,
];
})
.filter((x) => Boolean(x));
// Compose token tables with correct precedence (base < rules < create < local)
const baseWithRuleOverlay = tokenRulesOverlayEntries.length > 0
? Object.assign(Object.create(tokensProto), Object.fromEntries(tokenRulesOverlayEntries))
: Object.create(tokensProto);
const tokenTableWithRuleOverlay = $tweak?.token
? Object.assign(Object.create(baseWithRuleOverlay), $tweak.token)
: baseWithRuleOverlay;
// Local token overlay and local cache for overlay resolution
let activeTokens = tokenTableWithRuleOverlay;
let localResolvedCache;
if ($local?.token &&
Object.keys($local.token).length > 0) {
activeTokens = Object.assign(Object.create(tokenTableWithRuleOverlay), $local.token);
localResolvedCache = Object.create(null);
}
// Read per-slot customizations
const localSlotWhat = $local?.slot
? $local.slot[slotName]
: undefined;
const configSlotWhat = $tweak?.slot
? $tweak.slot[slotName]
: undefined;
const slotRules = rulesBySlot[slotKeyStr] ?? [];
// Fast path: nothing contributes
const nothingContributes = [
slotRules.length === 0,
!localSlotWhat,
!configSlotWhat,
].every(Boolean);
if (nothingContributes) {
if (key !== null) {
resultCache.set(key, "");
}
return "";
}
// Evaluate predicates and find last matching override index
const matches = slotRules.map((rule) => {
return rule.predicate(localEffective);
});
const anyMatch = matches.some(Boolean);
const lastOverrideIdx = slotRules.reduce((accumulator, rule, index) => {
if (matches[index] && rule.override) {
return index;
}
return accumulator;
}, -1);
let acc = [];
if (anyMatch) {
const startIndex = lastOverrideIdx >= 0 ? lastOverrideIdx : 0;
const visiting = new Set();
slotRules.slice(startIndex).forEach((rule, idx) => {
if (!matches[startIndex + idx]) {
return;
}
const resolvedWhat = resolve(rule.what, activeTokens, visiting, localResolvedCache);
if (resolvedWhat.override) {
acc = resolvedWhat.classes;
}
else {
acc = acc.concat(resolvedWhat.classes);
}
});
}
// Append slot-level whats (config first, user last)
if (configSlotWhat) {
const resolvedWhat = resolve(configSlotWhat, activeTokens, new Set(), localResolvedCache);
if (resolvedWhat.override) {
acc = resolvedWhat.classes;
}
else {
acc = acc.concat(resolvedWhat.classes);
}
}
if (localSlotWhat) {
const resolvedWhat = resolve(localSlotWhat, activeTokens, new Set(), localResolvedCache);
if (resolvedWhat.override) {
acc = resolvedWhat.classes;
}
else {
acc = acc.concat(resolvedWhat.classes);
}
}
const out = acc.length === 0 ? "" : tvc(acc);
if (key !== null) {
resultCache.set(key, out);
}
return out;
};
slotFunctionCache[slotName] = slotFunction;
return slotFunctionCache[slotName];
},
ownKeys() {
return slotKeys;
},
getOwnPropertyDescriptor() {
return {
enumerable: true,
configurable: true,
};
},
};
return {
slots: new Proxy({}, handler),
variant,
};
},
extend(childContract, childDefinitionFn) {
childContract["~use"] = contract;
return cls(childContract, childDefinitionFn);
},
use(sub) {
return sub;
},
tweak(...tweak) {
return tweaks(...tweak);
},
contract,
definition,
};
}
/**
* Creates a definition builder with the given state
*/
function builder$1(state) {
return {
token(token) {
return builder$1({
...state,
token,
});
},
tokens: {
rule(match, token, override = false) {
return builder$1({
...state,
rules: [
...state.rules,
{
match,
token,
override,
},
],
});
},
switch(key, whenTrue, whenFalse) {
return builder$1({
...state,
rules: [
...state.rules,
{
match: {
[key]: true,
},
token: whenTrue,
override: false,
},
{
match: {
[key]: false,
},
token: whenFalse,
override: false,
},
],
});
},
match(key, value, token, override = false) {
return builder$1({
...state,
rules: [
...state.rules,
{
match: {
[key]: value,
},
token,
override,
},
],
});
},
},
root(slot, override = false) {
return builder$1({
...state,
rules: [
...state.rules,
{
match: undefined,
slot: {
_target: "slot",
...slot,
},
override,
},
],
});
},
rule(match, slot, override = false) {
return builder$1({
...state,
rules: [
...state.rules,
{
match,
slot,
override,
},
],
});
},
switch(key, whenTrue, whenFalse) {
return builder$1({
...state,
rules: [
...state.rules,
{
match: {
[key]: true,
},
slot: whenTrue,
override: false,
},
{
match: {
[key]: false,
},
slot: whenFalse,
override: false,
},
],
});
},
match(key, value, slot, override = false) {
return builder$1({
...state,
rules: [
...state.rules,
{
match: {
[key]: value,
},
slot,
override,
},
],
});
},
defaults(defaults) {
return builder$1({
...state,
defaults,
});
},
cls() {
return cls(state.contract, {
token: state.token || {},
rules: state.rules,
defaults: state.defaults || {},
});
},
};
}
const definition = (contract, use) => {
return builder$1({
contract,
rules: [],
use,
});
};
/**
* Creates a contract builder with the given state
*/
function builder(state) {
return {
tokens(tokens) {
return builder({
...state,
tokens: dedupeConcat(state.tokens, tokens),
});
},
token(token) {
return builder({
...state,
tokens: dedupeConcat(state.tokens, [
token,
]),
});
},
slots(slots) {
return builder({
...state,
slot: dedupeConcat(state.slot, slots),
});
},
slot(slot) {
return builder({
...state,
slot: dedupeConcat(state.slot, [
slot,
]),
});
},
variants(variants) {
return builder({
...state,
variant: mergeVariants(state.variant, variants),
});
},
variant(name, values) {
return builder({
...state,
variant: mergeVariants(state.variant, {
[name]: values,
}),
});
},
bool(name) {
return builder({
...state,
variant: mergeVariants(state.variant, {
[name]: [
"bool",
],
}),
});
},
build() {
const { use, ...contract } = state;
return {
...contract,
/**
* Important piece - this will enable inheritance.
*/
"~use": use,
/**
* Definition - not yet
*/
"~definition": undefined,
};
},
def() {
return definition(this.build(), state.use);
},
};
}
function contract(use) {
return builder({
tokens: [],
slot: [],
variant: {},
use,
});
}
/**
* Context for providing token tweaks to child components.
* This affects only tokens, keeping naming clear.
*/
const TokenContext = createContext({});
/**
* Provider component that extracts tokens from a CLS instance and provides them via context.
*
* This component takes a CLS instance and extracts its token definitions to provide them
* to child components via TokenContext. This makes the API more intuitive as you can pass
* a complete CLS instance rather than manually extracting tokens.
*
* @template TContract - The contract type of the CLS instance
* @param cls - CLS instance to extract tokens from
* @param children - Child components that will receive the token context
*
* @example
* ```tsx
* // Pass a theme CLS instance
* <TokenProvider cls={ThemeCls}>
* <Button>Click me</Button>
* </TokenProvider>
* ```
*
* @example
* ```tsx
* // Pass a component CLS instance for token overrides
* <TokenProvider cls={CustomButtonCls}>
* <Button>Custom styled button</Button>
* </TokenProvider>
* ```
*/
function TokenProvider({ cls, children, }) {
return (jsx(TokenContext, { value: cls?.definition?.token, children: children }));
}
/**
* Hook to access the TokenContext.
*
* Returns the current token tweaks provided via context,
* or undefined if no provider is present. Only tokens are affected by this context.
*/
function useTokenContext() {
return useContext(TokenContext);
}
/**
* React context for "user-land" tweaks.
*
* Purpose:
* - Components using `useCls` automatically read from the global `TokenContext`.
* - `TweakContext` is the second layer consulted by `useCls` for overrides.
* - Pure user-land tweaks take precedence over all other sources.
*
* Use this to scope tweak overrides to a subtree. Values are merged by `useCls`
* after `TokenContext` and before internal defaults, with direct user tweaks
* winning on conflicts.
*
* @returns React context carrying tweak values for any contract.
*
* @example
* ```tsx
* <TweakContext value={{ size: ["lg"], intent: ["primary"] }}>
* <Child />
* </TweakContext>
*
* // Inside a descendant component
* const tweak = useTweakContext();
* ```
*/
const VariantContext = createContext({});
const useVariantContext = () => {
return useContext(VariantContext);
};
/**
* React hook to create a CLS kit from a cls instance, merging user tweaks with context.
*
* **Tweak Precedence (highest to lowest):**
* 1. **User tweak** - Direct user customization passed as parameter
* 2. **Context tweak** - Automatically merged from TokenContext and VariantContext
*
* **Context Integration:**
* - Automatically subscribes to `TokenContext` for token overrides
* - Automatically subscribes to `VariantContext` for variant overrides
* - Context values have lower precedence than user-provided tweaks
*
* **Tweak Merging:**
* - Uses the `tweaks()` function with parameter-based syntax
* - User tweak takes precedence over context values
* - Undefined values are automatically filtered out
*
* @template TContract - CLS contract type defining tokens, slots, and variants
* @param cls - CLS instance to create kit from
* @param tweaks - Optional user-provided tweak (variant/slot/token/override)
* @returns A `Cls.Kit<TContract>` with slot functions (e.g., `slots.root()`) and resolved variants
*
* @example
* ```tsx
* // Basic usage with user tweak
* const { slots, variant } = useCls(ButtonCls, {
* variant: { size: "lg", tone: "primary" },
* slot: { root: { class: ["font-bold"] } }
* });
* ```
*
* @example
* ```tsx
* // Without user tweak - uses context only
* const { slots, variant } = useCls(ButtonCls);
* ```
*
* @example
* ```tsx
* // User tweak overrides context values
* const { slots, variant } = useCls(ButtonCls, {
* variant: { size: "sm" } // Overrides context variant.size
* });
* ```
*/
function useCls(cls, ...tweaks) {
const token = useTokenContext();
const variant = useVariantContext();
return cls.create({
token,
variant,
}, ...tweaks);
}
/**
* Memoized version of `useCls` with explicit dependency control for performance optimization.
*
* **Tweak Precedence (highest to lowest):**
* 1. **User tweak** - Direct user customization passed as parameter
* 2. **Context tweak** - Automatically merged from TokenContext and VariantContext
*
* **Context Integration:**
* - Automatically subscribes to `TokenContext` for token overrides
* - Automatically subscribes to `VariantContext` for variant overrides
* - Context values have lower precedence than user-provided tweaks
*
* **Memoization Control:**
* - Uses `useMemo` with the provided dependency list
* - Recreates the CLS kit only when dependencies change
* - Essential for performance when tweaks depend on props or state
*
* **Tweak Merging:**
* - Uses the `tweaks()` function with parameter-based syntax
* - User tweak takes precedence over context values
* - Undefined values are automatically filtered out
*
* @template TContract - CLS contract type defining tokens, slots, and variants
* @param cls - CLS instance to create kit from
* @param tweaks - Optional user-provided tweak (variant/slot/token/override)
* @param deps - Dependency list controlling memoization (defaults to empty array)
* @returns A memoized `Cls.Kit<TContract>` with slot functions (e.g., `slots.root()`) and resolved variants
*
* @example
* ```tsx
* // Basic usage with memoization based on props
* const { slots, variant } = useClsMemo(
* ButtonCls,
* { variant: { size, tone } },
* [size, tone] // Re-memoize when size or tone changes
* );
* ```
*
* @example
* ```tsx
* // Complex tweak with multiple dependencies
* const { slots, variant } = useClsMemo(
* ButtonCls,
* {
* variant: { size, tone, disabled },
* slot: { root: { class: disabled ? ["opacity-50"] : [] } }
* },
* [size, tone, disabled] // Re-memoize when any dependency changes
* );
* ```
*
* @example
* ```tsx
* // No user tweak - memoizes context-only result
* const { slots, variant } = useClsMemo(
* ButtonCls,
* undefined,
* [] // Never re-memoize (context changes handled internally)
* );
* ```
*/
function useClsMemo(cls, tweaks, deps = []) {
const token = useTokenContext();
const variant = useVariantContext();
return useMemo(() => cls.create(
/**
* Context tweak has lowest priority
*/
{
token,
variant,
}, tweaks),
// biome-ignore lint/correctness/useExhaustiveDependencies: User driven
deps);
}
const VariantProvider = ({
/**
* Used only to infer types
*/
cls: _, variant, inherit = false, children, }) => {
/**
* A little lie here, but in general it should be somehow OK.
*/
const parent = useVariantContext();
return (jsx(VariantContext, { value: tweaks([
{
variant,
},
inherit
? {
variant: parent,
}
: undefined,
]).variant ?? {}, children: children }));
};
/**
* React wrapper - useful for preparing type-safe variants for React components.
*/
const wrap = (cls) => {
return {
VariantProvider(props) {
return (jsx(VariantProvider, { cls: cls, ...props }));
},
};
};
/**
* Just a true boolean.
*
* I don't like boolean hells, so this is a little compromise.
*
* You _can_ use this for rules, when you want override to keep it
* explicit, purely optional for you.
*/
const OVERRIDE = true;
export { OVERRIDE, TokenProvider, VariantProvider, cls, contract, definition, tvc, useCls, useClsMemo, useTokenContext, useVariantContext, wrap };
//# sourceMappingURL=index.js.map