feat(ui): drop classname utilities — object-form css() + token.* helper#2769
Merged
Conversation
Plans the deletion of the tailwind-ish token-string vocabulary from css()/variants(). Replaces it with plain CSS-property objects (camelCase), keeping scoped-styles as the API while removing the LLM-hostile shorthand. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Defines the typed object shape for the new css()/variants() API — camelCase CSS properties with string | number values and nested &/@-prefixed selectors. The token-string array shape remains in place until Phase 4; this just introduces the new type alongside. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Set of CSS properties that skip the px suffix was duplicated-in-waiting: about to be needed by the object-form css() walker and the Rust compiler extractor. Move it into packages/ui/src/css/unitless-properties.ts as the single TS source of truth. Rust will mirror it in a follow-up task with parity enforced by a lint script. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the runtime walker for object-form blocks. When a block's value is a plain object instead of a StyleEntry[], css() now: - camelCase → kebab-case property names (vendor prefixes handled) - appends 'px' to numeric values except for unitless properties and 0 - passes CSS custom properties (--*) through unchanged - recurses into & selectors (stacking with the class) and @ at-rules A fingerprint (sorted keys, recursed) feeds the class-name hash so key order doesn't change the output. Array-form blocks still work so the codebase keeps compiling during migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Widens VariantsConfig.base, each variant option, and compoundVariants[].styles to accept either StyleEntry[] or a StyleBlock object. Runtime routes each block through css(), which already dispatches per shape. deriveConfigKey and the emptiness check are updated to walk either representation. Mixed configs (array base, object options) work during migration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Rust compiler now accepts `css({ card: { padding: 16, color: 'red' } })`
as a static value, mirroring the TypeScript runtime's renderStyleBlock path.
This means object-form css() calls get AOT-extracted in production builds
just like the legacy array-form.
Adds a parallel StyleBlock AST (StyleBlockNode::{Declaration, Selector})
and a renderer that mirrors the TS implementation byte-for-byte:
camelCase → kebab-case, unitless property table via new css_unitless
module (mirror of packages/ui/src/css/unitless-properties.ts), custom
property passthrough, & resolution, @-rule wrapping, numeric auto-px.
Also fixes camel_to_kebab to match TS behavior for vendor prefixes
(WebkitTransform → -webkit-transform, previously produced webkit-transform).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The TS and Rust unitless tables diverging silently would cause CSS-output drift between dev-time runtime and AOT compilation (e.g. opacity: 1 → opacity: 1px in compiled builds). A runtime test that reads the Rust source and diffs against the TS set blocks pushes where the two drift. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Walkthrough migration for Phase 1 of the token-string drop. Replaces every `'p:4'` / `'bg:primary'` entry in `hero.tsx`'s two `css()` calls with plain camelCase `StyleBlock` objects — raw `var(--…)` references for theme tokens, inline constants for the expanded `transition:colors` and `shadow:2xl` strings so parity is obvious at the call site. `vtz run typecheck` on `packages/landing` median 0.415s → 0.355s (no regression; see `reviews/drop-classname-utilities/phase-01-perf-baseline.md` for the raw measurements). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wire TS↔Rust UNITLESS_PROPERTIES parity into `turbo run lint` via `packages/ui/scripts/check-unitless-parity.ts` so a file-scoped `vtz test` filter cannot silently skip the parity check. Resolves Phase 1 adversarial review Blocker 1. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Rust compiler hashes `filePath::blockName`; the TS runtime was hashing `filePath::blockName::fingerprint`. For a real filePath the two sides produced different class names, so SSR/HMR hybrid output could emit ghost classes. Drop the fingerprint from the TS runtime when `filePath` is a real source path. Keep it only for the `__runtime__` default — the one case the fingerprint was designed for (disambiguating ad-hoc `css()` calls sharing a block name in the same process). Lock the invariant behind a shared-fixture parity test: TS `class-name-parity.test.ts` and Rust `class_name_parity_matches_ts_runtime` assert the same expected hashes. Any drift in either implementation fails both tests. Resolves Phase 1 adversarial review Blocker 2. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pre-existing bug: the original token form was \`'opacity:40'\` which the token resolver passed through as raw \`opacity: 40\`, clamped by the CSS spec to fully opaque. The ping fade never rendered. Fix the value to 0.4 (clearly the authored intent — every other opacity in the file is 0/1 or a fraction). Resolves Phase 1 adversarial review Blocker 3. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Records the local adversarial review of Phase 1, the 126-test-failure triage confirming no Phase 1 regressions, and the deferred should-fix follow-up list. Per \`.claude/rules/local-phase-workflow.md\` these live in \`reviews/\` and are not committed to main (working artefacts). They exist on the feature branch for now so the final PR description can link them. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduces `token` — a Proxy-based helper that lazily produces `var(--<namespace>-<key>)` strings for theme tokens. Three augmentation interfaces (`VertzThemeColors`, `VertzThemeSpacing`, `VertzThemeFonts`) let projects narrow each namespace to their concrete theme; a conditional in `VertzThemeTokens` falls back to a `TokenPath` string intersection when no augmentation is present. Also updates `isStyleBlock()` to exclude token proxies so fingerprinting and recursive rendering treat them as primitive CSS values. Phase 2 of drop-classname-utilities. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The token export is a Proxy object (not a function). The subpath-exports whitelist, reference test, and main-barrel re-export test are extended to recognize it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Re-export VertzThemeColors/VertzThemeSpacing/VertzThemeFonts from @vertz/ui, @vertz/ui/css, and @vertz/ui/css/public so the documented `declare module '@vertz/ui'` augmentation path actually narrows types. - Guard variants() serializeBlockValue against token proxies — distinct tokens used as variant option values would previously collapse to the same fingerprint (both `ownKeys() === []`), causing class-name collisions across independent variants() calls. - Add token-augmentation-barrel.test-d.ts verifying the @vertz/ui augmentation path (complementing the defining-module test). - Add token-in-variants.test.ts with 5 regressions: variant option CSS, shade difference disambiguation, stable hashing, collision regression across variants() calls, compoundVariants token emission. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 3 scope is 138 files / 3014 shorthand strings across 8 packages. Per .claude/rules/phase-implementation-plans.md, break this into 7 tasks (migration script, theme-shadcn, landing, examples+sites, stragglers, test cleanup, review) with max 5 files per task. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Route object-form style blocks through a StrictStyleBlock mapped type so unknown keys collapse to `never`. Typos like `bacgroundColor` now raise a compile-time error at the call site through the generic inference path, where plain excess-property checking was bypassed. Covers all positions: top-level css() blocks, nested & / @ selectors, variants() base, variant options, and compoundVariants styles. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced Apr 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
css()andvariants()now accept object-formStyleBlock(camelCase property keys) alongside legacy token strings; the Rust compiler (css_transform.rs) extracts the object form AOT, with a standalone unitless-properties parity gate between the TS runtime and Rust, a class-name parity test, andlanding/hero.tsxrewritten as the first real consumer.token.*Proxy helper (lazyvar(--<ns>-<k>)emission) with per-namespace type augmentation (VertzThemeColors/VertzThemeSpacing/VertzThemeFonts) gated by aNamespaceShape<T>conditional; fingerprint guards incss()andvariants()prevent token proxies from being walked as style blocks (would otherwise collapse variant class names).plans/drop-classname-utilities/phase-03-migrate-call-sites.md) — migration script + 7 task breakdown. Phases 3–5 (migrate call sites, remove token-string parser, docs + changeset) remain.Test plan
vtz testonpackages/uigreen (includes newtoken.test.ts,token-in-css.test.ts,token-in-variants.test.ts,class-name-parity.test.ts,unitless-parity.test.ts,css-object-form.test.ts,variants-object-form.test.ts)vtz run typecheckclean (.test-d.tssuites covertoken.*vanilla mode, defining-module augmentation, and@vertz/uibarrel augmentation)oxlint0 errors,oxfmtcleanreviews/drop-classname-utilities/)