first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
# Button
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - button.variants — `buttonVariants` is the cva (clas…
- [button.variants](./button.variants.md) — `buttonVariants` is the cva (class-variance-authority) configuration that powers every `<Button>` render. It is split f…
- button.variants — `buttonVariants` is the cva (clas…
- [button.variants](./button.variants.md) — `buttonVariants` is the cva (class-variance-authority) configuration that powers every `<Button>` render. It is split f…
- button.variants — `buttonVariants` is the cva (clas…
- [button.variants](./button.variants.md) — `buttonVariants` is the cva (class-variance-authority) configuration that powers every `<Button>` render. It is split f…
- button.variants — `buttonVariants` is the cva (clas…
- [button.variants](./button.variants.md) — `buttonVariants` is the cva (class-variance-authority) configuration that powers every `<Button>` render. It is split f…
- [button.variants](./button.variants.md) — `buttonVariants` is the cva (class-variance-authority) configuration that powers every `<Button>` render. It is split f…
## Purpose
`<Button>` is the chassis primary action surface for `@suaveplan/react-ui`. It renders a native `<button>` (or polymorphically a child element via Radix `<Slot>` when `asChild` is set), threads its variant resolution through `useThemedVariants("Button")` so an active `ThemePlugin` may extend or replace any variant, and forwards refs to the underlying DOM node so consumer code (focus management, animation libraries, automated testing) can attach to the button directly.
The component deliberately does NOT carry an `isLoading` or `loading` prop. Per the upstream shadcn convention adopted across `@suaveplan/react-ui`, loading semantics are caller-driven: consumers compose `<Spinner />` + `disabled` + `data-icon="inline-start"` next to the action label rather than letting the button library guess what "loading" means. This keeps `<Button>` a pure styling + a11y surface and lets the caller own the orchestration.
## Features
- Six variants (`default`, `secondary`, `destructive`, `outline`, `ghost`, `link`) and four sizes (`sm`, `default`, `lg`, `icon`) shipped in the base cva config.
- `asChild` — when `true`, renders `props.children` with the button's classes / refs / props merged in via Radix `<Slot>`. The underlying child MUST be a single React element.
- Refs forwarded to the underlying `<button>` (or to the merged child element under `asChild`).
- Theme-driven variant overrides via `useThemedVariants("Button", buttonVariants)`. A `ThemePlugin.variantOverrides.Button` may add brand-new variants like `size: "xl"` (see the type-safety trade-off in `proposal.md` §"Type-safety trade-off").
- Icon-slot conventions: child `<svg>` elements receive `pointer-events-none size-4 shrink-0` so consumers may drop a Lucide icon next to text without sizing boilerplate.
- Accessible-name fallback: when the rendered button has no text children AND the consumer has not supplied `aria-label`, the chassis fills in the translated fallback `react-ui.button.iconOnly.fallback` so screen readers do not announce the empty button.
- `prefers-reduced-motion`: hover and focus transitions collapse to instant transitions under `motion-reduce`.
- Focus styling via `focus-visible:ring-2 focus-visible:ring-ring` so keyboard users see a visible focus ring; pointer focus does not.
- All native `ButtonHTMLAttributes` are forwarded so `onClick`, `disabled`, `form`, `formAction`, etc. work as expected.
## Usage: Basic Example
```tsx
import { Button } from "@suaveplan/react-ui";
export function SaveAction() {
return (
<Button onClick={() => console.warn("save clicked")}>
Save changes
</Button>
);
}
```
## Usage: Advanced Example
```tsx
import { Button, useThemedVariants } from "@suaveplan/react-ui";
import { ChevronRightIcon, MailIcon } from "lucide-react";
export function ComposedActions() {
return (
<div className="flex gap-2">
<Button variant="outline" size="sm">
<MailIcon data-icon="inline-start" />
Email me
</Button>
<Button variant="destructive" size="lg" disabled>
Delete account
</Button>
<Button asChild variant="link">
<a href="/settings">Account settings</a>
</Button>
<Button
variant="ghost"
size="icon"
aria-label="Open submenu"
onClick={(event) => event.preventDefault()}
>
<ChevronRightIcon />
</Button>
</div>
);
}
```
## API Reference
### `<Button>`
`forwardRef<HTMLButtonElement, ButtonProps>` — renders a `<button type={type}>` (default `"button"`) when `asChild` is unset, or merges into `props.children` via Radix `<Slot>` when `asChild === true`.
#### `ButtonProps`
| Prop | Type | Default | Description |
|---|---|---|---|
| `variant` | `"default" \| "secondary" \| "destructive" \| "outline" \| "ghost" \| "link"` | `"default"` | Visual variant. Layered with `useThemedVariants("Button")` so themes may extend. |
| `size` | `"sm" \| "default" \| "lg" \| "icon"` | `"default"` | Padding / height variant. `"icon"` renders a square `size-9` button intended for icon-only content. |
| `asChild` | `boolean` | `false` | When `true`, renders `props.children` via Radix `<Slot>` with the button's classes + refs merged in. |
| `accessibleNameKey` | `TranslationKey` | `"react-ui.button.iconOnly.fallback"` | Translation key used as the fallback `aria-label` when the button has no text children AND no explicit `aria-label`. |
| `type` | `"button" \| "submit" \| "reset"` | `"button"` | Native button type. Suppressed when `asChild` is true (consumer's element owns its `type`). |
| _all native_ `ButtonHTMLAttributes<HTMLButtonElement>` | _native_ | _native_ | Forwarded as-is. |
#### `buttonVariants`
The bare cva resolver. Exported so external consumers (`@suaveplan/react-ui-theme-*`, ejected source under `npx shadcn@latest add @suaveplan/button`) can re-use the variants directly without going through `useThemedVariants`.
#### Errors
- `useTranslation("react-ui")` is called unconditionally inside the component. When the surrounding tree lacks a `<TranslationProvider>` (or `<ReactUiProvider>`), the hook returns its identity-fallback shape and the component renders without throwing — every label resolves to the literal key. This matches the `react-i18n` "no-throw in production" contract.
## Implementation Notes
- The variants module is intentionally separate (`button.variants.ts`) so an ejected source copy keeps the cva config in a dedicated file, mirroring the upstream shadcn convention. The chassis component imports it; tree-shaking keeps the footprint identical.
- `useThemedVariants("Button", buttonVariants)` is called unconditionally so the hook order is stable across renders. The hook's memoisation key is `(themeName, "Button")` so a re-render with the same theme returns the same resolver reference.
- The `hasTextContent` walker is single-level (no recursion) — if a consumer wraps text inside a deeper element tree (`<Button><span><strong>label</strong></span></Button>`), the chassis will still inject the icon-only fallback because it cannot see through `<span>`. This is by design: deeply-nested text-bearing elements should declare their own accessible name on the inner element.
- `motion-reduce:transition-none` is hardcoded into the base classes rather than driven by `motionTokens.respectReducedMotion` because the cva-string-vs-CSS-variable boundary makes the variable approach awkward for transition shorthands. The chassis relies on Tailwind's `motion-reduce` variant which honours `prefers-reduced-motion` natively.
- `aria-label` precedence: when the consumer supplies `aria-label="…"`, it is preserved verbatim on the rendered element. When omitted AND the children carry no plain text, the icon-only fallback is filled in. When omitted AND text children are present, no `aria-label` is set (the text itself becomes the accessible name).