# @bureaulabs/web-ds — Figma Make Guidelines

A React component library and design-token system for Ebury products. This file is read by Figma Make to guide code generation. **Follow the rules in Section 1 before generating any code.**

---

## 1. HARD RULES — read these first

1. **Every page-level UI MUST use `<Page>` from this package as the root.** The user's content goes inside `<Page>`'s `children` prop. **Never** build a custom `<div className="page">` or `<main>` shell. The sidebar nav, action bar, page title, and content-area chrome are all provided by this package.

2. **Never re-implement a component that already exists in this package.**
   - Button → `<Button>` (don't write your own button)
   - Dropdown → `<Select>`
   - Icon → `<Icon>` (don't install `lucide-react`, `react-icons`, `@heroicons/react`, or any other icon library)
   - Sidebar → `<Nav>`
   - Page header → `<PageTitle>`
   - Header action row → handled automatically by `<Page>`. Do NOT render `<PageActionBar>` yourself — it's fixed and rendered internally.
   - App shell → `<Page>`
   - Table / list view → `<DataTable>` (don't write your own `<table>` or grid layout)
   - Status pill / badge / category label → `<Tag>` (don't write a styled `<span>` for "Pending" / "Settled" / "Failed")
   - Notification / toast / snackbar → `<Toast>` (don't write a styled `<div>` overlay with a hand-rolled close button)

3. **Never write raw colour values or pixel sizes.** Use the CSS custom properties listed in Section 5. `#f5f5f5` is wrong; `var(--control-primary-background-rest)` is right. `padding: 16px` is wrong; `padding: var(--sizing-16)` is right.

4. **Import the stylesheet exactly once at the app root:**
   ```tsx
   import '@bureaulabs/web-ds/styles'
   ```
   Without this line, no styles render. **Do not** add Tailwind, do not write custom CSS resets — the stylesheet handles all of it.

5. **If you see a wrapper package** (e.g. `@make-kits/web-ds-test`) that re-exports our components, the component names and prop shapes are unchanged. Components are still called `Button`, `Page`, `Nav`, etc. If the wrapper appears empty or missing exports, install `@bureaulabs/web-ds` directly and import from there.

6. **ANY pill-shaped label MUST be a `<Tag>` — no exceptions.** Status indicators, category chips, type tags, priority levels, "new" / "beta" / "draft" badges — anything pill-shaped uses `<Tag variant="...">`. This applies everywhere on the page, especially inside `<DataTable>`: status / category / type / priority columns MUST use the column's `render` callback to wrap each value in `<Tag>`. NEVER render a status as plain text. NEVER style a `<span>` to look like a pill. NEVER use a `<Button>` as a status indicator.

7. **`<PageActionBar />` is a FIXED, PERMANENT part of `<Page />`.** Every `<Page>` automatically renders a `<PageActionBar />` in its header containing fixed buttons (Pay, Convert & Pay, Drawdown, More, Notifications). You CANNOT change, replace, reorder, or restyle this bar. Do NOT render `<PageActionBar />` yourself. Do NOT pass `actionBar` to `<Page>` — that prop has been removed. The ONLY thing you may do is pass click handlers via `<Page actionBarProps={{ onPay, onConvertAndPay, onDrawdown, onMore, onNotifications }}>`. Page-specific actions belong on `<PageTitle actions={...}>`, not as a replacement action bar.

---

## 2. Canonical page template — start here, then fill in the body

Every page you generate should follow this structure. **Copy this template verbatim, then customise only `children`** to put the page's actual content (cards, tables, charts, forms, etc.) into the body slot.

```tsx
import { useState } from 'react'
import '@bureaulabs/web-ds/styles'
import {
  Page,
  Nav,
  PageTitle,
  PageActionBar,
  DataTable,
  Tag,
  Toast,
  Button,
  Select,
  Icon,
} from '@bureaulabs/web-ds'

export default function MyPage() {
  const [activePage, setActivePage] = useState('Accounts')

  return (
    <Page
      sidebar={
        <Nav
          activePage={activePage}
          onPageChange={setActivePage}
        />
      }
      title={
        <PageTitle
          title="Accounts"
          subtitle="All currencies"
        />
      }
      // PageActionBar is rendered automatically inside <Page> and is fixed.
      // Wire its handlers via actionBarProps (all optional).
    >
      {/* ───── PAGE BODY GOES HERE ─────
          Put cards, tables, lists, charts, forms — whatever the page
          is about. Use semantic tokens for any custom styling:
            color: var(--text-primary)
            background: var(--page-bg-color-subtle)
            padding: var(--sizing-24)
            border-radius: var(--radius-medium)
          Compose <Button>, <Select>, <Icon> from this package wherever
          you need interactive controls or iconography.
      */}
    </Page>
  )
}
```

### Choosing which slot of `<Page>` to use

| You want to add… | Pass it to `<Page>`'s prop… | Typical wrapper |
|---|---|---|
| A sidebar navigation | `sidebar` | `<Nav>` |
| A row of action buttons at the top of the content | `actionBar` | `<PageActionBar>` |
| The page heading + optional subtitle + heading-level actions | `title` | `<PageTitle>` |
| Cards, tables, charts, forms, the actual page content | `children` | (your own composition, using `<Button>` / `<Select>` / `<Icon>` from this package) |

Any of `sidebar`, `actionBar`, `title` is optional — omit the prop if the page doesn't need that section. `children` is where your real content lives.

---

## 3. Installation & setup

### Install
```bash
npm install @bureaulabs/web-ds
```
No runtime dependencies beyond `react` / `react-dom` (peer deps). Bundle includes all icons and fonts.

### Import the stylesheet **once** at the app root
```tsx
import '@bureaulabs/web-ds/styles'
```
Loads all tokens (`:root` CSS variables), `@font-face` declarations for KMR Apparat + Inter Variable, and global resets.

### Import components by name
```tsx
import { Button, Nav, Page, PageTitle, PageActionBar, Select, Icon } from '@bureaulabs/web-ds'
import type { ButtonProps, NavProps, PageProps /* … */ } from '@bureaulabs/web-ds'
```

---

## 4. Component reference

### `<Icon />`
Renders an icon inside a sized wrapper that inherits `currentColor` from its parent.

```tsx
<Icon name="IconHome" size={20} />
<Icon name="IconChevronRight" size={16} aria-label="Next" />
```

| Prop | Type | Default | Notes |
|---|---|---|---|
| `name` | `CentralIconName` | — | Required. See "Common icons" below. |
| `size` | `16 \| 20 \| 28` | `16` | Maps to `--icon-size-*` tokens |
| `aria-label` | `string` | — | Provide for meaningful icons; omit for decorative ones (`aria-hidden` set automatically) |

#### Common icons (use these names — all are confirmed to exist)

- **Navigation:** `IconHome`, `IconWallet1`, `IconBank`, `IconCreditCard1`, `IconPayment`, `IconCoins`, `IconUmbrellaSecurity`, `IconConnectors1`, `IconChecklist`, `IconSidebar`
- **UI affordances:** `IconChevronBottom`, `IconChevronTop`, `IconChevronLeft`, `IconChevronRight`, `IconPlusMedium`, `IconPlusSmall`, `IconCheckmark`, `IconArrowUpRight`
- **Currency / finance:** `IconCurrencyDollar`, `IconCurrencyEuro`, `IconCurrencyPounds`, `IconCurrencyYen`, `IconCurrencyLira`, `IconCurrencyPesos`, `IconCurrencyRupees`, `IconBank2`, `IconBanknote1`, `IconBanknote2`
- **Search / filter:** `IconSearchMenu`, `IconChartCompare`
- **Status / state:** `IconChecklist`, `IconBurger`
- **Other:** `IconSend`, `IconSendLater`, `IconCoinStack`, `IconCoinsAdd`, `IconAddToBasket`, `IconAddImage`

Any name from the `CentralIconName` type union is valid — TypeScript autocomplete will show you the full list (~2,000 icons). Stick to the names above unless you have a specific reason; they cover the vast majority of needs.

---

### `<Button />`
Primary interactive trigger. Extends all native `<button>` props.

```tsx
<Button variant="brand" size="md">Save changes</Button>
<Button variant="primary" size="sm" leadingIcon="IconHome">Home</Button>
<Button variant="ghost" size="sm" leadingIcon="IconHome" aria-label="Home" />  {/* icon-only */}
<Button variant="secondary" disabled>Disabled</Button>
```

| Prop | Type | Default |
|---|---|---|
| `variant` | `'action' \| 'brand' \| 'primary' \| 'secondary' \| 'ghost'` | `'action'` |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` |
| `leadingIcon` | `CentralIconName` | — |
| `trailingIcon` | `CentralIconName` | — |
| `disabled` | `boolean` | `false` |
| …plus all native `<button>` props (`onClick`, `type`, `aria-*`, etc.) |

**Variant guidance:**
- `brand` — the single most-important action on a page (e.g. "Save", "Create", "Submit")
- `action` — primary dark button (e.g. confirm dialogs)
- `primary` — neutral filled button (toolbar actions)
- `secondary` — outlined neutral (cancel, alternative actions)
- `ghost` — transparent (subtle / icon-only / nav-style buttons)

**Rules:**
- Icon-only buttons (`leadingIcon` without `children`) **must** have `aria-label`.
- Use `size="sm"` inside `<PageActionBar>` and `<PageTitle actions>` (matches 32 px row height).

---

### `<Select />`
Dropdown trigger button. Extends all native `<button>` props.

```tsx
<Select placeholder="Choose currency" />
<Select variant="secondary" size="md" leading={<Icon name="IconBank" size={20} />}>EUR — Euro</Select>
<Select error placeholder="Required" />
```

| Prop | Type | Default | Notes |
|---|---|---|---|
| `variant` | `'primary' \| 'secondary'` | `'primary'` | |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | |
| `error` | `boolean` | `false` | Red error border |
| `leading` | `ReactNode` | — | Typically an `<Icon>` |
| `placeholder` | `string` | — | |
| `children` | `ReactNode` | — | Displayed value; falls back to `placeholder` when empty |

---

### `<NavItem />`
Single navigation row. Used inside `<Nav>`; rarely standalone.

| Prop | Type | Default | Notes |
|---|---|---|---|
| `label` | `string` | — | Required |
| `icon` | `CentralIconName` | — | Only renders at `level="root"` |
| `level` | `'root' \| 'child'` | `'root'` | Child items are indented and don't show icons |
| `isSelected` | `boolean` | `false` | |
| `hasChevron` | `boolean` | `false` | Root only — indicates an expandable group |
| `isExpanded` | `boolean` | `false` | Controls chevron direction |
| `isCollapsed` | `boolean` | `false` | Root only — sidebar icon-only mode |
| `onClick` | `(e) => void` | — | |

---

### `<Nav />`
Application sidebar. Composes `Select` (account picker) + nav-toggle + `NavItem`s + ebury wordmark + 3-dot kebab. Default `items` mirror the full Figma nav catalog; pass `items` to override.

```tsx
<Nav activePage="Accounts" onPageChange={setPage} />
<Nav activePage="Hedge" isCollapsed onToggleCollapse={() => …} />
<Nav items={[...DEFAULT_NAV_ITEMS, { label: 'Reports', icon: 'IconChartBar' }]} />
```

| Prop | Type | Default |
|---|---|---|
| `items` | `NavItemConfig[]` | `DEFAULT_NAV_ITEMS` |
| `activePage` | `string` | — |
| `isCollapsed` | `boolean` | `false` |
| `onPageChange` | `(label: string) => void` | — |
| `onToggleCollapse` | `() => void` | — |
| `account` | `string` | `'CBC Canada'` |
| `onMenuClick` | `() => void` | — |

The group containing `activePage` auto-expands. **Width: 250 px expanded, 68 px collapsed.** Always use inside `<Page sidebar={…} />`, never standalone in a page-level UI.

---

### `<PageActionBar />` — fixed, auto-rendered by `<Page>`

**Do not render this yourself.** Every `<Page>` automatically renders a `<PageActionBar />` in its header. The bar's content is fixed:

- **Left:** `Pay` · `Convert & Pay` · `Drawdown` · `…` (More)
- **Right:** 🔔 Notifications

The labels, icons, order, count, and styling of these buttons are immutable. To wire click handlers, pass them via `<Page actionBarProps={...}>`:

```tsx
<Page
  sidebar={<Nav />}
  title={<PageTitle title="Accounts" />}
  actionBarProps={{
    onPay:            () => navigate('/pay'),
    onConvertAndPay:  () => navigate('/convert'),
    onDrawdown:       () => navigate('/drawdown'),
    onMore:           () => openMoreMenu(),
    onNotifications:  () => openNotifications(),
  }}
>
  {pageBody}
</Page>
```

All handlers are optional. Buttons without a handler still render identically — they just don't do anything on click.

Page-specific actions (Export, Filter, New transfer, etc.) belong on the `<PageTitle actions={...}>` slot — **not** as a replacement action bar.

---

### `<PageTitle />`
Page heading inside `<Page>`'s `title` slot. 32 px KMR Apparat title + optional subtitle (same size, `--text-tertiary`) + optional right-aligned actions.

```tsx
<PageTitle title="Accounts" />
<PageTitle title="All transactions" subtitle="March 2025" />
<PageTitle
  title="Hedge"
  actions={<>
    <Button variant="secondary" size="sm">Export</Button>
    <Button variant="brand" size="sm">New trade</Button>
  </>}
/>
```

| Prop | Type | Default |
|---|---|---|
| `title` | `ReactNode` | — |
| `subtitle` | `ReactNode` | — |
| `actions` | `ReactNode` | — |
| `as` | `'h1' \| 'h2' \| 'h3' \| 'h4'` | `'h1'` |

Use `as="h2"` on non-top-level pages — visual stays identical, semantic tag changes.

---

### `<DataTable />`
Generic tabular list. Pass a `columns` array (describing each header + alignment + optional cell renderer) and a `rows` array. Optional slots for filters (above the header) and footer controls (below the body).

```tsx
<DataTable
  // Optional filter row above the header — tabs on left, controls on right.
  filterTabs={
    <>
      {/* Active tab uses variant="primary"; unselected tabs use variant="ghost".
          First tab is selected by default. */}
      <Button variant="primary" size="sm">All</Button>
      <Button variant="ghost"   size="sm">Incoming</Button>
      <Button variant="ghost"   size="sm">Sent</Button>
      <Button variant="ghost"   size="sm">Settlements</Button>
    </>
  }
  filterControls={
    <>
      <Select size="sm" placeholder="Amount" />
      <Select size="sm" placeholder="Currency" />
      <Select size="sm" placeholder="Date" />
      <Button variant="ghost" size="sm">Filters</Button>
    </>
  }
  columns={[
    { key: 'beneficiary', header: 'Beneficiary', align: 'left',  width: 'md' },
    { key: 'reference',   header: 'Reference',   align: 'left',  width: '1fr' },
    { key: 'amount',      header: 'Amount',      align: 'right', width: 'sm' },
    { key: 'date',        header: 'Date',        align: 'right', width: 'sm' },
    { key: 'status',      header: 'Status',      align: 'right', width: 'sm',
      render: (row) => <StatusTag status={row.status} /> },  // optional custom cell
  ]}
  rows={transactions}
  footerControls={
    <>
      <Button variant="primary" size="sm" leadingIcon="IconChevronLeft"  aria-label="Previous" />
      <Button variant="primary" size="sm" leadingIcon="IconChevronRight" aria-label="Next" />
    </>
  }
  footerInfo="Showing 1–10 of 247"
  onRowClick={(row) => navigate(`/tx/${row.id}`)}
/>
```

| Prop | Type | Default | Notes |
|---|---|---|---|
| `columns` | `DataTableColumn<T>[]` | — | Required. Order = left-to-right column order. |
| `rows` | `T[]` | — | Required. |
| `filterTabs` | `ReactNode` | — | Optional left-side filter group above the header — typically tab-style `<Button variant="ghost" size="sm">`s for category/segment filters. |
| `filterControls` | `ReactNode` | — | Optional right-side filter group above the header — typically `<Select size="sm">` dropdowns plus a `<Button variant="ghost" size="sm">Filters</Button>` for an "all filters" popover. |
| `footerControls` | `ReactNode` | — | Left-aligned footer slot. Typically pagination `<Button>`s. |
| `footerInfo` | `ReactNode` | — | Right-aligned footer slot. Typically range text. |
| `onRowClick` | `(row, index) => void` | — | When provided, rows get hover styling + cursor pointer. |

**When to add filters:**
- Add `filterTabs` when the data has distinct segments the user might want to toggle between (Incoming / Sent / All).
- Add `filterControls` when there are column values worth filtering (amount range, currency, date) — use `<Select size="sm">` dropdowns + a final `<Button variant="ghost" size="sm">Filters</Button>` for a richer filter panel.
- Skip both for short tables that fit on screen with no segmentation.
- Each slot is independent — you can use one without the other.

**Tab-selection convention** (when using `filterTabs`):
- The **first tab is the default-selected tab** (typically `All`) and renders with `variant="primary"`.
- All other tabs render with `variant="ghost"`.
- On click, swap which tab has `variant="primary"`.
- Always include `All` (or equivalent) as the first tab.

**Per-column props (`DataTableColumn<T>`):**

| Prop | Type | Default | Notes |
|---|---|---|---|
| `key` | `keyof T & string` | — | Required. Used for React keys and default value lookup. |
| `header` | `ReactNode` | — | Required. |
| `align` | `'left' \| 'right'` | `'left'` | Right-align numeric / amount / status columns. |
| `width` | `'sm' \| 'md' \| '1fr' \| string` | `'sm'` | Named tokens: `'sm'` = 120px (short values: amounts, dates, statuses); `'md'` = 260px (longer text: names, references); `'1fr'` = grow to fill remaining space (use on at most ONE column per table). Any other CSS grid track value (e.g. `'200px'`, `'minmax(80px, 1fr)'`) is passed through. |
| `render` | `(row, index) => ReactNode` | — | Optional cell renderer. If omitted, `row[key]` is rendered as text. |

**Rules:**
- Use `width: 'sm'` (120 px) for short, atom-sized values: amounts, dates, statuses, single-letter codes. This is the default if `width` is omitted.
- Use `width: 'md'` (260 px) for longer text: names, references, descriptions.
- Use `width: '1fr'` on **one** column per table to absorb remaining space — typically the one with the longest, most variable-length content.
- Right-align amount / number / date / status columns (`align: 'right'`).
- Use the `render` callback for non-text cells — tags, icons, formatted numbers, links. Plain-string cell values truncate automatically with `…` ellipsis when they overflow.
- **Any column representing a status, category, type, priority, or other pill-shaped value MUST use `render` to wrap the value in `<Tag variant="...">`.** Status / category / type / priority columns are never rendered as plain text.

---

### `<Tag />`
Small outlined pill for status / category labels (24 px tall). Use for transaction statuses, category chips, anything that says "this row is in state X".

```tsx
<Tag variant="green">Settled</Tag>
<Tag variant="yellow">Pending</Tag>
<Tag variant="red">Failed</Tag>
<Tag variant="blue">Processing</Tag>
<Tag variant="purple">Reviewed</Tag>
<Tag variant="grey">Draft</Tag>
```

| Prop | Type | Default |
|---|---|---|
| `variant` | `'grey' \| 'yellow' \| 'red' \| 'green' \| 'blue' \| 'purple'` | `'grey'` |
| `children` | `ReactNode` | — (required) |

**Status-to-variant convention:**
- `green` — success / settled / active / approved
- `yellow` — pending / warning / in-progress
- `red` — failed / error / rejected
- `blue` — informational / processing
- `purple` — reviewed / special / on-hold
- `grey` — neutral / draft

**Common pattern: status column in `<DataTable>`** — use the column's `render` callback:

```tsx
{ key: 'status', header: 'Status', align: 'right', width: 'sm',
  render: (row) => <Tag variant={statusToVariant(row.status)}>{row.status}</Tag>
}
```

---

### `<Toast />`
Notification pill — dark background with white text, optional leading icon, optional dismiss (X) button. Always inverse-coloured for high contrast against any page background (does NOT flip in dark mode).

```tsx
<Toast>This is a toast</Toast>
<Toast icon="IconBell">New transfer received</Toast>
<Toast icon="IconCheckCircleDashed" dismissable={false}>Saved successfully</Toast>
<Toast icon="IconInfoSimple" onDismiss={() => removeToast(id)}>You have unread messages</Toast>
```

| Prop | Type | Default | Notes |
|---|---|---|---|
| `children` | `ReactNode` | — (required) | Toast message body |
| `icon` | `CentralIconName` | — | Optional leading icon |
| `dismissable` | `boolean` | `true` | Whether to show the dismiss (X) button |
| `onDismiss` | `() => void` | — | Called when the dismiss button is clicked |

`<Toast>` is presentational only — it renders one toast and does not own timing, stacking, queueing, or animations. Render it inside your own toast container (typically a `position: fixed` element pinned to a viewport corner).

---

### `<Page />`
Application shell — **the root of every page**. Sidebar + content area. The content area's header **always** contains a fixed `<PageActionBar />` on top, with an optional `<PageTitle />` below.

```tsx
<Page
  sidebar={<Nav activePage={page} onPageChange={setPage} />}
  title={<PageTitle title="…" subtitle="…" actions={…} />}
  actionBarProps={{ onPay, onConvertAndPay, onDrawdown, onMore, onNotifications }}
>
  {/* main page body content */}
</Page>
```

| Prop | Type | Notes |
|---|---|---|
| `sidebar` | `ReactNode` | Typically `<Nav>` |
| `title` | `ReactNode` | Typically `<PageTitle>` — optional |
| `actionBarProps` | `PageActionBarProps` | Optional click handlers for the (fixed) PageActionBar — the bar itself can't be replaced or customised |
| `children` | `ReactNode` | Page body content |

**There is no `actionBar` prop.** PageActionBar is rendered automatically and is fixed (Pay, Convert & Pay, Drawdown, More, Notifications).

**Layout chrome (don't try to override):**
- Sidebar pinned left at intrinsic width
- Content area: `pl: 32, pr: 24, pt: 24, pb: 0`; flex column; 32 px gap between header and body
- Header section: PageActionBar (auto) on top, PageTitle below, **72 px** between them

`<Page>` defaults to `min-height: 100vh` — it always fills at least the viewport, growing if the page body content overflows. No outer wrapper required.

---

## 5. Design tokens

Tokens are CSS custom properties exposed on `:root` after importing `@bureaulabs/web-ds/styles`. **Always reference tokens, never raw colours or pixel values.**

### Use semantic tokens, never base palette tokens

| ✅ Do | ❌ Don't |
|---|---|
| `var(--control-brand-background-rest)` | `var(--eds-brand-1000)` |
| `var(--text-primary)` | `var(--eds-grey-1000)` |
| `var(--border-primary)` | `#e5e5e5` |
| `var(--sizing-16)` | `16px` |
| `var(--radius-medium)` | `8px` |

### Token categories

| Prefix | Purpose | Example |
|---|---|---|
| `--page-*` | Page backgrounds | `--page-bg-color-default`, `--page-bg-color-subtle` |
| `--text-*` | Text colours | `--text-primary`, `--text-secondary`, `--text-tertiary` |
| `--border-*` | Border colours | `--border-primary`, `--border-subtle` |
| `--control-*` | Interactive controls | `--control-brand-background-rest`, `--control-text-dark`, `--control-error-stroke` |
| `--switch-*` | Switches | `--switch-base-on`, `--switch-knob-off` |
| `--tag-*` | Tags / badges | `--tag-blue-text`, `--tag-red-stroke` |
| `--font-family-*` | Font families | `--font-family-ui` (Inter), `--font-family-heading` (KMR Apparat) |
| `--font-weight-*` | Font weights | `--font-weight-default` (450), `--font-weight-medium` (500), `--font-weight-semibold` (600) |
| `--font-size-control-*` | Control font sizes | `--font-size-control-sm` (13), `--font-size-control-md` (14), `--font-size-control-lg` (15) |
| `--font-size-heading-*` | Heading font sizes | `--font-size-heading-h1` (32) |
| `--sizing-*` | Spacing scale (px) | `--sizing-4`, `--sizing-8`, `--sizing-12`, … `--sizing-80` |
| `--radius-*` | Border radii | `--radius-small` (4), `--radius-medium` (8), `--radius-large` (12), `--radius-infinite` |
| `--icon-size-*` | Icon dimensions | `--icon-size-16`, `--icon-size-20`, `--icon-size-28` |
| `--stroke-*` | Border widths | `--stroke-default` (1px) |
| `--eds-*` | **Base palette — DO NOT USE in app code.** Reserved for the semantic token layer. | `--eds-grey-500` |

### Sizing scale

Available sizing steps: `0, 4, 6, 8, 10, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 64, 72, 80`. Token name = the px value (`--sizing-16` = 16 px).

### Dark mode

Follows the OS preference by default. To override programmatically:
```ts
document.documentElement.setAttribute('data-theme', 'dark')   // force dark
document.documentElement.setAttribute('data-theme', 'light')  // force light
document.documentElement.removeAttribute('data-theme')        // back to OS preference
```
All semantic tokens flip automatically — components need no per-mode code.

---

## 6. CSS class naming (contributors only)

If you're authoring a new component for this package (not consuming it):

- Root class matches component name in kebab-case: `.button`, `.nav-item`, `.page-title`
- BEM modifiers with `--`: `.button--primary`, `.button--sm`, `.nav-item--selected`
- BEM elements with `__`: `.page-title__heading`, `.nav-item__label`
- No prefix (`eds-*` etc.) — keep selectors short
- One CSS file per component, colocated with the `.tsx`

---

## 7. Authoring conventions (contributors only)

### File layout
```
src/components/ComponentName/
├── ComponentName.tsx       ← Component + prop types (both exported)
├── ComponentName.css       ← Scoped BEM styles
├── ComponentName.figma.tsx ← Figma Code Connect mapping
└── index.ts                ← Re-export
```

### TypeScript
- Named exports only (no `default export`)
- Export the props type from the same file
- Extend native element props with `React.ComponentPropsWithoutRef<'button'>` (or `HTMLAttributes<…>`) when wrapping a DOM element

### Publishing
```bash
npm run build:check        # tsc --noEmit
npm run build              # tsup + bundle-styles → dist/
npm run publish:dry        # verify tarball contents
npm publish --access public --otp=<6-digit code>
npx @figma/code-connect publish   # update Figma Make mappings
```

---

*This file ships inside the npm tarball. Figma Make reads it to learn how to compose @bureaulabs/web-ds components. Keep it current as components are added or APIs change.*
