Skip to content

feat(ui): drop classname utilities — object-form css() + token.* helper#2769

Merged
viniciusdacal merged 19 commits into
mainfrom
viniciusdacal/drop-classname-utils
Apr 17, 2026
Merged

feat(ui): drop classname utilities — object-form css() + token.* helper#2769
viniciusdacal merged 19 commits into
mainfrom
viniciusdacal/drop-classname-utils

Conversation

@viniciusdacal
Copy link
Copy Markdown
Contributor

Summary

  • Phase 1: css() and variants() now accept object-form StyleBlock (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, and landing/hero.tsx rewritten as the first real consumer.
  • Phase 2: Adds the typed token.* Proxy helper (lazy var(--<ns>-<k>) emission) with per-namespace type augmentation (VertzThemeColors / VertzThemeSpacing / VertzThemeFonts) gated by a NamespaceShape<T> conditional; fingerprint guards in css() and variants() prevent token proxies from being walked as style blocks (would otherwise collapse variant class names).
  • Phase 3 plan is committed (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 test on packages/ui green (includes new token.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 typecheck clean (.test-d.ts suites cover token.* vanilla mode, defining-module augmentation, and @vertz/ui barrel augmentation)
  • oxlint 0 errors, oxfmt clean
  • Pre-push hook passed all 4 gates (trojan-source, lint, build-typecheck, test)
  • Adversarial reviews written for Phase 1 and Phase 2 with all blockers resolved (reviews/drop-classname-utilities/)

viniciusdacal and others added 19 commits April 17, 2026 14:49
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant