# @ossy/design-system

React component library for the **Ossy ecosystem** — themable UI primitives, layout, forms, and shell extension hooks. Built for React 19+ with server-side rendering and CSS-variable theming from `@ossy/themes`.

**This package is not** a home for product-specific or marketing widgets. Domain UI lives in feature packages (for example `@ossy/profile`, `@ossy/resumes`, `@ossy/booking`).

## What’s in the box

| Area | Examples |
|------|----------|
| **Primitives** | `Button`, `Text`, `Input`, `InputTitle`, `Card`, `Alert`, `Badge`, `Tabs`, … |
| **Layout** | `View`, `View.Item`, `Page`, `PageSection` |
| **Forms** | `Form`, `FieldFactory`, `Fields`, `applyFieldChange`, `FormStatus`, `useFormContext`, field slot keys |
| **Theme** | `Theme`, `useTheme` (requires `@ossy/themes`) |
| **Shell extension** | `ComponentSlotsProvider`, `Slot`, `useSlot` |
| **Icons** | `Icon` — kebab-case names, lazy [css.gg](https://css.gg/) chunks |

See [docs/SLOTS.md](./docs/SLOTS.md) for the three different “slot” concepts and [AUDIT.md](./AUDIT.md) for refactor history and open work.

## Four-layer mental model

Use this stack when deciding which API to reach for:

1. **Tokens & theme** — Install `@ossy/themes`, wrap the app in `Theme`, switch palettes with `useTheme`. Components read `var(--*)` tokens only at the global level.
2. **Layout primitives** — `View` (and optionally `Page` / `PageSection`) for structure. **Layout slots** are the `slot` **attribute** on children (`header`, `content`, `sidebar-primary`, …).
3. **Shell & form extension** — **Component slots**: app `export const slots` in `*.layout.jsx` + `<Slot />`; schema UI via ADR 0006 `metadata.id` on `*.component.jsx`. **Form fields:** `@ossy/design-system/input/{type}`.
4. **Components** — Buttons, inputs, feedback, display, navigation helpers.

Rule of thumb: *layout regions ≠ injectable app components ≠ form field overrides.*

### Typography (`Text`)

Single component for body and headings. Set **`as`** for semantics (`h1`, `label`, `p`, …), **`variant`** for theme tokens under `theme.text`, and optional **`text`** / **`params`** for locale keys. **`InputTitle`** is a separate borderless input styled like a heading — use it as a form-field slot override for resource name editing, not as display typography.

## Installation

```bash
npm install @ossy/design-system @ossy/themes
```

**Peer dependencies**

- `react` >= 19.0.0
- `react-dom` >= 19.0.0
- `@ossy/themes` — theme objects passed to `Theme`
- `markdown-to-jsx`, `react-syntax-highlighter` — only if you use `MarkdownViewer`

## Quick start

```jsx
import { Text, Theme } from '@ossy/design-system'
import { CloudLight } from '@ossy/themes'

export default function App() {
  return (
    <Theme theme={CloudLight}>
      <YourApp />
    </Theme>
  )
}
```

Multiple themes with runtime switching:

```jsx
import { Text, Theme } from '@ossy/design-system'
import { CloudLight, CloudDark } from '@ossy/themes'

<Theme
  theme={CloudLight}
  themes={{ light: CloudLight, dark: CloudDark }}
  defaultTheme="light"
>
  <YourApp />
</Theme>
```

## Storybook

Component docs and MDX guides live in the **ossy monorepo** Storybook. **Feature package UI** is previewed in **`@ossy/app-test`**, not Storybook.

From the monorepo root (`business/platform/ossy`):

```bash
npm install   # if needed
npm start     # storybook dev -p 6006
```

Open in the browser:

- **Design System → Getting Started** — `packages/design-system/docs/intro.mdx`
- **Design System → Layout Guide** and **Slots** — layout and injection docs (`layout-guide.mdx`, `slots.mdx`)
- **Design System → Principles**, **Theme Concepts**, **Patterns**, **Variants** — other `packages/design-system/docs/*.mdx`
- **Design System → Base / …** — `*.stories.jsx` next to components

Static build: `npm run build:storybook`.

Storybook resolves `@ossy/design-system` from **source** (`src/index.js`), so you do not need `lerna run build` before `npm start`.

## Layout guide

### When to use what

| Need | Use |
|------|-----|
| Flex/grid, gaps, surfaces, app chrome grid | `View` with a `layout` preset and **`data-region`** children |
| Standard in-app page body (scroll, header, slot spacing) | `Page` — optional `maxWidth`, built-in header via `page.*` slots |
| Centered content band with max-width only | `PageSection` (marketing heroes, standalone sections) |
| Bordered vertical/horizontal list of panels | `View` with `stack`, `horizontal`, `bordered`, and `View.Item` |
| Injectable header/sidebar/toolbar from packages | `<Slot view="…" region="…" />` + `*.component.jsx` — see [docs/SLOTS.md](./docs/SLOTS.md) |

### `View` layout presets

`View` is the primary layout primitive. Set `layout` to a preset (`page`, `sidebar`, `off-center`, `column`, `row`, …) and assign children with `data-region="header"`, `data-region="content"`, etc. Storybook **Design System → Base → View** shows each preset.

**`layout="header"`** — title row (`leading` + optional `far`), optional `description` and `children` rows below. The default grid is a single row (`"leading far"`); `:has([data-header-slot="…"])` expands to two or three rows when description and/or children slots are present, so row `gap` is not applied between empty tracks. Used by `ContentHeader` and `AppHeader` via `HeaderLayout`.

### `Page` and `PageSection`

**`Page`** is the standard in-app page body inside the shell content region. It renders a single root `View` (default `layout="page"`) with registered slots for each layout region. Built-in `title` / `description` / `near` / `far` props feed the `page.header` slot fallback (`ContentHeader`); override the region via `ComponentSlotsProvider` or the `header` prop. Unslotted `children` land in `page.content`. Children with `region` or `data-region` props become fallbacks for the matching page slot.

Props:

| Prop | Purpose |
|------|---------|
| `title`, `description`, `near`, `far`, … | Passed to `page.header` slot (fallback: built-in `ContentHeader`) |
| `header` | Custom header node for `page.header` fallback; when set, built-in header props are ignored |
| `maxWidth` | Wraps content in `PageSection` for a centered max-width band (`s`, `m`, `l`, `xl`, …) |
| `inset` | Default `s` (minimal edge padding on content); `none` for full-bleed; or any View inset token |
| `scroll` | `false` (default): page grows with content; the **shell** scrolls (`@ossy/app/layout/default`). `true`: page fills a height-locked shell `<main>` (e.g. `@ossy/app/layout/workspace`) and `page.content` scrolls (or nested panels via `contentStyle`). |
| `contentStyle` | Extra styles on `page.content` (e.g. `{ overflow: 'hidden' }` so child panels own scrolling, as on Storage) |
| `layout` | `page` (default) or `column` — `page` uses `View layout="page"` grid regions |
| `surface`, `gap` | Passed through to the root `View` (`gap` applies to content column) |

Page layout region slot keys:

| Key | Region |
|-----|--------|
| `page.header` | Top title row |
| `page.content` | Main body (scrolls when `scroll` is `true`) |
| `page.sidebar-primary` | Left rail |
| `page.sidebar-secondary` | Right rail |
| `page.footer` | Bottom bar |

```jsx
import { Page, Button, ComponentSlotsProvider } from '@ossy/design-system'

<Page
  title="tasks.home.title"
  description="tasks.home.description"
  near={<Tags tags={['Beta']} size="s" />}
  far={<Button variant="cta" label="design-system.add" />}
>
  {children}
</Page>

// Override page.header for a route or feature:
<ComponentSlotsProvider slots={{ 'page.header': MyPageHeader }}>
  <Page title="…">…</Page>
</ComponentSlotsProvider>
```

For a fully custom page header, pass `header={<ContentHeader … />}` or any node as the `page.header` fallback. Use `ContentHeader` as a body child for section intros within the page.

### `ContentHeader` and `AppHeader`

Both use `HeaderLayout` (`View layout="header"`) and share most props:

| Prop | Purpose |
|------|---------|
| `title` | i18n key (`string`) or custom title node (`ReactNode`) |
| `titleParams`, `titleAs`, `titleVariant` | Used when `title` is a string |
| `description`, `descriptionParams`, … | Lead paragraph below the title row |
| `lead` | Custom lead node (overrides `description`) |
| `near` | Slot beside the title (tags, status badges, etc.) |
| `far` | Slot on the right (CTA buttons, toolbar actions) |

- **`ContentHeader`** — page title row inside `Page` (`data-component="content-header"`, default `inset="list-row"`). `Page` uses this as the built-in `page.header` fallback.
- **`AppHeader`** — app shell top bar (`data-component="app-header"`, default `inset="m"`). Register on `app:header`; typically only the first header row (logo + actions).

```jsx
import { ContentHeader, AppHeader, Tags, Button } from '@ossy/design-system'

<ContentHeader
  title="tasks.home.title"
  near={<Tags tags={['Beta']} size="s" />}
  far={<Button variant="cta" label="design-system.add" />}
/>

<AppHeader
  title={<Text variant="heading-tertiary">Workspace</Text>}
  far={<Button variant="link" prefix="profile" label="Profile" />}
/>
```

**`PageSection`** is a max-width wrapper around arbitrary content — use for marketing heroes (`HeroCover`), sales blocks, or when you need a centered band without the full page chrome. For normal app pages, prefer `Page` with `maxWidth` instead of hand-rolling `View` + padding.

Shell canvas (full viewport behind `<main>`) uses `View surface="base"`, not `Page`.

### Stack → `View` (migration)

`Stack` and `Stack.Item` were removed. Use `View` with stack mode:

**Before**

```jsx
import { Text, Stack } from '@ossy/design-system'

<Stack horizontal bordered>
  <Stack.Item fill>…</Stack.Item>
  <Stack.Item>…</Stack.Item>
</Stack>
```

**After**

```jsx
import { Text, View } from '@ossy/design-system'

<View stack horizontal bordered>
  <View.Item fill>…</View.Item>
  <View.Item>…</View.Item>
</View>
```

- `stack` — enables stack item styling and borders
- `horizontal` — row direction (default column)
- `bordered` — separators between items
- `View.Item` — same as former `Stack.Item` (`fill` prop supported)

## Icons (migration)

Legacy **Material** `Icon` and **Icon2** are consolidated into a single **`Icon`** export.

| Before | After |
|--------|--------|
| `Icon2` with `name="arrow-right"` | `Icon` with `name="arrow-right"` |
| Legacy `Icon` with PascalCase / Material names | `Icon` with **kebab-case** css.gg name (see Storybook **Design System → Base → Icon**) |
| `icon=` prop on some internals | Prefer `name=` (`icon` still accepted as alias) |

Icons load **lazily** per name. Unknown names log a warning and render empty.

```jsx
import { Text, Icon } from '@ossy/design-system'

<Icon name="check" size="m" variant="primary" />
```

## Domain widgets (moved out)

These are **no longer** exported from `@ossy/design-system`:

| Former export | New home |
|---------------|----------|
| `Profile` | `@ossy/design-system` |
| `ResumeExperience`, `ResumeView` | `@ossy/resumes` |
| `sections/*` (Hero, marketing blocks, …) | Consumer feature packages or local app code — `sections/` removed from design-system |

Install the feature package that owns the domain, or copy patterns using `View` + primitives.

## Platform integration

- **Pages** — `*.page.jsx` primitives; use design-system for UI inside pages. [PRIMITIVES.md](../platform/PRIMITIVES.md)
- **App chrome** — app `export const slots` in `*.layout.jsx` maps `app:*` to component ids; platform sets `app:content` to the current page. [docs/SLOTS.md](./docs/SLOTS.md), [SHELL-SPEC.md](../../SHELL-SPEC.md)
- **Resource / schema UI** — `*.component.jsx` with `metadata.id` = `@ossy/…/view/…`, `@ossy/…/form/…`, or `@ossy/design-system/input/…`

## Deprecations and removed exports

### Removed (breaking)

- `Stack`, `Stack.Item` — use `View` + `stack` / `View.Item`
- Legacy Material `Icon` — use `Icon` with kebab-case `name`
- `Icon2` — renamed to `Icon` (same API)
- `ResumeExperience`, `ResumeView` — use `@ossy/resumes`
- `sections/*` exports
- `TemplateForm` — use `<Form><FieldFactory /></Form>`
- `Title` — use `Text` with `as` + heading variants (`heading-primary`, `heading-secondary`, …)
- `EditFields`, `ResourceTemplateFields` — use `Fields`
- `applyResourceTemplateFieldChange` — use `applyFieldChange`
- `ComponentsProvider`, `useComponentSlot` — use `ComponentSlotsProvider`, `useSlot`
- `resourceSlot`, `RESOURCE_SLOT_PREFIX`, `normalizeResourceTypeKey` — use `viewComponentId` / `formComponentId` from `@ossy/schema`

## Further reading

- [docs/SLOTS.md](./docs/SLOTS.md) — layout vs component vs form field slots
- [AUDIT.md](./AUDIT.md) — streamline audit and task status
- [packages/platform/PRIMITIVES.md](../platform/PRIMITIVES.md) — platform file primitives

## License

MIT
