ReviewOS

stacks/stx

Switch: unguarded sizeConfig[size] kills its own <script client> on any size outside sm/md/lg, leaving the control silently inert

#1974
Closed glennmichael123 opened this 9 days ago ·
9 days ago

packages/components — verified on 0.2.321, also present in 0.2.252.

src/ui/switch/Switch.stx violates the rule the component docs themselves state, using this component's own line as the cautionary example.

The bug

// <script server>
const sizeConfig = {
  sm: { track: 'h-5 w-9',        thumb: 'h-4 w-4',        translate: 'translate-x-4' },
  md: { track: 'h-[38px] w-[74px]', thumb: 'h-[34px] w-[34px]', translate: 'translate-x-9' },
  lg: { track: 'h-12 w-24',      thumb: 'h-11 w-11',      translate: 'translate-x-12' },
}

export const config = sizeConfig[size]   // <- no fallback
// <script client>
const config = {{ config }}

size defaults to 'md', but any caller-supplied value outside sm | md | lg makes config undefined. The mustache then resolves to nothing, the braces survive into the browser, and the emitted client script contains a literal const config = [object Function] — a SyntaxError that kills the entire scoped script.

toggle() is defined in that script. So the failure mode is:

  • the switch renders and looks completely normal
  • clicking it does nothing, forever
  • @change never fires
  • no error attributable to the component; the whole <script client> block simply never ran

useModel, defineExpose and the emit all die with it.

Reproduction

<Switch size="medium" :checked="on" @change="save()" />
<Switch size="small"  :checked="on" @change="save()" />
<Switch size="large"  :checked="on" @change="save()" />

Each renders; none toggles. sm/md/lg work, so a caller writing the obvious long-form spelling gets a control that is inert with no diagnostic.

This is the documented failure mode

docs/guide/using-components.md states the rule, and gives the fix using this exact identifier:

A mustache there is a server value spliced into JavaScript. If it resolves to undefined the braces survive into the browser, and const n = is a SyntaxError that kills the whole scoped script — every handler in it, with no message.

export const config = sizeConfig[size] || sizeConfig.md  // a typo'd size is not a reason to die

The docs are right and the component does not do it.

Fix

export const config = sizeConfig[size] || sizeConfig.md

I scanned all 102 components for the same shape — a bare map[key] export spliced into <script client> — and Switch is the only one, so this is a one-line fix rather than a sweep. (Button, TextInput, PasswordInput and Progress have unguarded lookups too, but theirs are interpolated in the template, where undefined yields an empty class attribute rather than a SyntaxError. Cosmetic, worth tightening, not urgent.)

Second, separate point: the md track is unthemeable

Not a crash, but it is why we did not adopt Switch anywhere:

  • md is a 74×38px track. That is very large for a settings row; our apps use 38×22.
  • on/off colours are hard-coded bg-blue-500 dark:bg-blue-600 / bg-neutral-400 dark:bg-neutral-600, not tokens.

There is no prop for either, so matching a product's design system means restyling the control to match the component rather than the other way round. A --stx-switch-* custom-property hook, or letting className reach the track and thumb, would make it adoptable. Happy to open that as its own issue if you would rather keep this one to the crash.

9 days ago

Fixed — export const config = sizeConfig[size] || sizeConfig.md, and now covered twice over: switch.test.ts pins the guard directly, renders-under-current-stx.test.ts parses every emitted script so an unresolved mustache in a client script is a failure rather than a silent SyntaxError, and the new unknown-prop-values.test.ts sweep includes <Switch size="no-such-value"> among its 31 discovered cases.

The library-wide version of this is #2001, fixed in d76cb46 — 31 lookup sites across 18 components, same shape.

Sign in to comment on this issue.