# @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 — when a user asks Figma Make to build a UI, it should compose components from this package rather than re-implementing them.

---

## 1. Package contents

| Category | What's in the package |
|---|---|
| **Tokens** | Colours (base palette + semantic), typography, spacing, radius, icon size, stroke width — all as CSS custom properties |
| **Fonts** | `KMR Apparat Medium` (headings/display) + `Inter Variable` (UI/body), bundled and `@font-face`-declared |
| **Components** | `Icon`, `Button`, `Select`, `NavItem`, `Nav`, `PageActionBar`, `PageTitle`, `Page` |
| **Code Connect** | Figma node → React component mappings published alongside each component |

---

## 2. Setup

### Install
```bash
npm install @bureaulabs/web-ds
```

### Import the stylesheet **once** at the app root
This loads all tokens (`:root` CSS variables), `@font-face` declarations, and global resets. Nothing renders correctly without it.

```tsx
import '@bureaulabs/web-ds/styles'
```

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

**Never re-implement components locally.** If a needed component isn't in the package, ask for it to be added rather than building a one-off.

---

## 3. Component reference

### `<Icon />`
Renders an icon from the central-icons library 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. e.g. `"IconHome"`, `"IconChevronBottom"`, `"IconWallet1"` |
| `size` | `16 \| 20 \| 28` | `16` | Maps to `--icon-size-*` tokens |
| `aria-label` | `string` | — | Provide for meaningful icons; omit for decorative ones (`aria-hidden` is set automatically) |

---

### `<Button />`
Primary interactive trigger. Extends `<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.) |

**Rules:**
- Icon-only buttons (`leadingIcon` without children) **must** have an `aria-label`.
- Use `variant="brand"` for the single most-important action on a page. `"action"` is the dark primary button (e.g. confirm). `"primary"` is the neutral filled button. `"secondary"` is outlined. `"ghost"` is transparent.

---

### `<Select />`
Dropdown trigger button. Extends `<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 |
|---|---|---|
| `variant` | `'primary' \| 'secondary'` | `'primary'` |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` |
| `error` | `boolean` | `false` |
| `leading` | `ReactNode` | — |
| `placeholder` | `string` | — |
| `children` | `ReactNode` | — | The displayed value text; falls back to `placeholder` when empty |

---

### `<NavItem />`
A 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), `NavItem`s, the ebury wordmark, and a 3-dot kebab.

```tsx
<Nav activePage="Accounts" onPageChange={setPage} />
<Nav activePage="Hedge" isCollapsed onToggleCollapse={() => …} />
<Nav items={customItems} activePage={page} />  // override default catalog
```

| 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` | — |

**Behaviour:** the group containing `activePage` is auto-expanded. Clicking a group toggles its expansion AND fires `onPageChange`. The default `items` mirror the full Figma nav catalog (Accounts ▸ Transactions, Payments ▸ Beneficiaries, Trades, Hedge ▸ Portfolio + Policies, Cards, Connectivity, Approval). Pass a custom `items` array to override.

**Width:** 250px expanded, 68px collapsed. Use inside `<Page sidebar={…} />`.

---

### `<PageActionBar />`
A horizontal row with `left` and `right` slots, `justify-content: space-between`, 6px gap between items in each slot. Pure layout — pass `Button`s into the slots.

```tsx
<PageActionBar
  left={<>
    <Button variant="primary" size="sm">Filter</Button>
    <Button variant="primary" size="sm">Sort</Button>
  </>}
  right={<>
    <Button variant="secondary" size="sm">Cancel</Button>
    <Button variant="brand" size="sm">Save</Button>
  </>}
/>
```

| Prop | Type |
|---|---|
| `left` | `ReactNode` |
| `right` | `ReactNode` |

Use `size="sm"` for buttons inside an action bar to match the 32px row height.

---

### `<PageTitle />`
Page-level header: 32px KMR Apparat heading + optional subtitle (same size, `--text-tertiary`) + optional actions row pinned right.

```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"` (or h3/h4) on non-top-level pages — the visual stays the same; only the semantic tag changes.

---

### `<Page />`
Application shell. Composes the full layout: sidebar + content area with a structured header (PageActionBar + PageTitle) and a body slot.

```tsx
<Page
  sidebar={<Nav activePage={page} onPageChange={setPage} />}
  actionBar={<PageActionBar left={…} right={…} />}
  title={<PageTitle title="…" subtitle="…" actions={…} />}
>
  {/* main page body content */}
</Page>
```

| Prop | Type |
|---|---|
| `sidebar` | `ReactNode` — typically `<Nav>` |
| `actionBar` | `ReactNode` — typically `<PageActionBar>` |
| `title` | `ReactNode` — typically `<PageTitle>` |
| `children` | `ReactNode` — body content below the header |

**Layout chrome:**
- Sidebar pinned left at its intrinsic width
- Content area: `pl: 32, pr: 24, pt: 24` padding; flex column; 32px gap between header and body
- Header section: PageActionBar on top, PageTitle below, **72px** between them

`Page` fills its parent's height. For a full-viewport app shell:
```tsx
<div style={{ minHeight: '100vh' }}><Page …>…</Page></div>
```

---

## 4. Composition patterns

### Standard application page
```tsx
import '@bureaulabs/web-ds/styles'
import { Page, Nav, PageActionBar, PageTitle, Button } from '@bureaulabs/web-ds'

function MyPage() {
  const [activePage, setActivePage] = useState('Accounts')
  return (
    <div style={{ minHeight: '100vh' }}>
      <Page
        sidebar={<Nav activePage={activePage} onPageChange={setActivePage} />}
        actionBar={
          <PageActionBar
            right={<>
              <Button variant="secondary" size="sm">Export</Button>
              <Button variant="brand" size="sm" leadingIcon="IconPlusMedium">New</Button>
            </>}
          />
        }
        title={<PageTitle title="Accounts" subtitle="All currencies" />}
      >
        {/* table, cards, charts, etc. */}
      </Page>
    </div>
  )
}
```

### Page without an action bar
Omit the `actionBar` prop — `Page` only renders the header wrapper if at least one of `actionBar` or `title` is provided.

### Collapsed sidebar
Drive `isCollapsed` from local state and wire `onToggleCollapse`:
```tsx
const [collapsed, setCollapsed] = useState(false)
<Nav isCollapsed={collapsed} onToggleCollapse={() => setCollapsed(c => !c)} … />
```

### Custom navigation items
Pass a custom `items` array — defaults are exported as `DEFAULT_NAV_ITEMS` so you can spread/extend:
```tsx
import { DEFAULT_NAV_ITEMS } from '@bureaulabs/web-ds'

<Nav
  items={[
    ...DEFAULT_NAV_ITEMS,
    { label: 'Reports', icon: 'IconChartBar', children: [{ label: 'Monthly' }] }
  ]}
  activePage="Reports"
/>
```

---

## 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)` | `var(--eds-grey-300)` |
| `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 values) | `--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
Sizing tokens are named by px value: `--sizing-4` = 4px, `--sizing-16` = 16px, etc. Available steps: 0, 4, 6, 8, 10, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 64, 72, 80.

### Dark mode
Dark mode follows the OS preference automatically. To override:
```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 — components require no per-mode code.

---

## 6. CSS class naming (for contributors)

If you're authoring a new component for this package:

- 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`

```css
/* ✅ Correct */
.button {
  background: var(--control-primary-background-rest);
  padding: 0 var(--sizing-12);
  border-radius: var(--radius-medium);
}

/* ❌ Wrong */
.button {
  background: #f5f5f5;
  padding: 0 12px;
}
```

---

## 7. Authoring conventions (for contributors)

### 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

### Public exports
Add the new component to `src/index.ts`:
```ts
export { Button } from './components/Button'
export type { ButtonProps } from './components/Button'
```

### 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
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.*
