Files
vue-tui/packages/components/src/spinner.vue
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

27 lines
1.1 KiB
Vue

<script setup lang="ts">
import { computed } from "vue";
import { useAnimation, Text } from "@vue-tui/runtime";
import { spinnerProps } from "./spinner-props.ts";
import { resolveSpinner } from "./spinners.ts";
defineOptions({ name: "Spinner" });
const props = defineProps(spinnerProps);
const set = computed(() => resolveSpinner(props));
const { frame } = useAnimation({ interval: () => set.value.interval });
const glyph = computed(() => set.value.frames[frame.value % set.value.frames.length]);
</script>
<template>
<!-- The outer <Text> establishes a shared text context (runtime TextContextKey) so
both inner spans render INLINE as <tui-virtual-text> (one line, `⠋ Loading`).
Two bare sibling top-level <Text> would each be a block <tui-text> node and stack
vertically under the root's column direction. The outer Text carries no color, so
only the glyph span is tinted; the label span stays default. The separating space
is an interpolation so Vue's whitespace:'condense' keeps it. -->
<Text
><Text :color="color">{{ glyph }}</Text
><Text v-if="label">{{ " " + label }}</Text></Text
>
</template>