import { useEffect, useState, type ReactNode } from "react"; import type { Preview } from "@storybook/react-vite"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { MemoryRouter } from "@/lib/router"; import { BreadcrumbProvider } from "@/context/BreadcrumbContext"; import { CompanyProvider } from "@/context/CompanyContext"; import { DialogProvider } from "@/context/DialogContext"; import { EditorAutocompleteProvider } from "@/context/EditorAutocompleteContext"; import { PanelProvider } from "@/context/PanelContext"; import { SidebarProvider } from "@/context/SidebarContext"; import { ThemeProvider } from "@/context/ThemeContext"; import { ToastProvider } from "@/context/ToastContext"; import { TooltipProvider } from "@/components/ui/tooltip"; import { storybookAgents, storybookApprovals, storybookAuthSession, storybookCompanies, storybookDashboardSummary, storybookIssues, storybookLiveRuns, storybookProjects, storybookSecretAccessEvents, storybookSecretBindings, storybookSecretProviderConfigs, storybookSecretProviderDiscoveryPreview, storybookSecretProviderHealth, storybookSecretProviders, storybookSecrets, storybookSidebarBadges, } from "../fixtures/paperclipData"; import "@mdxeditor/editor/style.css"; import "./tailwind-entry.css"; import "./styles.css"; // Install fetch monkeypatch eagerly so any module-load-time fetches (e.g. schema // caches in adapter config renderers) hit our fixtures before they reach the // network. Some renderers issue a fetch from useEffect on first paint, which // can otherwise race the StorybookProviders mount. installStorybookApiFixtures(); function installStorybookApiFixtures() { if (typeof window === "undefined") return; const currentWindow = window as typeof window & { __paperclipStorybookFetchInstalled?: boolean; }; if (currentWindow.__paperclipStorybookFetchInstalled) return; const originalFetch = window.fetch.bind(window); currentWindow.__paperclipStorybookFetchInstalled = true; window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; const url = new URL(rawUrl, window.location.origin); if (url.pathname === "/api/auth/get-session") { return Response.json(storybookAuthSession); } if (url.pathname === "/api/companies") { return Response.json(storybookCompanies); } if (url.pathname === "/api/companies/company-storybook/user-directory") { return Response.json({ users: [ { principalId: "user-board", status: "active", user: { id: "user-board", email: "board@paperclip.local", name: "Board Operator", image: null, }, }, { principalId: "user-product", status: "active", user: { id: "user-product", email: "product@paperclip.local", name: "Product Lead", image: null, }, }, ], }); } if (url.pathname === "/api/instance/settings/experimental") { return Response.json({ enableIsolatedWorkspaces: true, autoRestartDevServerWhenIdle: false, }); } if (url.pathname === "/api/adapters") { return Response.json([ { type: "claude_local", label: "Claude Local", source: "builtin", modelsCount: 2, loaded: true, disabled: false, capabilities: { supportsInstructionsBundle: true, supportsSkills: true, supportsLocalAgentJwt: true, requiresMaterializedRuntimeSkills: false, supportsModelProfiles: true, }, }, { type: "codex_local", label: "Codex Local", source: "builtin", modelsCount: 3, loaded: true, disabled: false, capabilities: { supportsInstructionsBundle: true, supportsSkills: true, supportsLocalAgentJwt: true, requiresMaterializedRuntimeSkills: false, supportsModelProfiles: true, }, }, ]); } const adapterModelsMatch = url.pathname.match( /^\/api\/companies\/[^/]+\/adapters\/([^/]+)\/(models|model-profiles)$/, ); if (adapterModelsMatch) { const [, , resource] = adapterModelsMatch; if (resource === "models") { return Response.json([ { id: "claude-opus-4-7", label: "Claude Opus 4.7" }, { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" }, { id: "claude-haiku-4-5", label: "Claude Haiku 4.5" }, ]); } return Response.json([ { key: "cheap", label: "Cheap", adapterConfig: { model: "claude-sonnet-4-6" }, source: "adapter_default", }, ]); } if (url.pathname === "/api/plugins/ui-contributions") { return Response.json([]); } const adapterSchemaMatch = url.pathname.match(/^\/api\/adapters\/([^/]+)\/config-schema$/); if (adapterSchemaMatch) { const [, adapterType] = adapterSchemaMatch; const schemas = (window as typeof window & { __paperclipStorybookAdapterSchemas?: Record; }).__paperclipStorybookAdapterSchemas; const schema = schemas?.[adapterType]; if (schema) return Response.json(schema); } const secretsListMatch = url.pathname.match(/^\/api\/companies\/([^/]+)\/secrets$/); if (secretsListMatch) { const [, companyId] = secretsListMatch; return Response.json(companyId === "company-storybook" ? storybookSecrets : []); } const secretProvidersMatch = url.pathname.match(/^\/api\/companies\/([^/]+)\/secret-providers$/); if (secretProvidersMatch) { return Response.json(storybookSecretProviders); } const secretProviderHealthMatch = url.pathname.match( /^\/api\/companies\/([^/]+)\/secret-providers\/health$/, ); if (secretProviderHealthMatch) { return Response.json(storybookSecretProviderHealth); } const secretProviderConfigsMatch = url.pathname.match( /^\/api\/companies\/([^/]+)\/secret-provider-configs$/, ); if (secretProviderConfigsMatch) { return Response.json(storybookSecretProviderConfigs); } const secretProviderConfigDiscoveryPreviewMatch = url.pathname.match( /^\/api\/companies\/([^/]+)\/secret-provider-configs\/discovery\/preview$/, ); if (secretProviderConfigDiscoveryPreviewMatch && init?.method?.toUpperCase() === "POST") { return Response.json(storybookSecretProviderDiscoveryPreview); } const secretUsageMatch = url.pathname.match(/^\/api\/secrets\/([^/]+)\/usage$/); if (secretUsageMatch) { const [, secretId] = secretUsageMatch; return Response.json({ secretId, bindings: storybookSecretBindings.filter((binding) => binding.secretId === secretId), }); } const secretEventsMatch = url.pathname.match(/^\/api\/secrets\/([^/]+)\/access-events$/); if (secretEventsMatch) { const [, secretId] = secretEventsMatch; return Response.json(storybookSecretAccessEvents.filter((event) => event.secretId === secretId)); } const companyResourceMatch = url.pathname.match(/^\/api\/companies\/([^/]+)\/([^/]+)$/); if (companyResourceMatch) { const [, companyId, resource] = companyResourceMatch; if (resource === "agents") { return Response.json(companyId === "company-storybook" ? storybookAgents : []); } if (resource === "projects") { return Response.json(companyId === "company-storybook" ? storybookProjects : []); } if (resource === "approvals") { return Response.json(companyId === "company-storybook" ? storybookApprovals : []); } if (resource === "dashboard") { return Response.json({ ...storybookDashboardSummary, companyId, }); } if (resource === "heartbeat-runs") { return Response.json([]); } if (resource === "live-runs") { return Response.json(companyId === "company-storybook" ? storybookLiveRuns : []); } if (resource === "inbox-dismissals") { return Response.json([]); } if (resource === "sidebar-badges") { return Response.json( companyId === "company-storybook" ? storybookSidebarBadges : { inbox: 0, approvals: 0, failedRuns: 0, joinRequests: 0 }, ); } if (resource === "join-requests") { return Response.json([]); } if (resource === "issues") { const query = url.searchParams.get("q")?.trim().toLowerCase(); const issues = companyId === "company-storybook" ? storybookIssues : []; return Response.json( query ? issues.filter((issue) => `${issue.identifier ?? ""} ${issue.title} ${issue.description ?? ""}`.toLowerCase().includes(query), ) : issues, ); } } if (url.pathname.startsWith("/api/invites/") && url.pathname.endsWith("/logo")) { return new Response(null, { status: 204 }); } return originalFetch(input, init); }; } // Install fetch fixtures at module load so React Query never sees a real network failure. if (typeof window !== "undefined") { installStorybookApiFixtures(); } function applyStorybookTheme(theme: "light" | "dark") { if (typeof document === "undefined") return; document.documentElement.classList.toggle("dark", theme === "dark"); document.documentElement.style.colorScheme = theme; } function StorybookProviders({ children, theme, }: { children: ReactNode; theme: "light" | "dark"; }) { const [queryClient] = useState( () => new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Number.POSITIVE_INFINITY, }, }, }), ); if (typeof window !== "undefined") { installStorybookApiFixtures(); } useEffect(() => { applyStorybookTheme(theme); }, [theme]); return ( {children} ); } const preview: Preview = { decorators: [ (Story, context) => { const theme = context.globals.theme === "light" ? "light" : "dark"; return ( ); }, ], globalTypes: { theme: { description: "Paperclip color mode", defaultValue: "dark", toolbar: { title: "Theme", icon: "mirror", items: [ { value: "dark", title: "Dark" }, { value: "light", title: "Light" }, ], dynamicTitle: true, }, }, }, parameters: { actions: { argTypesRegex: "^on[A-Z].*" }, a11y: { test: "error", }, backgrounds: { disable: true, }, controls: { expanded: true, matchers: { color: /(background|color)$/i, date: /Date$/i, }, }, docs: { toc: true, }, layout: "fullscreen", viewport: { viewports: { mobile: { name: "Mobile", styles: { width: "390px", height: "844px" }, }, tablet: { name: "Tablet", styles: { width: "834px", height: "1112px" }, }, desktop: { name: "Desktop", styles: { width: "1440px", height: "960px" }, }, }, }, }, }; export default preview;