{"id":35581346,"url":"https://github.com/ftzi/nextbook","last_synced_at":"2026-01-20T16:28:26.437Z","repository":{"id":327155826,"uuid":"1106288769","full_name":"ftzi/nextbook","owner":"ftzi","description":"🎨 Next-gen component stories for Next.js","archived":false,"fork":false,"pushed_at":"2025-12-03T04:21:36.000Z","size":965,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-12-04T19:51:59.457Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://nextbook.dev","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ftzi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-11-29T00:37:06.000Z","updated_at":"2025-12-03T04:21:40.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ftzi/nextbook","commit_stats":null,"previous_names":["ftzi/nextbook"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/ftzi/nextbook","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ftzi%2Fnextbook","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ftzi%2Fnextbook/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ftzi%2Fnextbook/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ftzi%2Fnextbook/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ftzi","download_url":"https://codeload.github.com/ftzi/nextbook/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ftzi%2Fnextbook/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28607113,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-20T16:10:39.856Z","status":"ssl_error","status_checked_at":"2026-01-20T16:10:39.493Z","response_time":117,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-04T20:54:11.508Z","updated_at":"2026-01-20T16:28:26.432Z","avatar_url":"https://github.com/ftzi.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://github.com/ftzi/storify/blob/main/assets/logo.svg\" alt=\"Storify\" width=\"400\" /\u003e\n\u003c/p\u003e\n\n\u003ch3 align=\"center\"\u003e\n  Zero-config component stories for React frameworks\n\u003c/h3\u003e\n\n\u003cdiv align=\"center\"\u003e\n\n[![npm](https://img.shields.io/npm/v/@ftzi/storify)](https://www.npmjs.com/package/@ftzi/storify)\n[![npm](https://img.shields.io/npm/dt/@ftzi/storify)](https://www.npmjs.com/package/@ftzi/storify)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)\n\n\u003c/div\u003e\n\u003cbr/\u003e\n\nStorify is a lightweight alternative to Storybook, designed for React frameworks like **Next.js** and **TanStack Start**. It uses your app's existing configuration - no separate build process, no Tailwind duplication, no webpack config.\n\n## ✨ Features\n\n- **Zero Dependencies** - No runtime dependencies. Just your React app.\n- **Zero Config** - Uses your app's existing setup\n- **Framework Agnostic** - Works with Next.js, TanStack Start, and more via router adapters\n- **Path-Based Hierarchy** - Keys become sidebar structure automatically\n- **Zod Controls** - Auto-generate interactive controls from Zod schemas\n- **Story Matrix** - Auto-generate ALL prop combinations from Zod schemas\n- **Type Safe** - Full TypeScript support with IntelliSense\n- **Lazy Loading** - Stories load on-demand for fast startup\n- **Background Switcher** - Toggle between default and striped backgrounds to spot component imperfections\n- **AI-First** - Generates `CLAUDE.md` and `AGENTS.md` files that teach AI assistants how to write stories\n- **API Mocking** - Optional MSW integration to mock API endpoints in stories\n\n## 🚀 Quick Start\n\n```bash\nnpx @ftzi/storify # or: bunx, pnpm dlx, yarn dlx, etc.\n```\n\nThis scaffolds the required files in `app/ui/` and creates an example story.\n\nThen visit `http://localhost:3000/ui` to see your stories.\n\n\u003cdetails\u003e\n\u003csummary\u003eManual Setup (alternative to CLI)\u003c/summary\u003e\n\n### 1. Install\n\n```bash\nnpm install @ftzi/storify\n# or\nbun add @ftzi/storify\n```\n\n### 2. Register your stories\n\n```tsx\n// app/ui/stories/index.ts\n\"use client\";\n\nimport { createStories } from \"@ftzi/storify\";\n\nexport const stories = createStories({\n  button: () =\u003e import(\"./button.story\"),\n  forms: {\n    input: () =\u003e import(\"./forms/input.story\"),\n    select: () =\u003e import(\"./forms/select.story\"),\n  },\n});\n```\n\nKeys become sidebar paths: `forms.input` → `Forms \u003e Input`\n\n### 3. Create the layout\n\n```tsx\n// app/ui/layout.tsx\nimport \"@/app/globals.css\";\nimport { StorifyShell } from \"@ftzi/storify\";\nimport { NextRouterAdapter } from \"@ftzi/storify/next\";\nimport { notFound } from \"next/navigation\";\nimport { stories } from \"./stories\";\n\nexport default function StorifyLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  if (process.env.NODE_ENV === \"production\") {\n    notFound();\n  }\n\n  return (\n    \u003cStorifyShell stories={stories} router={NextRouterAdapter}\u003e\n      {children}\n    \u003c/StorifyShell\u003e\n  );\n}\n```\n\n\u003e **Note:** Don't add `\u003chtml\u003e` or `\u003cbody\u003e` tags here - Next.js layouts nest, so your root layout already provides them.\n\n### 4. Create the page\n\n```tsx\n// app/ui/[[...path]]/page.tsx\nimport { StoryPage } from \"@ftzi/storify\";\nimport { stories } from \"../stories\";\n\nexport default async function Page({\n  params,\n}: {\n  params: Promise\u003c{ path?: string[] }\u003e;\n}) {\n  const { path = [] } = await params;\n  return \u003cStoryPage path={path} stories={stories} /\u003e;\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eTanStack Start Setup\u003c/summary\u003e\n\n### 1. Install\n\n```bash\nnpm install @ftzi/storify\n```\n\n### 2. Register your stories\n\n```tsx\n// src/stories/index.ts\n\"use client\";\n\nimport { createStories } from \"@ftzi/storify\";\n\nexport const stories = createStories({\n  button: () =\u003e import(\"./button.story\"),\n});\n```\n\n### 3. Create the layout route\n\n```tsx\n// src/routes/ui.tsx\nimport { StorifyShell } from \"@ftzi/storify\";\nimport { TanStackRouterAdapter } from \"@ftzi/storify/tanstack\";\nimport { createFileRoute, Outlet } from \"@tanstack/react-router\";\nimport { stories } from \"../stories\";\n\nexport const Route = createFileRoute(\"/ui\")({\n  component: UILayout,\n});\n\nfunction UILayout() {\n  return (\n    \u003cStorifyShell stories={stories} router={TanStackRouterAdapter}\u003e\n      \u003cOutlet /\u003e\n    \u003c/StorifyShell\u003e\n  );\n}\n```\n\n### 4. Create the index route\n\n```tsx\n// src/routes/ui/index.tsx\nimport { StoryPage } from \"@ftzi/storify\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport { stories } from \"../../stories\";\n\nexport const Route = createFileRoute(\"/ui/\")({\n  component: UIIndex,\n});\n\nfunction UIIndex() {\n  return \u003cStoryPage stories={stories} path={[]} basePath=\"/ui\" /\u003e;\n}\n```\n\n### 5. Create the catch-all route\n\n```tsx\n// src/routes/ui/$.tsx\nimport { StoryPage } from \"@ftzi/storify\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport { stories } from \"../../stories\";\n\nexport const Route = createFileRoute(\"/ui/$\")({\n  component: StoryRoute,\n});\n\nfunction StoryRoute() {\n  const { _splat } = Route.useParams();\n  const path = _splat ? _splat.split(\"/\").filter(Boolean) : [];\n\n  return \u003cStoryPage stories={stories} path={path} basePath=\"/ui\" /\u003e;\n}\n```\n\n\u003c/details\u003e\n\n## 📝 Writing Stories\n\n```tsx\n// app/ui/stories/button.story.tsx\nimport { story } from \"@ftzi/storify\";\nimport { Button } from \"@/components/ui/button\";\n\nexport const Default = story({\n  render: () =\u003e \u003cButton\u003eClick me\u003c/Button\u003e,\n});\n```\n\n### With Zod Controls\n\nUse Zod schemas to auto-generate interactive controls:\n\n```tsx\nimport { story } from \"@ftzi/storify\";\nimport { z } from \"zod\";\nimport { Button } from \"@/components/ui/button\";\n\nexport const Controlled = story({\n  schema: z.object({\n    variant: z\n      .enum([\"primary\", \"secondary\"])\n      .default(\"primary\")\n      .describe(\"Button variant\"),\n    disabled: z.boolean().default(false).describe(\"Disabled state\"),\n    children: z.string().default(\"Click me\").describe(\"Button text\"),\n  }),\n  render: (props) =\u003e \u003cButton {...props} /\u003e,\n});\n```\n\n### Zod → Control Mapping\n\n| Zod Type        | Control         |\n| --------------- | --------------- |\n| `z.string()`    | Text input      |\n| `z.number()`    | Number input    |\n| `z.boolean()`   | Toggle          |\n| `z.enum([...])` | Select dropdown |\n\n- `.default(value)` - Sets initial control value\n- `.describe(\"...\")` - Adds optional description (shown as tooltip on ℹ️ icon)\n\n### Story Matrix (Automatic Combinatorial Testing)\n\nThis is the killer feature. Instead of manually writing dozens of story variants, let Storify generate ALL combinations automatically:\n\n```tsx\nimport { storyMatrix } from \"@ftzi/storify\";\nimport { z } from \"zod\";\nimport { Button } from \"@/components/ui/button\";\n\n// This single export generates 12 visual tests automatically!\n// (3 variants × 2 sizes × 2 disabled states = 12 combinations)\nexport const Matrix = storyMatrix({\n  schema: z.object({\n    variant: z.enum([\"primary\", \"secondary\", \"ghost\"]),\n    size: z.enum([\"sm\", \"lg\"]),\n    disabled: z.boolean(),\n  }),\n  render: (props) =\u003e \u003cButton {...props}\u003eClick me\u003c/Button\u003e,\n});\n```\n\nThe matrix view displays all combinations in a grid:\n\n```\n┌─────────────────────┬─────────────────────┬─────────────────────┐\n│ primary, sm, false  │ secondary, sm, false│ ghost, sm, false    │\n│     [Button]        │     [Button]        │     [Button]        │\n├─────────────────────┼─────────────────────┼─────────────────────┤\n│ primary, sm, true   │ secondary, sm, true │ ghost, sm, true     │\n│     [Button]        │     [Button]        │     [Button]        │\n├─────────────────────┼─────────────────────┼─────────────────────┤\n│ primary, lg, false  │ secondary, lg, false│ ghost, lg, false    │\n│     [Button]        │     [Button]        │     [Button]        │\n└─────────────────────┴─────────────────────┴─────────────────────┘\n... and so on\n```\n\n**Why this matters:**\n- **Zero boilerplate** - No more writing `PrimarySmall`, `PrimaryLarge`, `SecondarySmall`...\n- **Complete coverage** - Never miss a combination again\n- **Always in sync** - Add a new variant? The matrix updates automatically\n- **Visual regression at scale** - See every state at once, catch issues instantly\n\n## 📁 File Organization\n\nStories are organized by the keys you provide to `createStories`:\n\n```tsx\nexport const stories = createStories({\n  button: () =\u003e import(\"./button.story\"), // → \"Button\"\n  forms: {\n    input: () =\u003e import(\"./forms/input.story\"), // → \"Forms \u003e Input\"\n    select: () =\u003e import(\"./forms/select.story\"), // → \"Forms \u003e Select\"\n  },\n  layout: {\n    card: () =\u003e import(\"./layout/card.story\"), // → \"Layout \u003e Card\"\n  },\n});\n```\n\nNamed exports become story variants:\n\n```tsx\n// button.story.tsx\nexport const Primary = story({ ... })   // → \"Button \u003e Primary\"\nexport const Secondary = story({ ... }) // → \"Button \u003e Secondary\"\n```\n\n## 🔌 Mocking API Requests\n\nStorify supports [MSW (Mock Service Worker)](https://mswjs.io/) for mocking API endpoints in your stories. This is useful for testing components that fetch data without hitting real backends.\n\n### Setup MSW\n\n```bash\n# Install MSW\nnpm install msw --save-dev\n\n# Initialize service worker (creates public/mockServiceWorker.js)\nnpx msw init public\n```\n\n### Basic Mocking\n\nAdd a `mocks` array to your story to intercept network requests:\n\n```tsx\nimport { story } from \"@ftzi/storify\";\nimport { http, HttpResponse } from \"msw\";\n\nexport const WithMockedData = story({\n  mocks: [\n    http.get(\"/api/user\", () =\u003e HttpResponse.json({ name: \"John Doe\" })),\n    http.get(\"/api/posts\", () =\u003e HttpResponse.json([\n      { id: 1, title: \"First Post\" },\n      { id: 2, title: \"Second Post\" },\n    ])),\n  ],\n  render: () =\u003e \u003cUserDashboard /\u003e,\n});\n```\n\nWhen viewing this story, a \"Mocks\" indicator appears in the header showing that API requests are being intercepted.\n\n### Mock Factories (Dynamic Mocks)\n\nMocks can be a function that receives control values, allowing dynamic mock responses:\n\n```tsx\nexport const Configurable = story({\n  schema: z.object({\n    userName: z.string().default(\"Jane\"),\n    shouldError: z.boolean().default(false).describe(\"Simulate API error\"),\n  }),\n  mocks: ({ userName, shouldError }) =\u003e [\n    http.get(\"/api/user\", () =\u003e {\n      if (shouldError) {\n        return new HttpResponse(null, { status: 500 });\n      }\n      return HttpResponse.json({ name: userName });\n    }),\n  ],\n  render: () =\u003e \u003cUserProfile /\u003e,\n});\n```\n\nNow you can toggle `shouldError` in the controls panel to test error states!\n\n### Testing Loading States\n\n```tsx\nimport { delay } from \"msw\";\n\nexport const Loading = story({\n  mocks: [\n    http.get(\"/api/user\", async () =\u003e {\n      await delay(\"infinite\"); // Never resolves\n      return HttpResponse.json({});\n    }),\n  ],\n  render: () =\u003e \u003cUserProfile /\u003e,\n});\n```\n\n### Alternative: Prop-Based Mocking\n\nFor simpler cases, you can pass mock functions as props (no MSW needed):\n\n```tsx\nexport const WithMockFetcher = story({\n  schema: z.object({\n    fetchUser: z.function().returns(z.promise(z.object({ name: z.string() }))),\n  }),\n  render: ({ fetchUser }) =\u003e \u003cUserProfile fetchUser={fetchUser} /\u003e,\n});\n```\n\nThis approach requires your component to accept the fetcher as a prop.\n\n### Generating Mock Data\n\nFor generating realistic mock data from Zod schemas, check out [@anatine/zod-mock](https://github.com/anatine/zod-plugins/tree/main/packages/zod-mock):\n\n```tsx\nimport { generateMock } from \"@anatine/zod-mock\";\n\nconst userSchema = z.object({\n  id: z.string().uuid(),\n  name: z.string(),\n  email: z.string().email(),\n});\n\nexport const WithGeneratedData = story({\n  mocks: [\n    http.get(\"/api/user\", () =\u003e HttpResponse.json(generateMock(userSchema))),\n  ],\n  render: () =\u003e \u003cUserProfile /\u003e,\n});\n```\n\n## 🔒 Layout Isolation\n\nIf your root layout has providers that conflict with Storify, use `useSelectedLayoutSegment` to skip them for the `/ui` route:\n\n```tsx\n// app/layout.tsx\n\"use client\";\nimport { useSelectedLayoutSegment } from \"next/navigation\";\nimport { Providers } from \"./providers\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  const segment = useSelectedLayoutSegment();\n\n  // Skip providers for Storify\n  if (segment === \"ui\") {\n    return (\n      \u003chtml lang=\"en\"\u003e\n        \u003cbody\u003e{children}\u003c/body\u003e\n      \u003c/html\u003e\n    );\n  }\n\n  return (\n    \u003chtml lang=\"en\"\u003e\n      \u003cbody\u003e\n        \u003cProviders\u003e{children}\u003c/Providers\u003e\n      \u003c/body\u003e\n    \u003c/html\u003e\n  );\n}\n```\n\nReference: [useSelectedLayoutSegment](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segment)\n\n## 🛡️ Access Control\n\nBy default, the generated layout blocks access in production. This condition can be changed to disable Storify based on your needs:\n\n```tsx\n// app/ui/layout.tsx\nexport default function StorifyLayout({ children }: { children: React.ReactNode }) {\n  // Change this condition to control when Storify is disabled\n  // if (process.env.NODE_ENV === \"production\") {\n  //   notFound();\n  // }\n\n  return (\n    \u003cStorifyShell stories={stories} router={NextRouterAdapter}\u003e\n      {children}\n    \u003c/StorifyShell\u003e\n  );\n}\n```\n\n## 💡 Why Storify?\n\n### The Storybook Problem\n\nWith Storybook, you manually write each variant:\n\n```tsx\n// Storybook: Write EVERY combination by hand 😩\nexport const Primary = () =\u003e \u003cButton variant=\"primary\" /\u003e\nexport const Secondary = () =\u003e \u003cButton variant=\"secondary\" /\u003e\nexport const Ghost = () =\u003e \u003cButton variant=\"ghost\" /\u003e\nexport const PrimarySmall = () =\u003e \u003cButton variant=\"primary\" size=\"sm\" /\u003e\nexport const PrimaryLarge = () =\u003e \u003cButton variant=\"primary\" size=\"lg\" /\u003e\nexport const PrimaryDisabled = () =\u003e \u003cButton variant=\"primary\" disabled /\u003e\nexport const SecondarySmall = () =\u003e \u003cButton variant=\"secondary\" size=\"sm\" /\u003e\nexport const SecondaryLarge = () =\u003e \u003cButton variant=\"secondary\" size=\"lg\" /\u003e\nexport const SecondaryDisabled = () =\u003e \u003cButton variant=\"secondary\" disabled /\u003e\n// ... 20+ more exports, and you STILL missed some combinations\n```\n\n### The Storify Solution\n\n```tsx\n// Generates ALL 36 combinations automatically 🎉\nexport const Matrix = storyMatrix({\n  schema: z.object({\n    variant: z.enum([\"primary\", \"secondary\", \"ghost\"]),\n    size: z.enum([\"sm\", \"md\", \"lg\"]),\n    disabled: z.boolean(),\n    loading: z.boolean(),\n  }),\n  render: (props) =\u003e \u003cButton {...props}\u003eClick me\u003c/Button\u003e,\n});\n```\n\n### Feature Comparison\n\n| Feature                 | Storybook                    | Storify                      |\n| ----------------------- | ---------------------------- | ---------------------------- |\n| Dependencies            | 100+ packages                | **Zero**                     |\n| Setup time              | ~30 min                      | ~5 min                       |\n| Separate build          | Yes                          | No                           |\n| Config duplication      | Yes (Tailwind, etc.)         | No                           |\n| Bundle size             | Large                        | Minimal                      |\n| Hot reload              | Separate process             | Same as app                  |\n| Framework support       | Many (via adapters)          | Next.js, TanStack Start      |\n| **Combinatorial testing** | Manual (write each variant) | **Automatic (storyMatrix)** |\n| Variant coverage        | Whatever you remember        | **100% guaranteed**          |\n| Maintenance burden      | High (keep variants in sync) | **Zero (schema is truth)**  |\n| AI assistant support    | No                           | **Yes (CLAUDE.md, AGENTS.md)** |\n| API Mocking             | Addon (msw-storybook-addon)  | Built-in (optional MSW)      |\n\n## 🤖 AI-First Design\n\nStorify is the **first component documentation tool with built-in AI assistant support**. When you run `npx @ftzi/storify`, it generates `CLAUDE.md` and `AGENTS.md` files in your stories directory that teach AI assistants how to write stories.\n\n**What this means:**\n\n- **Claude Code** reads `CLAUDE.md`\n- **Cursor** reads `AGENTS.md`\n- **Other AI tools** can reference these files\n\nThe generated instructions include:\n- How to write `story()` with examples\n- Zod schema patterns for controls\n- `storyMatrix()` for combinations\n- File naming conventions\n- Best practices\n\n**Just say:**\n\n\u003e \"Write a story for my Button component\"\n\nAnd your AI assistant already knows exactly how to create a comprehensive story with interactive controls, variants, and even a matrix for all combinations - no explanation needed.\n\n**Upgradable:** Re-running `npx @ftzi/storify` updates the AI instructions section (marked with `\u003c!-- STORIFY:START --\u003e` / `\u003c!-- STORIFY:END --\u003e`) while preserving any custom instructions you've added outside the markers.\n\n## 🛠️ Development\n\n\u003cdetails\u003e\n\u003csummary\u003eCommands\u003c/summary\u003e\n\n```bash\n# Install dependencies\nbun install\n\n# Start the example app\nbun dev\n\n# Type check and lint\nbun ok\n\n# Run visual regression tests\nbun test:e2e\n```\n\n\u003c/details\u003e\n\n## 📄 License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fftzi%2Fnextbook","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fftzi%2Fnextbook","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fftzi%2Fnextbook/lists"}