Detailed changes
@@ -0,0 +1,26 @@
+import {
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+ RouterProvider,
+} from "@tanstack/react-router";
+import type { Decorator } from "@storybook/react-vite";
+
+// Catch-all route so any <Link to="..."> resolves without errors.
+const rootRoute = createRootRoute();
+const catchAll = createRoute({
+ getParentRoute: () => rootRoute,
+ path: "$",
+});
+rootRoute.addChildren([catchAll]);
+
+const router = createRouter({
+ routeTree: rootRoute,
+ history: createMemoryHistory({ initialEntries: ["/"] }),
+});
+
+// Wraps a story in a TanStack Router context so components using <Link> render.
+export const withRouter: Decorator = (Story) => (
+ <RouterProvider router={router} defaultComponent={() => <Story />} />
+);
@@ -0,0 +1,74 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+import { Status } from "@/__generated__/graphql";
+import { withRouter } from "@/../.storybook/decorators";
+
+import { BugRow } from "./BugRow";
+
+const meta = {
+ component: BugRow,
+ decorators: [withRouter],
+} satisfies Meta<typeof BugRow>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+const baseArgs = {
+ id: "abc123",
+ humanId: "a1b2c3",
+ repo: "my-repo",
+ author: {
+ humanId: "user1",
+ displayName: "Jane Doe",
+ avatarUrl: null,
+ },
+ createdAt: new Date(Date.now() - 3600 * 1000).toISOString(),
+};
+
+export const OpenBug: Story = {
+ args: {
+ ...baseArgs,
+ status: Status.Open,
+ title: "Fix login page crash on empty email",
+ labels: [
+ { name: "bug", color: { R: 252, G: 41, B: 41 } },
+ { name: "priority", color: { R: 255, G: 152, B: 0 } },
+ ],
+ commentCount: 3,
+ },
+};
+
+export const ClosedBug: Story = {
+ args: {
+ ...baseArgs,
+ status: Status.Closed,
+ title: "Add dark mode support",
+ labels: [{ name: "enhancement", color: { R: 163, G: 230, B: 53 } }],
+ commentCount: 12,
+ },
+};
+
+export const NoLabels: Story = {
+ args: {
+ ...baseArgs,
+ status: Status.Open,
+ title: "Simple bug with no labels",
+ labels: [],
+ commentCount: 0,
+ },
+};
+
+export const LongTitle: Story = {
+ args: {
+ ...baseArgs,
+ status: Status.Open,
+ title:
+ "This is a very long bug title that should demonstrate how the component handles overflow and wrapping when the title extends beyond the available space",
+ labels: [
+ { name: "bug", color: { R: 252, G: 41, B: 41 } },
+ { name: "documentation", color: { R: 30, G: 80, B: 160 } },
+ { name: "help wanted", color: { R: 0, G: 150, B: 136 } },
+ ],
+ commentCount: 42,
+ },
+};
@@ -0,0 +1,54 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { fn } from "storybook/test";
+
+import { LabelBadge } from "./LabelBadge";
+
+const meta = {
+ component: LabelBadge,
+} satisfies Meta<typeof LabelBadge>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+export const Default: Story = {
+ args: {
+ name: "bug",
+ color: { R: 252, G: 41, B: 41 },
+ },
+};
+
+export const LightBackground: Story = {
+ args: {
+ name: "enhancement",
+ color: { R: 163, G: 230, B: 53 },
+ },
+};
+
+export const DarkBackground: Story = {
+ args: {
+ name: "documentation",
+ color: { R: 30, G: 80, B: 160 },
+ },
+};
+
+export const Clickable: Story = {
+ args: {
+ name: "feature",
+ color: { R: 100, G: 200, B: 150 },
+ onClick: fn(),
+ },
+};
+
+export const AllColors: Story = {
+ args: { name: "", color: { R: 0, G: 0, B: 0 } },
+ render: () => (
+ <div className="flex flex-wrap gap-2">
+ <LabelBadge name="bug" color={{ R: 252, G: 41, B: 41 }} />
+ <LabelBadge name="enhancement" color={{ R: 163, G: 230, B: 53 }} />
+ <LabelBadge name="documentation" color={{ R: 30, G: 80, B: 160 }} />
+ <LabelBadge name="help wanted" color={{ R: 0, G: 150, B: 136 }} />
+ <LabelBadge name="wontfix" color={{ R: 200, G: 200, B: 200 }} />
+ <LabelBadge name="priority" color={{ R: 255, G: 152, B: 0 }} />
+ </div>
+ ),
+};
@@ -0,0 +1,26 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+import { Status } from "@/__generated__/graphql";
+
+import { StatusBadge } from "./StatusBadge";
+
+const meta = {
+ component: StatusBadge,
+ argTypes: {
+ status: {
+ control: "select",
+ options: [Status.Open, Status.Closed],
+ },
+ },
+} satisfies Meta<typeof StatusBadge>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+export const Open: Story = {
+ args: { status: Status.Open },
+};
+
+export const Closed: Story = {
+ args: { status: Status.Closed },
+};
@@ -0,0 +1,40 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+import { withRouter } from "@/../.storybook/decorators";
+
+import { CodeBreadcrumb } from "./CodeBreadcrumb";
+
+const meta = {
+ component: CodeBreadcrumb,
+ decorators: [withRouter],
+} satisfies Meta<typeof CodeBreadcrumb>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+export const RootPath: Story = {
+ args: {
+ repoName: "git-bug",
+ currentRef: "main",
+ path: "",
+ repo: "git-bug",
+ },
+};
+
+export const FilePath: Story = {
+ args: {
+ repoName: "git-bug",
+ currentRef: "main",
+ path: "src/components/ui/button.tsx",
+ repo: "git-bug",
+ },
+};
+
+export const DeepPath: Story = {
+ args: {
+ repoName: "git-bug",
+ currentRef: "feature/auth",
+ path: "src/components/bugs/timeline/CommentItem.tsx",
+ repo: "git-bug",
+ },
+};
@@ -0,0 +1,107 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+import { GitObjectType } from "@/__generated__/graphql";
+import { withRouter } from "@/../.storybook/decorators";
+
+import { FileTree } from "./FileTree";
+
+const meta = {
+ component: FileTree,
+ decorators: [withRouter],
+} satisfies Meta<typeof FileTree>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+const entries = [
+ {
+ name: "src",
+ type: GitObjectType.Tree,
+ hash: "abc1",
+ lastCommit: {
+ hash: "def1",
+ shortHash: "def1",
+ message: "refactor: restructure source directory",
+ date: new Date(Date.now() - 86400 * 1000).toISOString(),
+ },
+ },
+ {
+ name: "docs",
+ type: GitObjectType.Tree,
+ hash: "abc2",
+ lastCommit: {
+ hash: "def2",
+ shortHash: "def2",
+ message: "docs: update getting started guide",
+ date: new Date(Date.now() - 172800 * 1000).toISOString(),
+ },
+ },
+ {
+ name: "README.md",
+ type: GitObjectType.Blob,
+ hash: "abc3",
+ lastCommit: {
+ hash: "def3",
+ shortHash: "def3",
+ message: "docs: add badges to README",
+ date: new Date(Date.now() - 3600 * 1000).toISOString(),
+ },
+ },
+ {
+ name: "package.json",
+ type: GitObjectType.Blob,
+ hash: "abc4",
+ lastCommit: {
+ hash: "def4",
+ shortHash: "def4",
+ message: "chore: bump dependencies",
+ date: new Date(Date.now() - 7200 * 1000).toISOString(),
+ },
+ },
+ {
+ name: ".gitignore",
+ type: GitObjectType.Blob,
+ hash: "abc5",
+ },
+];
+
+export const RootDirectory: Story = {
+ args: {
+ repo: "my-repo",
+ currentRef: "main",
+ currentPath: "",
+ entries,
+ },
+};
+
+export const SubDirectory: Story = {
+ args: {
+ repo: "my-repo",
+ currentRef: "main",
+ currentPath: "src",
+ entries: [
+ {
+ name: "components",
+ type: GitObjectType.Tree,
+ hash: "abc6",
+ lastCommit: {
+ hash: "def5",
+ shortHash: "def5",
+ message: "feat: add button component",
+ date: new Date(Date.now() - 3600 * 1000).toISOString(),
+ },
+ },
+ {
+ name: "index.ts",
+ type: GitObjectType.Blob,
+ hash: "abc7",
+ lastCommit: {
+ hash: "def6",
+ shortHash: "def6",
+ message: "fix: correct export paths",
+ date: new Date(Date.now() - 7200 * 1000).toISOString(),
+ },
+ },
+ ],
+ },
+};
@@ -0,0 +1,72 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+import { FileViewer } from "./FileViewer";
+
+const meta = {
+ component: FileViewer,
+} satisfies Meta<typeof FileViewer>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+export const TypeScriptFile: Story = {
+ args: {
+ blob: {
+ text: `import { useState } from "react";
+
+interface CounterProps {
+ initial?: number;
+}
+
+export function Counter({ initial = 0 }: CounterProps) {
+ const [count, setCount] = useState(initial);
+
+ return (
+ <div>
+ <p>Count: {count}</p>
+ <button onClick={() => setCount(c => c + 1)}>
+ Increment
+ </button>
+ </div>
+ );
+}`,
+ hash: "abc123",
+ path: "src/Counter.tsx",
+ size: 312,
+ isBinary: false,
+ isTruncated: false,
+ },
+ },
+};
+
+export const BinaryFile: Story = {
+ args: {
+ blob: {
+ text: null,
+ hash: "def456",
+ path: "logo.png",
+ size: 24576,
+ isBinary: true,
+ isTruncated: false,
+ },
+ },
+};
+
+export const TruncatedFile: Story = {
+ args: {
+ blob: {
+ text: "line 1\nline 2\nline 3\n... (truncated)",
+ hash: "ghi789",
+ path: "large-file.log",
+ size: 1048576,
+ isBinary: false,
+ isTruncated: true,
+ },
+ },
+};
+
+export const Loading: Story = {
+ args: {
+ blob: null,
+ },
+};
@@ -0,0 +1,47 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import { fn } from "storybook/test";
+
+import { GitRefType } from "@/__generated__/graphql";
+
+import { RefSelector } from "./RefSelector";
+
+const meta = {
+ component: RefSelector,
+} satisfies Meta<typeof RefSelector>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+const sampleRefs = [
+ { name: "refs/heads/main", shortName: "main", type: GitRefType.Branch, hash: "abc1", isDefault: true },
+ { name: "refs/heads/develop", shortName: "develop", type: GitRefType.Branch, hash: "abc2", isDefault: false },
+ { name: "refs/heads/feature/auth", shortName: "feature/auth", type: GitRefType.Branch, hash: "abc3", isDefault: false },
+ { name: "refs/heads/fix/login", shortName: "fix/login", type: GitRefType.Branch, hash: "abc4", isDefault: false },
+ { name: "refs/tags/v1.0.0", shortName: "v1.0.0", type: GitRefType.Tag, hash: "abc5", isDefault: false },
+ { name: "refs/tags/v1.1.0", shortName: "v1.1.0", type: GitRefType.Tag, hash: "abc6", isDefault: false },
+ { name: "refs/tags/v2.0.0-rc1", shortName: "v2.0.0-rc1", type: GitRefType.Tag, hash: "abc7", isDefault: false },
+];
+
+export const Default: Story = {
+ args: {
+ refs: sampleRefs,
+ currentRef: "main",
+ onSelect: fn(),
+ },
+};
+
+export const OnTag: Story = {
+ args: {
+ refs: sampleRefs,
+ currentRef: "v1.1.0",
+ onSelect: fn(),
+ },
+};
+
+export const BranchesOnly: Story = {
+ args: {
+ refs: sampleRefs.filter((r) => r.type === GitRefType.Branch),
+ currentRef: "develop",
+ onSelect: fn(),
+ },
+};
@@ -0,0 +1,95 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+import { withRouter } from "@/../.storybook/decorators";
+
+import { Markdown } from "./Markdown";
+
+const meta = {
+ component: Markdown,
+ decorators: [withRouter],
+} satisfies Meta<typeof Markdown>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+export const BasicFormatting: Story = {
+ args: {
+ content: `# Heading 1
+## Heading 2
+### Heading 3
+
+This is a paragraph with **bold**, *italic*, and \`inline code\`.
+
+- Unordered list item 1
+- Unordered list item 2
+ - Nested item
+
+1. Ordered list item 1
+2. Ordered list item 2
+
+> This is a blockquote.
+
+---
+
+[A link](https://example.com)`,
+ },
+};
+
+export const CodeBlock: Story = {
+ args: {
+ content: `Here is a code block:
+
+\`\`\`typescript
+interface User {
+ id: string;
+ name: string;
+ email: string;
+}
+
+function greet(user: User): string {
+ return \`Hello, \${user.name}!\`;
+}
+\`\`\``,
+ },
+};
+
+export const GithubFlavored: Story = {
+ args: {
+ content: `## Task list
+
+- [x] Completed task
+- [ ] Incomplete task
+- [ ] Another task
+
+## Table
+
+| Feature | Status | Priority |
+|---------|--------|----------|
+| Auth | Done | High |
+| Search | WIP | Medium |
+| Export | Todo | Low |
+
+## Strikethrough
+
+This is ~~deleted~~ text.
+
+## Emoji
+
+:rocket: :bug: :sparkles:`,
+ },
+};
+
+export const Comment: Story = {
+ args: {
+ content: `Fixed the issue by updating the query builder.
+
+The problem was that \`buildQuery()\` wasn't escaping special characters in label names containing spaces.
+
+\`\`\`diff
+- const q = \`label:\${name}\`;
++ const q = \`label:"\${name}"\`;
+\`\`\`
+
+Closes #42.`,
+ },
+};
@@ -0,0 +1,45 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+import { Avatar, AvatarFallback, AvatarImage } from "./avatar";
+
+const meta = {
+ component: Avatar,
+} satisfies Meta<typeof Avatar>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+export const WithImage: Story = {
+ render: () => (
+ <Avatar>
+ <AvatarImage src="https://github.com/shadcn.png" alt="User" />
+ <AvatarFallback>CN</AvatarFallback>
+ </Avatar>
+ ),
+};
+
+export const WithFallback: Story = {
+ render: () => (
+ <Avatar>
+ <AvatarImage src="/broken-url.png" alt="User" />
+ <AvatarFallback>JD</AvatarFallback>
+ </Avatar>
+ ),
+};
+
+export const Small: Story = {
+ render: () => (
+ <Avatar className="size-6">
+ <AvatarFallback className="text-[8px]">AB</AvatarFallback>
+ </Avatar>
+ ),
+};
+
+export const Large: Story = {
+ render: () => (
+ <Avatar className="size-16">
+ <AvatarImage src="https://github.com/shadcn.png" alt="User" />
+ <AvatarFallback>CN</AvatarFallback>
+ </Avatar>
+ ),
+};
@@ -0,0 +1,32 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+import { Badge } from "./badge";
+
+const meta = {
+ component: Badge,
+ argTypes: {
+ variant: {
+ control: "select",
+ options: ["default", "secondary", "destructive", "outline"],
+ },
+ },
+} satisfies Meta<typeof Badge>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+export const Default: Story = {
+ args: { children: "Badge" },
+};
+
+export const Secondary: Story = {
+ args: { children: "Secondary", variant: "secondary" },
+};
+
+export const Destructive: Story = {
+ args: { children: "Destructive", variant: "destructive" },
+};
+
+export const Outline: Story = {
+ args: { children: "Outline", variant: "outline" },
+};
@@ -0,0 +1,26 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+import { Input } from "./input";
+
+const meta = {
+ component: Input,
+} satisfies Meta<typeof Input>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+export const Default: Story = {
+ args: { placeholder: "Type somethingβ¦" },
+};
+
+export const WithValue: Story = {
+ args: { defaultValue: "Hello world" },
+};
+
+export const Disabled: Story = {
+ args: { placeholder: "Disabled", disabled: true },
+};
+
+export const Password: Story = {
+ args: { type: "password", placeholder: "Enter password" },
+};
@@ -0,0 +1,42 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+import { Separator } from "./separator";
+
+const meta = {
+ component: Separator,
+ argTypes: {
+ orientation: {
+ control: "select",
+ options: ["horizontal", "vertical"],
+ },
+ },
+} satisfies Meta<typeof Separator>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+export const Horizontal: Story = {
+ args: { orientation: "horizontal" },
+ decorators: [
+ (Story) => (
+ <div className="w-64 space-y-2">
+ <p className="text-sm">Above</p>
+ <Story />
+ <p className="text-sm">Below</p>
+ </div>
+ ),
+ ],
+};
+
+export const Vertical: Story = {
+ args: { orientation: "vertical" },
+ decorators: [
+ (Story) => (
+ <div className="flex h-8 items-center gap-2">
+ <span className="text-sm">Left</span>
+ <Story />
+ <span className="text-sm">Right</span>
+ </div>
+ ),
+ ],
+};
@@ -0,0 +1,30 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+import { Skeleton } from "./skeleton";
+
+const meta = {
+ component: Skeleton,
+} satisfies Meta<typeof Skeleton>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+export const Default: Story = {
+ args: { className: "h-4 w-48" },
+};
+
+export const Circle: Story = {
+ args: { className: "size-10 rounded-full" },
+};
+
+export const Card: Story = {
+ render: () => (
+ <div className="flex items-center gap-4">
+ <Skeleton className="size-12 rounded-full" />
+ <div className="space-y-2">
+ <Skeleton className="h-4 w-48" />
+ <Skeleton className="h-4 w-32" />
+ </div>
+ </div>
+ ),
+};
@@ -0,0 +1,22 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+import { Textarea } from "./textarea";
+
+const meta = {
+ component: Textarea,
+} satisfies Meta<typeof Textarea>;
+
+export default meta;
+type Story = StoryObj<typeof meta>;
+
+export const Default: Story = {
+ args: { placeholder: "Write a commentβ¦" },
+};
+
+export const WithValue: Story = {
+ args: { defaultValue: "This is some content in the textarea." },
+};
+
+export const Disabled: Story = {
+ args: { placeholder: "Disabled", disabled: true },
+};