-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Draft
Conversation
A theme can now say what it looks like at each viewport width. The four tiers — mobile, tablet, desktop, wide — partition the width axis, so exactly one matches at any width and no two ever compete. Declaring a tier turns it on; a theme that declares none emits no tier CSS. A tier is resolved as a complete theme input rather than as a patch: its input is merged along its `extends` chain and run through the same pipeline as the base theme, and only the result is diffed for emission. That is what keeps a tier's type scale from reverting the theme's font weights, and a tier's generated axis from beating the theme's explicit tokens. `extends` is value inheritance resolved at define time, defaulting to the theme's own values. Pointer is a separate axis nested inside a tier, because the 16px body floor is an iOS input-zoom fix — true on a phone and an iPad alike, never on a desktop window dragged narrow.
... a tier
Review pass over the width-tier feature. Three requirements and five defects.
Requirements
- An undeclared tier is no longer a breakpoint. `{mobile, desktop}` is one
boundary and two bands, not three bands with the middle one falling back to
the theme's own values. `wide` alone is refused: with nothing below it to
bound it, it would match every width.
- Declaring no tier emits no media query anywhere, pinned by its own test
rather than left to fall out of the others.
- Every tier block is emitted after everything without a media query, in the
built path as well as the runtime one. The two disagreed: the built path put
tier CSS before the `[data-astryx-media]` rules and the runtime path after.
Defects
- A pointer refinement was pruned against the base theme, but its competitor
is the tier it sits in — on a coarse-pointer phone both match. A refinement
value equal to the base theme's was dropped and the tier went on applying.
Each layer now diffs against the baseline it actually has to beat.
- A tier re-expanded every axis its theme was declared with, so a tier that
named one token also reverted whatever those axes had been resolved against.
Extending `butterTheme` and asking for one spacing token emitted a reverted
token and four unasked-for rules. Inherited axes are now a completion source
for the axes a tier states, never an input on their own: a tier resolves on
top of the theme's resolved values and changes what it names.
- That resolution input was also serialized into every built theme module —
14,105 bytes, 43% of `neutral`, for a theme with no tiers. The axes alone
are 766.
- Four places in the build never looked inside a tier: the variant
augmentation (a tier-only variant was styled but untypeable), the private-var
check, the unknown-component warning, and the unloaded-font notice.
- A missing `_generateTierCSS` silently dropped every tier from a built
stylesheet. It now fails the build like the other core imports.
Also: completing a partial scale recomputes the axis, and the shipped duration
table is hand-rounded — no single ratio reproduces it. Two of nine tokens land
5ms out. Pinned by test so it is a known difference rather than a surprise.
Test: 65 tier tests, 12 CLI build tests, 12,326 in the suite. lint:strict,
core typecheck and pnpm build all clean.
...ated axis
An explicit `tokens`/`components` override is applied last at the theme level,
above every generated axis — that is the whole of what pinning a token means.
A tier reaches its theme's values through the resolved seed, which sits at the
BOTTOM, so the moment a tier restated any part of an axis the regenerated axis
landed on top of the pin and reverted it.
A base pinning `--color-accent` red over a scale that generates blue, and a
child that adds nothing but `mobile: {color: {neutralStyle: 'warm'}}`: red
above the breakpoint, blue below it, and the base's heading override gone with
it. Neither the child nor the tier mentioned either one.
The previous commit fixed this for a theme's OWN overrides and reasoned wrongly
about inherited ones — the code comment claimed they "already reach the tier
through seed, at the precedence the root resolution gives them". They reach it
at the bottom, not at their precedence.
Which overrides to re-apply is the whole problem, and it is not the declared
ones: a base's pin is legitimately beaten by an extending theme's generated
axis, and re-applying the declaration would revert that instead — the same bug
in the other direction. It is the ones that WON.
Derived rather than tracked. Carrying the declarations down the `extends` chain
means serializing them into every built theme: measured at 10.5 KB on a 19 KB
module, most of the size regression this branch just removed. The axes are
already serialized, so the question can be asked of the values themselves — a
token is pinned exactly when it is not the value its axis computes. Same answer,
no bytes: `neutral` is 19,376, against 19,372 before this commit.
Also broadens the stale-built-base guard: a base from an older CLI has no
`__axes`, so it can neither complete a partial scale nor separate its pins from
its generated values. It was refused only when the base itself had tiers; now
whenever the extending theme will have them.
Test: 8 new tier tests — the reviewer's child-theme case for tokens and for
components, the tier still winning over an inherited pin, the mirror case that
must NOT resurrect a superseded pin, two levels of chain, a built-shaped base,
and the stale-base refusal. 74 tier tests, 13 CLI build tests, 12,336 in the
suite (13 known parallel-load timeouts, all green serially). lint:strict, core
typecheck and pnpm build clean.
Value comparison recovers ordinary explicit overrides, but cannot distinguish an explicit value from a generated one when the two happen to be equal. That is harmless until a tier regenerates the axis to a different value — exactly what responsive tiers do. A 14px scale plus an explicit 0.875rem base became 1rem in the mobile tier, while a visibly different 2rem pin held. Carry only the ambiguous paths as `__equalOverrides`: token names and component leaf paths, never values. Themes without an equal-value pin store nothing. The mask follows `extends`, drops paths an extending theme's own generated axis legitimately supersedes, and adds equal explicit paths from the child. Tier resolution adds the resolved values at those paths to the overrides it already derives by difference. This preserves explicitness without serializing the full declarations: neutral remains 19,376 bytes, and a theme with one ambiguous pin adds only its token name. Adds direct, inherited, two-level, tier-wins, child-axis-supersedes, component-path, distinct-pin and built-theme regression cases, plus CLI serialization tests for both the sparse and absent forms.
Self-audit found two serialization issues after the review blockers were fixed. First, an inherited equal-pin marker survived when a child explicitly replaced the same path with a distinct value. Resolution stayed correct, but the marker was no longer ambiguous and was serialized unnecessarily. Explicit child token and component paths now remove the inherited marker before equal-valued child pins are added back. A regression covers the 0.875rem -> 2rem replacement. Second, built modules serialized the full resolved `__tiers` map even though an extending theme never reads it: the tier CSS already has those values, and a child re-resolves from `__tierInput`, `__axes` and `__equalOverrides`. The redundancy made a one-tier module 6,894 bytes larger than its tierless twin. Omitting it reduces that exact delta to 124 bytes. The CLI test now imports the generated module and passes it to defineTheme(), proving the sparse fields are sufficient rather than merely checking their names appear in the output.
Core inputs already floor their text at 1rem under a coarse pointer, so raising typography.scale.base to 16 in the responsive-theme template solved an already solved problem while lifting every font-size token from 4xs through 5xl. Keep the pointer axis, but teach a use it uniquely owns: taller element-size tokens under a finger or stylus. Update the template, full and dense docs, source JSDoc, changeset and test commentary consistently. Also call out the intentional consequence of precedence: pinning one generated scale token keeps that step fixed when a tier moves its generated neighbours, producing an uneven ladder unless that is deliberate. Remove DEFAULT_TIER_MAX_WIDTH, tierWidthQuery and ThemeGenerativeAxes from the public theme barrel. The first two are now module-private; ThemeGenerativeAxes remains an internal cross-module type for __axes. Keep the author-facing tier vocabulary and input types public.
Round-three review found that the public wording still described the tiers as a full partition where exactly one always matches. Sparse declarations do not cover every width: declared bands are disjoint, the lowest declared bounded tier owns every width through its upper bound, and root values apply above the highest bounded tier unless wide is declared. Correct that model consistently in the template, full and dense docs, source JSDoc, changeset and PR summary. Also make TierBreakpoints module-private and remove it from the public theme barrel; it only types internal query construction.
...tations # Conflicts: # packages/cli/api/theme/build/build.mjs # packages/core/src/theme/defineTheme.ts
...tations # Conflicts: # docs/architecture/theme-authoring-contract.md
Resolve finite literal aliases when discovering component visual-prop domains, reject unsupported closed values at the root and in adaptations, and preserve real extensibility points including CustomTextTypes. Test Plan: - Full packages/cli Vitest suite: 215 files, 3186 tests - packages/cli strict TypeScript check - ESLint on changed files
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
@meta-cla
meta-cla
Bot
added
the
CLA Signed
This label is managed by the Meta Open Source bot.
label
Sep 4, 2026
Prefer target-specific component docs when generating visual-prop selectors so aggregate docs cannot leak values across targets. Test Plan: - pnpm build - probe-theme.test.mjs - generate-probe-theme.mjs --check - theme-probe build
imdreamrunner
marked this pull request as ready for review
September 5, 2026 06:03
Replace package-wide alias matching with TypeScript compiler resolution in each documented prop owner. Only finite string and number literal domains are accepted; ambiguous, malformed, generic, or open types remain unresolved. Test Plan: - Full packages/cli Vitest suite: 216 files, 3206 tests - pnpm build - pnpm check:repo - frozen lockfile install check - packages/cli strict TypeScript check - ESLint on changed files
imdreamrunner
force-pushed
the
feat/theme-tiers
branch
from
September 6, 2026 17:12
ecaf827 to
df9f1fb
Compare
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.
Uh oh!
There was an error while loading. Please reload this page.
Summary
CustomTextTypesThis is a focused follow-up to #5543 and intentionally targets its
feat/theme-tiersbranch.Resolution model
The resolver no longer builds a package-wide map keyed by bare alias names. It parses the documented type expression, then asks the TypeScript checker to resolve named references in the lexical scope of the component source that owns that prop. TypeScript therefore owns import aliases, re-exports, package entrypoints, extension substitution, and collisions. Ambiguous, malformed, generic, open, or otherwise non-literal types remain unresolved rather than being guessed.
Test plan
packages/cliVitest suite: 216 files, 3,206 tests passedpnpm buildpassedpnpm check:repopassed