Skip to main content

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).

PackageWorkspacePortFramework builderPurpose
packages/ui@repo/ui6006@storybook/react-webpack5Shared design-system primitives + design-token pages
apps/frontend-nextjsfrontend-nextjs6007@storybook/nextjsApp-level, provider-dependent components (auth, chat, coins, posts…)
apps/frontend-adminfrontend-admin6008@storybook/nextjsAdmin 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 swc hook forces jsc.transform.react.runtime = 'automatic' (otherwise JSX inside a story's render: throws "Can't find variable: React").
  • A custom webpackFinal appends postcss-loader to the CSS rule so the @tailwind directives in .storybook/tailwind.css compile through postcss.config.js.

.storybook/preview.ts:

  • Imports ./tailwind.css globally.
  • controls.matchers auto-detects color/date props.
  • backgrounds: light (#ffffff, default) and dark (#0c1014).
  • decorators: [withThemeByClassName({ themes: { light: '', dark: 'dark' }, defaultTheme: 'light' })] — a toolbar toggle that adds/removes the dark class on <html> so Tailwind dark: 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 no component, using local Swatch/Group render helpers over the real cg-* 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 (unlike packages/ui).
  • addons: ['@storybook/addon-themes'], staticDirs: ['../public']
  • A webpackFinal aliases @/lib/firebase.storybook/mocks/firebase.ts (which exports firebaseApp = null, auth = null, a stub googleProvider) so Storybook never boots real Firebase for components like Login.

.storybook/preview.tsx:

  • Imports ../src/app/globals.css and ../src/i18n/config (self-initializes i18next, so useTranslation() / t('key', 'fallback') render real text, not raw keys).
  • parameters.nextjs = { appDirectory: true } — mounts @storybook/nextjs's mocked App Router so useRouter/usePathname don't throw "invariant expected app router to be mounted".
  • Same backgrounds (light #ffffff / dark #0c1014) and withThemeByClassName dark-mode decorator as packages/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 (CommandPalette uses useRouter/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 withThemeByClassName dark-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 using meta.args; variant stories override just the differing args.
  • Gallery/comparison stories (AllVariants, Grid) use a custom render: function iterating over an enum/array to show every visual state side-by-side.
  • Token/foundation pages skip component entirely (Meta = { title } only) and define local Swatch/Group helpers inline.
  • Dark mode is never a per-story concern — it's handled globally by the withThemeByClassName decorator + toolbar toggle, since every component uses Tailwind dark: variants.
  • Provider-heavy app components (nextjs/admin) wrap MockedProvider (mocks: []) and app context providers in a per-file withProviders decorator rather than a global one.
  • No play: functions anywhere — Storybook here is documentation / visual review, plus (in packages/ui only) an automated render-smoke gate via test-storybook.