<div align="center">

# Clay

**Tokens, theming, and composable React components.**

[![npm](https://img.shields.io/npm/v/@shapesos/clay?style=flat-square&color=C4A882&label=npm)](https://www.npmjs.com/package/@shapesos/clay)
[![license](https://img.shields.io/npm/l/@shapesos/clay?style=flat-square&color=C4A882)](LICENSE)
[![bundle](https://img.shields.io/bundlephobia/minzip/@shapesos/clay?style=flat-square&color=C4A882&label=gzip)](https://bundlephobia.com/package/@shapesos/clay)

</div>

---

## Quick Start

```bash
npm install @shapesos/clay
```

```tsx
import { colors, typographyTypes } from "@shapesos/clay/tokens";
import { Chat } from "@shapesos/clay/chat";
import { Lottie } from "@shapesos/clay/lottie";
```

## Entry Points

Clay is tree-shakeable with multiple entry points — import only what you need.

| Import                    | Contents                              | Peer Dependencies        |
| ------------------------- | ------------------------------------- | ------------------------ |
| `@shapesos/clay/tokens`   | Colors, typography, font families     | None (pure JS)           |
| `@shapesos/clay/chat`     | Chat compound components + types      | React, styled-components |
| `@shapesos/clay/blocks`   | Typed content blocks (Block, BlockServices, BlockContext) | React, styled-components |
| `@shapesos/clay/artifacts`| Per-type artifact renderers + ArtifactServices map (TABLE, CHART) | React, styled-components, recharts (optional, only for CHART) |
| `@shapesos/clay/chart`    | Standalone chart library (`<BarChart>`, `<LineChart>`, `<PieChart>` + composable primitives) | React, recharts |
| `@shapesos/clay/icon`     | Icon, IconButton components + types   | React, styled-components |
| `@shapesos/clay/lottie`   | Lottie animation component + types    | React, styled-components |
| `@shapesos/clay`          | Everything (convenience re-export)    | React, styled-components |

---

## Design Tokens

Pure JavaScript color and typography tokens — no React required.

```typescript
import { colors, typographyStyles, typographyTypes, typographyMixin } from "@shapesos/clay/tokens";

// Color palette
colors["brown-100"];  // "#171716"
colors["brown-900"];  // "#F7F5F3"

// Typography definitions
typographyStyles[typographyTypes.GEIST_BODY_S_REGULAR];
// => { fontFamily: "Geist", fontSize: 16, fontWeight: 400, lineHeight: 24, letterSpacing: -0.08 }

// CSS mixin for styled-components
typographyMixin(typographyTypes.GEIST_BODY_XS_MEDIUM);
// => CSS string ready to use in template literals
```

---

## Chat

Composable compound components for building chat interfaces. `Chat.Root` provides context — children consume it, giving you full control over layout.

```tsx
import { Chat } from "@shapesos/clay/chat";
import type { ChatMessage } from "@shapesos/clay/chat";

function MyChat() {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  return (
    <Chat.Root
      messages={messages}
      onSendMessage={handleSend}
      isLoading={isLoading}
      onStop={handleStop}
      onCopyMessage={handleCopy}
      onThumbUpClick={handleThumbUp}
      onThumbDownClick={handleThumbDown}
    >
      <Chat.MessageList />
      <Chat.Composer placeholder="Ask anything..." />
    </Chat.Root>
  );
}
```

<details>
<summary><strong>Props Reference</strong></summary>

#### Chat.Root

| Prop               | Type                                                       | Required | Description                        |
| ------------------ | ---------------------------------------------------------- | -------- | ---------------------------------- |
| `messages`         | `ChatMessage[]`                                            | Yes      | Array of messages to display       |
| `onSendMessage`    | `(content: string) => void`                                | Yes      | Called when the user sends          |
| `isLoading`        | `boolean`                                                  | No       | Show stop button while loading     |
| `onStop`           | `() => void`                                               | No       | Called when the user clicks stop   |
| `onCopyMessage`    | `(messageId: string) => void`                              | No       | Called when a message is copied    |
| `onThumbUpClick`   | `(messageId: string, isHelpful: boolean \| null) => void`  | No       | Thumbs up feedback                 |
| `onThumbDownClick` | `(messageId: string, isHelpful: boolean \| null) => void`  | No       | Thumbs down feedback               |

#### Chat.Composer

| Prop          | Type     | Required | Default                | Description       |
| ------------- | -------- | -------- | ---------------------- | ----------------- |
| `placeholder` | `string` | No       | `"Type a message..."`  | Input placeholder |

#### Chat.MessageList

No props — reads messages from context.

#### Data Model

```typescript
type MessageRole = "user" | "assistant";

interface ChatMessage {
  id: string;
  role: MessageRole;
  blocks: BlockData[];        // ordered, type-discriminated content blocks (defined by @shapesos/clay/blocks)
  fallbackText: string;       // server-supplied plain-text version of the blocks; used for clipboard/accessibility
  isHelpful?: boolean | null;
}
```

`BlockData`, `BlockType`, and `TextBlockData` live in [`@shapesos/clay/blocks`](#blocks). Chat doesn't own block rendering — it composes the `Block` component from the blocks package and bridges chat-specific concerns (like passing the message's `id` into `TableActions` callbacks). This means the same block infrastructure renders chat messages, vibe-view embeds, marketing-site responses, or any other surface that wants typed content blocks — without coupling to chat.

</details>

---

## Blocks

Typed content-block infrastructure: a `Block` dispatcher, a `BlockServices` registry of concrete renderers (currently `TextBlockService`), and a `BlockContext` for renderer-level concerns like table actions. Chat consumes this internally — non-chat surfaces import directly.

```tsx
import { Block, BlockContext, type BlockData } from "@shapesos/clay/blocks";

const blocks: BlockData[] = [{ type: "TEXT", payload: { text: "**hello world**" } }];

function MyContent() {
  return (
    <BlockContext.Provider value={{ TableActions: ({ tableIndex }) => <ExportButton index={tableIndex} /> }}>
      {blocks.map((block, i) => <Block key={i} block={block} />)}
    </BlockContext.Provider>
  );
}
```

The wire shape (`{ type, payload }`) matches `dreamteam-io-server` / `shapes-agent` 1:1, so blocks fetched from the API can be passed straight to `Block` with no translation. Unknown block types render as no-ops (forward-compat). Adding a new block type is a registration in `BlockServices` plus a concrete component — no changes to the dispatcher.

<details>
<summary><strong>Public API</strong></summary>

| Export                | Kind      | Description                                                                                  |
| --------------------- | --------- | -------------------------------------------------------------------------------------------- |
| `Block`               | Component | Single-block dispatcher. Looks up the concrete renderer in `BlockServices` by `block.type`.  |
| `BlockContext`        | Context   | Renderer-level concerns (today: `TableActions`). Optional — `Block` works without it.        |
| `useBlockContext`     | Hook      | Reads the ambient `BlockContextValue`. Returns `{}` outside a provider.                      |
| `BlockServices`       | Registry  | `{ TEXT: TextBlockService }` today; additive over time.                                      |
| `TextBlockService`    | Service   | Concrete renderer entry for `TEXT` blocks.                                                   |
| `BlockData`           | Type      | Discriminated union of all block shapes.                                                     |
| `TextBlockData`       | Type      | `{ type: "TEXT"; payload: { text: string } }`.                                               |
| `BlockType`           | Type      | `"TEXT"` (literal union; widens as new types are added).                                     |
| `BlockComponentProps` | Type      | Props every concrete block component receives — `{ block }`.                                 |
| `BlockService`        | Type      | Service registry entry contract — `{ type, Component }`.                                     |
| `BlockContextValue`   | Type      | `{ TableActions?: ComponentType<{ tableIndex: number }> }`.                                  |

</details>

---

## Artifacts

Per-type renderers for the artifact union (`TABLE`, `CHART`) plus the `ArtifactServices` registry. The block layer's `ARTIFACT_REF` block consumes this map to dispatch the inlined artifact to its concrete component; non-block surfaces can render an artifact directly.

```tsx
import { TableArtifact, ChartArtifact, ArtifactServices } from "@shapesos/clay/artifacts";
import type {
  ArtifactCallbacksMap,
  ArtifactLabelsMap,
  ChartArtifactRecord,
  TableArtifactRecord,
} from "@shapesos/clay/artifacts";

// Direct render — picks the renderer at the call site.
<ChartArtifact
  artifact={chartRecord}
  labels={{ CHART: { download: "Download CSV", /* … */ } }}
  callbacks={{ CHART: { onDownload: (artifact) => analytics.track("chart_downloaded", artifact) } }}
/>

// Registry-driven dispatch — block layer does this.
const { Component } = ArtifactServices[artifact.type];
<Component artifact={artifact} labels={labels} callbacks={callbacks} />
```

Every artifact reads from a CSV at `artifact.protectedAsset.presignedUrl`. Loading / error / unavailable / empty states surface as inline status messages with consumer-translated copy from the `labels` prop (`Partial<ArtifactLabelsMap>`). Action callbacks (today: `onDownload`) wire through the `callbacks` prop (`Partial<ArtifactCallbacksMap>`) — each callback receives the artifact record so you can derive analytics context. Both props get passed through the block layer via `block.payload.labels` and `block.payload.callbacks`.

The `CHART` artifact requires `recharts` as a peer dep; bring your own if you import this entry. Adding a new artifact type is one `ArtifactService` registration plus a concrete component — no changes to the block dispatcher.

---

## Chart

Standalone chart library — usable beyond the artifact path. Three pre-composed high-level components with smart defaults, plus composable primitives for custom recharts trees.

```tsx
import { BarChart, LineChart, PieChart } from "@shapesos/clay/chart";

<BarChart
  data={[{ month: "Jan", us: 120, uk: 60 }, { month: "Feb", us: 132, uk: 64 }]}
  xKey="month"
  series={[{ key: "us", label: "US" }, { key: "uk", label: "UK" }]}
  stacked
/>

<PieChart
  data={departments}
  categoryKey="department"
  valueKey="headcount"
  // Localise the "Others" bucket detection in non-English UIs:
  othersCategoryLabels={["אחרים"]}
/>
```

Smart defaults: legend auto-hides for single-series charts; legend at top; x-axis labels truncate by category count; pie's "Others" bucket pins to the end of the sweep in neutral grey. Override any default with the named prop. Bring `recharts` as a peer dep.

---

## Icon

Render SVG icons with consistent sizing, and icon buttons with selection states.

```tsx
import { Icon, IconButton } from "@shapesos/clay/icon";
import { IconSearch, IconCopy } from "@tabler/icons-react";

<Icon icon={IconSearch} size={20} color="#333" aria-label="Search" />

<IconButton icon={IconCopy} size="small" onClick={handleCopy} aria-label="Copy" />
```

<details>
<summary><strong>Props Reference</strong></summary>

#### Icon

| Prop         | Type                                     | Required | Default | Description              |
| ------------ | ---------------------------------------- | -------- | ------- | ------------------------ |
| `icon`       | `ComponentType<SVGProps<SVGSVGElement>>` | Yes      |         | Icon component to render |
| `size`       | `number`                                 | No       | `16`    | Size in pixels           |
| `color`      | `string`                                 | No       |         | Icon color               |
| `className`  | `string`                                 | No       |         | CSS class                |
| `aria-label` | `string`                                 | No       |         | Accessible label         |

#### IconButton

| Prop         | Type                                     | Required | Default   | Description              |
| ------------ | ---------------------------------------- | -------- | --------- | ------------------------ |
| `icon`       | `ComponentType<SVGProps<SVGSVGElement>>` | Yes      |           | Icon component to render |
| `size`       | `"small" \| "medium"`                    | No       | `"small"` | Button size              |
| `isSelected` | `boolean`                                | No       | `false`   | Selected state           |
| `disabled`   | `boolean`                                | No       | `false`   | Disabled state           |
| `onClick`    | `() => void`                             | No       |           | Click handler            |
| `className`  | `string`                                 | No       |           | CSS class                |
| `aria-label` | `string`                                 | No       |           | Accessible label         |

</details>

---

## Lottie

Render [Lottie](https://airbnb.io/lottie/) animations with declarative props, hover interactions, and imperative playback control.

```tsx
import { Lottie } from "@shapesos/clay/lottie";
import animationData from "./my-animation.json";

// Basic — loops forever
<Lottie animationData={animationData} />

// Interactive — plays once, then replays on hover
<Lottie
  animationData={animationData}
  loop={false}
  playOnHover
  loopOnHover
  width={80}
  height={80}
/>
```

**Imperative control** via ref:

```tsx
import type { LottieRef } from "@shapesos/clay/lottie";

const ref = useRef<LottieRef>(null);

<Lottie ref={ref} animationData={animationData} autoplay={false} />
<button onClick={() => ref.current?.play()}>Play</button>
```

<details>
<summary><strong>Props Reference</strong></summary>

#### Lottie

| Prop                | Type                                                             | Required | Default | Description                    |
| ------------------- | ---------------------------------------------------------------- | -------- | ------- | ------------------------------ |
| `animationData`     | `LottieAnimationData`                                            | Yes      |         | Lottie JSON data               |
| `autoplay`          | `boolean`                                                        | No       | `true`  | Play on mount                  |
| `loop`              | `boolean \| number`                                              | No       | `true`  | Loop forever, or N times       |
| `speed`             | `number`                                                         | No       | `1`     | Playback speed                 |
| `direction`         | `1 \| -1`                                                        | No       | `1`     | Forward or reverse             |
| `width`             | `string \| number`                                               | No       |         | Container width                |
| `height`            | `string \| number`                                               | No       |         | Container height               |
| `playOnHover`       | `boolean`                                                        | No       | `false` | Replay on mouse enter          |
| `loopOnHover`       | `boolean`                                                        | No       | `false` | Loop while hovering            |
| `className`         | `string`                                                         | No       |         | CSS class                      |
| `aria-label`        | `string`                                                         | No       |         | Accessible label               |
| `onAnimationLoaded` | `(animation: AnimationItem) => void`                             | No       |         | Fired when animation loads     |
| `onComplete`        | `() => void`                                                     | No       |         | Fired on completion            |
| `onLoopComplete`    | `() => void`                                                     | No       |         | Fired on each loop             |
| `onEnterFrame`      | `(frame: { currentTime: number; totalTime: number }) => void`    | No       |         | Fired on each frame            |

#### LottieRef (imperative handle)

| Method                             | Description                        |
| ---------------------------------- | ---------------------------------- |
| `play()`                           | Start playback                     |
| `pause()`                          | Pause playback                     |
| `stop()`                           | Stop and reset to frame 0          |
| `goToAndPlay(value, isFrame?)`     | Jump to value and play             |
| `goToAndStop(value, isFrame?)`     | Jump to value and stop             |
| `setDirection(direction)`          | Set direction (1 or -1)            |
| `setSpeed(speed)`                  | Set playback speed                 |
| `getAnimationItem()`              | Access underlying lottie-web instance |

</details>

---

## TypeScript

All components export their prop types:

```typescript
import type { ColorToken, TypographyStyle, TypographyType } from "@shapesos/clay/tokens";
import type { ChatMessage, MessageRole, ChatContextValue } from "@shapesos/clay/chat";
import type { IconProps, IconButtonProps, IconButtonSize } from "@shapesos/clay/icon";
import type { LottieProps, LottieRef, LottieAnimationData } from "@shapesos/clay/lottie";
```

---

## Development

```bash
git clone git@github.com:dreamteamapp/shapes-clay.git
cd shapes-clay
bun install
```

| Command                 | Description                                 |
| ----------------------- | ------------------------------------------- |
| `bun run storybook`     | Component playground (localhost:6006)        |
| `bun run build`         | Build package (tsup)                        |
| `bun run dev`           | Build in watch mode                         |
| `bun run test`          | Unit tests (Vitest)                         |
| `bun run test:coverage` | Unit tests with coverage                    |
| `bun run test:e2e`      | E2E tests (Playwright + Storybook)          |
| `bun run check`         | All quality gates (lint + typecheck + test)  |

### Releasing

Clay uses [changesets](https://github.com/changesets/changesets) for versioning. When making changes:

```bash
bunx changeset          # Describe your change and its semver impact
git add .changeset/     # Commit the changeset with your PR
```

On merge to `master`, a "Version Packages" PR is created automatically. Merging that PR publishes to npm.

---

## License

[MIT](LICENSE)
