# @skdx/react

Accessible React components for the SkandaDX design system — the core layer, styled by `@skdx/skins` and `@skdx/tokens` or by your own CSS.

Every component ships **behavior, accessibility, and state — no styles**. Components render plain semantic HTML decorated with `data-*` attributes; you (or a SkandaDX brand package) style them with CSS against those attributes, typically using the `--skdx-*` tokens from [`@skdx/tokens`](../tokens/skdxtokens.md). One component tree, any brand.

```text
@skdx/react (behavior) + --skdx-* variables (theme) + your CSS (skin) = branded UI
```

## Installation

```sh
pnpm add @skdx/react
# or: npm install @skdx/react
```

React 19 is required (`react` and `react-dom` are peer dependencies). The only runtime dependency is `@floating-ui/react-dom`, used for positioning overlays. The package ships ESM and CJS builds with TypeScript declarations and is tree-shakeable (`sideEffects: false`).

## Quick start

```tsx
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogOverlay,
  DialogPortal,
  DialogTitle,
  DialogTrigger,
} from '@skdx/react';

<Dialog>
  <DialogTrigger>Open settings</DialogTrigger>
  <DialogPortal>
    <DialogOverlay className="overlay" />
    <DialogContent className="panel">
      <DialogTitle>Settings</DialogTitle>…<DialogClose>Close</DialogClose>
    </DialogContent>
  </DialogPortal>
</Dialog>;
```

Nothing is styled until you say so:

```css
.panel {
  background: var(--skdx-color-semantic-surface);
  color: var(--skdx-color-semantic-text);
  border-radius: var(--skdx-radius-lg);
  box-shadow: var(--skdx-shadow-overlay);
  padding: var(--skdx-space-6);
}
.panel[data-state='open'] {
  animation: fade-in var(--skdx-motion-duration-normal) var(--skdx-motion-easing);
}
```

## Design principles

### Compound components

Complex widgets are broken into small parts (`Tabs` + `TabsList` + `TabsTrigger` + `TabsContent`) that communicate through context. You compose exactly the DOM you want and style each part separately.

### `asChild` — render onto your own element

Every DOM-rendering part accepts `asChild`. Instead of rendering its own tag, the component merges its props, behavior, and ref onto your child element:

```tsx
<DialogTrigger asChild>
  <MyDesignSystemButton>Open</MyDesignSystemButton>
</DialogTrigger>
```

### Data attributes are the styling API

Components expose their state as `data-*` attributes instead of class names — `data-state="open"`, `data-disabled`, `data-loading`, `data-orientation="horizontal"`, and so on. CSS attribute selectors do the rest; no styling props, no theme provider.

### Controlled or uncontrolled — your choice

Stateful components follow the `value` / `defaultValue` / `onValueChange` convention (`open` / `defaultOpen` / `onOpenChange` for overlays). Pass the controlled prop to own the state, or the `default*` prop to let the component manage it.

### Accessible by default

Parts render the correct roles, ARIA attributes, and keyboard interactions: focus trapping and scroll locking in modal overlays, roving tabindex in composite widgets (menus, toolbars, radio groups, trees), typeahead in selects and menus, and dismiss via <kbd>Esc</kbd> or outside-click.

## Components

| Category      | Components                                                                                                                                                             |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Basic         | `Button`, `Label`, `Divider`, `Avatar`, `Badge`, `Kbd`, `Card`, `SplitButton`, `Progress`                                                                              |
| Feedback      | `Alert`, `Skeleton`, `Spinner`, `Toast` (with `ToastProvider`/`ToastViewport`)                                                                                         |
| Layout        | `AspectRatio`, `ScrollArea`, `Resizable` (panel group / panel / handle), `Carousel`                                                                                    |
| Form controls | `Form`, `TextField`, `Textarea`, `NumberField`, `PinInput`, `TagsInput`, `FileUpload`, `Checkbox`, `RadioGroup`, `Switch`, `Slider`, `Rating`, `Toggle`, `ToggleGroup` |
| Disclosure    | `Collapsible`, `Accordion`, `Tabs`                                                                                                                                     |
| Navigation    | `Toolbar`, `NavigationMenu`, `Breadcrumb`, `Pagination`, `Menubar`                                                                                                     |
| Data display  | `Table` (with sorting and selection state), `Tree`, `Timeline`                                                                                                         |
| Date          | `Calendar` (plus date helpers: `addDays`, `addMonths`, `isSameDay`, `isSameMonth`, `getVisibleWeeks`), `DatePicker`                                                    |
| Overlays      | `Dialog`, `Popover` (with `PopoverProvider`), `Menu`, `Select`, `Combobox`, `Command`                                                                                  |

55 components in total. Each lives in `src/<name>/` with its parts, types, tests, and an `index.ts` barrel colocated; every part and its props type are exported from the package root (`TabsTrigger`, `TabsTriggerProps`, …), by way of that folder's `index.ts`.

App-level providers: wrap your app in `ToastProvider` (plus a `ToastViewport`) to use toasts, and `PopoverProvider` to share hover-open delay behavior.

## Core building blocks

The low-level utilities the components are built from are exported too, so brand packages and custom components can follow the same conventions:

| Export                            | What it does                                                                     |
| --------------------------------- | -------------------------------------------------------------------------------- |
| `Slot`                            | Merges props/ref onto a child element — the engine behind `asChild`              |
| `Primitive`                       | `Primitive.div`, `Primitive.button`, … — base parts that all accept `asChild`    |
| `composeEventHandlers`            | Chains a user handler with an internal one, honoring `preventDefault`            |
| `Portal`                          | Renders children into `document.body` (or a custom container)                    |
| `VisuallyHidden`                  | Hides content visually, keeps it for screen readers                              |
| `composeRefs` / `useComposedRefs` | Merge multiple refs into one                                                     |
| `useControllableState`            | The controlled/uncontrolled state pattern in hook form                           |
| `useCallbackRef`                  | Stable function identity for callbacks in effects                                |
| `createSafeContext`               | Context factory that throws a helpful error when a part is used outside its root |

## Theming

This package never reads theme values at runtime — theming happens entirely in CSS. Generate or apply `--skdx-*` variables with `@skdx/tokens` (`foundationToCss` once, then `applyTheme` / `themeToCss` per theme), then reference them in the stylesheets that target these components' data attributes. Switching `data-skdx-theme` on `<html>` re-brands every component instantly. See the [tokens README](../tokens/skdxtokens.md) for the full variable reference.

## Development

```sh
pnpm --filter @skdx/react build       # tsup → dist/
pnpm --filter @skdx/react test        # vitest + testing-library
pnpm --filter @skdx/react typecheck   # tsc --noEmit
```

Component demos live in the docs repository ([SkandaDX/docs.ui.skdx](https://github.com/SkandaDX/docs.ui.skdx)); run `pnpm dev` there.
