Storybook
Closegram has three independent Storybook installs, one per publishable/app package, each on its own port. They're a local/dev tool for building and reviewing components in isolation — none is wired into CI (.github/workflows/ci.yml has no Storybook step).
| Package | Workspace | Port | Framework builder | Purpose |
|---|---|---|---|---|
packages/ui | @repo/ui | 6006 | @storybook/react-webpack5 | Shared design-system primitives + design-token pages |
apps/frontend-nextjs | frontend-nextjs | 6007 | @storybook/nextjs | App-level, provider-dependent components (auth, chat, coins, posts…) |
apps/frontend-admin | frontend-admin | 6008 | @storybook/nextjs | Admin UI-kit components (Table, Modal, CommandPalette…) |
All three pin storybook/@storybook/* at ^10.5.4 and share one addon, @storybook/addon-themes. None declares @storybook/addon-essentials — Storybook 9+ folded controls/actions/viewport/backgrounds/toolbars/measure/outline into core, so it isn't needed.
For component APIs and props, see UI Components. For the automated test setups (Vitest/Jest/Playwright/XCTest), see Testing.
Run commands
All from the repo root using npm workspaces (-w <package>), or cd into the package first. Note the script-name inconsistency between packages/ui and the two apps:
# packages/ui — design system (port 6006)
npm run dev -w @repo/ui # storybook dev -p 6006
npm run build -w @repo/ui # storybook build
npm run test-storybook -w @repo/ui # render-smoke runner (needs a running/served Storybook)
# apps/frontend-nextjs — public app (port 6007)
npm run storybook -w frontend-nextjs # storybook dev -p 6007
npm run build-storybook -w frontend-nextjs # storybook build
# apps/frontend-admin — admin dashboard (port 6008)
npm run storybook -w frontend-admin # storybook dev -p 6008
npm run build-storybook -w frontend-admin # storybook build
In packages/ui the dev command is dev and the build is the unprefixed build; in both apps they're explicitly namespaced storybook / build-storybook. test-storybook exists only in packages/ui.
packages/ui (port 6006) — the design system
The home for @repo/ui's primitives (Button, Avatar, Badge, Tooltip, Spinner, Form, IconButton, DateTimePicker, Skeleton, AnimatedCount, RollingNumber, StoryRingAvatar, VerifiedBadge, the icon set) plus "Foundations" pages documenting design tokens. It doubles as a render-smoke target via test-storybook.
.storybook/main.ts:
stories: ['../src/**/*.stories.@(ts|tsx)']framework: { name: '@storybook/react-webpack5', options: {} }addons: ['@storybook/addon-webpack5-compiler-swc', '@storybook/addon-themes']— the SWC compiler addon is required because Storybook 8+'s webpack5 builder ships no compiler by default.core: { disableTelemetry: true }- A custom
swchook forcesjsc.transform.react.runtime = 'automatic'(otherwise JSX inside a story'srender:throws"Can't find variable: React"). - A custom
webpackFinalappendspostcss-loaderto the CSS rule so the@tailwinddirectives in.storybook/tailwind.csscompile throughpostcss.config.js.
.storybook/preview.ts:
- Imports
./tailwind.cssglobally. controls.matchersauto-detects color/date props.backgrounds:light(#ffffff, default) anddark(#0c1014).decorators: [withThemeByClassName({ themes: { light: '', dark: 'dark' }, defaultTheme: 'light' })]— a toolbar toggle that adds/removes thedarkclass on<html>so Tailwinddark:variants are previewable per-story.
Story organization — titles under three top-level groups:
Primitives/*— one file per component (Primitives/Button,Primitives/Avatar,Primitives/Badge, …).Foundations/Colors(src/Foundations/Colors.stories.tsx) — a token-documentation page with nocomponent, using localSwatch/Grouprender helpers over the realcg-*Tailwind classes (e.g.bg-cg-blue-action,bg-cg-dark-elevated) so the palette stays in sync with the config.Icons/Gallery(src/icons/Icons.stories.tsx) — the icon-set gallery.
The test-storybook render-smoke runner
packages/ui is the only package with @storybook/test-runner wired up ("test-storybook": "test-storybook"). It runs against a built/served Storybook and asserts every story renders without throwing — a render smoke test across all Primitives/*, Foundations/*, and Icons/* stories. No custom test-runner.ts / Jest override exists, and no story anywhere in the repo defines a play: function, so this is purely render-smoke, not interaction testing. Start (or build-and-serve) Storybook first, then:
npm run test-storybook -w @repo/ui
apps/frontend-nextjs (port 6007) — the public app
Previews app-level, provider-dependent components (auth screens, chat, notifications, coins/rewards modals, post cards, navigation) that need mocked Apollo/Firebase/Next context to render in isolation.
.storybook/main.ts:
stories: ['../src/**/*.stories.@(ts|tsx)']framework: { name: '@storybook/nextjs', options: {} }— handles App Router internals,next/image, etc. natively, so no manual webpack CSS/SWC wiring is needed (unlikepackages/ui).addons: ['@storybook/addon-themes'],staticDirs: ['../public']- A
webpackFinalaliases@/lib/firebase→.storybook/mocks/firebase.ts(which exportsfirebaseApp = null,auth = null, a stubgoogleProvider) so Storybook never boots real Firebase for components likeLogin.
.storybook/preview.tsx:
- Imports
../src/app/globals.cssand../src/i18n/config(self-initializes i18next, souseTranslation()/t('key', 'fallback')render real text, not raw keys). parameters.nextjs = { appDirectory: true }— mounts@storybook/nextjs's mocked App Router souseRouter/usePathnamedon't throw"invariant expected app router to be mounted".- Same
backgrounds(light#ffffff/ dark#0c1014) andwithThemeByClassNamedark-mode decorator aspackages/ui.
Story organization — everything nests under App/*, with feature sub-namespaces. Flat titles like App/Auth, App/Login, App/Navigation, App/CreatePostModal, App/PostCard, App/Notifications; nested ones like App/Chat/MessageBubble, App/Chat/ConversationList, App/Coins/CoinBalanceBadge, App/Coins/Modals, App/Rewards/RewardFanModal.
Components needing app context use a per-file withProviders decorator wrapping MockedProvider + AuthProvider + ThemeProvider + ToastProvider (repeated per file rather than applied globally, since not every component needs the full stack); those metas usually also set parameters: { layout: 'fullscreen' } for modal/screen previews.
There's no test-runner here. vitest.config.ts excludes **/*.stories.tsx from the unit run — these stories are visual-only.
apps/frontend-admin (port 6008) — the admin dashboard
Previews admin-only UI-kit components (Table, Pagination, Dropdown, Modal, CommandPalette, StatCard, EmptyState, Card, Input, Avatar) plus its own design-token page.
.storybook/main.ts — the simplest of the three, with no webpackFinal/mocks:
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(ts|tsx)'],
addons: ['@storybook/addon-themes'],
framework: { name: '@storybook/nextjs', options: {} },
core: { disableTelemetry: true },
};
.storybook/preview.tsx:
- Imports
../src/app/globals.css; no i18n import here. nextjs: { appDirectory: true }for the same App Router mock reason (CommandPaletteusesuseRouter/usePathname).- A distinct, admin-branded background palette rather than generic light/dark:
backgrounds: { default: 'admin', values: [{ name: 'admin', value: '#f6f6f7' }, { name: 'admin-dark', value: '#0a0b0d' }] }. - Same
withThemeByClassNamedark-mode toolbar decorator.
Story organization — everything under Admin/*: Admin/Foundations/Colors (token page, same Swatch/Group pattern as packages/ui), plus Admin/Avatar, Admin/Card, Admin/CommandPalette, Admin/Dropdown, Admin/EmptyState, Admin/Input, Admin/Modal, Admin/Pagination, Admin/StatCard, Admin/Table.
Shared CSF conventions
All three installs use CSF3, TypeScript:
import type { Meta, StoryObj } from '@storybook/react';
import { Button, type ButtonVariant } from './Button';
const meta: Meta<typeof Button> = {
title: 'Primitives/Button',
component: Button,
args: { children: 'Guardar' },
argTypes: {
variant: { control: 'select', options: ['primary', 'secondary', 'ghost', 'danger', /* … */] },
size: { control: 'inline-radio', options: ['sm', 'md'] },
disabled: { control: 'boolean' },
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = { args: { variant: 'primary' } };
const ALL: ButtonVariant[] = [/* … */];
export const AllVariants: Story = {
render: () => (
<div className="flex flex-wrap items-center gap-3">
{ALL.map((v) => <Button key={v} variant={v}>{v}</Button>)}
</div>
),
};
- Title taxonomy doubles as the sidebar namespace and signals scope:
Primitives/*+Foundations/*+Icons/*(the design system),App/*(+ feature subpaths) for the consumer app,Admin/*(+Admin/Foundations/*) for the admin app. - Baseline story is usually a bare
export const Default: Story = {}or a primary-state export usingmeta.args; variant stories override just the differingargs. - Gallery/comparison stories (
AllVariants,Grid) use a customrender:function iterating over an enum/array to show every visual state side-by-side. - Token/foundation pages skip
componententirely (Meta = { title }only) and define localSwatch/Grouphelpers inline. - Dark mode is never a per-story concern — it's handled globally by the
withThemeByClassNamedecorator + toolbar toggle, since every component uses Tailwinddark:variants. - Provider-heavy app components (nextjs/admin) wrap
MockedProvider(mocks: []) and app context providers in a per-filewithProvidersdecorator rather than a global one. - No
play:functions anywhere — Storybook here is documentation / visual review, plus (inpackages/uionly) an automated render-smoke gate viatest-storybook.