ReviewOS

stacks/stx

An icon from a non-lucide collection silently becomes a plain circle on native screens

#2135
Closed glennmichael123 opened this 4 days ago ·
4 days ago

What happens

An icon class from any collection other than lucide renders on a native screen as a plain circle. Nothing is logged, at build time or at runtime, so the first sign is seeing it on a device.

We shipped one to a phone. A 48px accent icon came out as a bare ring, and it was caught by looking at the screenshot, not by any check.

Repro

In a native screen, with @iconify-json/hugeicons installed (it arrives with @stacksjs/defaults):

<i class="h-12 w-12 i-hugeicons-workout-run"></i>

Build and run: a circle. The web build of the same template draws the runner correctly, since ts-css resolves any installed collection.

Mechanism

native/compiler/icons.ts, resolveIconName:

const prefixed = /^i-([a-z0-9]+)-(.+)$/.exec(value) ?? ...
const collection = prefixed ? prefixed[1] : 'lucide'
if (collection === 'lucide') {
  const symbol = LUCIDE_TO_SF[icon]
  if (symbol) return { symbol, iconify: `i-lucide-${icon}`, known: true }
  ...
}
return { symbol: FALLBACK_SYMBOL, iconify: ..., known: false }   // FALLBACK_SYMBOL = 'circle'

collection === 'lucide' is the only branch that consults a table, so every other collection takes the fallback by construction — not because a name is missing, but because the collection isn't lucide. There is no way to express a mapping for one, even where an exact SF Symbol exists: hugeicons:workout-run is figure.run, the same symbol footprints already maps to.

The silence is the actual bug

known: false is computed and then discarded. Its only consumer is layout-chrome.ts, which uses it to pick between the symbol and the raw class — never to report. No console.warn in the file, and nothing downstream logs it.

The doc comment on LUCIDE_TO_SF says otherwise:

Anything missing falls back to circle and the compiler says which name it did not know.

It doesn't. We checked a full build:ios log for any mention of the icon, the collection, or a fallback: nothing.

What makes this stand out is how close the counterexample is. In native/runtime/native-host.ts:

:474   console.warn(`[STX Native] unknown-class: ${id}: ${name}`)
:477   const icon = resolveIconName(classes.icon)

An unrecognised class warns. Three lines later an icon resolves to the fallback and says nothing — in the same function, for the same kind of authoring mistake. unknown-binding warns too. Icons are the gap.

Suggested shape

Separable, in order of value:

  1. Say it. Emit the unknown-icon warning the docs already promise, at compile time where the template and name are in hand — matching unknown-class. This alone turns a device-only discovery into a build line.
  2. Let other collections map. Key the table by collection, or let a project supply its own entries, so an installed collection isn't fallback-by-construction.
  3. Consider a better fallback. A circle is indistinguishable from a deliberate ring. Something visibly wrong, or the SF Symbol questionmark.square.dashed, reads as a mistake.

Worth noting (1) stands on its own: with a warning, the current behaviour is defensible — without one, a valid, installed collection degrades invisibly between web and native builds of the same template.

4 days ago

Fixed on main in 2a8dcd7780 and a11ce4930a, included in integration tip 77d8cd6702.

hugeicons:workout-run and its utility-class spelling now resolve to figure.run through a collection-keyed table. Unmapped static icons appear as unknown-icon in IR/public bundle diagnostics, including icons in removed layout chrome. Reactive class/name changes warn with the node, missing name and remedy; repeated style refreshes do not repeat the same warning. Fallback is questionmark.square.dashed; deliberate circles and explicit SF Symbols remain valid. Installed collections are not falsely advertised as automatically supported native mappings.

The original resolver returned circle/known=false for the reported name; the new resolver returns figure.run/known=true. Eight dedicated regression cases cover static, reactive, remount and normal/minified iOS/Android protocol execution. Ten layout-chrome cases pass, including public bundle diagnostics. Compiler/icon compatibility checks also pass. No Craft source changes.

Version: unreleased source after v0.2.746. The concurrent 0.2.746 release predates these fixes; no new patch was cut by this batch. Integrated-tip CI is queued: https://github.com/stacksjs/stx/actions/runs/40915422118 . Closing the implemented issue; publication is not being claimed.

4 days ago

Release follow-up: STX / bun-plugin-stx / @stacksjs/components v0.2.747 is now published and latest on npm. Release commit: 239d660dd833bf3ecefe89836a0252e16c8568fe.

Verified all three public archives against their integrity checksums and installed them in a fresh consumer project. The shipped native icons module contains the Hugeicons workout-run mapping and question-mark fallback. All 7 consumer checks passed (48 assertions), covering compiler diagnostics, reactive icon updates and DialogPanel behavior in normal/minified iOS/Android protocol bundles. Published 0.2.745 correctly fails the new flow-layout regression as a control.

All five release-commit CI jobs passed: https://github.com/stacksjs/stx/actions/runs/40918690528 . The release gate passed 17,437 tests with zero failures (48 existing skips): https://github.com/stacksjs/stx/actions/runs/40918689961 . Remaining package publication is still running at the time of this comment. This establishes 0.2.747 as the published version floor for #2135; no device-rendering certification beyond the recorded acceptance evidence is newly claimed.

Sign in to comment on this issue.