Skip to content

feat(spinner): add Spinner atom (no variants, sm/md/lg, role=status)#55

Merged
dgraciac merged 2 commits into
mainfrom
feat/spinner-atom
Apr 29, 2026
Merged

feat(spinner): add Spinner atom (no variants, sm/md/lg, role=status)#55
dgraciac merged 2 commits into
mainfrom
feat/spinner-atom

Conversation

@dgraciac
Copy link
Copy Markdown
Member

Summary

Adds the Spinner atom — visual loading indicator with role="status" for assistive tech. Three sizes (sm / md / lg) aligned with the form-control grid (16 / 20 / 24 px), and currentColor inheritance so composition cases like <Button intent="primary"><Spinner /></Button> work out of the box (the Spinner picks up the Button's text colour automatically). Re-opens the cola of AsyncLoadingButton / Publish/Unpublish/AddButton wrappers deferred from wave 1 Button (#777).

  • Inline SVG with CSS @keyframes rotation. Zero icon-library dependencies (no lucide-react, no framer-motion).
  • Honours prefers-reduced-motion by slowing rotation to 4 s instead of removing it entirely (assistive UX still benefits from the in-progress cue).
  • srLabel prop (default "Loading…") renders as visually-hidden text inside the status node; pass an action-specific label ("Saving template…", "Deleting…") when relevant.
  • No tone axis on purpose. State-of-the-art (Radix Themes, shadcn, Mantine, Material UI) keep the Spinner as a single chrome that uses currentColor; the consumer picks the colour via the parent's color or className. Keeps the atom small and removes the need for a tone palette mapping.

API

import { Spinner } from '@code-sherpas/pharos-react';

<Spinner />                                  // md, "Loading…"
<Spinner size="sm" />                        // 16 px
<Spinner srLabel="Saving template…" />       // action-specific
<Spinner aria-label="Loading users" />       // overrides srLabel
<Spinner ref={spinnerRef} />                 // ref forwarded to <span>

// Inside a Button — currentColor inherits the Button's text colour
<Button intent="primary" disabled>
  <Spinner size="sm" srLabel="Saving…" />
  <span>Saving</span>
</Button>

SpinnerProps extends ComponentProps<'span'> plus the typed size axis and srLabel.

Storybook

Components/Spinner: Playground, Default, Sizes (3-up grid), InsideButton (composition), ColourFromParent (4 tones via color style), Matrix (sizes × tones grid).

Test plan

  • pnpm typecheck — clean.
  • pnpm lint — clean.
  • pnpm format:check — clean.
  • pnpm build — Vite library build + verify:dist-types smoke pass.
  • pnpm test — 91 tests across 7 files, 0 failures (12 new Spinner tests).
  • Chromatic baseline — published by CI on push; manual approval required for the new Spinner story snapshots.
  • CI checks on PR.

Out of scope

  • Adoption in alexandria-web-application — separate PR after this publishes (0.9.0). Initial targets:
    • The wrappers deferred from #777 wave 1 Button: AsyncLoadingButton, Publish/UnpublishButton, AddButton — these implement their own isLoading state today and the Pharos plan in wave 1 was to revisit when Spinner lands.
    • Bare <Loader /> / animate-spin div usages across the codebase (TBD by audit).
  • Pharos-side Button isLoading prop. Plan §7 names this as a possible enhancement once Spinner is shipped. Composition (<Button><Spinner /></Button>) covers the use-case today; lifting it into the Button API is a deliberate future call.

🤖 Generated with Claude Code

Visual loading indicator with role="status" for assistive tech, three
sizes (sm/md/lg) aligned with the form-control grid (16/20/24 px), and
currentColor inheritance — composition cases like
<Button intent="primary"><Spinner /></Button> work out of the box because
the Spinner picks up the Button text colour automatically. No
`Button isLoading` prop is required at the atom level.

Inline SVG with CSS @Keyframes rotation. Zero icon-library dependencies
(no lucide-react, no framer-motion). Honours prefers-reduced-motion by
slowing rotation to 4s instead of removing it entirely.

`srLabel` (default "Loading…") rendered as visually-hidden text inside
the status node; pass an action-specific label ("Saving template…",
"Deleting…") when relevant. No `tone` axis — consumers set the colour
by setting `color` on the parent or via className. Same pattern Radix
Themes / shadcn / Mantine use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chromatic-com
Copy link
Copy Markdown

chromatic-com Bot commented Apr 29, 2026

Tip

All tests passed and all changes approved!

🟢 UI Tests: 6 visual and accessibility changes accepted as baselines
🟢 UI Review: Approved by David Gracia
Storybook icon Storybook Publish: 52 stories published

@dgraciac dgraciac merged commit 92d84b0 into main Apr 29, 2026
10 checks passed
@dgraciac dgraciac deleted the feat/spinner-atom branch April 29, 2026 02:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant