Skip to content

Commit 943e580

Browse files
authored
Merge pull request #125 from PMDevSolutions/feat/widget-storybook
feat(widget): expand Storybook coverage, theme switcher, and Pages deploy
2 parents ebf543b + 7e5de1b commit 943e580

18 files changed

Lines changed: 674 additions & 110 deletions

.github/workflows/ci.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,6 @@ jobs:
4848
- name: Build
4949
run: pnpm build
5050

51-
- name: Build Storybook
52-
run: pnpm build-storybook
53-
5451
worker:
5552
name: Validate Worker
5653
runs-on: ubuntu-latest

.github/workflows/storybook.yml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: Storybook
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- "widget/**"
8+
- ".github/workflows/storybook.yml"
9+
pull_request:
10+
branches: [main]
11+
paths:
12+
- "widget/**"
13+
- ".github/workflows/storybook.yml"
14+
workflow_dispatch:
15+
16+
concurrency:
17+
group: ${{ github.workflow }}-${{ github.ref }}
18+
cancel-in-progress: true
19+
20+
jobs:
21+
build:
22+
name: Build Storybook
23+
runs-on: ubuntu-latest
24+
defaults:
25+
run:
26+
working-directory: widget
27+
steps:
28+
- uses: actions/checkout@v4
29+
30+
- uses: pnpm/action-setup@v4
31+
with:
32+
version: 9
33+
34+
- uses: actions/setup-node@v4
35+
with:
36+
node-version: 20
37+
cache: pnpm
38+
cache-dependency-path: widget/pnpm-lock.yaml
39+
40+
- name: Install dependencies
41+
run: pnpm install --frozen-lockfile
42+
43+
- name: Build Storybook
44+
run: pnpm build-storybook
45+
46+
- uses: actions/upload-artifact@v4
47+
with:
48+
name: storybook-static
49+
path: widget/storybook-static
50+
51+
deploy:
52+
name: Deploy to Cloudflare Pages
53+
# Deploys need the CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID repo
54+
# secrets (already used by the Docs workflow), and a one-time
55+
# `wrangler pages project create claudius-storybook`.
56+
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
57+
needs: build
58+
runs-on: ubuntu-latest
59+
permissions:
60+
contents: read
61+
deployments: write
62+
steps:
63+
- uses: actions/checkout@v4
64+
65+
# wrangler-action installs wrangler with the package manager it detects
66+
# from widget/pnpm-lock.yaml, so pnpm must be on PATH in this job too.
67+
- uses: pnpm/action-setup@v4
68+
with:
69+
version: 9
70+
71+
- uses: actions/download-artifact@v4
72+
with:
73+
name: storybook-static
74+
path: widget/storybook-static
75+
76+
- name: Deploy
77+
uses: cloudflare/wrangler-action@v3
78+
with:
79+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
80+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
81+
workingDirectory: widget
82+
command: pages deploy storybook-static --project-name=claudius-storybook

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
[![npm](https://img.shields.io/npm/v/claudius-chat-widget.svg)](https://www.npmjs.com/package/claudius-chat-widget)
44
[![create-claudius](https://img.shields.io/npm/v/create-claudius.svg?label=create-claudius)](https://www.npmjs.com/package/create-claudius)
5+
[![Storybook](https://img.shields.io/badge/Storybook-live-ff4785?logo=storybook&logoColor=white)](https://claudius-storybook.pages.dev)
56
[![license](https://img.shields.io/npm/l/claudius-chat-widget.svg)](LICENSE)
67

78
An open-source, embeddable AI chat widget powered by Claude. Drop it into any
@@ -110,6 +111,10 @@ cd widget && pnpm install && pnpm dev # http://localhost:5173
110111
Tests: `pnpm test` in `widget/` and `worker/`. Details in the
111112
[local development guide](https://claudius-docs.pages.dev/getting-started/local-development/).
112113

114+
Browse the components in Storybook — hosted at
115+
[claudius-storybook.pages.dev](https://claudius-storybook.pages.dev), or run it
116+
locally with `cd widget && pnpm storybook`.
117+
113118
## Tech stack
114119

115120
React 18 + TypeScript + Tailwind (widget) · Cloudflare Workers + Hono +

docs/astro.config.mjs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,15 @@ export default defineConfig({
5252
},
5353
{
5454
label: "Plugins & Tools",
55-
items: [{ autogenerate: { directory: "plugins" } }],
55+
items: [
56+
{ autogenerate: { directory: "plugins" } },
57+
{
58+
label: "Component Storybook",
59+
link: "https://claudius-storybook.pages.dev",
60+
attrs: { target: "_blank", rel: "noopener noreferrer" },
61+
badge: { text: "live", variant: "success" },
62+
},
63+
],
5664
},
5765
{ label: "RAG", items: [{ autogenerate: { directory: "rag" } }] },
5866
{

docs/src/content/docs/getting-started/local-development.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,24 @@ pnpm test # API, validation, rate-limit, analytics tests
4848
The E2E suite mocks `**/api/chat`, so the worker doesn't need to run during
4949
Playwright tests.
5050

51+
## Component Storybook
52+
53+
Every widget component has stories covering its meaningful states — themes
54+
(light / dark / auto), error and empty states, the mobile bottom sheet, RTL
55+
layout, and the proactive greeting. It's a fast feedback loop for UI work and
56+
doubles as living documentation.
57+
58+
```bash
59+
cd widget
60+
pnpm storybook # http://localhost:6006
61+
pnpm build-storybook # static build -> widget/storybook-static
62+
```
63+
64+
The latest `main` is published at
65+
[claudius-storybook.pages.dev](https://claudius-storybook.pages.dev). Use the
66+
toolbar **Theme**, **Locale**, and **Direction** controls to preview each
67+
component across color schemes, languages, and writing direction.
68+
5169
## Repository layout
5270

5371
| Path | What it is |

widget/.storybook/preview.ts

Lines changed: 0 additions & 34 deletions
This file was deleted.

widget/.storybook/preview.tsx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import type { Preview, Decorator } from "@storybook/react-vite";
2+
import type { LocaleCode } from "../src/locales";
3+
import "../src/styles.css";
4+
5+
const LOCALE_ITEMS: { value: LocaleCode; title: string }[] = [
6+
{ value: "en", title: "English" },
7+
{ value: "es", title: "Español" },
8+
{ value: "fr", title: "Français" },
9+
{ value: "de", title: "Deutsch" },
10+
];
11+
12+
type ThemeMode = "light" | "dark" | "auto";
13+
14+
/**
15+
* Mirrors ChatWidget's own logic: dark mode is driven by a `data-claudius-dark`
16+
* ancestor (see styles.css), and "auto" follows the OS `prefers-color-scheme`.
17+
*/
18+
function resolveDark(theme: ThemeMode): boolean {
19+
if (theme === "dark") return true;
20+
if (theme === "light") return false;
21+
return (
22+
typeof window !== "undefined" &&
23+
typeof window.matchMedia === "function" &&
24+
window.matchMedia("(prefers-color-scheme: dark)").matches
25+
);
26+
}
27+
28+
/**
29+
* Wraps every story in the widget's theming context:
30+
* - `.claudius-root` + `data-claudius-dark` so `--cl-*` tokens resolve exactly
31+
* as they do in production (light is the default; dark flips the tokens).
32+
* - `dir` for RTL coverage.
33+
* - an optional widget-shaped frame for components that normally live inside
34+
* the chat panel, opted into via `parameters.widgetFrame` ("panel" renders
35+
* edge-to-edge like the header/input; "messages" pads like the message list).
36+
*/
37+
const withWidgetTheme: Decorator = (Story, context) => {
38+
const theme = (context.globals.theme as ThemeMode) ?? "light";
39+
const direction = (context.globals.direction as "ltr" | "rtl") ?? "ltr";
40+
const dark = resolveDark(theme);
41+
const frame = context.parameters.widgetFrame as
42+
| "panel"
43+
| "messages"
44+
| undefined;
45+
46+
let inner = <Story />;
47+
if (frame === "panel") {
48+
inner = (
49+
<div className="w-[380px] overflow-hidden rounded-claudius-lg bg-claudius-surface font-body shadow-claudius-elevated">
50+
{inner}
51+
</div>
52+
);
53+
} else if (frame === "messages") {
54+
inner = (
55+
<div className="w-[380px] space-y-3 rounded-claudius-lg bg-claudius-surface p-4 font-body shadow-claudius-elevated">
56+
{inner}
57+
</div>
58+
);
59+
}
60+
61+
return (
62+
<div
63+
className="claudius-root"
64+
data-claudius-dark={dark ? "true" : "false"}
65+
dir={direction}
66+
style={{
67+
minHeight: "100vh",
68+
display: "flex",
69+
alignItems: frame ? "center" : "stretch",
70+
justifyContent: frame ? "center" : "stretch",
71+
padding: frame ? "2rem" : 0,
72+
background: dark ? "#030712" : "#f1f5f9",
73+
}}
74+
>
75+
{inner}
76+
</div>
77+
);
78+
};
79+
80+
const preview: Preview = {
81+
parameters: {
82+
layout: "fullscreen",
83+
controls: {
84+
matchers: { color: /(background|color)$/i, date: /Date$/i },
85+
},
86+
},
87+
initialGlobals: {
88+
locale: "en",
89+
theme: "light",
90+
direction: "ltr",
91+
},
92+
globalTypes: {
93+
locale: {
94+
description: "Widget UI locale",
95+
toolbar: {
96+
title: "Locale",
97+
icon: "globe",
98+
items: LOCALE_ITEMS,
99+
dynamicTitle: true,
100+
},
101+
},
102+
theme: {
103+
description: "Color scheme",
104+
toolbar: {
105+
title: "Theme",
106+
icon: "contrast",
107+
items: [
108+
{ value: "light", title: "Light", icon: "sun" },
109+
{ value: "dark", title: "Dark", icon: "moon" },
110+
{ value: "auto", title: "Auto (OS)", icon: "browser" },
111+
],
112+
dynamicTitle: true,
113+
},
114+
},
115+
direction: {
116+
description: "Text direction",
117+
toolbar: {
118+
title: "Direction",
119+
icon: "transfer",
120+
items: [
121+
{ value: "ltr", title: "LTR" },
122+
{ value: "rtl", title: "RTL" },
123+
],
124+
dynamicTitle: true,
125+
},
126+
},
127+
},
128+
decorators: [withWidgetTheme],
129+
};
130+
131+
export default preview;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { Meta, StoryObj } from "@storybook/react-vite";
2+
import { fn } from "storybook/test";
3+
import { ChatHeader } from "./ChatHeader";
4+
import { locales, type LocaleCode } from "../locales";
5+
6+
const meta = {
7+
title: "Widget/Header",
8+
component: ChatHeader,
9+
parameters: { widgetFrame: "panel" },
10+
args: {
11+
title: "Support",
12+
subtitle: "Ask me anything",
13+
onClose: fn(),
14+
},
15+
render: (args, { globals }) => (
16+
<ChatHeader
17+
{...args}
18+
closeLabel={locales[globals.locale as LocaleCode].closeChat}
19+
/>
20+
),
21+
} satisfies Meta<typeof ChatHeader>;
22+
23+
export default meta;
24+
25+
type Story = StoryObj<typeof meta>;
26+
27+
export const Default: Story = {};
28+
29+
// Long titles stay on one line; the avatar uses the first character.
30+
export const LongTitle: Story = {
31+
args: {
32+
title: "PMDS Customer Success Team",
33+
subtitle: "We typically reply within a few minutes",
34+
},
35+
};

0 commit comments

Comments
 (0)