Skip to content

v4: framework-agnostic Tailwind + tailwind-variants rewrite - #180

Open
jssee wants to merge 12 commits into
masterfrom
next
Open

v4: framework-agnostic Tailwind + tailwind-variants rewrite#180
jssee wants to merge 12 commits into
masterfrom
next

Conversation

@jssee

@jssee jssee commented May 29, 2026

Copy link
Copy Markdown

v4: framework-agnostic Tailwind + tailwind-variants rewrite

A complete, breaking rewrite of @mojotech/mojo-ui. It stops being a React + Emotion + onno-react component library and becomes a framework-agnostic design system: a Tailwind v4 theme plus tailwind-variants recipes that return className strings. The same recipe powers a React, Vue, or Svelte component the consumer writes and owns — no runtime, no framework lock-in, SSR/RSC-safe.

Full design + migration cheat sheet: docs/v4-vision.md.

Highlights

  • Scorched earth on legacy — Emotion 10, onno-react, babel, webpack 4, Storybook 5, and CircleCI are gone. (Also retires the entire vulnerable dep tree behind the 97 Dependabot alerts on master.)
  • Recipes are the productsrc/recipes/*.ts, 12 recipes (button, text, gridSystem, textInput, …), each exporting a tv() recipe + its VariantProps.
  • theme.css — every v3 token re-expressed as Tailwind v4 @theme / @utility / @custom-variant: fluid clamp type (with paired line-height/tracking), fluid *-fluid-* spacing scale, brand colors, mojo-md/mojo-lg breakpoints (stock sm/md/lg/xl/2xl untouched), named z-stack, and data-scheme color schemes.
  • Modern, cutting-edge, ESM-only chain — Bun (pm/test/runner), tsdown (Rolldown/oxc), Biome, TypeScript 6, Changesets, GitHub Actions. CJS intentionally dropped.
  • Agent-firstAGENTS.md documents architecture, invariants, and how to add a recipe/token.

Bugs fixed from v3

  • Button's invalid background-color: dark literal → routed through theme colors.
  • TextInput's floating label was opacity: 0 always (never visible) → CSS peer + :placeholder-shown float, no JS.
  • body-sm clamp had invalid + -0.07vw math → corrected.

Validated end-to-end (not assumed)

  • The fluid @utility syntax and every recipe class were checked against the real Tailwind 4.3 compiler — a test compiles every class every recipe emits (151 classes) so nothing silently purges.
  • twMerge config proven: consumer className overrides win for every custom scale.
  • bun run check (Biome + tsc + 21 tests) and bun run validate (tsdown build + publint "All good" + attw 🟢 ESM/bundler) both pass.
  • Published tarball is LICENSE + README + dist/ only (ESM + .d.ts + theme.css), 54 kB.

Deferred to follow-ups

Docs catalog (Storybook replacement), examples/ apps, the migration codemod, and @font-face files (Adobe Fonts licensing for altis-mojoregular). Nothing is published — that's a Changesets call.

Test plan

  • bun install --frozen-lockfile
  • bun run check — 21 pass
  • bun run validate — publint + attw clean
  • Reviewer: confirm the breaking-change scope and the deferred items before publishing 4.0.0

🤖 Generated with Claude Code

Jesse Hoyos and others added 12 commits May 29, 2026 13:59
Scorched-earth removal of the entire v3 toolchain ahead of the v4 rewrite
(see docs/v4-vision.md): Emotion 10 + onno-react components, babel, webpack 4,
Storybook 5, CircleCI, and all associated config. The repo is reset to a
clean slate for a framework-agnostic Tailwind v4 + tailwind-variants library.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Establish the v4 toolchain (see docs/v4-vision.md), ESM-only and
framework-agnostic:

- Bun 1.3 as package manager, test runner, and script runner
- tsdown (Rolldown/oxc) as the library bundler — ESM-only, neutral platform
- Biome 2 for lint + format in a single pass (CSS excluded; Tailwind v4
  at-rules are hand-maintained)
- TypeScript 6 (bundler resolution, verbatimModuleSyntax, strict + noUncheckedIndexedAccess)
- Changesets for releases (public access, baseBranch master)
- tailwind-variants 3 runtime dep; tailwindcss 4 peer
- mise pins bun/node; publint wired via the validate script

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The v3 theme.ts is re-expressed as a Tailwind v4 @theme/@utility/@custom-variant
stylesheet — the single source of truth for brand tokens. Validated end-to-end
against the tailwindcss 4.3 compiler (116/117 candidate classes resolve; the
lone 'miss' is the no-output `peer` marker).

- ADD brand colors, fonts, fluid type (clamp, with paired line-height/tracking),
  raw leading/tracking scales, max-widths, namespaced easings
- fluid spacing as a custom --space-fluid-* namespace driving functional
  @Utility families (p/px/py/m/mb/gap/...-fluid-*), exposing the full 0-6 scale
- breakpoints ADDED namespaced (mojo-md/mojo-lg) so stock md:/lg: are untouched
- z-stack as named @Utility (v4 has no --z-index-* namespace)
- schemes via [data-scheme] runtime vars + @theme inline + @custom-variant
- reset residue beyond Preflight only (th text-align, replaced-element max-width)
- body-sm clamp corrected from the v3 source's invalid `+ -0.07vw`

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- src/tv.ts: brand-configured createTV() that teaches tailwind-merge about the
  custom scales (fluid type via the font-size group, fluid spacing across all
  padding/margin/gap groups via a fluid-step validator, brand max-w/w, z-stack)
  so recipe defaults and consumer className overrides dedupe predictably.
  Verified: every custom utility collapses to the override value.
- src/tokens.ts: typed JS mirror of theme.css tokens (colors, schemes, fonts,
  fluid type/space scales, breakpoints, easings, zStack) for JS consumers.
- add tailwind-merge as a direct dependency (tailwind-variants v3 peer) so
  consumers get predictable merging zero-config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
flex, grid, gridSystem, autoGrid, wrap, section — the v3 layout components as
tailwind-variants recipes. gridSystem/autoGrid keep their FIXED 24/48px gutters
(gap-6/gap-12, not fluid) to preserve v3 behavior; responsiveness is hand-coded
mojo-md: prefixes (responsiveVariants is gone in Tailwind v4). All emitted classes
verified against the compiler.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
text, listItem, textLink. text maps size 0-5 to the fluid type scale (line-height
and tracking ride on the paired theme tokens) plus the per-size bottom margin.
listItem and textLink extend text via tv `extend`. textLink's underline is a
currentColor gradient wipe; the v3 opacity:0 default (invisible link) is dropped
as a bug.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
button, textInput, image, scheme. button is slotted (root + sliding shine) and
fixes the v3 invalid `background-color: dark`. textInput is a JS-free floating
label via the CSS peer + :placeholder-shown pattern, fixing the v3 always-0 label
opacity. scheme applies the semantic bg/fg tokens that follow [data-scheme],
replacing the v3 SchemeProvider React context.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- src/index.ts re-exports every recipe (+ its VariantProps), the brand tv/cn/cx
  authoring helpers, and the design tokens.
- wire publint (--strict) + attw (esm-only profile) into the validate script.

Verified end-to-end: tsdown build emits clean ESM + per-recipe .d.ts + a shared
tv chunk + copied theme.css; publint passes; attw is green for ESM/bundler
resolution; runtime smoke test resolves the barrel and subpath imports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tailwind-variants always exposes a `base` slot; naming the primary slot `root`
left an empty `base` alongside it. Rename button/text-input's primary slot to
`base` so the returned slots are exactly { base, shine } / { base, input, label }.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- tokens.test.ts: pin zStack/colors/schemes/breakpoints against brand values
- tv.test.ts: assert consumer className overrides win for every custom scale
- recipes.test.ts: pin key recipe outputs (defaults, fixed gutters, CSS-driven
  float, fixed v3 bugs) + a 'no recipe class is purged' guard that compiles every
  class every recipe emits against the real theme
- theme.test.ts: assert fluid utilities, paired type tokens, semantic scheme
  tokens, and brand+stock breakpoints all compile via the tailwindcss 4 API
- tw-compile.ts: shared in-memory Tailwind compile helper

21 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ci.yml: on push/PR, bun install + 'bun run check' (biome/tsc/test) +
  'bun run validate' (tsdown build + publint + attw)
- release.yml: Changesets action on master to open release PRs / publish to npm
  with provenance (OIDC id-token)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- README: framework-agnostic positioning, install, the REQUIRED @source step,
  React + Vue usage, schemes, the stock-Tailwind divergences, font-licensing note
- AGENTS.md: architecture, toolchain, layout, commands, and the invariants
  (recipes return strings, static class strings only, no purged classes, mojo-md/lg
  prefixes, base slot, tokens in two homes) for agents/contributors

Co-Authored-By: Claude Opus 4.8 (1M context) <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