Jixoai UI v0.5: loading feedback covered, loaders included
jixoai-ui v0.5 adds the spin loading indicator, addressed by name. The spinner prop resolves through two lanes, the 59-name cli-spinners text catalog and the compile-time svg artifact. The wait animation runs as pure CSS on the compositor, with a tunable fade trail. The plugin-side spinners feature mirrors the icon system, ships 94+12 verified loaders, and the channel API opens custom namespaces. 42 commits, no breaking changes.
이 글은 영어로 작성되었습니다. 중국어 버전 읽기 →
$ git log v0.4.0..v0.5.1 --oneline | wc -l
42
jixoai-ui v0.5 (2026-09-13, currently 0.5.1) collects 42 commits since v0.4.0: 7 feat, 14 fix, 15 docs; the rest are tests, chores, and merges. The main threads are the spin loading-indicator component and the plugin-side spinners feature (the channel API landed in 0.5.1), plus two small items: the tree-view expand glyph and the prototype layout trio. No commit carries a breaking-change mark; upgrading needs no migration.
The loading indicator takes a name
New <Spin> component. The loading indicator takes a name as its parameter, after ora: plain text output, no [ ] wrapping. The spinner prop resolves through two lanes, svg artifact names first, text catalog names second.
<!-- svg lane: compile-time artifact names -->
<Spin spinner="blocks-wave" />
<!-- text lane: the 59-name cli-spinners catalog, frames verbatim from cli-spinners@2.9.2; dots when unset -->
<Spin spinner="dots" />
Names are a compile-time union, so a typo fails type checking; an unknown name that slips past the types produces one cached runtime warning and falls back to dots. Leaving spinner unset also renders dots, ora's own default. blocks-wave is the default in a different place: the plugin's bare spinners: {} config generates an artifact containing exactly that one loader, unrelated to the component default.
Sizing introduces no units of its own. The text lane paints var(--jx-text), the svg lane var(--jx-icon), both density-ruler variables sharing their values with jixoai.com's site typography.
Accessibility is built in: the bare posture emits role="status" with aria-label; the wrapping posture adds aria-busy, a scrim, and pointer blocking on the container; under prefers-reduced-motion the text lane rests on frame 0 and the svg lane pauses its SMIL. No caller configuration.
The wait animation runs without a JS clock
The text lane renders every frame once into a one-cell grid; white-space: pre holds each frame at the widest frame's advance width, so the box keeps one constant size through the cycle. JS only fills animation parameters: one injected @keyframes rule per parameter set, one negative animation-delay per frame phasing it into its slot.
The consequences: frame switches cause no DOM mutations; the animation runs on the compositor, so main-thread jank does not touch the beat; DevTools' Animations panel inspects every slot and linger interval frame by frame.
The fade trail is tunable
The residue parameter is linger: the previous frame stays visible for linger milliseconds after the handoff, fading linearly. lingerType picks the fade's shape from three modes.
end(default): hold the full slot, then a linear fade-outstart: the fade-in completes before the handoff boundary, fully displayed when the next frame takes overboth: fades on both ends
'auto' values are hand-tuned per name in the curator script: dots 80/160; dots2 120/0; pipe 120/120; line 160/0; simpleDots 160/160 (the · glyph). interval and linger both take number | 'auto', with explicit values prioritized as explicit value > Defaults slot > the catalog's tuned pair.
Details: two animation issues fixed while building the text engine
Same-percentage keyframe stops merge: in
10%{opacity:1}10%{opacity:0}the latter swallows the former, degrading the whole span to a linear transition; a discrete hide needssteps(1,start)on the stop.The solid-frame constraint: when linger ≥ interval, the entry length must cap at interval/2, or opacity peaks at a single instant.
The svg loaders are ready to use
The svg lane runs the artwork's own SMIL, and three scenarios all play: multiple instances on one page, name switches, and dynamic insertion, with no browser work left to the caller. All 94 magecdn loaders were pixel-verified through the component path: 83 passed directly, 11 depend on the activation handling; 15 context-dependent dynamic loaders are excluded from the pack with receipts, their raw artwork still available through custom sources. Colors normalize to currentColor, following the text color, so there is no per-theme configuration. The demos in this post are the live component; switching the site theme verifies the dark rendering directly.
Details: the three component-side handles on the svg lane
Per-instance id namespacing: SMIL syncbase references (
begin="x.end"and kin) resolve across the whole document, so a second same-name instance would hang its timing chain on the first instance's long-past begin instants; each instance rewrites its own ids and references at render, keeping the timing graph self-contained.A
{#key}full rebuild on switch: replacing innerHTML inside a persistent svg element activates the animation in Chrome 1 time out of 12, versus 12/12 for fresh whole insertion; keying on the data object rides the always-works insertion path, and prop-only changes keep the node.Activation for dynamic insertions: stylesheet text reprocessing plus
setCurrentTime(0), executed only whendocument.readyState === 'complete', so hydration never resets a running animation.
Custom loaders share the icon pipeline
@jixoai/ui-vite-plugin adds the spinners feature (introduced in 0.4.0; the channel API completed it in 0.5.1), mirroring the icon library's interface. jixoai({ spinners }) is off by default, and code generation runs only when enabled; sources are inline svg strings or { file } references; the RAW safety gate rejects untrusted shapes; the artifact has a single writer, where gen:spins generates and verify:spins gates, so a hand-edit fails CI.
The feature does not use svgo. convertShapeToPath rewrites <circle> into <path>, while SMIL <animate> targets elements by shape type, so the animation targets are lost after the pass. Loader svgs pass through under RAW semantics, with normalization limited to an explicit checklist (strip xml declarations, #fff → currentColor).
Custom extensibility aligns with the icon system through the channel API (0.5.1). A channel is a first-class instance: defineSpinnerChannel({ id, prefix, spinners, peerPackage?, defaultsNote? }), with id grammar /^[a-z][a-z0-9-]*$/ and prefix grammar /^[a-z][a-z0-9]*$/.
Validation runs in two phases: the factory checks shape and grammar, config normalization checks uniqueness (one entry per id, one channel per prefix, and two channels sharing a myco: namespace fail at startup by name). A channel's entries join the artifact as prefix:name keys, such as myco:pulse, magecdn:clock, sam:tail-spin; merge order is built-in manifest, then channels in registration order, then the flat record, with a same full name overriding last (the icons override law).
The full-name grammar accepts both flat (/^[a-z0-9][a-z0-9-]*$/, digit-leading legal, so site-URL names like 180-ring stay verbatim) and prefix:name, so the flat record can also override a channel entry with an explicit myco:pulse key. The generated SpinName union closes with the artifact, making a typo a compile error; the component is unchanged, namespaced names being ordinary union strings.
import { jixoai, defineSpinnerChannel } from '@jixoai/ui-vite-plugin';
import { magecdn } from '@jixoai/ui-vite-plugin/spinners/magecdn';
import { sam } from '@jixoai/ui-vite-plugin/spinners/svg-loaders';
export default defineConfig({
plugins: [
jixoai({
spinners: {
channels: [
// your own channel: entries join as myco:…
defineSpinnerChannel({
id: 'myco',
prefix: 'myco',
spinners: {
cadence: '<svg …>…</svg>', // inline literal
'wave-loader': { file: './src/loaders/wave.svg' }, // {file} reference
},
}),
// a pack as a channel: pick is a filter (2 of the 94)
magecdn({ pick: ['clock', 'bars-scale'] }),
// SamHerbert's 12 under the sam: prefix
sam(),
],
spinners: {
'flat-loader': { file: './src/loaders/flat.svg' }, // the flat lane unchanged
'myco:cadence': '<svg …>…</svg>', // same FULL name overrides the channel entry
},
},
}),
],
});
// regenerate the artifact: npm run gen:spins
// <Spin spinner="myco:cadence" />, <Spin spinner="magecdn:clock" />,
// <Spin spinner="sam:tail-spin" /> all join the type union
Two packs ship as sub-entries: @jixoai/ui-vite-plugin/spinners/magecdn (94 loaders, each verified) and @jixoai/ui-vite-plugin/spinners/svg-loaders (SamHerbert's 12, MIT).
Each sub-entry offers two wiring forms: the spread lane (magecdnSpinners Record, spread by name, same-name override by spread order; unchanged since 0.4.0) and the channel factory (magecdn({ pick? }) / sam({ pick? }), pick as a filter). The docs:cadence loader on this site's docs page is a live example wired through the channel lane.

Other changes
Icon baseline: a0a512e9 (2026-09-08) added gripVertical as the 39th built-in icon without updating the frozen expectations, leaving 14 test failures on main. This release re-records every affected expectation; the plugin suite went fully green for the first time (489/489 at 0.5.0, 510/510 at 0.5.1 with the channel battery).
Playground controls: new PlayTiming (an auto/custom segmented control with gated number inputs); PlaySegmented gained a
bind:channel; PlayNumber switched to event-driven commits, fixing typed values being reverted by a stale resync (typing 32 reverted to 16).tree-view expand glyph: the expand-glyph column is now owned by consumers (the caret snippet, with its www mirror caught up; design-studio #35).
prototype layout trio: prototype-flex / prototype-grid / prototype-waterfall joined the registry as the first meta.alpha items, dependency-free and host-agnostic.
CI and deploy: deploy builds the betlang wasm before the composite gate, so the npm mirror stops depending on local artifacts; the ghostty wasm asset pins caught up with upstream's re-shipped files; the standards gate stopped censusing gitignored local route mirrors. The remaining 8 fixes all come from the spin walkthrough rounds.
Upgrading
No breaking changes:
npx jixoai-ui@latest add spin
Existing projects: npx jixoai-ui@latest upgrade.
Acknowledgements
The text catalog's corpus comes from cli-spinners (ora's spinner corpus, MIT); the svg packs' upstream is SamHerbert's SVG-Loaders (MIT), aggregated by shubhamjain/svg-spinners and distributed through magecdn.
Links
Changelog: GitHub Release v0.5.0 · GitHub Release v0.5.1 · compare v0.4.0...v0.5.1
Docs & registry: ui.jixoai.com · the spin docs page · README
Contract sources: the spin-ora-svg-lane proposal · the spinner-channel-api proposal
Feedback: GitHub Issues
This site's series: Jixoai UI v0.4.0 · Jixoai UI v0.3.0
Chinese version: /zh/blog/2026-09-13-ui-0-5-0/
