fix: substitute $name refs in pass-through style values - #264
Conversation
A `$name` reference is classified as a color only when the name ends with `-color`; otherwise it lands in the parser's value bucket. Handlers that read one bucket and fall back to their raw input emitted the authored DSL verbatim, which browsers drop as an invalid declaration — `fill: '$my-fill'` produced `background-color: $my-fill`. The mirror case broke too: a `$name-color` reference used as a length falls out of `values[]`, so `fontSize: '$my-size-color'` emitted `font-size: $my-size-color`. Handlers for keyword-valued properties never parsed their input at all, since there are no units or color tokens to resolve, so every one of them leaked the raw reference: display, overflow, whiteSpace, flow, place/align/justify and their longhands, textTransform, font/fontFamily, color, background-clip / -origin / -repeat / -attachment, and outlineOffset. Adds `resolveCustomProperties()` and routes those values through it. It early-returns unless the value contains a `$`, because the parser case-folds: blanket-parsing would turn `fontFamily: '"Inter", Arial'` into `'"inter", arial'` and `var(--myColor)` into `var(--mycolor)`. Values that were already valid CSS take exactly the path they took before. The new suite sweeps every registered style prop and asserts no `$` survives into declarations, so a new pass-through handler cannot reintroduce this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
📦 Snapshot releasePublished |
`#name` is the color syntax; the `$name-color` form the parser buckets as a color exists to reference a raw CSS custom property, not as the way colors are written. So a plain `$name` token in a `<width> <style> <color>` shorthand should not be competing for the color slot — it belongs to the style slot, which has no keyword for a custom property to match and was dropped entirely: `border: '1bw $my-style'` emitted `1px solid var(--border-color, currentColor)`. A reference now fills the first free slot — width, then style. Lengths are left alone: a second length is not valid in these shorthands, and promoting one to the style slot would emit an invalid declaration instead of ignoring the extra value. `$name-color` references still land in the color slot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Follow-up pushed: Rationale (per review): // Before: `1px solid var(--border-color, currentColor)` — reference dropped.
// After: `1px var(--my-style) var(--border-color, currentColor)`
tasty({ styles: { border: '1bw $my-style' } });A reference fills the first free slot — width, then style — so Two boundaries worth noting:
Applies per comma group in multi-group syntax ( Also updated the Tests: 11 new cases across |
Filling only width and style left `border: '1bw dashed $my-color'` dropping the
reference: both value slots were taken, the color slot was free, and nothing
claimed it. A reference now fills the first free slot in shorthand order —
width, then style, then color — which is the same rule stated once instead of
stopping at the style slot. An explicit `#name` token still wins the color slot,
and lengths are still left alone.
Also fixes a `whiteSpace` leak the one-prop-at-a-time sweep hid: the value is
emitted a second time by `processTextOverflow` for the clamp, so
`{ textOverflow: 'ellipsis', whiteSpace: '$my-ws' }` still reached CSS as raw
DSL. It is now resolved once at handler entry and passed down. The sweep renders
every prop under companion contexts (`display`, `textOverflow`) so branches that
need a second prop can no longer pass by returning early.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three ways a reference could silently do nothing, all rooted in the parser guessing at something it cannot know. A `-color` suffix is the only hint the parser has about an untyped reference, and it used to decide the bucket outright: the reference went to `colors` alone, so a handler reading `values` came up empty and emitted its own default. Across 35 style props the authored value vanished — `padding: '$brand-color'` became `padding: var(--gap)`. Such references are now filed under both buckets (Bucket.ColorValue), listed once in `all`; border/outline place them once rather than in both the style and color slots. The parser lowercased its whole input before classifying, folding custom-property names, which are case-sensitive in CSS. A camelCase name could never resolve: the definition emitted `--myVar`, the reference asked for `var(--myvar)`. Identifier bodies now keep their case via foldDslCase, and every site deriving a CSS name from one shares normalizeDslName, so definitions and references agree. A leading capital is not a supported name and folds instead of being kebab-cased (`$Foo` → `--foo`). Keywords, units, function names and hex literals fold exactly as before, and predefined-token lookup stays case-insensitive — only the emitted name preserves case. `preset` and `transition` interpolate their input into a custom-property *name*, which cannot be indirected through a reference: the name is needed at build time and a reference only resolves in the browser. They built `var(--var(--x)-font-size)` — valid syntax, unusable name, dropped silently by the browser. Both now warn once in dev and fall back (preset to `inherit`, transition by skipping the entry). Value slots are untouched. foldDslCase is on the parse hot path, so it early-returns for input that was already lowercase or holds no sigil, keeping the common cases at ~13ns over the single fold it replaced; the parser bench is unchanged. Size budgets are raised 0.25-0.5 kB per entry for the ~250 B of added logic, with headroom kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Fixed all three follow-up issues. Each traced back to the parser guessing at something it cannot know about an untyped reference. 1. A
|
The snapshot release job published `0.0.0-snapshot.<sha>` successfully and then failed posting its PR comment with a 503, during GitHub's 2026-08-17 incident. Re-running that job cannot succeed for the same sha — npm already holds the version — so this empty commit gives it a fresh one. No source change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Second attempt: the previous run published its snapshot and then failed posting the PR comment while GitHub's Issues API was degraded. That API is answering again, so retry with a fresh sha (npm holds the version for the old one). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
What
$namecustom-property references leaked into CSS unsubstituted from any style value a handler emits verbatim.The parser was doing its job —
parseStyle('$current-fill-hover')returnsoutput: 'var(--current-fill-hover)'. The problem is which bucket the token lands in:classify.tsputs a$namereference in the color bucket only when the name ends with-color, otherwise in the value bucket.fillStylereads only the color bucket and, finding it empty, fell back to the raw input:So the parsed
var(...)was computed and thrown away, and the browser dropped the declaration.Scope
Sweeping all 84 registered style props with
$my-prop,$my-prop-color, and($my-prop, 1x)turned up 23 leaking props — two mirror-image variants of the same mistake:fill/backgroundColor,svgFill, plus the reverse case where a$name-colorreference used as a length falls out ofvalues[]:fontSize,lineHeight,letterSpacing,fontWeight,outlineOffset.display,overflow,whiteSpace,flow,place/align/justifyand their longhands,textTransform,font/fontFamily,color,background-clip/-origin/-repeat/-attachment.The reported case was invalid usage (a color held in a custom property not named
$*-color), butdisplay: '$my-display'is perfectly reasonable authoring and was equally broken.How
Adds
resolveCustomProperties()insrc/utils/styles.tsand routes every pass-through value and raw fallback through it.The one detail worth a reviewer's attention: it early-returns unless the value contains a
$, becauseparseStylecase-folds. Blanket-parsing these values would have turnedfontFamily: '"Inter", Arial'into'"inter", arial'andvar(--myColor)intovar(--mycolor)— custom property names are case-sensitive in CSS. Gating on$means values that were already valid CSS take exactly the path they took before, and the blast radius is limited to values that emit invalid CSS today.Left alone
border: '1bw \$my-fill'andoutline: '1bw \$my-fill'still drop the reference and substitutevar(--border-color)/var(--outline-color). The shorthand parsers need a color-bucket token to place it and can't guess — that's the documented consequence of the-colorsuffix rule (parser.md:167), and\$my-fill-colorworks there. Making those warn in dev instead of silently defaulting is a separate change.Testing
src/styles/custom-property-refs.test.tssweeps every registered style prop and asserts no$survives into declarations, so a new pass-through handler can't reintroduce this class of bug. Plus per-handler assertions for both bucket directions, and negative cases pinning that$-free values are not case-folded.tsc --noEmit,eslint src, andknipall clean.🤖 Generated with Claude Code