Skip to content

feat(sve-ui): add Kbd, ButtonGroup, Empty and InputGroup - #81

Merged
rodriabregu merged 2 commits into
mainfrom
feat/tier1-components
Sep 1, 2026
Merged

rodriabregu merged 2 commits into
mainfrom
feat/tier1-components

Conversation

@rodriabregu

Copy link
Copy Markdown
Owner

What & why

Four components, and the first ones added since the library finished wrapping all 41 of Bits UI's primitives — so there are no cheap components left and all four are hand-built.

They were picked by diffing the catalog against shadcn-svelte rather than by taste. Each is a gap people currently fill with a hand-rolled div, and none needs a new dependency (the package still has exactly one: bits-ui).

Why it earns a place
Kbd The library shipped Command, a command palette, with no way to draw the shortcut it advertises. label is the substance: ⌘, ⇧, ⌥ are punctuation to a screen reader and some voices skip them entirely, so a glyph-only shortcut can announce as nothing.
ButtonGroup Collapses the doubled border between attached buttons and lifts the focused child so its ring is not clipped by a neighbour.
Empty Every Table, list and search needs one, and today each consumer writes it again.
InputGroup An addon next to a bordered input is two boxes touching. Impossible to fix from outside the library without hacks.

Review path

Start with the three index.ts / component headers — they carry the reasoning, and the defaults are the design:

  • Empty/index.ts — Media always aria-hidden (the icon restates the title), Title a <p> not a heading (an empty state in a card must not inject "No results" into the document outline), announce opt-in.
  • InputGroup/index.ts — the group owns the border and :focus-within ring, the input owns neither. Addon is aria-hidden by default and cannot be the label.
  • ButtonGroup.svelte — why a name is required, and the isolation/z-index pair.

Then apps/example/scripts/check-coverage.mjs, which is where the two findings below came from.

Intentionally out of scope: the rest of the diff — Chart (needs a charting dependency; the package has one dep and that number is an asset), Carousel and Resizable (real, and real work), and a rows-as-data DataTable, which Table/index.ts already refuses in writing.

Two findings that came from tightening a guard, not from reading code

1. The example's coverage guard accepted a namespace member as proof of a top-level export. \bEmpty\b matched Command.Empty on the browse screen, so the guard reported Empty as covered while nothing rendered it. Same shape as the sve-nav-menu false positive already documented in the roadmap: a word-boundary match cannot tell a qualified name from a bare one. A negative lookbehind fixes it.

With it fixed, the guard immediately reported the standalone Calendar as never rendered — and that was true, for months. Only DatePicker.Calendar and RangeCalendar.* were on screen. A calendar inside a picker is a different mount: it opens in a portal, on demand. The bare month grid is now on the booking screen.

2. A test of mine was measuring nothing. Empty > is not a live region unless asked passed announce: false from the fixture, so it asserted the fixture's default, not the component's — flipping the component default to true left all six tests green. The fixture now renders the prop genuinely absent, and the mutation fails it.

Mutation-verified, all three load-bearing defaults:

drop aria-hidden from Empty.Media          → FAIL ✓
flip Empty.Root announce default to true   → FAIL ✓  (only after the fixture fix)
flip Addon decorative default to false     → FAIL ✓
restored                                   → 12 passed

Changes

Area Change
packages/sve-ui/src/lib/components/{Kbd,ButtonGroup,Empty,InputGroup} The four components (9 files)
src/lib/index.ts Exports, beside their nearest neighbours
src/tests/ 21 new tests + 4 fixtures; all four added to A11yFixture, which ssr.test.ts reuses — so each gets axe and server-render coverage
apps/docs/src/routes/components/{kbd,button-group,empty,input-group} Four docs pages
apps/docs/src/lib/docs/registry.ts Registry entries, marked new
packages/sve-ui/README.md Component table → 64, plus two entries in "easy to miss"
skills/sve-ui-usage/references/components.md Agent-skill catalog entries
apps/example/** Real usage on three screens, plus the standalone Calendar
apps/example/scripts/check-coverage.mjs The lookbehind fix
apps/docs/render-baseline.json +4 pages, and +8 elements on every page with the sidebar

On that +8 elements

The render guard reported a uniform +8 on all 67 existing pages, which is the kind of number worth explaining before rubber-stamping. DocsNav renders each registry item as an <a> plus, for status: 'new', a <span> tag — 2 elements × 4 new components = 8, on every page carrying the sidebar. Verified before updating the baseline.

Test plan

  • turbo run test --force — 665 tests across 76 files (was 644/72)
  • Every load-bearing default mutation-verified (above)
  • pnpm lint && pnpm check && pnpm build green; pnpm format:check clean
  • Package guard chain: check-dts 189 types, check-css-coverage 187 components, check-docs-coverage 64/64/64, check-readme-coverage 64, publint clean
  • check-example-coverage: all 66 exports rendered (was silently 64 of 66)
  • check:render — 71 pages, baseline updated in this commit
  • gen:props:check — props.json is 208 pure insertions, 0 deletions, and its 9 new keys match the PropsTable component="…" names on the docs pages
  • Screenshot baselines for the four new pages — updateSnapshots: 'none' makes a missing baseline a failure, and they can only be written on Linux. Triggering visual-baselines on this branch next, so the PNGs land here as a reviewable diff.

Checklist

  • Tests added/updated and passing (pnpm test)
  • pnpm lint && pnpm check && pnpm build all green
  • Changeset included — minor
  • Docs updated — four pages, registry, README, agent skill
  • No Tailwind / config required in consumer projects; no new runtime dependency

The library finished wrapping all 41 Bits UI primitives, so there are no cheap
components left — these four are hand-built, and were picked by diffing the
catalog against shadcn-svelte for gaps people fill with a hand-rolled div.

- Kbd: a keycap, with `label` so `⌘` is announced as "Command" instead of being
  skipped as punctuation. `Command` shipped as a palette with no way to draw the
  shortcut it advertises.
- ButtonGroup: attaches independent buttons, collapses the doubled border, lifts
  the focused child so its ring is not clipped. A name is required because
  `role="group"` without one names a boundary it cannot describe.
- Empty: media always `aria-hidden`, title a `<p>` unless given a level, and
  `announce` opt-in for emptiness that replaces results after a search.
- InputGroup: the group owns the border and focus ring, the input owns neither.
  Addons are `aria-hidden` by default and cannot be the label.

Two findings came out of tightening a guard rather than reading code:

The example's coverage guard matched `\bEmpty\b` against `Command.Empty`, so a
namespace member counted as proof of a top-level export. A negative lookbehind
fixes it — and with it fixed the guard reported the standalone `Calendar` as
never rendered, which had been true for months. It is on the booking screen now.

A test asserting `Empty.Root` is not a live region by default was asserting its
fixture's default instead. Flipping the component default to `true` left it
green. The fixture now renders the prop genuinely absent, and the mutation fails
it. The `aria-hidden` defaults on Empty.Media and InputGroup.Addon were verified
the same way.
@vercel

vercel Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
sve-ui Ready Ready Preview Sep 1, 2026 4:48pm UTC

baselines for the four new component pages: kbd, button-group, empty, input-group

308 baselines. Generated by the visual-baselines workflow, which is
the only sanctioned path: they are byte-compared and macOS font
rasterisation does not match this runner.
@rodriabregu
rodriabregu merged commit 126272e into main Sep 1, 2026
3 checks passed
@rodriabregu
rodriabregu deleted the feat/tier1-components branch September 1, 2026 16:54

This branch was successfully deployed

1 active deployment
Preview — 49b1992f Deployed Sep 1, 2026 by vercel[bot]
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