first commit
This commit is contained in:
118
docs/ui/react/react-ui/src/components/button/button.md
Normal file
118
docs/ui/react/react-ui/src/components/button/button.md
Normal 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).
|
||||
Reference in New Issue
Block a user