npm package: @matthieuglaisner/react-ui-components-library
Description: React + Tailwind CSS + TypeScript UI components
Latest version: 1.1.0

# @matthieuglaisner/react-ui-components-library

React + Tailwind CSS + TypeScript UI components. Most building blocks are **Tailwind-only**; overlays, menus, tabs, and several form primitives wrap [**Radix UI**](https://www.radix-ui.com/) for accessibility (focus traps, portals, keyboard support). Everything ships as compiled JS plus TypeScript declarations; **your app must run Tailwind** so utility classes in `dist` are generated.

## Requirements

- **React** 18 or 19 (`react` and `react-dom`)
- **Tailwind CSS** in the host app (v3-style `content` config below; adapt if you use Tailwind v4)
- **Radix UI** packages — **required peers** for components that import them. Install the `@radix-ui/react-*` packages you use (see [peerDependencies](package.json) for exact names and ranges). If you use every exported Radix-backed piece, install all listed Radix peers.
- **react-router-dom** 6.28+ or 7.x is **optional**. Install it only if you use [`Link`](#link-props-summary) with `as={NavLink}` or `as={RouterLink}`.

This package does not ship a separate CSS bundle. Class names live in `dist/**/*.js` (and `.cjs`), and your Tailwind build must scan those files.

## Installation

```bash
npm install @matthieuglaisner/react-ui-components-library
```

Install Radix peers as needed (example — matches current [package.json](package.json) peer ranges):

```bash
npm install @radix-ui/react-accordion @radix-ui/react-alert-dialog @radix-ui/react-dialog @radix-ui/react-popover @radix-ui/react-scroll-area @radix-ui/react-select @radix-ui/react-slider @radix-ui/react-switch @radix-ui/react-tabs @radix-ui/react-toast @radix-ui/react-toggle-group @radix-ui/react-tooltip
```

To install from Git instead of the registry:

```bash
npm install github:MattGlsn/ui-components-library
```

Adjust the URL to your fork or organization.

## Tailwind setup (required)

Tailwind’s JIT compiler only emits utilities for class names it finds in the **`content`** paths you configure. Because this library publishes **`dist`** only, add a glob that includes the ESM and CJS bundles (`.js` and `.cjs`):

```js
// tailwind.config.js (or .ts)
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
    "./node_modules/@matthieuglaisner/react-ui-components-library/dist/**/*.{js,cjs}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};
```

Adjust the first entries to match your app. If the package is hoisted differently, point the glob at the folder that contains `@matthieuglaisner/react-ui-components-library/dist`.

### Dark mode

Components use Tailwind `dark:` variants (primary `#6a7cbd` in light, `#8fa2e6` in dark). Set `darkMode` to **`"class"`** or **`"media"`** in your Tailwind config. [`Link`](#link-props-summary) is a styled text link (hover color only; use `aria-current="page"` or `NavLink` for the current route). For SPA routes use `<Link as={RouterLink} to="…">` or `<Link as={NavLink} to="…">` from `react-router-dom` when needed. See Storybook for per-component focus, error, and layout details.

## Component catalog

Exports are named (tree-shakeable) from the package root, for example `import { DialogRoot, Table } from "@matthieuglaisner/react-ui-components-library"`.

| Area | Components |
| ---- | ---------- |
| **Layout** | `Container`, `Divider`, `Grid`, `Group`, `Section`, `Stack` |
| **Typography** | `Label`, `Text`, `Title` |
| **Feedback** | `Alert`, `Badge`, `Progress`, `Skeleton`, `Spinner`, `Toast` |
| **Toasts (Radix)** | `ToasterProvider`, `ToasterViewport`, `ToasterRoot`, `ToasterTitle`, `ToasterDescription`, `ToasterClose`, `ToasterAction` — stackable notifications; compose with Radix patterns (see Storybook). |
| **Overlays** | `DialogRoot`, `DialogTrigger`, `DialogContent`, `DialogTitle`, `DialogDescription`, `DialogClose`, `DialogPortal`, `DialogOverlay` |
| **Confirm** | `AlertDialogRoot`, `AlertDialogTrigger`, `AlertDialogContent`, `AlertDialogTitle`, `AlertDialogDescription`, `AlertDialogAction`, `AlertDialogCancel`, … |
| **Drawer** | `DrawerRoot`, `DrawerTrigger`, `DrawerContent`, `DrawerTitle`, `DrawerDescription`, `DrawerClose`, `DrawerOverlay` |
| **Floating** | `PopoverRoot`, `PopoverTrigger`, `PopoverContent`, `PopoverAnchor`, `PopoverPortal` |
| **Tooltip** | `TooltipProvider`, `TooltipRoot`, `TooltipTrigger`, `TooltipContent`, `TooltipPortal` — wrap the app (or layout) with `TooltipProvider` where you use tooltips. |
| **Select** | `SelectRoot`, `SelectTrigger`, `SelectValue`, `SelectContent`, `SelectItem`, `SelectGroup`, `SelectLabel`, `SelectSeparator`, `SelectPortal`, `SelectViewport` |
| **Navigation** | `TabsRoot`, `TabsList`, `TabsTrigger`, `TabsContent`; `AccordionRoot`, `AccordionItem`, `AccordionHeader`, `AccordionTrigger`, `AccordionContent`; `Pagination`; `Stepper`; `Navbar`; `DropdownMenu`; `Breadcrumbs` |
| **Data display** | `Card`; `Table`, `TableHeader`, `TableBody`, `TableRow`, `TableHeadCell`, `TableCell`; `Avatar`; `EmptyState` |
| **Forms** | `Button`, `Input`, `TextArea`, `Checkbox`, `CheckboxList`, `Radio`, `RadioList`, `Form`, `Switch`, `Slider`, `FileInput`, `SegmentedControlRoot`, `SegmentedControlItem` |
| **Content / chrome** | `Link` |
| **Utility** | `ScrollArea`; `useClickAway` |

**Toast vs Toaster:** [`Toast`](#toast-props-summary) is a single presentational banner (variants, dismiss). **Toaster**\* primitives are Radix-based and suit queued, viewport-anchored notifications; use them when you need provider + multiple live regions.

## Usage

```tsx
import { useState } from "react";
import {
  Breadcrumbs,
  Button,
  Checkbox,
  CheckboxList,
  DropdownMenu,
  Form,
  Input,
  Link,
  Navbar,
  TextArea,
  Title,
  Toast,
  type BreadcrumbItem,
} from "@matthieuglaisner/react-ui-components-library";

export function Example() {
  const [agree, setAgree] = useState(false);
  const [channels, setChannels] = useState<string[]>(["email"]);
  const [email, setEmail] = useState("");
  const [formError, setFormError] = useState<string | undefined>();

  const notifyItems = [
    { id: "email", label: "Email" },
    { id: "sms", label: "SMS" },
  ];

  const breadcrumbItems: BreadcrumbItem[] = [
    { label: "Home", href: "#" },
    { label: "Projects", href: "#" },
    { label: "Project Alpha" },
  ];

  return (
    <div className="flex flex-col gap-6">
      <Title as="h2" size="md" accent>
        Example
      </Title>
      <Toast type="info" title="Tip" onDismiss={() => {}}>
        Optional body text.
      </Toast>
      <Navbar
        aria-label="Example"
        brand={<span className="text-lg font-semibold text-[#6a7cbd]">Acme</span>}
        linksAlign="center"
        trailing={<Button size="sm">Sign in</Button>}
      >
        <Link href="#" aria-current="page">
          Home
        </Link>
        <Link href="#">Docs</Link>
      </Navbar>
      <Breadcrumbs items={breadcrumbItems} />
      <div className="flex flex-wrap gap-3">
        <Button variant="primary" size="md">
          Save
        </Button>
        <Button variant="secondary" size="sm">
          Cancel
        </Button>
        <Button variant="tertiary" fullWidth disabled>
          Disabled
        </Button>
        <Button className="uppercase tracking-wide">Custom classes</Button>
      </div>
      <div className="flex w-full max-w-sm flex-col gap-3">
        <Input
          label="Email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="you@example.com"
          helperText="We’ll only use this for account recovery."
        />
        <Input type="password" placeholder="Password" aria-label="Password" />
        <Input size="sm" invalid placeholder="Fix validation errors" aria-label="Invalid example" />
        <Input fullWidth placeholder="Full width in a block layout" aria-label="Wide field" />
      </div>
      <TextArea
        label="Notes"
        placeholder="Add details…"
        rows={4}
        helperText="Optional. Shown below the field when there is no error."
      />
      <TextArea error="This field is required." defaultValue="" rows={3} />
      <Form
        className="w-full max-w-sm"
        error={formError}
        onSubmit={(e) => {
          e.preventDefault();
          setFormError("Example error — change the field below to clear it.");
        }}
        submitLabel="Submit form"
      >
        <Input
          label="Field in Form"
          name="inForm"
          fullWidth
          placeholder="Type to clear the error"
          onChange={() => setFormError(undefined)}
        />
      </Form>
      <div className="flex flex-col gap-2">
        <Checkbox label="Remember me" defaultChecked />
        <Checkbox aria-label="Standalone option" />
        <Checkbox
          label="I agree (controlled)"
          checked={agree}
          onChange={(e) => setAgree(e.target.checked)}
        />
        <CheckboxList
          legend="Notifications"
          items={notifyItems}
          orientation="column"
          value={channels}
          onValueChange={setChannels}
          name="notify"
        />
        <CheckboxList
          aria-label="Layout row example"
          items={[
            { id: "a", label: "Alpha" },
            { id: "b", label: "Bravo" },
            { id: "c", label: "Charlie" },
          ]}
          orientation="row"
          defaultValue={["b"]}
        />
        <DropdownMenu
          triggerLabel="Actions"
          items={[
            { id: "edit", label: "Edit" },
            { id: "copy", label: "Copy" },
            { id: "remove", label: "Remove", disabled: true },
          ]}
        />
      </div>
    </div>
  );
}
```

### Button props (summary)

| Prop         | Description                                                                 |
| ------------ | --------------------------------------------------------------------------- |
| `variant`    | `"primary"` (default), `"secondary"`, or `"tertiary"`                       |
| `size`       | `"sm"`, `"md"` (default), or `"lg"` — text and padding scale together       |
| `fullWidth`  | When `true`, the button spans the width of its container                    |
| `disabled`   | Native disabled behavior and muted appearance                               |
| `className`  | Merged with the component’s base classes                                    |
| …            | Other native `<button>` attributes are forwarded (for example `type`, `onClick`, `aria-*`) |

### Form props (summary)

| Prop                 | Description                                                                 |
| -------------------- | --------------------------------------------------------------------------- |
| `showSubmitButton`   | When `false`, the built-in submit `Button` is not rendered. Default `true`. |
| `submitLabel`        | Label for the default submit button. Default `"Submit"`.                    |
| `submitDisabled`     | When `true`, disables the default submit (combined with `submitButtonProps.disabled`). |
| `submitButtonProps`  | Props forwarded to the default submit `Button` except `type` (always `"submit"`) and `children` (from `submitLabel`). |
| `error`              | Form-level message (`ReactNode`). Empty or whitespace-only strings are ignored. Renders in `role="alert"` with error styling; sets `aria-invalid` on the form; merges into `aria-describedby`. **While shown, the default submit is disabled** — clear `error` to re-enable. |
| `className`          | Merged onto the `<form>`.                                                   |
| …                    | Other native `<form>` attributes are forwarded (for example `onSubmit`, `method`, `action`, `ref`, `aria-describedby`). |

### Input props (summary)

| Prop         | Description                                                                 |
| ------------ | --------------------------------------------------------------------------- |
| `type`       | Any native input type (defaults to `"text"`).                               |
| `label`      | Optional `ReactNode`. When set, wraps the field in a `<label>` and wires `id` / `htmlFor` if you omit `id`. |
| `size`       | `"sm"`, `"md"` (default), or `"lg"` — padding and text scale (not the HTML `size` attribute; use `className` / `style` for character width if needed). |
| `fullWidth`  | When `true`, the input spans the width of its container.                    |
| `helperText` | Optional `ReactNode` rendered below the field and linked via `aria-describedby` (merged with your `aria-describedby`). Color follows status: invalid (enabled) is red; disabled is muted (wins over invalid). |
| `invalid`    | When `true`, sets `aria-invalid` and applies error border and focus ring.   |
| `disabled`   | Native disabled behavior and muted appearance                               |
| `className`  | Merged with the component’s base classes                                    |
| …            | Other native `<input>` attributes are forwarded. For checkboxes, consider the library `Checkbox` for tailored styling. |

### TextArea props (summary)

| Prop          | Description                                                                 |
| ------------- | --------------------------------------------------------------------------- |
| `label`       | Optional label; associates with the control via `id` or a generated id      |
| `helperText`  | Hint below the field (hidden when `error` is a non-empty string)            |
| `error`       | `true` sets `aria-invalid`; a string also shows that message below the field |
| `size`        | `"sm"`, `"md"` (default), or `"lg"` — text, padding, and minimum height     |
| `fullWidth`   | When `true`, the textarea spans the width of its container                  |
| `disabled`    | Native disabled behavior and muted appearance                               |
| `className`   | Merged with the component’s base classes                                    |
| …             | Other native `<textarea>` attributes are forwarded (for example `rows`, `value`, `onChange`, `aria-*`) |

### Title props (summary)

| Prop         | Description                                                                 |
| ------------ | --------------------------------------------------------------------------- |
| `as`         | `"h1"` (default) through `"h6"` — semantic heading element                  |
| `children`   | Heading content                                                             |
| `size`       | `"xs"`, `"sm"`, `"md"` (default), `"lg"`, or `"xl"` — visual scale         |
| `align`      | `"left"` (default), `"center"`, or `"right"`                                |
| `accent`     | When `true`, uses the library primary color for the text                    |
| `truncate`   | When `true`, single-line ellipsis overflow                                  |
| `className`  | Merged with the component’s base classes                                    |
| …            | Other native heading attributes are forwarded for the chosen `as` tag     |

### Radio props (summary)

| Prop         | Description                                                                 |
| ------------ | --------------------------------------------------------------------------- |
| `children`   | Optional label content; when set, wraps the input in an associated `<label>`. |
| `className`  | Merged with the component’s base classes                                    |
| …            | Other native `<input>` attributes are forwarded except `type` (always `"radio"`). |

### RadioList props (summary)

| Prop              | Description                                                                 |
| ----------------- | --------------------------------------------------------------------------- |
| `name`            | Shared `name` for each radio in the group.                                  |
| `options`         | Array of `{ value, label, disabled? }`.                                     |
| `orientation`     | `"vertical"` (default) or `"horizontal"`.                                     |
| `value` / `defaultValue` / `onChange` | Controlled or uncontrolled single selection (native change events). |
| `legend`          | Optional fieldset legend; otherwise set `aria-label` on the fieldset.       |
| `disabled`        | When `true`, all options are non-interactive.                                 |
| `className`       | Merged onto the `<fieldset>`.                                               |
| …                 | Other native `<fieldset>` attributes are forwarded (except `children` / `onChange`). |

### DropdownMenu props (summary)

| Prop              | Description                                                                 |
| ----------------- | --------------------------------------------------------------------------- |
| `items`           | Array of `{ id, label, disabled? }`.                                        |
| `onItemSelect`    | Optional `(id: string) => void` when an enabled item is chosen; menu closes afterward. |
| `triggerLabel`    | Use **either** this **or** `renderTrigger`: built-in `<button>` text.       |
| `renderTrigger`   | `(props: DropdownMenuTriggerProps) => ReactNode` — spread `props` onto your button (ref, `onClick`, ARIA). |
| `align`           | `"start"` (default) or `"end"` — horizontal alignment of the panel under the trigger. |
| `open` / `onOpenChange` | Optional controlled open state.                                       |
| `menuClassName`   | Extra classes for the menu panel.                                           |
| `className`       | Merged onto the outer wrapper `<div>`.                                      |
| …                 | Other native `<div>` attributes are forwarded (except `children`).          |

### Checkbox props (summary)

| Prop              | Description                                                                 |
| ----------------- | --------------------------------------------------------------------------- |
| `label`           | Optional `ReactNode`. When set, wraps the input in a `<label>` so text is clickable and associated; label text is semibold while checked. |
| `className`       | Merged with the component’s base classes                                    |
| `disabled`        | Native disabled behavior and muted appearance                               |
| `checked` / `onChange` | Controlled usage: drive `checked` from state and update in `onChange`.   |
| `defaultChecked`  | Uncontrolled default checked state                                          |
| …                 | Other native `<input>` attributes are forwarded except `type` (always `"checkbox"`). Use `aria-label` when there is no visible `label`. |

### CheckboxList props (summary)

| Prop              | Description                                                                 |
| ----------------- | --------------------------------------------------------------------------- |
| `items`           | Array of `{ id, label, disabled? }` — ids are used for selection and form `value`. |
| `orientation`     | `"column"` (default) or `"row"` (horizontal with wrap).                     |
| `value` / `onValueChange` | Controlled multi-select: selected ids and updater.                    |
| `defaultValue`    | Initial selected ids when uncontrolled.                                     |
| `legend`          | Optional `<legend>` for the fieldset; otherwise set `aria-label` on the list. |
| `name`            | Optional shared `name` on each checkbox for HTML forms.                     |
| `className`       | Merged onto the `<fieldset>`.                                               |
| `listClassName`   | Classes for the inner flex container.                                       |
| …                 | Other native `<fieldset>` attributes are forwarded (except `children` / `onChange`). |

### Navbar props (summary)

| Prop           | Description                                                                 |
| -------------- | --------------------------------------------------------------------------- |
| `brand`        | Optional left cluster (logo, title, etc.).                                  |
| `children`     | Primary nav content; horizontal row between `brand` and `trailing`.         |
| `trailing`     | Optional right cluster (e.g. `Button`).                                     |
| `linksAlign`   | `"left"` (default), `"center"`, or `"right"` — placement of `children` in the middle flex region. |
| `sticky`       | When `true`, adds `sticky top-0 z-50` on the root `<nav>`.                  |
| `className`    | Merged onto the root `<nav>`.                                               |
| …              | Other native `<nav>` attributes are forwarded (for example `aria-label`, `ref`). |

### Link props (summary)

| Prop         | Description                                                                 |
| ------------ | --------------------------------------------------------------------------- |
| `as`         | Optional `React.ElementType`; default `"a"`. Use `RouterLink` or `NavLink` from `react-router-dom` for in-app navigation (`to`, etc.). |
| `className`  | Merged with the component’s base classes.                                   |
| `aria-current` | Set to `"page"` on the active route for current-page styling (or use `NavLink`, which sets this when active). |
| …            | Other props are forwarded to the underlying element (`href` for `<a>`, `to` for router links, `target`, `rel`, etc.). |

### Breadcrumbs props (summary)

| Prop         | Description                                                                 |
| ------------ | --------------------------------------------------------------------------- |
| `items`      | Ordered path segments from root to current. Each item supports `label`, optional `href`, optional `onClick`, and optional `ariaCurrent: "page"` (defaults to current page for the last item). |
| `separator`  | Separator between items (`ReactNode`). Defaults to `/`.                     |
| `maxItems`   | When set and exceeded, collapses the middle behind an ellipsis.             |
| `renderItem` | Optional custom render hook `(item, { isCurrent }) => ReactNode`. When provided, you control link/current styling and attributes. |
| `className`  | Merged onto the root `<nav>`.                                               |
| …            | Other native `<nav>` attributes are forwarded (for example `aria-label`, `ref`). |

### Toast props (summary)

| Prop                 | Description                                                                 |
| -------------------- | --------------------------------------------------------------------------- |
| `title`              | Short summary shown as the heading                                          |
| `type`               | `"error"`, `"warning"`, `"info"`, or `"success"`                            |
| `children`           | Optional body below the title                                               |
| `role`               | Optional `"status"` or `"alert"`; defaults by `type` (alert for error/warning) |
| `onDismiss`          | When set, renders a dismiss button and calls this handler (you remove the toast from the UI) |
| `dismissButtonProps` | Optional props for the dismiss button (`aria-label`, `className`, etc.)     |
| `className`          | Merged onto the root                                                        |
| …                    | Other native `<div>` attributes are forwarded (except `title` / `children`) |

### useClickAway (summary)

| Argument   | Description                                                                 |
| ---------- | --------------------------------------------------------------------------- |
| `ref`      | Ref to the element; the handler runs when `pointerdown` occurs outside it |
| `handler`  | `(event: PointerEvent) => void`                                             |
| `options`  | `{ enabled?: boolean }` — when `false`, document listeners are not attached |

## Development

Clone the repo, install dependencies, then:

| Command                    | Description                |
| -------------------------- | -------------------------- |
| `npm run build`            | Build `dist/` (ESM + CJS + types) |
| `npm test`                 | Run tests                  |
| `npm run storybook`        | Component docs / playground |
| `npm run build-storybook`  | Static Storybook build     |

See [AGENTS.md](AGENTS.md) for contribution conventions (structure, typing, tests, Storybook).

## License

[MIT](LICENSE)