npm package: @leodamours/ds-components
Description: Vue 3 component library with built-in design tokens, theming via CSS custom properties, and algorithmic palette generation.
Latest version: 0.3.1

# @leodamours/ds-components

Vue 3 component library with built-in design tokens, theming via CSS custom properties, and algorithmic palette generation.

## Install

```bash
npm install @leodamours/ds-components
```

**Peer dependencies:** `vue ^3.5.0`

**Optional:** [`@leodamours/ds-icons`](https://www.npmjs.com/package/@leodamours/ds-icons) — 1,920 Lucide-based icon components that pair with this library.

## Setup

```ts
// main.ts
import '@leodamours/ds-components/style.css'
```

The stylesheet bundles the default light theme, dark theme (`[data-theme="dark"]`), and all component styles. No additional CSS imports needed.

## Usage

```vue
<script setup>
import { DsButton, DsInput } from '@leodamours/ds-components'
</script>

<template>
  <DsInput label="Email" placeholder="you@example.com" />
  <DsButton variant="primary" label="Submit" />
</template>
```

## Components

| Component | Description |
|---|---|
| `DsAvatar` | User avatar with image or initials fallback |
| `DsBadge` | Status badge / label |
| `DsButton` | Button with variants: primary, secondary, outline, ghost, danger, success |
| `DsCard` | Container card with variants: default, outlined, elevated |
| `DsCheckbox` | Checkbox input |
| `DsDropdownSelect` | Dropdown select with floating UI positioning |
| `DsExpandableCard` | Collapsible card with title, supports `v-model` for expand state |
| `DsInput` | Text input with label, help text, error state |
| `DsLoadingSpinner` | Animated loading spinner |
| `DsModal` | Modal dialog with backdrop |
| `DsRadio` | Radio input |
| `DsSelect` | Native select wrapper |
| `DsSlider` | Range slider with optional value display |
| `DsSwitch` | Toggle switch for boolean settings |
| `DsTable` | Data table with sorting and pagination |
| `DsTabs` | Tab navigation |
| `DsTextarea` | Multiline text input |
| `DsToast` / `DsToastContainer` | Toast notifications (see [Toast system](#toast-system) below) |
| `DsTooltip` | Tooltip on hover |
| `DsUnderConstruction` | Placeholder for pages not yet built |

All components export their prop types (e.g., `DsButtonProps`, `DsCardProps`).

### Component props

#### DsAvatar

| Prop | Type | Default | Description |
|---|---|---|---|
| `src` | `string` | — | Image URL |
| `alt` | `string` | — | Alt text for image |
| `name` | `string` | — | Full name (used for initials fallback) |
| `size` | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Avatar size |
| `variant` | `'brand' \| 'neutral'` | `'brand'` | Color variant for initials background |

#### DsBadge

| Prop | Type | Default | Description |
|---|---|---|---|
| `label` | `string` | — | Badge text (can also use default slot) |
| `variant` | `'brand' \| 'neutral' \| 'error' \| 'success' \| 'warning' \| 'info' \| 'processing'` | `'neutral'` | Color variant |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Badge size |

#### DsButton

| Prop | Type | Default | Description |
|---|---|---|---|
| `label` | `string` | — | Button text (can also use default slot) |
| `variant` | `'primary' \| 'secondary' \| 'outline' \| 'ghost' \| 'danger' \| 'success' \| 'warning'` | `'primary'` | Visual style |
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Button size |
| `htmlType` | `'button' \| 'submit' \| 'reset'` | `'button'` | Native button type |
| `disabled` | `boolean` | `false` | Disable interaction |
| `loading` | `boolean` | `false` | Show loading spinner |
| `fullWidth` | `boolean` | `false` | Expand to fill container |
| `iconLeft` | `Component` | — | Vue component icon on the left |
| `iconRight` | `Component` | — | Vue component icon on the right |
| `ariaLabel` | `string` | — | Accessible label override |

#### DsCard

| Prop | Type | Default | Description |
|---|---|---|---|
| `variant` | `'default' \| 'outlined' \| 'elevated'` | `'default'` | Card style |
| `noPadding` | `boolean` | `false` | Remove inner padding |

Default slot for card content.

#### DsCheckbox

| Prop | Type | Default | Description |
|---|---|---|---|
| `modelValue` | `boolean` | `false` | Checked state (`v-model`) |
| `label` | `string` | — | Checkbox label |
| `id` | `string` | auto | HTML id attribute |
| `value` | `string \| number \| boolean` | — | Form value |
| `disabled` | `boolean` | `false` | Disable interaction |
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Checkbox size |

#### DsDropdownSelect

| Prop | Type | Default | Description |
|---|---|---|---|
| `modelValue` | `string \| number` | — | Selected value (`v-model`) |
| `options` | `(string \| number \| { label, value, disabled? })[]` | `[]` | List of options |
| `placeholder` | `string` | `'Select an option'` | Placeholder text |
| `variant` | `'primary' \| 'secondary' \| 'ghost'` | `'primary'` | Trigger button style |
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Trigger button size |
| `disabled` | `boolean` | `false` | Disable interaction |
| `loading` | `boolean` | `false` | Show loading state |
| `iconLeft` | `Component` | — | Icon in the trigger button |
| `fullWidth` | `boolean` | `false` | Expand to fill container |
| `emptyText` | `string` | `'No options available'` | Text shown when options list is empty |

#### DsExpandableCard

| Prop | Type | Default | Description |
|---|---|---|---|
| `title` | `string` | **required** | Card header text |
| `modelValue` | `boolean` | — | Controlled expand state (`v-model`) |
| `defaultExpanded` | `boolean` | `false` | Initial expand state (uncontrolled) |
| `variant` | `'default' \| 'outlined' \| 'elevated'` | `'default'` | Card style |

Default slot for expandable content.

#### DsInput

| Prop | Type | Default | Description |
|---|---|---|---|
| `modelValue` | `string \| number` | — | Input value (`v-model`) |
| `label` | `string` | — | Label text |
| `type` | `'text' \| 'email' \| 'password' \| 'number' \| 'tel' \| 'url' \| 'search'` | `'text'` | Input type |
| `placeholder` | `string` | — | Placeholder text |
| `helpText` | `string` | — | Help text below the input |
| `error` | `string` | — | Error message (replaces help text) |
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Input size |
| `disabled` | `boolean` | `false` | Disable interaction |
| `readonly` | `boolean` | `false` | Read-only state |
| `required` | `boolean` | `false` | Mark as required |
| `clearable` | `boolean` | `false` | Show clear button when filled |
| `iconLeft` | `Component` | — | Icon on the left |
| `iconRight` | `Component` | — | Icon on the right |
| `autocomplete` | `string` | — | HTML autocomplete attribute |
| `maxLength` | `number` | — | Character limit |
| `min` | `number \| string` | — | Minimum value (number input) |
| `max` | `number \| string` | — | Maximum value (number input) |
| `step` | `number \| string` | — | Step increment (number input) |

#### DsLoadingSpinner

| Prop | Type | Default | Description |
|---|---|---|---|
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Spinner size |
| `variant` | `'primary' \| 'secondary' \| 'white' \| 'current'` | `'primary'` | Color variant |

#### DsModal

| Prop | Type | Default | Description |
|---|---|---|---|
| `modelValue` | `boolean` | **required** | Open state (`v-model`) |
| `title` | `string` | — | Modal title |
| `size` | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'md'` | Modal width |
| `closable` | `boolean` | `true` | Show close button |
| `persistent` | `boolean` | `false` | Prevent closing on backdrop click |

Slots: `default` (body), `header`, `footer`.

#### DsRadio

| Prop | Type | Default | Description |
|---|---|---|---|
| `modelValue` | `string \| number \| boolean` | — | Selected value (`v-model`) |
| `label` | `string` | — | Radio label |
| `id` | `string` | auto | HTML id attribute |
| `name` | `string` | — | Radio group name |
| `value` | `string \| number \| boolean` | — | This radio's value |
| `disabled` | `boolean` | `false` | Disable interaction |
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Radio size |

#### DsSelect

| Prop | Type | Default | Description |
|---|---|---|---|
| `modelValue` | `string \| number` | — | Selected value (`v-model`) |
| `label` | `string` | — | Label text |
| `placeholder` | `string` | — | Placeholder text |
| `helpText` | `string` | — | Help text below the select |
| `error` | `string` | — | Error message |
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Select size |
| `disabled` | `boolean` | `false` | Disable interaction |
| `required` | `boolean` | `false` | Mark as required |
| `options` | `DsSelectOption[]` | **required** | `{ label: string, value: string \| number, disabled?: boolean }` |

#### DsSlider

| Prop | Type | Default | Description |
|---|---|---|---|
| `modelValue` | `number` | `0` | Slider value (`v-model`) |
| `min` | `number` | `0` | Minimum value |
| `max` | `number` | `100` | Maximum value |
| `step` | `number` | `1` | Step increment |
| `disabled` | `boolean` | `false` | Disable interaction |
| `label` | `string` | — | Label text |
| `showValue` | `boolean` | `false` | Display current value |

#### DsSwitch

| Prop | Type | Default | Description |
|---|---|---|---|
| `modelValue` | `boolean` | `false` | Checked state (`v-model`) |
| `label` | `string` | — | Label text |
| `id` | `string` | auto | HTML id attribute |
| `disabled` | `boolean` | `false` | Disable interaction |
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Switch size |
| `labelPosition` | `'left' \| 'right'` | `'right'` | Label placement |

#### DsTable

| Prop | Type | Default | Description |
|---|---|---|---|
| `columns` | `DsTableColumn[]` | **required** | Column definitions (see below) |
| `rows` | `Record<string, any>[]` | **required** | Row data |
| `sortBy` | `string` | — | Active sort column key |
| `sortOrder` | `'asc' \| 'desc'` | — | Sort direction |
| `loading` | `boolean` | `false` | Show loading state |
| `emptyText` | `string` | `'No data available'` | Text when no rows |
| `striped` | `boolean` | `false` | Alternate row shading |
| `stickyHeader` | `boolean` | `false` | Sticky column headers |
| `pagination` | `DsTablePagination` | — | Enable pagination (see below) |
| `nextPageLabel` | `string` | `'Next'` | Pagination next label |
| `previousPageLabel` | `string` | `'Previous'` | Pagination previous label |
| `ofLabel` | `string` | — | Pagination "of" label |

**`DsTableColumn`**: `{ key: string, label: string, sortable?: boolean, width?: string, align?: 'left' | 'center' | 'right' }`

**`DsTablePagination`**: `{ page: number, perPage: number, total: number }`

Column cells can be customized via named slots: `#cell-{key}="{ row, column }"`.

#### DsTabs

| Prop | Type | Default | Description |
|---|---|---|---|
| `modelValue` | `string` | **required** | Active tab key (`v-model`) |
| `tabs` | `DsTab[]` | **required** | Tab definitions |
| `variant` | `'line' \| 'pill'` | `'line'` | Tab style |
| `ariaLabel` | `string` | `'Tabs'` | Accessible label |

**`DsTab`**: `{ key: string, label: string, disabled?: boolean }`

Tab content is rendered via named slots: `#tab-{key}`.

#### DsTextarea

| Prop | Type | Default | Description |
|---|---|---|---|
| `modelValue` | `string` | — | Textarea value (`v-model`) |
| `label` | `string` | — | Label text |
| `placeholder` | `string` | — | Placeholder text |
| `helpText` | `string` | — | Help text below the textarea |
| `error` | `string` | — | Error message |
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Textarea size |
| `disabled` | `boolean` | `false` | Disable interaction |
| `readonly` | `boolean` | `false` | Read-only state |
| `required` | `boolean` | `false` | Mark as required |
| `rows` | `number` | `3` | Visible rows |
| `maxLength` | `number` | — | Character limit (shows counter) |

#### DsTooltip

| Prop | Type | Default | Description |
|---|---|---|---|
| `content` | `string` | **required** | Tooltip text |
| `placement` | `Placement` | `'top'` | Position (`'top'`, `'bottom'`, `'left'`, `'right'`, etc.) |
| `disabled` | `boolean` | `false` | Disable tooltip |

Wrap any element in `<DsTooltip>` — the default slot is the trigger.

#### DsUnderConstruction

| Prop | Type | Default | Description |
|---|---|---|---|
| `title` | `string` | `'Under Construction'` | Heading text |
| `description` | `string` | `'This page is under construction...'` | Body text |

## Toast system

Place `<DsToastContainer />` once at the root of your app, then trigger toasts from anywhere via the `useToast` composable:

```vue
<!-- App.vue -->
<script setup>
import { DsToastContainer } from '@leodamours/ds-components'
</script>

<template>
  <router-view />
  <DsToastContainer />
</template>
```

```ts
// Any component or composable
import { useToast } from '@leodamours/ds-components'

const toast = useToast()

toast.success('Saved successfully')
toast.error('Something went wrong', { title: 'Error' })
toast.warning('Session expiring soon', { duration: 8000 })
toast.info('New version available')
toast.brand('Welcome back!')
```

Each method accepts `(message, options?)` where options are `{ title?: string, duration?: number }`. Default duration is 5000ms.

## Theming

Every color in the library flows through CSS custom properties prefixed with `--ds-`. The default theme is included in `style.css`.

### Option 1: CSS overrides

Override only the variables you need:

```css
/* my-theme.css */
:root {
  --ds-brand: #8B5CF6;
  --ds-brand-hover: #7C3AED;
  --ds-brand-active: #6D28D9;
  --ds-brand-on-solid: #FFFFFF;
  --ds-bg: #FAFAFA;
  /* Only override what differs — everything else inherits defaults */
}
```

```ts
import '@leodamours/ds-components/style.css' // defaults
import './my-theme.css'                       // your overrides
```

### Option 2: Generate a full theme from 7 base colors

The `generateFullTheme` function derives all hover, soft, border, contrast, and disabled variants automatically from just one hex per semantic role:

```ts
import { generateFullTheme } from '@leodamours/ds-components'

const css = generateFullTheme(
  {
    brand: '#8B5CF6',
    error: '#E11D48',
    success: '#059669',
    warning: '#EA580C',
    info: '#6366F1',
    processing: '#D946EF',
    neutral: '#6B5F7D',
  },
  '[data-theme="purple"]', // CSS selector (defaults to ':root')
)

// Inject into a <style> tag
const style = document.createElement('style')
style.textContent = css
document.head.appendChild(style)
```

For finer control, `generatePalette(hex)` returns the full variant set for a single color:

```ts
import { generatePalette } from '@leodamours/ds-components'

const brandPalette = generatePalette('#8B5CF6')
// => { base, hover, active, disabled, soft, soft-hover, soft-active,
//      on-solid, on-soft, border, border-hover, border-strong }
```

### Option 3: Override individual tokens at runtime

```ts
import { generateThemeCss } from '@leodamours/ds-components'

const css = generateThemeCss(
  { brand: '#8B5CF6', 'brand-hover': '#7C3AED' },
  '[data-theme="purple"]',
)
```

### Runtime theme switching

A dark theme is bundled and activates via `data-theme`:

```ts
// Switch to dark
document.documentElement.dataset.theme = 'dark'

// Switch back to light
delete document.documentElement.dataset.theme
```

### Token naming convention

Tokens follow a **role + tone** pattern: `--ds-{role}[-{tone}][-{state}]`

**Core UI tokens:**

| Token | Purpose |
|---|---|
| `bg`, `bg-secondary`, `bg-tertiary` | Page backgrounds |
| `bg-inverse`, `bg-inverse-secondary` | Inverted backgrounds |
| `fg`, `fg-secondary`, `fg-tertiary`, `fg-inverse`, `fg-disabled` | Text colors |
| `icon`, `icon-hover`, `icon-active`, `icon-inverse`, `icon-disabled` | Icon colors |
| `border`, `border-light`, `border-strong`, `border-focus` | Border colors |
| `surface-hover`, `surface-hover-light`, `surface-active`, `surface-disabled` | Interactive surfaces |
| `overlay`, `focus-ring` | Overlays and focus states |

**Semantic role tokens** — each role (`brand`, `error`, `success`, `warning`, `info`, `processing`, `neutral`) has:

| Tone | Purpose | Example |
|---|---|---|
| `{role}` | Solid color | `--ds-brand` |
| `{role}-hover` | Hover state | `--ds-brand-hover` |
| `{role}-active` | Active/pressed state | `--ds-brand-active` |
| `{role}-soft` | Light background | `--ds-error-soft` |
| `{role}-soft-hover` | Soft hover state | `--ds-error-soft-hover` |
| `{role}-soft-active` | Soft active state | `--ds-error-soft-active` |
| `{role}-on-solid` | Text on solid bg | `--ds-success-on-solid` |
| `{role}-on-soft` | Text on soft bg | `--ds-success-on-soft` |
| `{role}-border` | Border color | `--ds-warning-border` |
| `{role}-border-hover` | Border hover | `--ds-warning-border-hover` |
| `{role}-border-strong` | Strong border | `--ds-info-border-strong` |

## UnoCSS Preset

If your app uses UnoCSS, import the design system preset instead of configuring tokens manually. It includes the full theme, rules, shortcuts, variants, and CSS custom properties.

### Basic setup

```ts
// uno.config.ts
import { defineConfig, presetUno } from 'unocss'
import { dsPreset } from '@leodamours/ds-components/uno'

export default defineConfig({
  presets: [presetUno(), dsPreset()],
})
```

### Custom palette

Pass base colors to override the default palette. All hover, soft, border, and contrast variants are derived automatically:

```ts
dsPreset({
  brand: '#8B5CF6',
  error: '#E11D48',
  success: '#059669',
  warning: '#EA580C',
  info: '#6366F1',
  processing: '#D946EF',
  neutral: '#6B5F7D',
})
```

Only override what differs — unspecified roles keep their defaults. The preset accepts these roles: `brand`, `error`, `success`, `warning`, `info`, `processing`, `neutral`.

## Full export reference

### Components

```ts
import {
  DsAvatar,
  DsBadge,
  DsButton,
  DsCard,
  DsCheckbox,
  DsDropdownSelect,
  DsExpandableCard,
  DsInput,
  DsLoadingSpinner,
  DsModal,
  DsRadio,
  DsSelect,
  DsSlider,
  DsSwitch,
  DsTable,
  DsTabs,
  DsTextarea,
  DsToast,
  DsToastContainer,
  DsTooltip,
  DsUnderConstruction,
} from '@leodamours/ds-components'
```

### Prop types

```ts
import type {
  DsAvatarProps,
  DsBadgeProps,
  DsButtonProps,
  DsCardProps,
  DsCheckboxProps,
  DsDropdownSelectProps,
  DsExpandableCardProps,
  DsInputProps,
  DsLoadingSpinnerProps,
  DsModalProps,
  DsRadioProps,
  DsSelectProps,
  DsSelectOption,
  DsSliderProps,
  DsSwitchProps,
  DsTableProps,
  DsTableColumn,
  DsTablePagination,
  DsTabsProps,
  DsTab,
  DsTextareaProps,
  DsTooltipProps,
  DsUnderConstructionProps,
  Toast,
  ToastVariant,
} from '@leodamours/ds-components'
```

### Token utilities

```ts
import {
  colorTokens,          // All color token values as a JS object
  spacingTokens,        // Spacing scale values
  radiusTokens,         // Border radius values
  shadowTokens,         // Box shadow values
  allTokens,            // All tokens combined
  generateThemeCss,     // Override specific tokens → CSS string
  tokenToCssVar,        // Convert token name to CSS var reference
} from '@leodamours/ds-components'

import type { ColorToken, SpacingToken, RadiusToken, ShadowToken, ThemeOverride } from '@leodamours/ds-components'
```

### Palette generation

```ts
import {
  generatePalette,    // Single hex → full SemanticPalette (12 variants)
  generateFullTheme,  // 7 base colors → complete CSS theme string
  expandBases,        // 7 base colors → flat token map (no CSS wrapping)
  hexToHSL,           // Hex → { h, s, l }
  hslToHex,           // (h, s, l) → hex string
} from '@leodamours/ds-components'

import type { SemanticPalette, ThemeBases, SemanticRole } from '@leodamours/ds-components'
```

### Composables

```ts
import { useToast, useId } from '@leodamours/ds-components'
```

- **`useToast()`** — returns `{ toasts, removeToast, success, error, warning, info, brand }` (see [Toast system](#toast-system))
- **`useId()`** — generates unique IDs for accessible form element associations