Files
vue-tui/.agents/docs/components/spinner.md
T
Yunfei He 8aea881ffa feat(components): add @vue-tui/components + Spinner (first component) (#229)
* feat(components): scaffold @vue-tui/components with spinner preset data

New private package (0.0.0) for high-level components composed from runtime
primitives. Ships the dots/line preset data + a pure resolveSpinner() with
edge-guards (empty frames / unknown type → dots; interval threads both modes),
fully unit-tested incl. a width-safety guard (string-width === 1 per frame).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(components): add the Spinner component

Spinner is a <Text> + useAnimation pure composition: `type` selects an inline
preset (dots/line), `frames`/`interval` is the escape hatch, and it always
animates (no interactivity gate — there is no public signal; matches Ink).
Renders a visible glyph non-interactively. Typed props via ExtractPublicPropTypes.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(components): Spinner color + label

`color` tints the glyph only (label stays default, matching ora/@inkjs/ui);
`label` renders after the glyph with a separating space (interpolated so Vue
whitespace-condense keeps it). Two <Text> spans share an outer <Text> context so
they render inline on one line rather than stacking vertically.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(components): add @vue-tui/components to the CI task graph

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(components): record Spinner decisions

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(components): refresh package status and clarify ink-spinner parity note

The design-principles status blockquote said the package was 'planned'
with no code yet; this branch ships Spinner, so mark it active. Also
reword the spinner Behavior note to name the third-party ink-spinner
explicitly and soften it to an unverified, un-run-checked observation.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(readme): list @vue-tui/components + <Spinner>

Add the new package to the hero line + Packages table, and <Spinner> to the
Components table. Marked "New; early".

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(readme): give @vue-tui/components its own section

Move <Spinner> out of the runtime Components table into a separate
"High-level Components" section so the package's API surface stays decoupled
from the runtime primitives. Add a ToC entry.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(readme): drop the "New; early" status tag for @vue-tui/components

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 16:42:01 +08:00

2.5 KiB

Spinner — decision record

Decisions specific to @vue-tui/components' Spinner. Shared conventions live in components-design-principles.md. Tracking: #218.

A pure composition of <Text> + useAnimation — no new runtime export needed.

Style selection & the escape hatch

  • type selects an inlined preset; only dots (default) and line ship.
  • Inclusion bar for a preset: universal default + functional fallback only; every frame must be width-safe (exactly 1 column, verified with string-width); non-novelty. dots is the universal default; line is the only pure-ASCII fallback (braille needs a braille font). Everything else — including the full cli-spinners set — is reachable via the escape hatch.
  • Escape hatch: frames: string[] + interval?: number override type. A cli-spinners entry ({ interval, frames }) can be spread in verbatim. Empty frames and an unknown type both fall back to dots; interval overrides in either mode.
  • No cli-spinners dependency. This is a set-membership decision under the inclusion bar, recorded here — not an Ink divergence (ink-spinner/cli-spinners are third-party npm, not Ink-core v7.0.4, which has no Spinner; there is nothing to diverge from).

Behavior

  • Always animates. It does NOT gate on app interactivity — there is no public signal for interactive (it lives only on the internal AppContext), and reaching for it would break the pure-composition rule. This mirrors the third-party ink-spinner, which likewise just animates (unverified parity — not run-checked); the runtime governs non-interactive output one layer down. (If a static-when-non-interactive affordance is ever wanted, it needs the runtime to expose interactivity publicly first.)
  • Switching type changes the preset interval; useAnimation resets frame to 0 on a live interval change — acceptable for a spinner.

API shape

  • color tints the glyph only; the label stays default-colored (matches ora / @inkjs/ui).
  • label is a string prop (type-friendly + the common Vue idiom for simple text). If rich label content is ever needed, add a same-purpose default slot later — non-breaking.

Non-goals

  • succeed / fail / pending terminal states (✔/✖) are NOT Spinner's job — they belong to a future TaskList / StatusMessage (render = f(state) in a separate component).
  • Screen-reader handling: deferred (niche; no surveyed spinner implements it).