Skip to content

Jixoai UI v0.5: loading feedback covered, loaders included

jixoai v0.5.1 ui

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.

dots
arc
line
blocks-wave
tail-spin
the spinner prop's two resolution lanes: dots / arc / line are text-catalog names, blocks-wave / tail-spin svg artifact names. Rendered by the live component, not a screenshot

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-out

  • start: the fade-in completes before the handoff boundary, fully displayed when the next frame takes over

  • both: 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.

dots
dots
dots
growVertical
dots under interval 160 with linger 0 / 160 / 480; growVertical carries its tuned pair 120/120. Rendered by the live component
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 needs steps(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.

dots
dots2
line
pipe
simpleDots
arc
arrow
bounce
growVertical
toggle3
star
one representative per catalog family. Rendered by the live component

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 when document.readyState === 'complete', so hydration never resets a running animation.

blocks-wave
3-dots-bounce
bars-scale
clock
tail-spin
spinning-circles
the six loaders of the svg artifact set, SMIL running live. Rendered by the live component

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.

24 of the magecdn pack's 94 loaders (every fourth), currentColor painting

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