Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,43 @@ pnpm create mbga
| [`@mbga/flashnet`](./packages/flashnet) | Flashnet authentication and orchestration |
| [`create-mbga`](./packages/create-mbga) | Scaffold a new MBGA project |

## Docs Deployment

The docs site and Storybook previews are deployed as two separate Vercel projects.

### Local Docs Workflow

Run the docs site with embedded local Storybook assets:

```bash
pnpm docs:dev
```

This builds the workspace packages, generates the `@mbga/kit` Storybook bundle, copies it into `site/public/storybook`, and starts the Vocs site with `VITE_STORYBOOK_BASE_URL=/storybook`.

### Vercel Projects

#### Site project

- Root directory: `site`
- Build command: `pnpm build`
- Output directory: `dist`
- Environment variable: `VITE_STORYBOOK_BASE_URL=https://<storybook-domain>`

#### Storybook project

- Root directory: repository root
- Framework preset: `Other`
- Install command: `pnpm install --frozen-lockfile`
- Build command: `pnpm storybook:build:deploy`
- Output directory: `packages/kit/storybook-static`

### Rollout Order

1. Deploy the Storybook project and capture its stable URL.
2. Set `VITE_STORYBOOK_BASE_URL` on the site project for preview and production.
3. Redeploy the site project so embedded previews and "Open in Storybook" links point to the external Storybook deployment.

## Community

- [GitHub Discussions](https://github.com/refrakts/mbga/discussions) — ask questions and share ideas
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
"clean": "pnpm run --r --parallel clean",
"storybook": "pnpm --filter @mbga/kit storybook",
"storybook:build": "pnpm --filter @mbga/kit storybook:build",
"storybook:build:deploy": "pnpm build && pnpm --filter @mbga/kit storybook:build",
"storybook:sync-static": "pnpm storybook:build:deploy && rm -rf site/public/storybook && cp -r packages/kit/storybook-static site/public/storybook",
"docs:typedoc": "typedoc",
"docs:dev": "pnpm storybook:build && rm -rf site/public/storybook && cp -r packages/kit/storybook-static site/public/storybook && pnpm --filter site dev",
"docs:build": "pnpm storybook:build && rm -rf site/public/storybook && cp -r packages/kit/storybook-static site/public/storybook && pnpm --filter site build",
"docs:dev": "VITE_STORYBOOK_BASE_URL=/storybook pnpm storybook:sync-static && VITE_STORYBOOK_BASE_URL=/storybook pnpm --filter site dev",
"docs:build": "VITE_STORYBOOK_BASE_URL=/storybook pnpm storybook:sync-static && VITE_STORYBOOK_BASE_URL=/storybook pnpm --filter site build",
"docs:preview": "pnpm --filter site preview",
"format": "biome format --write",
"test": "vitest",
Expand Down
47 changes: 47 additions & 0 deletions site/lib/storybook.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest'

import {
createStorybookIframeUrl,
createStorybookManagerUrl,
normalizeStorybookBaseUrl,
resolveStorybookUrls,
} from './storybook'

describe('storybook helpers', () => {
it('trims trailing slashes from the base URL', () => {
expect(normalizeStorybookBaseUrl('https://storybook.mbga.dev///')).toBe(
'https://storybook.mbga.dev',
)
})

it('builds iframe URLs for embedded story previews', () => {
expect(
createStorybookIframeUrl(
'https://storybook.mbga.dev/',
'components-accountmodal--default',
),
).toBe(
'https://storybook.mbga.dev/iframe.html?id=components-accountmodal--default&viewMode=story',
)
})

it('builds manager URLs for full Storybook pages', () => {
expect(
createStorybookManagerUrl(
'https://storybook.mbga.dev/',
'components-connectbutton--connected',
),
).toBe(
'https://storybook.mbga.dev/?path=/story/components-connectbutton--connected',
)
})

it('returns null when the Storybook base URL is missing', () => {
expect(
resolveStorybookUrls(undefined, 'components-connectmodal--default'),
).toBeNull()
expect(
resolveStorybookUrls(' ', 'components-connectmodal--default'),
).toBeNull()
})
})
36 changes: 36 additions & 0 deletions site/lib/storybook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export type StorybookUrls = {
iframeSrc: string
managerHref: string
}

export function normalizeStorybookBaseUrl(baseUrl: string) {
const trimmedBaseUrl = baseUrl.trim()

if (trimmedBaseUrl === '/') return ''

return trimmedBaseUrl.replace(/\/+$/, '')
}

export function createStorybookIframeUrl(baseUrl: string, id: string) {
const normalizedBaseUrl = normalizeStorybookBaseUrl(baseUrl)

return `${normalizedBaseUrl}/iframe.html?id=${id}&viewMode=story`
}

export function createStorybookManagerUrl(baseUrl: string, id: string) {
const normalizedBaseUrl = normalizeStorybookBaseUrl(baseUrl)

return `${normalizedBaseUrl}/?path=/story/${id}`
}

export function resolveStorybookUrls(
baseUrl: string | undefined,
id: string,
): StorybookUrls | null {
if (!baseUrl?.trim()) return null

return {
iframeSrc: createStorybookIframeUrl(baseUrl, id),
managerHref: createStorybookManagerUrl(baseUrl, id),
}
}
66 changes: 54 additions & 12 deletions site/pages/components/StorybookEmbed.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,66 @@
'use client'

import { resolveStorybookUrls } from '../../lib/storybook'

export function StorybookEmbed({
id,
height = 200,
}: {
id: string
height?: number
}) {
const storybookUrls = resolveStorybookUrls(
import.meta.env.VITE_STORYBOOK_BASE_URL,
id,
)

if (!storybookUrls) {
return (
<div
style={{
border: '1px solid var(--vocs-color_border, #e2e2e2)',
borderRadius: '8px',
marginTop: '12px',
marginBottom: '12px',
padding: '16px',
background: 'var(--vocs-color_background, transparent)',
}}
>
<strong style={{ display: 'block', marginBottom: '6px' }}>
Storybook preview unavailable
</strong>
<span>
Set <code>VITE_STORYBOOK_BASE_URL</code> to enable embedded component
previews.
</span>
</div>
)
}

return (
<iframe
src={`/storybook/iframe.html?id=${id}&viewMode=story`}
width="100%"
height={height}
style={{
border: '1px solid var(--vocs-color_border, #e2e2e2)',
borderRadius: '8px',
marginTop: '12px',
marginBottom: '12px',
}}
title={`Component preview: ${id}`}
/>
<div style={{ marginTop: '12px', marginBottom: '12px' }}>
<iframe
src={storybookUrls.iframeSrc}
width="100%"
height={height}
style={{
border: '1px solid var(--vocs-color_border, #e2e2e2)',
borderRadius: '8px',
display: 'block',
}}
title={`Component preview: ${id}`}
/>
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
marginTop: '8px',
}}
>
<a href={storybookUrls.managerHref} rel="noreferrer" target="_blank">
Open in Storybook
</a>
</div>
</div>
)
}
1 change: 1 addition & 0 deletions site/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
7 changes: 7 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ export default defineConfig({
},
resolve: { alias },
},
{
test: {
name: 'site',
include: ['./site/**/*.test.ts?(x)'],
environment: 'node',
},
},
{
test: {
name: 'typetests',
Expand Down
Loading