From 8aea881ffa911fe972a03bfc2f0c751555d2b6e9 Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Sun, 28 Jun 2026 16:42:01 +0800 Subject: [PATCH] feat(components): add @vue-tui/components + Spinner (first component) (#229) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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) * feat(components): add the Spinner component Spinner is a + 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) * 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 spans share an outer context so they render inline on one line rather than stacking vertically. Co-authored-by: Claude Opus 4.8 (1M context) * chore(components): add @vue-tui/components to the CI task graph Co-authored-by: Claude Opus 4.8 (1M context) * docs(components): record Spinner decisions Co-authored-by: Claude Opus 4.8 (1M context) * 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) * docs(readme): list @vue-tui/components + Add the new package to the hero line + Packages table, and to the Components table. Marked "New; early". Co-authored-by: Claude Opus 4.8 (1M context) * docs(readme): give @vue-tui/components its own section Move 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) * docs(readme): drop the "New; early" status tag for @vue-tui/components Co-authored-by: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .agents/docs/components-design-principles.md | 6 +- .agents/docs/components/spinner.md | 43 +++++++++++ README.md | 22 ++++-- packages/components/package.json | 39 ++++++++++ packages/components/src/index.ts | 2 + packages/components/src/spinner-props.ts | 13 ++++ packages/components/src/spinner.test.tsx | 79 ++++++++++++++++++++ packages/components/src/spinner.vue | 26 +++++++ packages/components/src/spinners.test.tsx | 55 ++++++++++++++ packages/components/src/spinners.ts | 24 ++++++ packages/components/tsconfig.json | 22 ++++++ packages/components/vite.config.ts | 16 ++++ pnpm-lock.yaml | 39 ++++++++++ vite.config.ts | 9 +++ 14 files changed, 387 insertions(+), 8 deletions(-) create mode 100644 .agents/docs/components/spinner.md create mode 100644 packages/components/package.json create mode 100644 packages/components/src/index.ts create mode 100644 packages/components/src/spinner-props.ts create mode 100644 packages/components/src/spinner.test.tsx create mode 100644 packages/components/src/spinner.vue create mode 100644 packages/components/src/spinners.test.tsx create mode 100644 packages/components/src/spinners.ts create mode 100644 packages/components/tsconfig.json create mode 100644 packages/components/vite.config.ts diff --git a/.agents/docs/components-design-principles.md b/.agents/docs/components-design-principles.md index 72268a4..149fd58 100644 --- a/.agents/docs/components-design-principles.md +++ b/.agents/docs/components-design-principles.md @@ -5,8 +5,10 @@ > issue, not here. It records how components in `@vue-tui/components` should be _shaped_ and > _styled_, and the bar for adding one in the first place. > -> **Status:** planned — no `@vue-tui/components` code exists yet. This is design intent, not a -> description of shipped code. +> **Status:** active — the package now ships its first component, `Spinner` (see per-component +> records below). The principles here are design intent for the package as a whole. +> +> **Per-component records:** [spinner](./components/spinner.md). **The governing idea:** components in `@vue-tui/components` are **pure compositions of `@vue-tui/runtime` primitives**. The runtime owns the terminal-I/O and layout/commit boundary; diff --git a/.agents/docs/components/spinner.md b/.agents/docs/components/spinner.md new file mode 100644 index 0000000..6f06aaa --- /dev/null +++ b/.agents/docs/components/spinner.md @@ -0,0 +1,43 @@ +# Spinner — decision record + +> Decisions specific to `@vue-tui/components`' `Spinner`. Shared conventions live in +> [components-design-principles.md](../components-design-principles.md). Tracking: #218. + +A pure composition of `` + `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). diff --git a/README.md b/README.md index 25d40bb..b73bf2d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The Vue framework for terminal UIs. Build with components, develop with HMR, test with confidence.

- @vue-tui/runtime · @vue-tui/vite · @vue-tui/testing + @vue-tui/runtime · @vue-tui/components · @vue-tui/vite · @vue-tui/testing

- **Vue SFC & JSX** — write terminal interfaces with `