You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(16): svelte :style switches to string-form when auto-fallthrough active
ITEM-2-RESIDUAL closure. Fully closes the Svelte arm of PHASE_14_1_FOLLOWUP
— matrix VR cell now lands on baseline (was 7 085 px diff post-Item-2);
all 12 themed-button × svelte cells pass; 'svelte' removed from
PHASE_14_1_FOLLOWUP_TARGETS entirely. Full VR matrix: 253 passed, 1 skipped
(down from 13 skipped — the lone remaining skip is the Solid 20px width
residual, which has a different root cause).
Root cause. Svelte 5's compiled output places per-property `style:<prop>=`
directive state under a Symbol-keyed slot processed AFTER any spread inside
the generated props object. The original Phase-5 design ("wrapper directives
win over spread style") was correct for the wrapper-defaults-survive-no-
matter-what use case. But for cross-target auto-fallthrough — where the
consumer's spread `style="..."` MUST win over the wrapper's `:style="{...}"`
defaults to match Vue/React/Solid/Lit/Angular — the directive precedence
was exactly wrong.
Fix. Detect bare-`$attrs` spreadBinding on the same element in
`emitElement`; thread the flag through `EmitAttrCtx.hasFallthroughSpread`.
When set, `tryEmitStyleObjectLiteral` routes the literal-object `:style`
emit through a NEW string-form path: `style="prop: value; prop2: value2"`
instead of per-property `style:<prop>={value}` directives. String literals
splice verbatim; dynamic expressions splice as Svelte template-literal
interpolation (`style="--btn-bg: {bgExpr}"`).
Consumer's spread `style="..."` then overwrites the wrapper's string-form
attribute via `setAttribute`, restoring cross-target consumer-wins
precedence. Wrapper's un-overridden defaults survive via the
`var(--prop, fallback)` CSS fallback the wrappers idiomatically declare
(`color: var(--btn-fg, #ffffff)`).
When the element is NOT a fallthrough root, the original `style:<prop>=`
directive lowering is preserved — Svelte's per-key reactivity is the
better choice for non-fallthrough cases.
Files:
- packages/targets/svelte/src/emit/emitTemplateAttribute.ts —
`EmitAttrCtx.hasFallthroughSpread` new field; new branch in
`tryEmitStyleObjectLiteral` that emits string-form when active;
handles string-literal, numeric-literal, and dynamic-expression
property values
- packages/targets/svelte/src/emit/emitTemplateNode.ts —
computes `hasFallthroughSpread` from `node.attributes` (bare-`$attrs`
Identifier in any spreadBinding) and threads to `emitAttributes`
- packages/targets/svelte/src/__tests__/emitTemplateAttribute.test.ts —
the 3 existing `:style` literal-object tests now use
`inherit-attrs="false"` to opt OUT of auto-fallthrough (isolating
the pre-residual `style:` directive lowering); 2 NEW tests cover
the Item-2-residual string-form path (string-literal values +
dynamic-expression interpolation)
- tests/dist-parity/fixtures/ThemedButton*.svelte — rebless the 4
wrapper fixtures whose `:style="{...}"` defaults now emit as
`style="--btn-bg: #3b82f6; --btn-fg: #ffffff"` instead of
`style:--btn-bg={'#3b82f6'} style:--btn-fg={'#ffffff'}`
- matrix.spec.ts PHASE_14_1_FOLLOWUP set — `'ThemedButtonConsumer::svelte'`
REMOVED; closure comment captures the two-stage fix path
- themed-button.spec.ts PHASE_14_1_FOLLOWUP_TARGETS — set now empty;
closure comment notes Item 2 + Item-2-residual closed the Svelte arm
Verification:
- turbo run typecheck --force --continue: 47/47 green
- turbo run test --continue: 46/46 green (target-svelte 216/216,
dist-parity 512/512)
- svelte-vite test:e2e: 14/14 green
- tools/ci-repro/vr.sh -g ThemedButtonConsumer.*svelte|themed-button.*svelte:
12/12 cells pass (was 0/12 pre-fix; 10/12 post-Item-2)
- tools/ci-repro/vr.sh (full matrix): 253 passed, 1 skipped, 0 failed
— only the Solid 20 px width residual remains gated
The Item-2-residual closure leaves Rozie's cross-target attribute-
fallthrough story FULLY symmetric across all 5 non-Lit targets: a
consumer-passed `style="…"` always wins over the wrapper's `:style="{…}"`
defaults regardless of target.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0 commit comments