@shapesos/clay
v0.13.1
Published
Shapes.co design system — tokens and theming
Readme
Clay
Tokens, theming, and composable React components.
Quick Start
npm install @shapesos/clayimport { 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.
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 literalsChat
Composable compound components for building chat interfaces. Chat.Root provides context — children consume it, giving you full control over layout.
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>
);
}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
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. 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.
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.
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.
| 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 }> }. |
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.
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.
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.
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" />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 |
Lottie
Render Lottie animations with declarative props, hover interactions, and imperative playback control.
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:
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>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 |
TypeScript
All components export their prop types:
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
git clone [email protected]: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 for versioning. When making changes:
bunx changeset # Describe your change and its semver impact
git add .changeset/ # Commit the changeset with your PROn merge to master, a "Version Packages" PR is created automatically. Merging that PR publishes to npm.
