npm package: @jiwambe/components
Description: A production-ready, accessible React component library built with Tailwind CSS and a semantic token design system. Ships as ESM with full TypeScript support.
Author: Jiwambe
Latest version: 0.5.0

# @jiwambe/components

A production-ready, accessible React component library built with Tailwind CSS and a semantic token design system. Ships as ESM with full TypeScript support.

## Installation

```bash
pnpm add @jiwambe/components
```

### Peer Dependencies

Make sure you have the following installed in your project:

```bash
pnpm add react react-dom tailwindcss
```

### Font Loading

The design system uses **Instrument Sans** and **Inter** fonts. Add them to your project (for example via Google Fonts):

```html
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
  href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600&display=swap"
  rel="stylesheet"
/>
```

---

## Setup

### 1. Install

```bash
pnpm add @jiwambe/components
```

Peer dependencies: react, react-dom, tailwindcss

### 2. Configure Tailwind

In your project's `tailwind.config.ts`, use the Jiwambe preset. The preset includes the plugin (which injects all design token CSS variables) and the safelist (which ensures all spacing utility classes survive purging).

```ts
import uiPreset from '@jiwambe/components/tailwind.preset';

export default {
  presets: [uiPreset],
  content: [
    './src/**/*.{js,ts,jsx,tsx,mdx}',
    // Recommended: prevents spacing classes from being purged
    './node_modules/@jiwambe/components/dist/**/*.js',
  ],
};
```

If you need to add your own plugins, extend the preset:

```ts
export default {
  presets: [uiPreset],
  plugins: [yourOtherPlugin],
  content: [...],
};
```

### 3. Usage

```tsx
import { Box, Stack, Container, Grid } from '@jiwambe/components';

// Spacing tokens match the Jiwambe design token vocabulary:
<Container>
  <Stack spacing="space-4" mt="space-9" direction="column">
    <Box p="space-4">Fixed padding</Box>
    <Box px="fluid-4-9" py="space-6">Fluid horizontal padding</Box>
    <Box m="auto" px="fluid-4-9">Centered box</Box>
  </Stack>
</Container>

<Grid gap="space-4" p="space-2">
  <Box>Cell 1</Box>
  <Box>Cell 2</Box>
</Grid>

// Container gutters default to fluid-4-9 and can be overridden:
<Container />                         // fluid-4-9 gutters (default)
<Container px="space-9" />            // custom gutters
<Container disableGutters />          // no gutters
```

### Spacing token reference

| Token         | CSS variable              | Resolved value                    |
|---------------|---------------------------|-----------------------------------|
| space-0       | var(--space-0)            | 0rem                              |
| space-0-25    | var(--space-0-25)         | 0.0625rem                         |
| space-1       | var(--space-1)            | 0.25rem                           |
| space-2       | var(--space-2)            | 0.5rem                            |
| space-3       | var(--space-3)            | 0.75rem                           |
| space-4       | var(--space-4)            | 1rem                              |
| space-5       | var(--space-5)            | 1.25rem                           |
| space-6       | var(--space-6)            | 1.5rem                            |
| space-9       | var(--space-9)            | 2.25rem                           |
| space-12      | var(--space-12)           | 3rem                              |
| space-16      | var(--space-16)           | 4rem                              |
| space-18      | var(--space-18)           | 4.5rem                            |
| space-24      | var(--space-24)           | 6rem                              |
| space-30      | var(--space-30)           | 7.5rem                            |
| space-36      | var(--space-36)           | 9rem                              |
| space-48      | var(--space-48)           | 12rem                             |
| space-72      | var(--space-72)           | 18rem                             |
| space-list-indent | var(--space-list-indent) | 1.6875rem (27px)                  |
| fluid-1-2     | var(--space-fluid-1-2)    | clamp(0.25rem, 0.1706rem + 0.3968vw, 0.5rem)      |
| fluid-2-4     | var(--space-fluid-2-4)    | clamp(0.5rem, 0.3413rem + 0.7937vw, 1rem)         |
| fluid-4-5     | var(--space-fluid-4-5)    | clamp(1rem, 0.9206rem + 0.3968vw, 1.25rem)        |
| fluid-4-6     | var(--space-fluid-4-6)    | clamp(1rem, 0.8413rem + 0.7937vw, 1.5rem)         |
| fluid-4-8     | var(--space-fluid-4-8)    | clamp(1rem, 0.6825rem + 1.5873vw, 2rem)           |
| fluid-4-9     | var(--space-fluid-4-9)    | clamp(1rem, 0.6825rem + 1.5873vw, 2.25rem)        |
| fluid-5-6     | var(--space-fluid-5-6)    | clamp(1.25rem, 1.1706rem + 0.3968vw, 1.5rem)      |
| fluid-6-9     | var(--space-fluid-6-9)    | clamp(1.5rem, 1.2619rem + 1.1905vw, 2.25rem)      |
| fluid-8-16    | var(--space-fluid-8-16)   | clamp(2rem, 1.3651rem + 3.1746vw, 4rem)           |
| fluid-16-18   | var(--space-fluid-16-18)  | clamp(4rem, 3.8413rem + 0.7937vw, 4.5rem)         |
| fluid-30-36   | var(--space-fluid-30-36)  | clamp(7.5rem, 7.0238rem + 2.381vw, 9rem)           |
| fluid-48-72   | var(--space-fluid-48-72)  | clamp(12rem, 10.0952rem + 9.5238vw, 18rem)         |

---

## Components

### Typography

Renders text with design-system typographic styles.

```tsx
<Typography variant="title-xl" as="h1">Big Headline</Typography>
<Typography variant="text-md">Body copy here.</Typography>
<Typography variant="form-label" as="label">Email</Typography>
```

**Props:**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `TypographyVariant` | — | Required. One of `title-sm`, `title-md`, `title-lg`, `title-xl`, `text-xs`, `text-xs-bold`, `text-sm`, `text-sm-bold`, `text-md`, `text-md-bold`, `text-xl`, `form-text`, `form-label`, `btn-small`, `btn-reg`, `link-md`. |
| `as` | `string` | `"p"` | HTML element to render (`h1`–`h6`, `p`, `span`, `label`). |
| `className` | `string` | — | Additional CSS classes. |

### Button

Interactive button with four variants and two sizes.

```tsx
<Button variant="primary">Submit</Button>
<Button variant="secondary" size="small">Cancel</Button>
<Button variant="ghost" disabled>Disabled</Button>
```

**Props:**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"primary" \| "secondary" \| "inverse" \| "ghost"` | — | Required. Visual style. |
| `size` | `"small" \| "regular"` | `"regular"` | Button size. |
| `disabled` | `boolean` | `false` | Disables the button. |

### Link

Anchor element with variant-based styling.

```tsx
<Link variant="primary" href="/about">About Us</Link>
<Link variant="inverse" href="/contact">Contact</Link>
```

**Props:**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"primary" \| "secondary" \| "tertiary" \| "inverse"` | `"primary"` | Visual style. |
| `disabled` | `boolean` | `false` | Prevents navigation and applies disabled styles. |

### Accordion

Single-expansion accordion with keyboard navigation.

```tsx
<Accordion
  items={[
    { title: 'FAQ 1', content: <p>Answer to FAQ 1</p> },
    { title: 'FAQ 2', content: <p>Answer to FAQ 2</p> },
    { title: 'FAQ 3', content: <p>Answer to FAQ 3</p>, disabled: true },
  ]}
  defaultOpenIndex={0}
/>
```

**Props:**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `AccordionItem[]` | — | Required. Array of `{ title, content, disabled? }`. |
| `defaultOpenIndex` | `number` | — | Initially open item index (uncontrolled). |
| `value` | `number` | — | Open item index (controlled). |
| `onChange` | `(index: number) => void` | — | Called when the open item changes (controlled). |

### FAQ

FAQ section with heading, accordion of question/answer pairs, and optional footer link. Supports single or multiple items open (controlled or uncontrolled) and full keyboard navigation. Layout and spacing match the Figma templates (xs+, lg+, xl+ breakpoints): fluid section padding, title-lg heading, title-md questions, text-md answers, and link-md footer link.

```tsx
<FAQ
  heading="Questions"
  items={[
    { question: 'What is X?', answer: 'X is...' },
    { question: 'How do I Y?', answer: 'You can Y by...', disabled: true },
  ]}
  defaultOpenIndex={0}
  link={{ label: 'View all FAQs', href: '/faq' }}
  linkAs={NextLink}
/>
```

**Props:**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `heading` | `string` | `"FAQ"` | Section heading above the list. |
| `items` | `FAQItem[]` | — | Required. Array of `{ question, answer, disabled? }`. |
| `defaultOpenIndex` | `number \| number[]` | `[]` | Index(es) open by default (uncontrolled). |
| `value` | `number \| number[]` | — | Open index(es) (controlled). |
| `onChange` | `(indices: number[]) => void` | — | Called when open indices change (controlled). |
| `link` | `{ label: string; href: string }` | — | Optional footer link (e.g. "View all FAQs"). |
| `linkAs` | `ElementType` | `"a"` | Component for the footer link (e.g. `next/link`). |

### Tab

Horizontal tabs with full ARIA support.

```tsx
<Tab
  tabs={[
    { label: 'Overview', content: <p>Overview content</p> },
    { label: 'Details', content: <p>Detail content</p> },
    { label: 'Settings', content: <p>Settings content</p>, disabled: true },
  ]}
  defaultActiveIndex={0}
/>
```

**Props:**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `tabs` | `TabItem[]` | — | Required. Array of `{ label, content, disabled? }`. |
| `defaultActiveIndex` | `number` | `0` | Initially active tab (uncontrolled). |
| `activeIndex` | `number` | — | Active tab index (controlled). |
| `onTabChange` | `(index: number) => void` | — | Called when the active tab changes (controlled). |

### List

Semantic list with correct `ul`/`ol` structure. For `marker="disc"` and `marker="decimal"` uses block layout and item margin (not flex/gap) so markers render reliably. Use `indent` and `itemPadding="none"` for legal/body-copy lists.

```tsx
<List spacing="space-2">
  <List.Item size="sm">Item one</List.Item>
  <List.Item size="sm">Item two</List.Item>
</List>

<List marker="decimal" spacing="space-3">
  <List.Item size="md">Step one</List.Item>
  <List.Item size="md">Step two</List.Item>
</List>

{/* Legal/body-copy: 27px indent, no extra item padding */}
<List indent="space-list-indent" itemPadding="none" spacing="space-2">
  <List.Item size="md">First term.</List.Item>
  <List.Item size="md">Second term.</List.Item>
</List>

<List marker="none" spacing="space-1">
  <List.Item><Link href="/about">About</Link></List.Item>
</List>
```

**Props:**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `marker` | `"disc" \| "decimal" \| "none"` | `"disc"` | Marker style; `decimal` renders `<ol>`. |
| `spacing` | `SpacingToken` | `"space-2"` | Vertical spacing between items (margin for disc/decimal, gap for none). |
| `indent` | `SpacingToken \| string` | — | Left indent (e.g. `"space-6"`, `"space-list-indent"` for 27px, or `"27px"`). |
| `itemPadding` | `"default" \| "none"` | `"default"` | Item left padding; `"none"` for body-copy lists. |

### Card

Card with optional image, title, description, and action. Aligned with Figma (web · library): media cards use secondary background, fluid padding/gaps, title-md/text-md typography, and 48px action button with rounded-rad-md.

```tsx
<Card type="text-only" title="Title" description="Description." label="Action" onLabelClick={() => {}} />

<Card
  type="media-horizontal"
  imageSrc="/img.jpg"
  imageAlt="Product"
  title="Product"
  description="Description."
  label="View"
  labelHref="/product"
/>

<Card
  type="media-horizontal"
  imageSrc="/img.jpg"
  imageAlt="Product"
  message="Special Offer"
  title="Product"
  description="Description."
  label="Get started"
/>
```

**Props:** `type` (text-only | media-horizontal | media-vertical), `imageSrc`, `imageAlt`, `message` (overlay on image), `title`, `description`, `label`, `onLabelClick`, `labelHref`, `linkAs`.

### Breadcrumb

Breadcrumb navigation with slash or chevron separator. Optional truncation for long paths (first … second-to-last / current).

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

<Breadcrumb items={manyItems} truncate separator="slash" />
```

**Props:** `items` (array of `{ label, href? }`), `truncate`, `linkVariant`, `separator` (`"slash"` \| `"chevron"`).

### Grid

CSS Grid layout with responsive column counts.

```tsx
<Grid>
  <div className="col-span-6">Left</div>
  <div className="col-span-6">Right</div>
</Grid>
```

**Props:**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `columns` | `number` | 9 (XS), 12 (SM+) | Override responsive column count. |
| `gap` | `string` | Fluid token | Override gap (e.g. `"space-4"` or any CSS value). |

### Container

Width-constrained content wrapper with responsive padding.

```tsx
<Container>
  <Grid>
    <div className="col-span-12">Full-width inside container</div>
  </Grid>
</Container>
```

**Props:**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | — | Additional CSS classes. |

---

## Theming

The Jiwambe plugin registers all design tokens as Tailwind utilities. You can override semantic color mappings by passing options to the plugin:

```js
import jiwambePlugin from '@jiwambe/components/plugin';

export default {
  content: [
    './src/**/*.{js,ts,jsx,tsx}',
    './node_modules/@jiwambe/components/dist/**/*.js',
  ],
  plugins: [
    jiwambePlugin({
      colors: {
        'text-primary': '#005748',
        'link-primary': '#19AE8A',
        'fill-action-primary': '#19AE8A',
        'fill-action-primary-hover': '#109274',
      },
    }),
  ],
};
```

### Available Token Categories

| Category | Example Tailwind Class | Description |
| --- | --- | --- |
| Text colors | `text-text-primary` | Semantic text color |
| Link colors | `text-link-primary` | Semantic link color |
| Fill colors | `bg-fill-action-primary` | Background fills |
| Border colors | `border-border-light` | Border colors |
| Icon colors | `text-icon-primary` | Icon colors |
| Typography | `text-title-lg` | Composite typography styles |
| Spacing | `p-space-4`, `gap-space-fluid-2-4` | Fixed and fluid spacing |
| Border radius | `rounded-rad-md` | Border radius |
| Box shadow | `shadow-elevation-low` | Elevation shadows |

### CSS Custom Properties

All tokens are also available as CSS custom properties on `:root`:

```css
.custom-element {
  color: var(--color-text-primary);
  padding: var(--space-4);
  border-radius: var(--rad-md);
  box-shadow: var(--elevation-low);
}
```

---

## Design Tokens Reference

### Breakpoints

| Name | Value |
| --- | --- |
| `xs` | 0px |
| `sm` | 600px |
| `md` | 800px |
| `lg` | 940px |
| `xl` | 1440px |

### Grid

| Breakpoint | Columns | Gap |
| --- | --- | --- |
| XS (0+) | 9 | `space-2` (8px) |
| SM+ (600+) | 12 | `space-fluid-2-4` (8–16px) |

### Container

| Breakpoint | Behaviour |
| --- | --- |
| XS–LG | Horizontal padding `space-fluid-4-9` (16–36px) |
| XL+ | Max width 83rem (1328px), centred, no padding |

---

## Development

```bash
# Install dependencies
pnpm install

# Type-check
pnpm typecheck

# Build
pnpm build
```

The build output lands in `dist/` as ESM with TypeScript declarations and source maps.