@edux-design/utils
Shared Edux class composition and React child helpers.
import { cx, filterChildrenByDisplayName, getSlot } from "@edux-design/utils";
cx("p-8 text-sm", condition && "px-24", "text-fg-base");
// p-8 text-sm px-24 text-fg-base
cx("px-24 py-8", "p-16");
// p-16
cx(...inputs) retains the existing conditional input contract: strings, conditional
objects, numbers and nested arrays; falsy inputs are omitted. It then applies an
independently authored Edux input flattener and utility conflict resolver.
Neither clsx nor tailwind-merge is a runtime or declaration dependency.
The flattener uses an explicit stack for nested arrays, preserves inherited
enumerable object keys, and ignores unsupported primitive inputs.
Cyclic arrays are invalid inputs and throw instead of looping indefinitely;
reusing an array in separate positions is supported.
The declaration accepts unknown inputs, preserving readonly tuples and values
that the existing normalisation ignores rather than introducing a type-only
restriction.
The other exports are unchanged:
filterChildrenByDisplayName(children, displayName)selects matching direct React children, including named ref-forwarding components.getSlot(children, slotComponent)returns the matched direct child's content.
The package root requires React and React DOM ^19.1.0 because it
exports React child helpers. The class resolver itself imports no React or DOM
APIs and runs during server rendering without browser globals. ESM, CJS and own
strict public declarations are provided at the package root. Deep imports are not
a supported API.
Conflict semantics
Within the same modifier and importance scope, the last utility wins for its
declaration group. Shorthand replacement is directional: a later p-16 replaces
earlier px-24 py-8, while a later px-24 retains the vertical part of p-16.
Logical and physical edges remain independent where writing-mode differences
matter. Width/height, font size/colour, border width/colour, shadow size/colour,
gradient stops, filters and masks have separate declaration groups.
Normal pseudo-classes commute within a selector segment. Pseudo-elements,
wildcard variants and arbitrary selector variants preserve their relative order.
Both !p-4 and p-4! are supported; important and ordinary declarations are
separate scopes. Arbitrary values, typed custom properties, arbitrary CSS
properties, negative utilities and the existing Edux semantic colour classes are
recognised. An arbitrary CSS property conflicts with the same property, without
guessing that it overrides an unrelated ordinary utility.
Unknown application and custom theme classes are retained, including repeated
classes. For example, custom animate-drawer-* and cursor-pagination-* classes
are not inferred to override standard animation/cursor utilities. The resolver
does not load a consumer's Tailwind configuration or inspect browser stylesheets.
Consumers who introduce custom utility names retain the established default
cx behaviour rather than getting speculative conflict removal.
The established colour and font-family namespaces are deliberately open: a
bg-custom name is still a colour utility, for example. Unknown-class retention
does not override those existing default namespace rules.
Compatibility includes a historical important-prefix font/line-height quirk:
!text-lg/8 and text-lg/8! remain distinct in the established conflict model.
The suffix form follows normal font-size/line-height grouping. This preserves
the accepted output of the previous public API; it is not a recommendation to
mix important syntaxes.
Ownership and validation
The resolver was written from CSS property relationships, Edux requirements and independent public black-box observations. No third-party merge implementation, declaration or test code was read, copied or translated. Retained dependencies still carry their own licence terms.
The independent acceptance corpus covers 32,757 conditional, observed and
extended Tailwind 4 cases plus large/deep adversarial inputs. Its portable
repository pool and the separately recorded optional Payload snapshot are
distinguished in the acceptance receipts. Native generated-CSS
fixtures compare computed styles, pseudo-element paint and measured dimensions
with the accepted public Utils implementation. See the coordinator's
docs/original-drag-and-classes/class-acceptance/ evidence for exact candidate
hashes and browser coverage; source diagnostics are not installed-artifact proof.
The owned conditional input layer also has colocated edge-case tests and an optional 50,000-case black-box comparison against an ordinary old consumer:
node packages/utils/test/compare-class-inputs.mjs /absolute/old-utils-consumer
That comparison executes only the old dependency's public function. The normal Utils test suite does not require clsx to be installed.
The resolver keeps bounded FIFO caches: at most 500 complete inputs of up to 16,384 characters and 2,000 classified base tokens of up to 1,024 characters. Larger values are resolved without caching. Parsing is iterative, with no DOM subscriptions, timers or recursive arbitrary-value traversal. Benchmarks are descriptive; extreme 10,000-token cases are not an interaction latency guarantee.
npm run test --workspace=@edux-design/utils
npm run lint --workspace=@edux-design/utils
npm run check-types --workspace=@edux-design/utils
npm run build --workspace=@edux-design/utils