npm package: @juanitte/inoui
Description: A modern React UI component library
Author: Juan Francisco Hermo López
Latest version: 1.1.4

# Ino-UI

[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)

> **🇪🇸 [Leer en Español](./LEAME.md)**

A modern, lightweight React component library with built-in theming support.

## Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Theme System](#theme-system)
  - [ThemeProvider](#themeprovider)
  - [useTheme Hook](#usetheme-hook)
  - [Color System](#color-system)
  - [Theme Tokens](#theme-tokens)
- [Semantic DOM Styling](#semantic-dom-styling)
  - [CSS Class Customization](#css-class-customization)
- [Utils](#utils)
  - [DOM Utilities](#dom-utilities)
  - [Object Utilities](#object-utilities)
  - [Breakpoint Utilities](#breakpoint-utilities)
  - [Hooks](#hooks-1)
- [Components](#components)
  - [Affix](#affix)
  - [Alert](#alert)
  - [AutoComplete](#autocomplete)
  - [Avatar](#avatar)
  - [Anchor](#anchor)
  - [App](#app)
  - [Badge](#badge)
  - [Breadcrumb](#breadcrumb)
  - [Bubble](#bubble)
  - [Button](#button)
  - [Card](#card)
  - [Calendar](#calendar)
  - [Carousel](#carousel)
  - [Checkbox](#checkbox)
  - [ColorPicker](#colorpicker)
  - [Collapse](#collapse)
  - [ConfigProvider](#configprovider)
  - [DataDisplay](#datadisplay)
  - [DatePicker](#datepicker)
  - [Divider](#divider)
  - [Drawer](#drawer)
  - [Dropdown](#dropdown)
  - [Empty](#empty)
  - [Form](#form)
  - [Input](#input)
  - [Image](#image)
  - [InputNumber](#inputnumber)
  - [Flex](#flex)
  - [Grid](#grid)
  - [Layout](#layout)
  - [Menu](#menu)
  - [Mention](#mention)
  - [Modal](#modal)
  - [NestedSelect](#nestedselect)
  - [Pagination](#pagination)
  - [PopAlert](#popalert)
  - [PopConfirm](#popconfirm)
  - [Popover](#popover)
  - [Progress](#progress)
  - [QRCode](#qrcode)
  - [Radio](#radio)
  - [Rate](#rate)
  - [Result](#result)
  - [Select](#select)
  - [Slider](#slider)
  - [Space](#space)
  - [Spinner](#spinner)
  - [Splitter](#splitter)
  - [Steps](#steps)
  - [Statistic](#statistic)
  - [Switch](#switch)
  - [Table](#table)
  - [Tabs](#tabs)
  - [Tag](#tag)
  - [Text](#text)
  - [TimePicker](#timepicker)
  - [Timeline](#timeline)
  - [Toggle](#toggle)
  - [Tooltip](#tooltip)
  - [Tour](#tour)
  - [Transfer](#transfer)
  - [Tree](#tree)
  - [TreeSelect](#treeselect)
  - [Upload](#upload)
  - [Waterfall](#waterfall)
  - [Watermark](#watermark)

## Features

- Fully typed with TypeScript
- Built-in dark/light mode support
- Automatic color palette generation
- CSS variables for easy customization
- Tree-shakeable and lightweight
- React 18 & 19 compatible

## Installation

```bash
# npm
npm install @juanitte/inoui

# yarn
yarn add @juanitte/inoui

# pnpm
pnpm add @juanitte/inoui
```

## Quick Start

Import the stylesheet once in your app entry point, wrap your application with `ThemeProvider`, and start using components:

```tsx
import '@juanitte/inoui/styles.css'
import { ThemeProvider, Button, Tooltip } from '@juanitte/inoui'

function App() {
  return (
    <ThemeProvider>
      <Tooltip content="Click me!">
        <Button>Hello Ino-UI</Button>
      </Tooltip>
    </ThemeProvider>
  )
}
```

---

## Theme System

Ino-UI includes a powerful theming system that automatically generates color palettes and handles light/dark mode switching.

### ThemeProvider

The `ThemeProvider` component must wrap your application to enable theming functionality.

#### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | — | Application content |
| `config` | `ThemeConfig` | `{}` | Theme configuration object |

#### ThemeConfig

```typescript
interface ThemeConfig {
  colors?: ThemeColors      // Custom color palette
  defaultMode?: ThemeMode   // 'light' | 'dark'
}

interface ThemeColors {
  primary?: string    // Default: '#8c75d1'
  secondary?: string  // Default: '#78808b'
  success?: string    // Default: '#79bc58'
  warning?: string    // Default: '#d5ac59'
  error?: string      // Default: '#d7595b'
  info?: string       // Default: '#6591c7'
}
```

#### Usage

```tsx
// Basic usage
<ThemeProvider>
  <App />
</ThemeProvider>

// With custom configuration
<ThemeProvider
  config={{
    defaultMode: 'dark',
    colors: {
      primary: '#FF6B6B',
      success: '#4ECDC4'
    }
  }}
>
  <App />
</ThemeProvider>
```

### useTheme Hook

Access and control the current theme from any component within the `ThemeProvider`.

```typescript
const { mode, setMode, toggleMode } = useTheme()
```

#### Returns

| Property | Type | Description |
|----------|------|-------------|
| `mode` | `'light' \| 'dark'` | Current theme mode |
| `setMode` | `(mode: ThemeMode) => void` | Set theme mode explicitly |
| `toggleMode` | `() => void` | Toggle between light and dark |

#### Example

```tsx
import { useTheme, Button } from '@juanitte/inoui'

function ThemeToggle() {
  const { mode, toggleMode } = useTheme()

  return (
    <Button onClick={toggleMode}>
      Current: {mode}
    </Button>
  )
}
```

### Color System

Ino-UI automatically generates 10 shades (50-900) for each semantic color. These are available as CSS variables:

```css
/* Base color */
var(--j-primary)

/* Color variants */
var(--j-primary-50)   /* Lightest */
var(--j-primary-100)
var(--j-primary-200)
var(--j-primary-300)
var(--j-primary-400)
var(--j-primary-500)
var(--j-primary-600)
var(--j-primary-700)
var(--j-primary-800)
var(--j-primary-900)  /* Darkest */

/* Semantic aliases */
var(--j-primary-light)     /* Light variant */
var(--j-primary-dark)      /* Dark variant */
var(--j-primary-hover)     /* Hover state */
var(--j-primary-border)    /* Border color */
var(--j-primary-contrast)  /* Contrast text color */
```

#### Neutral Colors

These automatically adapt based on the current theme mode:

| Variable | Description |
|----------|-------------|
| `--j-bg` | Background color |
| `--j-bgSubtle` | Subtle background |
| `--j-bgMuted` | Muted background |
| `--j-border` | Border color |
| `--j-borderHover` | Border hover color |
| `--j-text` | Primary text color |
| `--j-textMuted` | Muted text |
| `--j-textSubtle` | Subtle text |
| `--j-shadowSm` | Small shadow |
| `--j-shadowMd` | Medium shadow |
| `--j-shadowLg` | Large shadow |

### Theme Tokens

Ino-UI provides a `tokens` object with typed references to all CSS variables. This enables autocomplete in your IDE and type-safe styling in JavaScript/TypeScript.

#### Import

```tsx
import { tokens } from '@juanitte/inoui'
```

#### Usage

```tsx
// In inline styles
<div style={{
  backgroundColor: tokens.colorPrimaryBg,
  color: tokens.colorPrimary,
  boxShadow: tokens.shadowMd
}}>
  Styled with tokens
</div>

// In styled-components, emotion, etc.
const StyledCard = styled.div`
  background: ${tokens.colorBgSubtle};
  border: 1px solid ${tokens.colorBorder};
`
```

#### Available Tokens

**Semantic Colors** (for each: primary, secondary, success, warning, error, info):

| Token | CSS Variable |
|-------|--------------|
| `tokens.colorPrimary` | `var(--j-primary)` |
| `tokens.colorPrimaryHover` | `var(--j-primary-hover)` |
| `tokens.colorPrimaryBg` | `var(--j-primary-dark)` |
| `tokens.colorPrimaryBorder` | `var(--j-primary-border)` |
| `tokens.colorPrimaryContrast` | `var(--j-primary-contrast)` |
| `tokens.colorPrimary50` - `tokens.colorPrimary900` | `var(--j-primary-50)` - `var(--j-primary-900)` |

**Neutral Colors**:

| Token | CSS Variable |
|-------|--------------|
| `tokens.colorBg` | `var(--j-bg)` |
| `tokens.colorBgSubtle` | `var(--j-bgSubtle)` |
| `tokens.colorBgMuted` | `var(--j-bgMuted)` |
| `tokens.colorBorder` | `var(--j-border)` |
| `tokens.colorBorderHover` | `var(--j-borderHover)` |
| `tokens.colorText` | `var(--j-text)` |
| `tokens.colorTextMuted` | `var(--j-textMuted)` |
| `tokens.colorTextSubtle` | `var(--j-textSubtle)` |

**Shadows**:

| Token | CSS Variable |
|-------|--------------|
| `tokens.shadowSm` | `var(--j-shadowSm)` |
| `tokens.shadowMd` | `var(--j-shadowMd)` |
| `tokens.shadowLg` | `var(--j-shadowLg)` |

---

## Semantic DOM Styling

Most Ino-UI components expose `classNames` and `styles` props that allow you to style their internal parts (semantic slots) without needing to override CSS or inspect the DOM structure.

### How it works

Each component defines named **slots** representing its internal parts. You can target these slots using:

- **`classNames`** — apply CSS classes to specific internal parts
- **`styles`** — apply inline styles to specific internal parts

```tsx
// Apply a CSS class to the popup part of a Tooltip
<Tooltip
  content="Hello"
  classNames={{ popup: 'my-custom-popup' }}
>
  <Button>Hover me</Button>
</Tooltip>

// Apply inline styles to the icon and content parts of a Button
<Button
  icon={<SearchIcon />}
  styles={{
    icon: { color: 'red' },
    content: { fontWeight: 'bold' },
  }}
>
  Search
</Button>
```

### Priority order

Styles are merged with the following priority (highest wins):

1. **Component base styles** — internal defaults
2. **`styles.slot`** — semantic styles for each slot
3. **`style` prop** — direct style prop (only applies to `root`)

For `className`, the component-level `className` prop and `classNames.root` are merged together.

### Available slots per component

| Component | Slots |
|-----------|-------|
| Affix | `root`, `affix` |
| AutoComplete | `root`, `input`, `dropdown`, `option` |
| Anchor | `root`, `track`, `indicator`, `link` |
| Badge | `root`, `indicator` |
| Badge.Ribbon | `wrapper`, `ribbon`, `content`, `corner` |
| Breadcrumb | `root`, `list`, `item`, `separator`, `link`, `overlay` |
| Bubble | `root`, `icon`, `badge`, `tooltip`, `tooltipArrow` |
| Bubble.Menu | `root`, `trigger`, `menu` |
| Button | `root`, `icon`, `spinner`, `content` |
| Checkbox | `root`, `checkbox`, `indicator`, `label` |
| Checkbox.Group | `root` |
| ColorPicker | `root`, `trigger`, `panel`, `presets` |
| DatePicker | `root`, `input`, `popup`, `header`, `body`, `cell`, `footer` |
| Divider | `root`, `line`, `text` |
| Dropdown | `root`, `overlay`, `item`, `arrow` |
| Form | `root` |
| Form.Item | `root`, `label`, `control`, `help`, `extra` |
| Layout.Sider | `root`, `content`, `trigger` |
| Menu | `root`, `item`, `submenu`, `group`, `groupTitle`, `divider` |
| NestedSelect | `root`, `selector`, `dropdown`, `menu`, `option` |
| Pagination | `root`, `item`, `options` |
| Space | `root`, `item`, `separator` |
| Splitter | `root`, `panel`, `bar`, `collapseButton` |
| Steps | `root`, `step`, `icon`, `content`, `tail` |
| Tabs | `root`, `tabBar`, `tab`, `content`, `inkBar` |
| Text | `root`, `content`, `copyButton`, `expandButton` |
| Tooltip | `root`, `popup`, `arrow` |
| Waterfall | `root`, `column`, `item` |

### CSS Class Customization

All components use [BEM](https://getbem.com/) class names with the `ino-` prefix, which you can target directly in your stylesheets.

**Naming convention:**

| Pattern | Example | Description |
|---------|---------|-------------|
| `ino-{block}` | `ino-btn` | Component root |
| `ino-{block}--{modifier}` | `ino-btn--primary`, `ino-btn--sm` | Variant, size, or state |
| `ino-{block}__{element}` | `ino-btn__icon` | Internal part |

**Example — custom button styles:**

```css
/* Your stylesheet — loaded after ino-ui */
.ino-btn--primary {
  border-radius: 999px;
}

.ino-tag--sm {
  font-size: 0.7rem;
}
```

You can combine this with the `classNames` prop for scoped overrides:

```tsx
<Button classNames={{ root: 'my-btn' }}>Click</Button>
```

```css
.my-btn {
  border-radius: 999px;
}
```

---

## Utils

Ino-UI exports a set of utility functions and hooks that can be used independently in your application.

```tsx
import {
  getScrollBarSize, scrollTo, canUseDom,
  omit, classNames,
  BREAKPOINT_VALUES, BREAKPOINT_ORDER, getResponsiveValue,
  useEvent, useMergedState, useWindowWidth, useBreakpoint,
} from '@juanitte/inoui'
import type {
  ScrollToOptions, ClassValue,
  Breakpoint, ResponsiveValue,
  UseMergedStateOptions,
} from '@juanitte/inoui'
```

---

### DOM Utilities

#### `getScrollBarSize(fresh?)`

Returns the browser's native scrollbar width in pixels. The result is cached after the first call.

```tsx
import { getScrollBarSize } from '@juanitte/inoui'

const width = getScrollBarSize()      // e.g. 17 (cached)
const fresh = getScrollBarSize(true)  // re-measures even if cached
```

Returns `0` in SSR / non-DOM environments.

#### `scrollTo(top, options?)`

Smoothly scrolls a container to a target vertical position using an easeInOutCubic easing curve.

```tsx
import { scrollTo } from '@juanitte/inoui'
import type { ScrollToOptions } from '@juanitte/inoui'

scrollTo(500)                        // scroll window to y=500
scrollTo(0, {
  container: myRef.current,         // scroll a custom element
  duration: 600,                    // animation duration in ms (default: 450)
  callback: () => console.log('done'),
})
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `container` | `HTMLElement \| Window` | `window` | Scroll target |
| `duration` | `number` | `450` | Animation duration in ms |
| `callback` | `() => void` | — | Called when animation completes |

#### `canUseDom()`

Returns `true` when running in a real browser environment (DOM is available). SSR-safe alternative to checking `typeof window !== 'undefined'`.

```tsx
import { canUseDom } from '@juanitte/inoui'

if (canUseDom()) {
  // safe to access document, window, etc.
}
```

---

### Object Utilities

#### `omit(obj, keys)`

Returns a shallow copy of `obj` with the specified keys removed. Fully typed.

```tsx
import { omit } from '@juanitte/inoui'

const clean = omit(props, ['className', 'style'])
// clean is typed as Omit<typeof props, 'className' | 'style'>
```

#### `classNames(...args)`

Builds a class name string from any mix of strings, numbers, objects, and arrays — falsy values are automatically ignored.

```tsx
import { classNames } from '@juanitte/inoui'
import type { ClassValue } from '@juanitte/inoui'

classNames('foo', undefined, 'bar')           // 'foo bar'
classNames({ active: true, disabled: false }) // 'active'
classNames(['a', ['b', 'c']], 'z')            // 'a b c z'
```

---

### Breakpoint Utilities

These utilities implement a shared responsive breakpoint system used by [Avatar](#avatar), [DataDisplay](#datadisplay), [Grid](#grid), and [Waterfall](#waterfall).

```tsx
import { BREAKPOINT_VALUES, BREAKPOINT_ORDER, getResponsiveValue } from '@juanitte/inoui'
import type { Breakpoint, ResponsiveValue } from '@juanitte/inoui'
```

#### `Breakpoint` type

```typescript
type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'
```

#### `BREAKPOINT_VALUES`

```typescript
const BREAKPOINT_VALUES: Record<Breakpoint, number> = {
  xs: 0,
  sm: 576,
  md: 768,
  lg: 992,
  xl: 1200,
  xxl: 1600,
}
```

#### `BREAKPOINT_ORDER`

Breakpoints sorted from largest to smallest, used when resolving `ResponsiveValue`:

```typescript
const BREAKPOINT_ORDER: Breakpoint[] = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs']
```

#### `ResponsiveValue<T>`

A value that can either be a scalar or a per-breakpoint map:

```typescript
type ResponsiveValue<T> = T | Partial<Record<Breakpoint, T>>
```

#### `getResponsiveValue(value, windowWidth)`

Resolves a `ResponsiveValue<T>` to a concrete value given the current window width. If `value` is a scalar it is returned as-is; if it is an object, the largest active breakpoint wins.

```tsx
import { getResponsiveValue } from '@juanitte/inoui'

getResponsiveValue(3, 1024)                        // 3
getResponsiveValue({ xs: 1, md: 2, xl: 4 }, 1024) // 2  (lg ≥ md, xl not yet active)
```

---

### Hooks

#### `useEvent(fn)`

Returns a stable function reference that always calls the latest version of `fn`. The returned reference never changes between renders, so it is safe to pass to memoized children without causing re-renders.

```tsx
import { useEvent } from '@juanitte/inoui'

const handleClick = useEvent((e: MouseEvent) => {
  console.log(count) // always reads the latest `count`
})
```

#### `useMergedState(defaultValue, options?)`

Merges controlled and uncontrolled state into a single interface. Behaves like `useState` but also accepts an optional `value` / `onChange` pair for controlled use.

```tsx
import { useMergedState } from '@juanitte/inoui'
import type { UseMergedStateOptions } from '@juanitte/inoui'

// Uncontrolled
const [value, setValue] = useMergedState('hello')

// Controlled
const [value, setValue] = useMergedState('hello', {
  value: controlledValue,
  onChange: (next, prev) => setControlledValue(next),
  postState: (v) => v.trim(), // optional transform before returning
})
```

| Option | Type | Description |
|--------|------|-------------|
| `value` | `T \| undefined` | External controlled value. When defined, the hook is in controlled mode. |
| `onChange` | `(next: T, prev: T) => void` | Called on every state change (controlled and uncontrolled). |
| `postState` | `(value: T) => T` | Optional transform applied before the value is returned. |

#### `useWindowWidth()`

Returns the current `window.innerWidth`, updating on every resize event. SSR-safe — returns `1200` on the server.

```tsx
import { useWindowWidth } from '@juanitte/inoui'

function MyComponent() {
  const width = useWindowWidth()
  return <p>Window is {width}px wide</p>
}
```

#### `useBreakpoint()`

Returns a map of active breakpoints based on the current window width.

```tsx
import { useBreakpoint } from '@juanitte/inoui'

function MyComponent() {
  const screens = useBreakpoint()
  // { xs: true, sm: true, md: true, lg: false, xl: false, xxl: false } at 800px
  return <p>{screens.md ? 'Medium or larger' : 'Smaller than md'}</p>
}
```

Returns `Record<Breakpoint, boolean>` where each key is `true` if the window width meets or exceeds that breakpoint's minimum width.

---

## Components

<details>
<summary><strong>Affix</strong> - Stick an element to the viewport edge while scrolling</summary>

### Affix

`Affix` keeps its children pinned to the top or bottom of the viewport (or a custom scroll container) once the user scrolls past a defined threshold. A placeholder `div` of equal size replaces the content in the normal flow so the page layout does not jump when the element becomes fixed.

```tsx
import { Affix } from '@juanitte/inoui'
```

#### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `offsetTop` | `number` | `0`* | Distance in px from the top of the container at which to pin the element. *Defaults to `0` when neither `offsetTop` nor `offsetBottom` is set |
| `offsetBottom` | `number` | — | Distance in px from the bottom of the container at which to pin the element. `offsetTop` takes priority if both are set |
| `target` | `() => HTMLElement \| Window` | `window` | Returns the scrollable container to observe. Defaults to `window` |
| `onChange` | `(affixed: boolean) => void` | — | Called when the affixed state changes |
| `children` | `ReactNode` | — | Content to affix |
| `className` | `string` | — | CSS class on the root placeholder |
| `style` | `CSSProperties` | — | Inline style on the root placeholder |
| `classNames` | `AffixClassNames` | — | Semantic class names per slot |
| `styles` | `AffixStyles` | — | Semantic inline styles per slot |

#### Types

```ts
type AffixSemanticSlot = 'root' | 'affix'
type AffixClassNames   = SemanticClassNames<AffixSemanticSlot>
type AffixStyles       = SemanticStyles<AffixSemanticSlot>
```

#### Behaviour

- **Two divs:** `root` is a normal-flow placeholder that holds the reserved height when affixed; `affix` is the inner element that switches to `position: fixed`.
- **Affixed styles:** `position: fixed; top | bottom: <computed>; left: <measured>; width: <measured>; z-index: 10`.
- **Scroll listeners:** passive listener on the target container + `window` resize. When `target` is a custom element, an additional passive `window` scroll listener keeps the affixed element tracking the container as it moves within the page.
- **onChange:** fires with `true` on the first tick that triggers pinning, and `false` on the first tick that releases it.

#### Examples

**1. Stick to top of window**
```tsx
<Affix>
  <Button>Fixed at top</Button>
</Affix>
```

**2. Offset from top**
```tsx
<Affix offsetTop={64}>
  <nav>Site navigation</nav>
</Affix>
```

**3. Stick to bottom**
```tsx
<Affix offsetBottom={24}>
  <div>Footer actions</div>
</Affix>
```

**4. onChange callback**
```tsx
<Affix offsetTop={0} onChange={(affixed) => console.log('affixed:', affixed)}>
  <Button>Track me</Button>
</Affix>
```

**5. Custom scroll container**
```tsx
const containerRef = useRef<HTMLDivElement>(null)

<div ref={containerRef} style={{ height: 400, overflowY: 'auto' }}>
  <Affix offsetTop={8} target={() => containerRef.current!}>
    <Button>Sticky inside scroll area</Button>
  </Affix>
  <div style={{ height: 1200 }}>Long content…</div>
</div>
```

**6. Sticky toolbar**
```tsx
<Affix offsetTop={0}>
  <div style={{ background: tokens.colorBg, padding: '0.5rem 1rem', borderBottom: `1px solid ${tokens.colorBorder}` }}>
    <Button>Save</Button>
    <Button variant="ghost">Cancel</Button>
  </div>
</Affix>
```

**7. Semantic styles**
```tsx
<Affix
  offsetTop={0}
  styles={{
    affix: { boxShadow: '0 2px 8px rgba(0,0,0,0.15)', background: tokens.colorBg },
  }}
>
  <nav>Navigation</nav>
</Affix>
```

</details>

---

<details>
<summary><strong>Alert</strong> - Feedback banner with type, icon, close, auto-dismiss, and error boundary</summary>

### Alert

`Alert` is a feedback component that displays contextual messages to the user. It supports four severity types (`success`, `info`, `warning`, `error`) with automatic color theming (light/dark aware via `color-mix`), an optional icon, a close button with slide-out animation, a custom action slot, and a full-width `banner` mode. The compound `Alert.ErrorBoundary` wraps children and renders an error alert when a React error is caught.

```tsx
import { Alert } from '@juanitte/inoui'
```

#### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `type` | `'success' \| 'info' \| 'warning' \| 'error'` | `'info'` (or `'warning'` in banner mode) | Severity level — controls background, border, and icon color |
| `title` | `ReactNode` | — | Main message text (bold when `description` is present) |
| `description` | `ReactNode` | — | Secondary description below the title |
| `showIcon` | `boolean` | `false` (or `true` in banner mode) | Show the type icon |
| `icon` | `ReactNode` | — | Custom icon replacing the default type icon |
| `closable` | `boolean \| AlertClosable` | `false` | Show close button; pass an object for `closeIcon`, `onClose`, and `afterClose` callbacks |
| `action` | `ReactNode` | — | Extra content (e.g. a button) placed on the right side |
| `banner` | `boolean` | `false` | Full-width banner mode: no border-radius, bottom border only, `showIcon` and `type='warning'` by default |
| `className` | `string` | — | CSS class on the root element |
| `style` | `CSSProperties` | — | Inline style on the root element |
| `classNames` | `AlertClassNames` | — | Semantic class names per slot |
| `styles` | `AlertStyles` | — | Semantic inline styles per slot |

#### AlertClosable

| Field | Type | Description |
|-------|------|-------------|
| `closeIcon` | `ReactNode` | Custom close icon |
| `onClose` | `(e: React.MouseEvent) => void` | Called when the close button is clicked |
| `afterClose` | `() => void` | Called after the close animation completes |

#### Alert.ErrorBoundary

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | — | Content to wrap |
| `title` | `ReactNode` | `'Something went wrong'` | Error alert title |
| `description` | `ReactNode` | `error.message` | Error alert description |

#### Types

```ts
type AlertType = 'success' | 'info' | 'warning' | 'error'

type AlertSemanticSlot = 'root' | 'icon' | 'content' | 'message' | 'description' | 'action' | 'closeBtn'
type AlertClassNames   = SemanticClassNames<AlertSemanticSlot>
type AlertStyles       = SemanticStyles<AlertSemanticSlot>
```

#### Close animation

When the alert is dismissed via the close button, the component:
1. Fades out the alert card (opacity → 0, 250 ms ease).
2. Collapses the wrapper height to 0 using `grid-template-rows: 0fr` (300 ms ease).
3. After the collapse transition ends, calls `afterClose` and unmounts.

#### Examples

**1. Basic types**
```tsx
<Alert type="success" title="Operation completed successfully" />
<Alert type="info"    title="A new version is available" />
<Alert type="warning" title="Disk space running low" />
<Alert type="error"   title="Failed to save changes" />
```

**2. With description**
```tsx
<Alert
  type="success"
  title="File uploaded"
  description="document.pdf has been uploaded and is being processed. You will receive a notification once it's ready."
  showIcon
/>
```

**3. Show icon**
```tsx
<Alert type="info" title="Tip: press Ctrl+K for quick search" showIcon />
```

**4. Custom icon**
```tsx
<Alert type="info" title="Scheduled maintenance tonight" icon={<span>🔧</span>} showIcon />
```

**5. Closable**
```tsx
<Alert
  type="warning"
  title="Your session will expire in 5 minutes"
  closable
  showIcon
/>
```

**6. Closable with callbacks**
```tsx
<Alert
  type="error"
  title="Connection lost"
  closable={{
    onClose: () => console.log('Alert closed'),
    afterClose: () => console.log('Close animation finished'),
  }}
  showIcon
/>
```

**7. Custom close icon**
```tsx
<Alert
  type="info"
  title="Notification"
  closable={{ closeIcon: <span>✕</span> }}
/>
```

**8. Action slot**
```tsx
<Alert
  type="warning"
  title="Unsaved changes"
  description="You have unsaved changes that will be lost."
  showIcon
  action={<button onClick={() => save()}>Save now</button>}
  closable
/>
```

**9. Banner mode**
```tsx
// Full-width banner — defaults to type="warning" and showIcon=true
<Alert banner title="The site will be under maintenance from 2:00 AM to 4:00 AM." />
```

**10. Banner with custom type**
```tsx
<Alert banner type="error" title="Service outage detected. Our team has been notified." />
```

**11. Title only (no description)**
```tsx
<Alert type="info" title="Press Enter to confirm" showIcon />
```

**12. Description only (no title)**
```tsx
<Alert type="warning" description="Some features may not work as expected in this browser." showIcon />
```

**13. Error boundary**
```tsx
import { Alert } from '@juanitte/inoui'

function RiskyWidget() {
  // This might throw during render
  return <div>{JSON.parse('invalid')}</div>
}

<Alert.ErrorBoundary title="Widget crashed" description="Please refresh the page.">
  <RiskyWidget />
</Alert.ErrorBoundary>
```

**14. Semantic styles**
```tsx
<Alert
  type="success"
  title="Payment received"
  description="Your invoice has been updated."
  showIcon
  styles={{
    root: { borderRadius: 12 },
    message: { fontSize: '1rem' },
    description: { fontStyle: 'italic' },
  }}
/>
```

**15. Multiple stacked alerts**
```tsx
import { useState } from 'react'
import { Alert, Button } from '@juanitte/inoui'

function Notifications() {
  const [alerts, setAlerts] = useState([
    { id: 1, type: 'info' as const, title: 'Welcome back!' },
    { id: 2, type: 'warning' as const, title: 'Your trial expires tomorrow' },
    { id: 3, type: 'success' as const, title: 'Profile updated' },
  ])

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
      {alerts.map((a) => (
        <Alert
          key={a.id}
          type={a.type}
          title={a.title}
          showIcon
          closable={{
            afterClose: () => setAlerts(prev => prev.filter(x => x.id !== a.id)),
          }}
        />
      ))}
    </div>
  )
}
```

</details>

---

<details>
<summary><strong>AutoComplete</strong> - Input with auto-suggestion dropdown</summary>

### AutoComplete

An input component with auto-complete suggestions. Supports filtering, grouped options, keyboard navigation, backfill, controlled/uncontrolled state, multiple variants, validation status, clear button, and auto-flip dropdown positioning.

#### Import

```tsx
import { AutoComplete } from '@juanitte/inoui'
```

#### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `options` | `AutoCompleteOption[]` | `[]` | Available options |
| `value` | `string` | — | Controlled input value |
| `defaultValue` | `string` | `''` | Default input value (uncontrolled) |
| `placeholder` | `string` | — | Input placeholder text |
| `open` | `boolean` | — | Controlled dropdown visibility |
| `defaultOpen` | `boolean` | `false` | Default dropdown visibility |
| `disabled` | `boolean` | `false` | Disable the component |
| `allowClear` | `boolean` | `false` | Show clear button |
| `autoFocus` | `boolean` | `false` | Focus input on mount |
| `backfill` | `boolean` | `false` | Fill input with highlighted option value on keyboard navigation |
| `defaultActiveFirstOption` | `boolean` | `true` | Automatically highlight the first option |
| `variant` | `'outlined' \| 'filled' \| 'borderless'` | `'outlined'` | Input visual variant |
| `status` | `'error' \| 'warning'` | — | Validation status |
| `filterOption` | `boolean \| (inputValue, option) => boolean` | `true` | Filter function. `true` = case-insensitive includes, `false` = no filtering |
| `notFoundContent` | `ReactNode` | `null` | Content when no matches. `null` = hide dropdown |
| `popupMatchSelectWidth` | `boolean \| number` | `true` | `true` = match input width, number = fixed width |
| `onChange` | `(value: string) => void` | — | Called when value changes |
| `onSearch` | `(value: string) => void` | — | Called when user types |
| `onSelect` | `(value, option) => void` | — | Called when an option is selected |
| `onFocus` | `(e) => void` | — | Called on focus |
| `onBlur` | `(e) => void` | — | Called on blur |
| `onDropdownVisibleChange` | `(open: boolean) => void` | — | Called when dropdown visibility changes |
| `onClear` | `() => void` | — | Called when input is cleared |
| `prefix` | `ReactNode` | — | Prefix content in the input (e.g. icon on the left) |
| `suffix` | `ReactNode` | — | Suffix content in the input (e.g. icon on the right) |
| `className` | `string` | — | CSS class for the root element |
| `style` | `CSSProperties` | — | Inline styles for the root element |
| `classNames` | `AutoCompleteClassNames` | — | CSS classes for internal parts |
| `styles` | `AutoCompleteStyles` | — | Inline styles for internal parts |

#### Types

```tsx
type AutoCompleteVariant = 'outlined' | 'filled' | 'borderless'

type AutoCompleteStatus = 'error' | 'warning'

interface AutoCompleteOption {
  value: string
  label?: ReactNode
  disabled?: boolean
  options?: AutoCompleteOption[]  // for grouped options
}
```

#### Semantic DOM

| Slot | Description |
|------|-------------|
| `root` | Outer wrapper container |
| `input` | Input element |
| `dropdown` | Dropdown suggestions container |
| `option` | Individual option item |

#### Examples

**Basic usage**

```tsx
<AutoComplete
  options={[
    { value: 'React' },
    { value: 'Vue' },
    { value: 'Angular' },
  ]}
  placeholder="Search framework..."
/>
```

**Controlled value**

```tsx
const [value, setValue] = useState('')

<AutoComplete
  value={value}
  onChange={setValue}
  options={[
    { value: 'React' },
    { value: 'Vue' },
    { value: 'Angular' },
  ]}
/>
```

**Custom labels**

```tsx
<AutoComplete
  options={[
    { value: 'react', label: <span><strong>React</strong> - A JavaScript library</span> },
    { value: 'vue', label: <span><strong>Vue</strong> - The Progressive Framework</span> },
  ]}
/>
```

**Grouped options**

```tsx
<AutoComplete
  options={[
    {
      value: 'frameworks',
      label: 'Frameworks',
      options: [
        { value: 'React' },
        { value: 'Vue' },
      ],
    },
    {
      value: 'languages',
      label: 'Languages',
      options: [
        { value: 'TypeScript' },
        { value: 'JavaScript' },
      ],
    },
  ]}
/>
```

**Custom filter**

```tsx
<AutoComplete
  filterOption={(inputValue, option) =>
    option.value.toUpperCase().startsWith(inputValue.toUpperCase())
  }
  options={[
    { value: 'React' },
    { value: 'Redux' },
    { value: 'Vue' },
  ]}
/>
```

**No filtering (async search)**

```tsx
const [options, setOptions] = useState([])

<AutoComplete
  filterOption={false}
  options={options}
  onSearch={async (text) => {
    const results = await fetchSuggestions(text)
    setOptions(results.map((r) => ({ value: r })))
  }}
/>
```

**Backfill mode**

```tsx
<AutoComplete
  backfill
  options={[
    { value: 'apple' },
    { value: 'banana' },
    { value: 'cherry' },
  ]}
/>
```

**Prefix & Suffix**

```tsx
// Prefix only
<AutoComplete
  prefix={<MailIcon />}
  options={[{ value: 'Option 1' }, { value: 'Option 2' }]}
  placeholder="With prefix"
/>

// Suffix only
<AutoComplete
  suffix={<SearchIcon />}
  options={[{ value: 'Option 1' }, { value: 'Option 2' }]}
  placeholder="With suffix"
/>

// Prefix + Suffix + Clear
<AutoComplete
  prefix={<GlobeIcon />}
  suffix={<SearchIcon />}
  allowClear
  options={[{ value: 'Option 1' }, { value: 'Option 2' }]}
  placeholder="With both"
/>
```

**Variants**

```tsx
<AutoComplete variant="outlined" options={[...]} placeholder="Outlined" />
<AutoComplete variant="filled" options={[...]} placeholder="Filled" />
<AutoComplete variant="borderless" options={[...]} placeholder="Borderless" />
```

**Validation status**

```tsx
<AutoComplete status="error" options={[...]} placeholder="Error" />
<AutoComplete status="warning" options={[...]} placeholder="Warning" />
```

**Not found content**

```tsx
<AutoComplete
  options={[]}
  notFoundContent="No results found"
  placeholder="Search..."
/>
```

**Disabled**

```tsx
<AutoComplete disabled options={[{ value: 'React' }]} value="React" />
```

</details>

---

<details>
<summary><strong>Avatar</strong> - User profile representation</summary>

### Avatar

`Avatar` displays a user's profile picture, icon, or initials. It supports images, custom icons, text fallbacks, and badge indicators. Includes `Avatar.Group` for displaying overlapping avatar lists.

**Import:**
```tsx
import { Avatar } from '@juanitte/inoui';
```

#### Avatar Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `src` | `string` | `undefined` | Image source URL |
| `srcSet` | `string` | `undefined` | Responsive image sources |
| `alt` | `string` | `undefined` | Alternative text for image |
| `icon` | `ReactNode` | `<UserIcon />` | Custom icon (shown when no image) |
| `shape` | `'circle' \| 'square'` | `'circle'` | Avatar shape |
| `size` | `'small' \| 'default' \| 'large' \| number \| AvatarResponsiveSize` | `'default'` | Avatar size |
| `gap` | `number` | `4` | Gap between text and container edge (for auto-scaling) |
| `count` | `number` | `undefined` | Badge count (shows as "99+" if > 99) |
| `dot` | `boolean` | `false` | Show dot indicator |
| `draggable` | `boolean \| 'true' \| 'false'` | `true` | Whether image is draggable |
| `crossOrigin` | `'' \| 'anonymous' \| 'use-credentials'` | `undefined` | CORS setting for image |
| `onError` | `() => boolean` | `undefined` | Callback when image fails to load (return false to prevent fallback) |
| `children` | `ReactNode` | `undefined` | Text content (usually initials) |
| `className` | `string` | `undefined` | Root element class name |
| `style` | `CSSProperties` | `undefined` | Root element inline styles |
| `classNames` | `SemanticClassNames<AvatarSemanticSlot>` | `undefined` | Semantic class names |
| `styles` | `SemanticStyles<AvatarSemanticSlot>` | `undefined` | Semantic inline styles |

#### Avatar.Group Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `max` | `{ count?: number; style?: CSSProperties }` | `undefined` | Maximum avatars to show (rest shown as "+N") |
| `size` | `AvatarSize` | `'default'` | Size applied to all child avatars |
| `shape` | `'circle' \| 'square'` | `'circle'` | Shape applied to all child avatars |
| `children` | `ReactNode` | `undefined` | Avatar components |
| `className` | `string` | `undefined` | Container class name |
| `style` | `CSSProperties` | `undefined` | Container inline styles |

#### AvatarResponsiveSize Type

`AvatarResponsiveSize` is an alias for `ResponsiveValue<number>` from the shared [Utils](#utils) breakpoint system:

```typescript
// Equivalent to:
type AvatarResponsiveSize = Partial<Record<Breakpoint, number>>
// where Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'
```

See [Breakpoint Utilities](#breakpoint-utilities) for the full type definitions and breakpoint values.

#### Size Configuration

| Size | Dimension |
|------|-----------|
| `small` | 24px (1.5rem) |
| `default` | 32px (2rem) |
| `large` | 40px (2.5rem) |
| Custom number | Specified pixels |
| Responsive object | Size based on viewport breakpoint |

#### Semantic DOM Slots

| Slot | Description |
|------|-------------|
| `root` | The root container element |
| `image` | The image element (when src is provided) |
| `icon` | The icon container (when icon is provided) |
| `text` | The text container (when children text is provided) |

#### Examples

**Basic usage with image:**
```tsx
import { Avatar } from '@juanitte/inoui';

<Avatar src="https://i.pravatar.cc/150?img=1" alt="User" />
```

**With initials:**
```tsx
<Avatar>JD</Avatar>
```

**With custom icon:**
```tsx
<Avatar icon={<UserIcon />} />
```

**Different shapes:**
```tsx
<Avatar src="user.jpg" shape="circle" />
<Avatar src="user.jpg" shape="square" />
```

**Different sizes:**
```tsx
<Avatar src="user.jpg" size="small" />
<Avatar src="user.jpg" size="default" />
<Avatar src="user.jpg" size="large" />
<Avatar src="user.jpg" size={64} />
```

**Responsive size:**
```tsx
<Avatar
  src="user.jpg"
  size={{
    xs: 24,
    sm: 32,
    md: 40,
    lg: 48,
    xl: 64,
  }}
/>
```

**With badge count:**
```tsx
<Avatar src="user.jpg" count={5} />
<Avatar src="user.jpg" count={99} />
<Avatar src="user.jpg" count={100} /> {/* Shows "99+" */}
```

**With dot indicator:**
```tsx
<Avatar src="user.jpg" dot />
```

**Error handling:**
```tsx
<Avatar
  src="invalid-url.jpg"
  onError={() => {
    console.log('Image failed to load');
    return true; // Allow fallback to icon/text
  }}
>
  FB
</Avatar>
```

**Avatar Group:**
```tsx
<Avatar.Group>
  <Avatar src="https://i.pravatar.cc/150?img=1" />
  <Avatar src="https://i.pravatar.cc/150?img=2" />
  <Avatar src="https://i.pravatar.cc/150?img=3" />
  <Avatar src="https://i.pravatar.cc/150?img=4" />
</Avatar.Group>
```

**Group with max count:**
```tsx
<Avatar.Group max={{ count: 3 }}>
  <Avatar src="https://i.pravatar.cc/150?img=1" />
  <Avatar src="https://i.pravatar.cc/150?img=2" />
  <Avatar src="https://i.pravatar.cc/150?img=3" />
  <Avatar src="https://i.pravatar.cc/150?img=4" />
  <Avatar src="https://i.pravatar.cc/150?img=5" />
</Avatar.Group>
{/* Shows first 3 avatars + "+2" */}
```

**Group with custom size and shape:**
```tsx
<Avatar.Group size="large" shape="square">
  <Avatar src="user1.jpg" />
  <Avatar src="user2.jpg" />
  <Avatar>AB</Avatar>
  <Avatar icon={<UserIcon />} />
</Avatar.Group>
```

**Custom styling for overflow avatar:**
```tsx
<Avatar.Group
  max={{
    count: 2,
    style: { backgroundColor: '#f56a00', color: '#fff' }
  }}
>
  <Avatar src="user1.jpg" />
  <Avatar src="user2.jpg" />
  <Avatar src="user3.jpg" />
</Avatar.Group>
```

</details>

---

<details>
<summary><strong>Anchor</strong> - Navigation links that track scroll position</summary>

### Anchor

A navigation component that renders a list of anchor links and highlights the currently active one based on scroll position. Supports vertical and horizontal layouts with nested links.

#### Import

```tsx
import { Anchor } from '@juanitte/inoui'
```

#### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `items` | `AnchorLinkItemProps[]` | `[]` | Declarative list of anchor links |
| `direction` | `'vertical' \| 'horizontal'` | `'vertical'` | Navigation direction |
| `offsetTop` | `number` | `0` | Offset in px from the top when calculating scroll position |
| `targetOffset` | `number` | `offsetTop` | Offset for scroll destination when clicking a link |
| `bounds` | `number` | `5` | Tolerance distance in px for detecting the active section |
| `getContainer` | `() => HTMLElement \| Window` | `() => window` | Scrollable container |
| `getCurrentAnchor` | `(activeLink: string) => string` | — | Custom function to determine the active link |
| `onChange` | `(currentActiveLink: string) => void` | — | Callback when the active link changes |
| `onClick` | `(e: MouseEvent, link: { title: ReactNode; href: string }) => void` | — | Callback when a link is clicked |
| `replace` | `boolean` | `false` | Use `replaceState` instead of `pushState` for URL updates |
| `className` | `string` | — | Additional CSS class |
| `style` | `CSSProperties` | — | Additional inline styles |

#### AnchorLinkItemProps

```typescript
interface AnchorLinkItemProps {
  key: string            // Unique key for the link
  href: string           // Link destination (must start with #)
  title: ReactNode       // Link content
  children?: AnchorLinkItemProps[]  // Nested links (vertical only)
}
```

#### Semantic DOM

| Slot | Description |
|------|-------------|
| `root` | Outer wrapper element |
| `track` | Background track line (vertical mode) |
| `indicator` | Active link indicator |
| `link` | Individual anchor link |

#### Examples

```tsx
// Basic vertical anchor
<Anchor
  items={[
    { key: 'section1', href: '#section1', title: 'Section 1' },
    { key: 'section2', href: '#section2', title: 'Section 2' },
    { key: 'section3', href: '#section3', title: 'Section 3' },
  ]}
/>

// Horizontal anchor
<Anchor
  direction="horizontal"
  items={[
    { key: 'overview', href: '#overview', title: 'Overview' },
    { key: 'features', href: '#features', title: 'Features' },
    { key: 'api', href: '#api', title: 'API' },
  ]}
/>

// Nested links (vertical only)
<Anchor
  items={[
    {
      key: 'components',
      href: '#components',
      title: 'Components',
      children: [
        { key: 'button', href: '#button', title: 'Button' },
        { key: 'input', href: '#input', title: 'Input' },
      ],
    },
    { key: 'hooks', href: '#hooks', title: 'Hooks' },
  ]}
/>

// With offset (e.g., for fixed header)
<Anchor
  offsetTop={64}
  items={[
    { key: 'intro', href: '#intro', title: 'Introduction' },
    { key: 'guide', href: '#guide', title: 'Guide' },
  ]}
/>

// Custom scroll container
<Anchor
  getContainer={() => document.getElementById('my-container')!}
  items={[
    { key: 'part1', href: '#part1', title: 'Part 1' },
    { key: 'part2', href: '#part2', title: 'Part 2' },
  ]}
/>

// With onChange callback
<Anchor
  onChange={(activeLink) => console.log('Active:', activeLink)}
  items={[
    { key: 'a', href: '#a', title: 'Section A' },
    { key: 'b', href: '#b', title: 'Section B' },
  ]}
/>

// Replace URL instead of push
<Anchor
  replace
  items={[
    { key: 'tab1', href: '#tab1', title: 'Tab 1' },
    { key: 'tab2', href: '#tab2', title: 'Tab 2' },
  ]}
/>
```

</details>

---

<details>
<summary><strong>App</strong> - Root provider that wires Modal and PopAlert APIs into context</summary>

### App

`App` is a thin root-level provider that internally calls `useModal()` and `usePopAlert()`, mounts their `contextHolder` nodes, and exposes both APIs through React context. Any descendant component can call `App.useApp()` to access `modal` and `notification` without managing hooks or `contextHolder` placement itself.

```tsx
import { App } from '@juanitte/inoui'
```

#### Setup

```tsx
// main.tsx (or your app entry point)
import { App } from '@juanitte/inoui'

createRoot(document.getElementById('root')!).render(
  <App>
    <MyApp />
  </App>
)
```

#### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `notification` | `PopAlertHookConfig` | — | Configuration forwarded to the internal `usePopAlert()` call (placement, size, duration, maxCount, offset) |
| `children` | `ReactNode` | — | Application content |
| `className` | `string` | — | CSS class on the root `<div>` |
| `style` | `CSSProperties` | — | Inline style on the root `<div>` |

#### App.useApp()

```ts
const { modal, notification } = App.useApp()
```

Returns the `AppContextValue` from the nearest `<App>` ancestor. Throws if called outside an `<App>`.

| Field | Type | Description |
|-------|------|-------------|
| `modal` | `ModalHookApi` | Programmatic modal API — same as the `modal` returned by `useModal()` |
| `notification` | `PopAlertApi` | Toast notification API — same as the `api` returned by `usePopAlert()` |

See [Modal](#modal) → `useModal hook` and [PopAlert](#popalert) → `PopAlertApi` for the full method reference.

#### Context value types

```ts
interface AppContextValue {
  modal:        ModalHookApi  // confirm, info, success, warning, error, destroyAll
  notification: PopAlertApi   // open, success, error, info, warning, loading, destroy
}
```

#### Examples

**1. Basic setup**
```tsx
import { App } from '@juanitte/inoui'

createRoot(document.getElementById('root')!).render(
  <App>
    <MyApp />
  </App>
)
```

**2. With notification defaults**
```tsx
<App notification={{ placement: 'topRight', duration: 4, maxCount: 5 }}>
  <MyApp />
</App>
```

**3. Using App.useApp() in a child component**
```tsx
import { App, Button } from '@juanitte/inoui'

function DeleteButton({ id }: { id: number }) {
  const { modal, notification } = App.useApp()

  const handleClick = () => {
    modal.confirm({
      title: 'Delete this item?',
      content: 'This action cannot be undone.',
      onOk: async () => {
        await deleteItem(id)
        notification.success('Item deleted.')
      },
    })
  }

  return <Button variant="danger" onClick={handleClick}>Delete</Button>
}
```

**4. Notifications from anywhere in the tree**
```tsx
function SaveButton() {
  const { notification } = App.useApp()

  return (
    <Button onClick={async () => {
      await save()
      notification.success('Saved!')
    }}>
      Save
    </Button>
  )
}
```

**5. Programmatic confirm dialog**
```tsx
function LogoutButton() {
  const { modal } = App.useApp()

  return (
    <Button onClick={() => modal.confirm({
      title: 'Log out?',
      onOk: logout,
    })}>
      Log out
    </Button>
  )
}
```

</details>

---

<details>
<summary><strong>Badge</strong> - Numeric indicator and status dot</summary>

### Badge

A small numerical value or status indicator for UI elements. Wraps any content with a floating count badge, dot indicator, or displays standalone status dots.

#### Import

```tsx
import { Badge } from '@juanitte/inoui'
```

#### Badge Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | — | Content to wrap (badge floats at top-right) |
| `count` | `ReactNode` | — | Number or custom content shown in the badge |
| `overflowCount` | `number` | `99` | Max count before showing `{overflowCount}+` |
| `dot` | `boolean` | `false` | Show a dot indicator instead of a count |
| `showZero` | `boolean` | `false` | Show the badge when `count` is zero |
| `size` | `'default' \| 'small'` | `'default'` | Badge size |
| `status` | `'success' \| 'processing' \| 'default' \| 'error' \| 'warning'` | — | Status indicator (standalone or with child) |
| `text` | `ReactNode` | — | Text shown next to the status dot (standalone mode only) |
| `color` | `string` | — | Custom color (preset name or any CSS color) |
| `offset` | `[number, number]` | — | `[right, top]` offset in pixels for the badge position |
| `title` | `string` | — | Tooltip title for the badge indicator |
| `className` | `string` | — | Class for the root element |
| `style` | `CSSProperties` | — | Style for the root element |
| `classNames` | `BadgeClassNames` | — | Semantic class names |
| `styles` | `BadgeStyles` | — | Semantic styles |

#### Preset Colors

`pink` · `red` · `yellow` · `orange` · `cyan` · `green` · `blue` · `purple` · `geekblue` · `magenta` · `volcano` · `gold` · `lime`

#### Badge.Ribbon Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | — | Content to wrap |
| `text` | `ReactNode` | — | Text displayed inside the ribbon |
| `color` | `string` | `tokens.colorPrimary` | Ribbon color (preset name or CSS color) |
| `placement` | `'start' \| 'end'` | `'end'` | Which corner the ribbon appears on |
| `className` | `string` | — | Class for the wrapper element |
| `style` | `CSSProperties` | — | Style for the wrapper element |
| `classNames` | `RibbonClassNames` | — | Semantic class names |
| `styles` | `RibbonStyles` | — | Semantic styles |

#### Semantic DOM – Badge

| Slot | Description |
|------|-------------|
| `root` | Outer wrapper element |
| `indicator` | The count badge / dot element (`<sup>`) |

#### Semantic DOM – Badge.Ribbon

| Slot | Description |
|------|-------------|
| `wrapper` | Outer wrapper element |
| `ribbon` | The ribbon strip |
| `content` | Text content inside the ribbon |
| `corner` | The folded corner triangle |

#### Examples

```tsx
// Count badge wrapping content
<Badge count={5}>
  <Avatar shape="square" />
</Badge>

// Overflow count (shows "99+")
<Badge count={120}>
  <Avatar shape="square" />
</Badge>

// Show zero
<Badge count={0} showZero>
  <Avatar shape="square" />
</Badge>

// Dot indicator
<Badge dot>
  <NotificationIcon />
</Badge>

// Small size
<Badge count={5} size="small">
  <Avatar shape="square" />
</Badge>

// Custom offset
<Badge count={5} offset={[10, 10]}>
  <Avatar shape="square" />
</Badge>

// Status dots (standalone, no children)
<Badge status="success" text="Completed" />
<Badge status="processing" text="In Progress" />
<Badge status="error" text="Failed" />
<Badge status="warning" text="Warning" />
<Badge status="default" text="Inactive" />

// Preset colors
<Badge color="blue" count={8}>
  <Avatar shape="square" />
</Badge>
<Badge color="volcano" count={3}>
  <Avatar shape="square" />
</Badge>

// Custom CSS color
<Badge color="#faad14" count={10}>
  <Avatar shape="square" />
</Badge>

// Standalone count (no children)
<Badge count={25} />

// Badge.Ribbon
<Badge.Ribbon text="Featured">
  <Card>Card content</Card>
</Badge.Ribbon>

<Badge.Ribbon text="Sale" color="red" placement="start">
  <Card>Card content</Card>
</Badge.Ribbon>
```

</details>

---

<details>
<summary><strong>Breadcrumb</strong> - Navigation breadcrumb trail</summary>

### Breadcrumb

A navigation component that displays the current location within a hierarchical structure. Supports custom separators, icons, dropdown menus, custom item rendering, and route parameters.

#### Import

```tsx
import { Breadcrumb } from '@juanitte/inoui'
```

#### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `items` | `BreadcrumbItemType[]` | `[]` | Breadcrumb items |
| `separator` | `ReactNode` | `'/'` | Separator between items |
| `itemRender` | `(item, params, items, paths) => ReactNode` | — | Custom render function for each item |
| `params` | `Record<string, string>` | — | Route parameters |
| `className` | `string` | — | Additional CSS class |
| `style` | `CSSProperties` | — | Additional inline styles |
| `classNames` | `BreadcrumbClassNames` | — | CSS classes for internal parts |
| `styles` | `BreadcrumbStyles` | — | Styles for internal parts |

#### BreadcrumbItemType

```typescript
interface BreadcrumbItemType {
  title?: ReactNode       // Text/content of the item
  href?: string           // URL (renders as <a>)
  path?: string           // Path segment (accumulated for itemRender)
  icon?: ReactNode        // Icon before the title
  onClick?: (e: MouseEvent) => void  // Click handler
  className?: string      // Individual CSS class
  style?: CSSProperties   // Individual inline styles
  menu?: { items: BreadcrumbMenuItemType[] }  // Dropdown menu
}
```

#### BreadcrumbMenuItemType

```typescript
interface BreadcrumbMenuItemType {
  key: string             // Unique key
  title: ReactNode        // Item text
  href?: string           // Item URL
  icon?: ReactNode        // Item icon
  onClick?: (e: MouseEvent) => void  // Click handler
}
```

#### Semantic DOM

| Slot | Description |
|------|-------------|
| `root` | Outer `<nav>` element |
| `list` | `<ol>` list container |
| `item` | Individual `<li>` breadcrumb item |
| `separator` | Separator `<li>` element between items |
| `link` | Link or text element inside each item |
| `overlay` | Dropdown menu container |

#### Examples

```tsx
// Basic breadcrumb
<Breadcrumb
  items={[
    { title: 'Home', href: '/' },
    { title: 'Products', href: '/products' },
    { title: 'Detail' },
  ]}
/>

// With icons
<Breadcrumb
  items={[
    { title: 'Home', href: '/', icon: <HomeIcon /> },
    { title: 'Settings', href: '/settings', icon: <SettingsIcon /> },
    { title: 'Profile' },
  ]}
/>

// Custom separator
<Breadcrumb
  separator=">"
  items={[
    { title: 'Home', href: '/' },
    { title: 'Category', href: '/category' },
    { title: 'Item' },
  ]}
/>

// ReactNode separator
<Breadcrumb
  separator={<span style={{ color: 'red' }}>→</span>}
  items={[
    { title: 'Step 1', href: '#' },
    { title: 'Step 2', href: '#' },
    { title: 'Step 3' },
  ]}
/>

// With dropdown menu
<Breadcrumb
  items={[
    { title: 'Home', href: '/' },
    {
      title: 'Category',
      href: '/category',
      menu: {
        items: [
          { key: 'electronics', title: 'Electronics', href: '/electronics' },
          { key: 'clothing', title: 'Clothing', href: '/clothing' },
          { key: 'books', title: 'Books', href: '/books' },
        ],
      },
    },
    { title: 'Product' },
  ]}
/>

// With onClick handlers
<Breadcrumb
  items={[
    { title: 'Home', onClick: () => navigate('/') },
    { title: 'Users', onClick: () => navigate('/users') },
    { title: 'John Doe' },
  ]}
/>

// Custom item render with paths
<Breadcrumb
  items={[
    { title: 'Home', path: '' },
    { title: 'Users', path: 'users' },
    { title: ':id', path: ':id' },
  ]}
  params={{ id: '42' }}
  itemRender={(item, params, items, paths) => {
    const last = items.indexOf(item) === items.length - 1
    let path = paths[items.indexOf(item)]
    // Replace params
    if (params) {
      Object.entries(params).forEach(([key, value]) => {
        path = path.replace(`:${key}`, value)
      })
    }
    return last ? (
      <span>{item.title}</span>
    ) : (
      <a href={`/${path}`}>{item.title}</a>
    )
  }}
/>

// With semantic DOM styling
<Breadcrumb
  items={[
    { title: 'Home', href: '/' },
    { title: 'Products', href: '/products' },
    { title: 'Detail' },
  ]}
  classNames={{ link: 'custom-link' }}
  styles={{
    separator: { color: '#999', margin: '0 12px' },
    overlay: { borderRadius: 8 },
  }}
/>
```

</details>

---

<details>
<summary><strong>Bubble</strong> - Floating action button with menus and groups</summary>

### Bubble

A floating action button (FAB) component for quick actions, with support for badges, tooltips, compact groups, and expandable menus.

#### Import

```tsx
import { Bubble, BackToTopIcon, ChatIcon, BellIcon, CloseIcon } from '@juanitte/inoui'
```

#### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `icon` | `ReactNode` | — | Icon to display |
| `description` | `string` | — | Text to display (only if no icon) |
| `position` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` | `'bottom-right'` | Fixed position on screen |
| `shape` | `'circle' \| 'square'` | `'circle'` | Bubble shape |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Bubble size |
| `color` | `'primary' \| 'secondary' \| 'success' \| 'warning' \| 'error' \| 'info'` | `'primary'` | Semantic color |
| `badge` | `number \| boolean` | — | Show badge with number or dot |
| `badgeColor` | `'primary' \| 'secondary' \| 'success' \| 'warning' \| 'error' \| 'info'` | `'error'` | Badge color |
| `tooltip` | `string` | — | Tooltip text |
| `tooltipPosition` | `'left' \| 'right' \| 'top' \| 'bottom'` | auto | Tooltip position |
| `offsetX` | `number` | `24` | Horizontal offset from edge (px) |
| `offsetY` | `number` | `24` | Vertical offset from edge (px) |
| `shadow` | `boolean \| 'sm' \| 'md' \| 'lg'` | `'lg'` | Apply shadow |
| `bordered` | `boolean` | `true` | Show border |
| `onBackToTop` | `() => void` | — | Callback when scrolling to top |
| `visibleOnScroll` | `number` | — | Show only after scrolling (px) |
| `disabled` | `boolean` | `false` | Disable bubble |

Also accepts all standard `<button>` HTML attributes.

#### Sizes

| Size | Dimensions | Icon Size |
|------|------------|-----------|
| `sm` | 40px | 16px |
| `md` | 48px | 20px |
| `lg` | 56px | 24px |

#### Built-in Icons

Ino-UI provides utility icons for common FAB use cases:

| Icon | Description |
|------|-------------|
| `BackToTopIcon` | Arrow pointing up |
| `ChatIcon` | Chat/message bubble |
| `BellIcon` | Notification bell |
| `CloseIcon` | X close icon |

#### Semantic DOM

| Slot | Description |
|------|-------------|
| `root` | Outer wrapper element |
| `icon` | Icon element |
| `badge` | Badge counter element |
| `tooltip` | Tooltip popup element |
| `tooltipArrow` | Tooltip arrow element |

**Bubble.Menu:**

| Slot | Description |
|------|-------------|
| `root` | Menu wrapper element |
| `trigger` | Menu trigger element |
| `menu` | Menu container element |

#### Examples

```tsx
// Basic
<Bubble icon={<ChatIcon />} />

// With tooltip
<Bubble icon={<ChatIcon />} tooltip="Open chat" />

// Different positions
<Bubble icon={<BellIcon />} position="top-right" />
<Bubble icon={<ChatIcon />} position="bottom-left" />

// With badge (number)
<Bubble icon={<BellIcon />} badge={5} />

// With badge (dot)
<Bubble icon={<ChatIcon />} badge={true} />

// Badge with custom color
<Bubble icon={<BellIcon />} badge={3} badgeColor="warning" />

// Different colors
<Bubble icon={<ChatIcon />} color="success" />
<Bubble icon={<BellIcon />} color="info" />

// Different sizes
<Bubble icon={<ChatIcon />} size="sm" />
<Bubble icon={<ChatIcon />} size="lg" />

// Square shape
<Bubble icon={<ChatIcon />} shape="square" />

// Without border
<Bubble icon={<ChatIcon />} bordered={false} />

// Back to top button (appears after scrolling 200px)
<Bubble
  icon={<BackToTopIcon />}
  tooltip="Back to top"
  visibleOnScroll={200}
  onBackToTop={() => console.log('Scrolled to top')}
/>

// Custom offset
<Bubble icon={<ChatIcon />} offsetX={40} offsetY={40} />

// With text instead of icon
<Bubble description="?" tooltip="Help" />
```

---

### Bubble.Group

A compact button bar that joins multiple Bubbles together with unified styling. The bubbles are rendered as a seamless group with shared shadow and automatic border-radius handling.

#### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | — | Child Bubble components |
| `position` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` | `'bottom-right'` | Fixed position on screen |
| `direction` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'top'` | Layout direction |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size for all children |
| `offsetX` | `number` | `24` | Horizontal offset from edge (px) |
| `offsetY` | `number` | `24` | Vertical offset from edge (px) |
| `shadow` | `boolean \| 'sm' \| 'md' \| 'lg'` | `'lg'` | Shadow for the group container |

#### Direction

| Direction | Description |
|-----------|-------------|
| `top` | Bubbles stack upward from position |
| `bottom` | Bubbles stack downward from position |
| `left` | Bubbles stack leftward from position |
| `right` | Bubbles stack rightward from position |

#### Examples

```tsx
// Vertical compact group (stacks upward)
<Bubble.Group>
  <Bubble icon={<ChatIcon />} color="info" />
  <Bubble icon={<BellIcon />} color="warning" />
  <Bubble icon={<BackToTopIcon />} color="success" />
</Bubble.Group>

// Horizontal compact group
<Bubble.Group direction="left">
  <Bubble icon={<ChatIcon />} color="primary" />
  <Bubble icon={<BellIcon />} color="secondary" />
</Bubble.Group>

// Different position
<Bubble.Group position="top-right" direction="bottom">
  <Bubble icon={<ChatIcon />} />
  <Bubble icon={<BellIcon />} />
</Bubble.Group>

// With badges (badges extend outside the group)
<Bubble.Group>
  <Bubble icon={<ChatIcon />} badge={3} />
  <Bubble icon={<BellIcon />} badge={true} badgeColor="warning" />
</Bubble.Group>

// Custom size
<Bubble.Group size="lg">
  <Bubble icon={<ChatIcon />} />
  <Bubble icon={<BellIcon />} />
</Bubble.Group>
```

---

### Bubble.Menu

An expandable floating menu that shows/hides child Bubbles with animation. Supports click or hover activation.

#### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | — | Child Bubble components |
| `position` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` | `'bottom-right'` | Fixed position on screen |
| `direction` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'top'` | Expansion direction |
| `trigger` | `'click' \| 'hover'` | `'click'` | Activation mode |
| `icon` | `ReactNode` | `+` | Trigger icon when closed |
| `openIcon` | `ReactNode` | — | Trigger icon when open (defaults to rotating the icon 45deg) |
| `shape` | `'circle' \| 'square'` | `'circle'` | Shape for trigger and children |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size for trigger and children |
| `color` | `'primary' \| 'secondary' \| 'success' \| 'warning' \| 'error' \| 'info'` | `'primary'` | Trigger color |
| `offsetX` | `number` | `24` | Horizontal offset from edge (px) |
| `offsetY` | `number` | `24` | Vertical offset from edge (px) |
| `shadow` | `boolean \| 'sm' \| 'md' \| 'lg'` | `'lg'` | Apply shadow |
| `tooltip` | `string` | — | Tooltip for trigger (shown when closed) |
| `defaultOpen` | `boolean` | `false` | Initially open (uncontrolled) |
| `open` | `boolean` | — | Controlled open state |
| `onOpenChange` | `(open: boolean) => void` | — | Callback when state changes |
| `gap` | `number` | `12` | Space between bubbles (px) |

#### Examples

```tsx
// Basic expandable menu (click to open)
<Bubble.Menu>
  <Bubble icon={<ChatIcon />} tooltip="Chat" color="info" />
  <Bubble icon={<BellIcon />} tooltip="Notifications" color="warning" />
</Bubble.Menu>

// Hover to open
<Bubble.Menu trigger="hover">
  <Bubble icon={<ChatIcon />} tooltip="Chat" />
  <Bubble icon={<BellIcon />} tooltip="Alerts" />
</Bubble.Menu>

// Custom trigger icons
<Bubble.Menu
  icon={<ChatIcon />}
  openIcon={<CloseIcon />}
  color="success"
>
  <Bubble icon={<BellIcon />} tooltip="Notifications" />
  <Bubble icon={<BackToTopIcon />} tooltip="Back to top" />
</Bubble.Menu>

// Horizontal expansion
<Bubble.Menu direction="left">
  <Bubble icon={<ChatIcon />} />
  <Bubble icon={<BellIcon />} />
</Bubble.Menu>

// Controlled state
function MyComponent() {
  const [open, setOpen] = useState(false)

  return (
    <Bubble.Menu
      open={open}
      onOpenChange={setOpen}
      tooltip="Actions"
    >
      <Bubble icon={<ChatIcon />} onClick={() => openChat()} />
      <Bubble icon={<BellIcon />} onClick={() => openNotifications()} />
    </Bubble.Menu>
  )
}

// Different position
<Bubble.Menu position="top-left" direction="bottom">
  <Bubble icon={<ChatIcon />} />
  <Bubble icon={<BellIcon />} />
</Bubble.Menu>

// Inline mode (not fixed, flows with content)
<Bubble.Menu style={{ position: 'relative' }}>
  <Bubble icon={<ChatIcon />} />
  <Bubble icon={<BellIcon />} />
</Bubble.Menu>

// Custom gap
<Bubble.Menu gap={20}>
  <Bubble icon={<ChatIcon />} />
  <Bubble icon={<BellIcon />} />
</Bubble.Menu>
```

</details>

---

<details>
<summary><strong>Button</strong> - Versatile button with variants and animations</summary>

### Button

A versatile button component with multiple variants, sizes, and states.

#### Import

```tsx
import { Button } from '@juanitte/inoui'
```

#### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `variant` | `'primary' \| 'secondary' \| 'outline' \| 'dashed' \| 'ghost' \| 'link'` | `'primary'` | Button style variant |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Button size |
| `color` | `'primary' \| 'secondary' \| 'success' \| 'warning' \| 'error' \| 'info'` | `'primary'` | Semantic color |
| `icon` | `ReactNode` | — | Icon element |
| `iconPlacement` | `'start' \| 'end'` | `'start'` | Icon position relative to text |
| `loading` | `boolean` | `false` | Show loading spinner |
| `shadow` | `boolean \| 'sm' \| 'md' \| 'lg'` | `false` | Apply shadow |
| `clickAnimation` | `'pulse' \| 'ripple' \| 'shake' \| 'firecracker' \| 'confetti'` | — | Animation on click |
| `hoverAnimation` | `'pulse' \| 'ripple' \| 'shake' \| 'firecracker' \| 'confetti'` | — | Animation on hover |
| `gradient` | `'primary' \| 'secondary' \| 'success' \| 'warning' \| 'error' \| 'info'` | — | Preset gradient using theme color |
| `gradi