mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 16:27:18 +02:00
feat(ui): add Storybook setup with foundation and product stories
Set up Storybook 10 with React-Vite, Tailwind, dark/light theme toggle, and comprehensive provider wrapping. Includes five story files covering UI primitives, status language, control-plane surfaces, and converted UX lab pages with fixture-backed data. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
7a329fb8bb
commit
fcf2b8efff
16 files changed
+2291
-17
No files matched your search
@@ -43,6 +43,17 @@ This starts:
|
||||
|
||||
`pnpm dev` and `pnpm dev:once` are now idempotent for the current repo and instance: if the matching Paperclip dev runner is already alive, Paperclip reports the existing process instead of starting a duplicate.
|
||||
|
||||
## Storybook
|
||||
|
||||
The board UI Storybook keeps stories and Storybook config under `ui/storybook/` so component review files stay out of the app source routes.
|
||||
|
||||
```sh
|
||||
pnpm storybook
|
||||
pnpm build-storybook
|
||||
```
|
||||
|
||||
These run the `@paperclipai/ui` Storybook on port `6006` and build the static output to `ui/storybook-static/`.
|
||||
|
||||
Inspect or stop the current repo's managed dev runner:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"dev:stop": "pnpm --filter @paperclipai/server exec tsx ../scripts/dev-service.ts stop",
|
||||
"dev:server": "pnpm --filter @paperclipai/server dev",
|
||||
"dev:ui": "pnpm --filter @paperclipai/ui dev",
|
||||
"storybook": "pnpm --filter @paperclipai/ui storybook",
|
||||
"build-storybook": "pnpm --filter @paperclipai/ui build-storybook",
|
||||
"build": "pnpm run preflight:workspace-links && pnpm -r build",
|
||||
"typecheck": "pnpm run preflight:workspace-links && pnpm -r typecheck",
|
||||
"test": "pnpm run test:run",
|
||||
|
||||
Generated
+616
-7
File diff suppressed because it is too large.
Load diff
@@ -6,6 +6,15 @@ Published static assets for the Paperclip board UI.
|
||||
|
||||
The npm package contains the production build under `dist/`. It does not ship the UI source tree or workspace-only dependencies.
|
||||
|
||||
## Storybook
|
||||
|
||||
Storybook config, stories, and fixtures live under `ui/storybook/`.
|
||||
|
||||
```sh
|
||||
pnpm --filter @paperclipai/ui storybook
|
||||
pnpm --filter @paperclipai/ui build-storybook
|
||||
```
|
||||
|
||||
## Typical use
|
||||
|
||||
Install the package, then serve or copy the built files from `node_modules/@paperclipai/ui/dist`.
|
||||
+7
-1
@@ -16,9 +16,11 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"storybook": "storybook dev -p 6006 -c storybook/.storybook",
|
||||
"build-storybook": "storybook build -c storybook/.storybook -o storybook-static",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc -b",
|
||||
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
||||
"clean": "rm -rf dist storybook-static tsconfig.tsbuildinfo",
|
||||
"prepack": "rm -f package.dev.json && cp package.json package.dev.json && node ../scripts/generate-ui-package-json.mjs",
|
||||
"postpack": "if [ -f package.dev.json ]; then mv package.dev.json package.json; fi"
|
||||
},
|
||||
@@ -61,12 +63,16 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.7",
|
||||
"@storybook/addon-a11y": "10.3.5",
|
||||
"@storybook/addon-docs": "10.3.5",
|
||||
"@storybook/react-vite": "10.3.5",
|
||||
"@types/node": "^25.2.3",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"tailwindcss": "^4.0.7",
|
||||
"typescript": "^5.7.3",
|
||||
"storybook": "10.3.5",
|
||||
"vite": "^6.1.0",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
|
||||
@@ -40,9 +40,6 @@ import { PluginManager } from "./pages/PluginManager";
|
||||
import { PluginSettings } from "./pages/PluginSettings";
|
||||
import { AdapterManager } from "./pages/AdapterManager";
|
||||
import { PluginPage } from "./pages/PluginPage";
|
||||
import { IssueChatUxLab } from "./pages/IssueChatUxLab";
|
||||
import { InviteUxLab } from "./pages/InviteUxLab";
|
||||
import { RunTranscriptUxLab } from "./pages/RunTranscriptUxLab";
|
||||
import { OrgChart } from "./pages/OrgChart";
|
||||
import { NewAgent } from "./pages/NewAgent";
|
||||
import { AuthPage } from "./pages/Auth";
|
||||
@@ -122,9 +119,6 @@ function boardRoutes() {
|
||||
<Route path="inbox/new" element={<Navigate to="/inbox/mine" replace />} />
|
||||
<Route path="u/:userSlug" element={<UserProfile />} />
|
||||
<Route path="design-guide" element={<DesignGuide />} />
|
||||
<Route path="tests/ux/chat" element={<IssueChatUxLab />} />
|
||||
<Route path="tests/ux/invites" element={<InviteUxLab />} />
|
||||
<Route path="tests/ux/runs" element={<RunTranscriptUxLab />} />
|
||||
<Route path="instance/settings/adapters" element={<AdapterManager />} />
|
||||
<Route path=":pluginRoutePath" element={<PluginPage />} />
|
||||
<Route path="*" element={<NotFoundPage scope="board" />} />
|
||||
@@ -303,8 +297,6 @@ export function App() {
|
||||
<Route path="execution-workspaces/:workspaceId/configuration" element={<UnprefixedBoardRedirect />} />
|
||||
<Route path="execution-workspaces/:workspaceId/runtime-logs" element={<UnprefixedBoardRedirect />} />
|
||||
<Route path="execution-workspaces/:workspaceId/issues" element={<UnprefixedBoardRedirect />} />
|
||||
<Route path="tests/ux/chat" element={<UnprefixedBoardRedirect />} />
|
||||
<Route path="tests/ux/runs" element={<UnprefixedBoardRedirect />} />
|
||||
<Route path=":companyPrefix" element={<Layout />}>
|
||||
{boardRoutes()}
|
||||
</Route>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { StorybookConfig } from "@storybook/react-vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import { mergeConfig } from "vite";
|
||||
|
||||
const storybookConfigDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../stories/**/*.stories.@(ts|tsx|mdx)"],
|
||||
staticDirs: ["../../public"],
|
||||
addons: ["@storybook/addon-docs", "@storybook/addon-a11y"],
|
||||
framework: {
|
||||
name: "@storybook/react-vite",
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: true,
|
||||
},
|
||||
viteFinal: async (baseConfig) =>
|
||||
mergeConfig(baseConfig, {
|
||||
plugins: [tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(storybookConfigDir, "../../src"),
|
||||
lexical: path.resolve(storybookConfigDir, "../../node_modules/lexical/Lexical.mjs"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,189 @@
|
||||
import { 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 "@mdxeditor/editor/style.css";
|
||||
import "@/index.css";
|
||||
import "./styles.css";
|
||||
|
||||
const storybookCompany = {
|
||||
id: "company-storybook",
|
||||
name: "Paperclip Storybook",
|
||||
description: "Fixture company for isolated UI review.",
|
||||
status: "active",
|
||||
pauseReason: null,
|
||||
pausedAt: null,
|
||||
issuePrefix: "PAP",
|
||||
issueCounter: 1641,
|
||||
budgetMonthlyCents: 250_000,
|
||||
spentMonthlyCents: 67_500,
|
||||
requireBoardApprovalForNewAgents: true,
|
||||
feedbackDataSharingEnabled: true,
|
||||
feedbackDataSharingConsentAt: null,
|
||||
feedbackDataSharingConsentByUserId: null,
|
||||
feedbackDataSharingTermsVersion: null,
|
||||
brandColor: "#0f766e",
|
||||
logoAssetId: null,
|
||||
logoUrl: null,
|
||||
createdAt: new Date("2026-04-01T09:00:00.000Z"),
|
||||
updatedAt: new Date("2026-04-20T12:00:00.000Z"),
|
||||
};
|
||||
|
||||
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/companies") {
|
||||
return Response.json([storybookCompany]);
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/api/invites/") && url.pathname.endsWith("/logo")) {
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
return originalFetch(input, init);
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
applyStorybookTheme(theme);
|
||||
installStorybookApiFixtures();
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<MemoryRouter initialEntries={["/PAP/storybook"]}>
|
||||
<CompanyProvider>
|
||||
<EditorAutocompleteProvider>
|
||||
<ToastProvider>
|
||||
<TooltipProvider>
|
||||
<BreadcrumbProvider>
|
||||
<SidebarProvider>
|
||||
<PanelProvider>
|
||||
<DialogProvider>{children}</DialogProvider>
|
||||
</PanelProvider>
|
||||
</SidebarProvider>
|
||||
</BreadcrumbProvider>
|
||||
</TooltipProvider>
|
||||
</ToastProvider>
|
||||
</EditorAutocompleteProvider>
|
||||
</CompanyProvider>
|
||||
</MemoryRouter>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const preview: Preview = {
|
||||
decorators: [
|
||||
(Story, context) => {
|
||||
const theme = context.globals.theme === "light" ? "light" : "dark";
|
||||
return (
|
||||
<StorybookProviders key={theme} theme={theme}>
|
||||
<Story />
|
||||
</StorybookProviders>
|
||||
);
|
||||
},
|
||||
],
|
||||
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: "todo",
|
||||
},
|
||||
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;
|
||||
@@ -0,0 +1,49 @@
|
||||
html,
|
||||
body,
|
||||
#storybook-root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: auto;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.sb-show-main {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.paperclip-story {
|
||||
min-height: 100vh;
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in oklab, var(--muted) 28%, transparent), transparent 340px),
|
||||
var(--background);
|
||||
color: var(--foreground);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.paperclip-story__inner {
|
||||
margin: 0 auto;
|
||||
max-width: 1180px;
|
||||
}
|
||||
|
||||
.paperclip-story__frame {
|
||||
border: 1px solid var(--border);
|
||||
background: color-mix(in oklab, var(--card) 86%, transparent);
|
||||
box-shadow: 0 24px 90px color-mix(in oklab, black 16%, transparent);
|
||||
}
|
||||
|
||||
.paperclip-story__label {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.paperclip-story {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
import type {
|
||||
ActivityEvent,
|
||||
Agent,
|
||||
Approval,
|
||||
BudgetPolicySummary,
|
||||
Issue,
|
||||
} from "@paperclipai/shared";
|
||||
|
||||
const now = new Date("2026-04-20T12:00:00.000Z");
|
||||
const recent = (minutesAgo: number) => new Date(now.getTime() - minutesAgo * 60_000);
|
||||
|
||||
export const storybookAgents: Agent[] = [
|
||||
{
|
||||
id: "agent-codex",
|
||||
companyId: "company-storybook",
|
||||
name: "CodexCoder",
|
||||
urlKey: "codexcoder",
|
||||
role: "engineer",
|
||||
title: "Senior Product Engineer",
|
||||
icon: "code",
|
||||
status: "running",
|
||||
reportsTo: "agent-cto",
|
||||
capabilities: "Ships full-stack Paperclip product tasks, Storybook coverage, and verification.",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
budgetMonthlyCents: 125_000,
|
||||
spentMonthlyCents: 43_200,
|
||||
pauseReason: null,
|
||||
pausedAt: null,
|
||||
permissions: { canCreateAgents: false },
|
||||
lastHeartbeatAt: recent(3),
|
||||
metadata: null,
|
||||
createdAt: recent(12_000),
|
||||
updatedAt: recent(3),
|
||||
},
|
||||
{
|
||||
id: "agent-qa",
|
||||
companyId: "company-storybook",
|
||||
name: "QAChecker",
|
||||
urlKey: "qachecker",
|
||||
role: "qa",
|
||||
title: "QA Engineer",
|
||||
icon: "shield",
|
||||
status: "idle",
|
||||
reportsTo: "agent-cto",
|
||||
capabilities: "Validates browser flows, acceptance criteria, and release smoke tests.",
|
||||
adapterType: "claude_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
budgetMonthlyCents: 80_000,
|
||||
spentMonthlyCents: 18_900,
|
||||
pauseReason: null,
|
||||
pausedAt: null,
|
||||
permissions: { canCreateAgents: false },
|
||||
lastHeartbeatAt: recent(24),
|
||||
metadata: null,
|
||||
createdAt: recent(11_000),
|
||||
updatedAt: recent(24),
|
||||
},
|
||||
{
|
||||
id: "agent-cto",
|
||||
companyId: "company-storybook",
|
||||
name: "CTO",
|
||||
urlKey: "cto",
|
||||
role: "cto",
|
||||
title: "CTO",
|
||||
icon: "crown",
|
||||
status: "active",
|
||||
reportsTo: null,
|
||||
capabilities: "Reviews architecture, quality gates, and engineering priority tradeoffs.",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: {},
|
||||
runtimeConfig: {},
|
||||
budgetMonthlyCents: 200_000,
|
||||
spentMonthlyCents: 54_000,
|
||||
pauseReason: null,
|
||||
pausedAt: null,
|
||||
permissions: { canCreateAgents: true },
|
||||
lastHeartbeatAt: recent(41),
|
||||
metadata: null,
|
||||
createdAt: recent(14_000),
|
||||
updatedAt: recent(41),
|
||||
},
|
||||
];
|
||||
|
||||
export const storybookAgentMap = new Map(storybookAgents.map((agent) => [agent.id, agent]));
|
||||
|
||||
export function createIssue(overrides: Partial<Issue> = {}): Issue {
|
||||
return {
|
||||
id: "issue-storybook-1",
|
||||
companyId: "company-storybook",
|
||||
projectId: "project-board-ui",
|
||||
projectWorkspaceId: "workspace-board-ui",
|
||||
goalId: "goal-company",
|
||||
parentId: null,
|
||||
title: "Create super-detailed storybooks for the project",
|
||||
description: "Set up Storybook and move UX review surfaces into stories.",
|
||||
status: "in_progress",
|
||||
priority: "high",
|
||||
assigneeAgentId: "agent-codex",
|
||||
assigneeUserId: null,
|
||||
checkoutRunId: "run-storybook",
|
||||
executionRunId: "run-storybook",
|
||||
executionAgentNameKey: "codexcoder",
|
||||
executionLockedAt: recent(28),
|
||||
createdByAgentId: null,
|
||||
createdByUserId: "user-board",
|
||||
issueNumber: 1641,
|
||||
identifier: "PAP-1641",
|
||||
requestDepth: 0,
|
||||
billingCode: "product",
|
||||
assigneeAdapterOverrides: null,
|
||||
executionWorkspaceId: "execution-workspace-storybook",
|
||||
executionWorkspacePreference: "project",
|
||||
executionWorkspaceSettings: null,
|
||||
startedAt: recent(28),
|
||||
completedAt: null,
|
||||
cancelledAt: null,
|
||||
hiddenAt: null,
|
||||
labelIds: ["label-ui", "label-docs"],
|
||||
labels: [
|
||||
{
|
||||
id: "label-ui",
|
||||
companyId: "company-storybook",
|
||||
name: "UI",
|
||||
color: "#0f766e",
|
||||
createdAt: recent(20_000),
|
||||
updatedAt: recent(20_000),
|
||||
},
|
||||
{
|
||||
id: "label-docs",
|
||||
companyId: "company-storybook",
|
||||
name: "Design system",
|
||||
color: "#f59e0b",
|
||||
createdAt: recent(20_000),
|
||||
updatedAt: recent(20_000),
|
||||
},
|
||||
],
|
||||
blockedBy: [],
|
||||
blocks: [],
|
||||
planDocument: null,
|
||||
documentSummaries: [],
|
||||
legacyPlanDocument: null,
|
||||
project: null,
|
||||
goal: null,
|
||||
currentExecutionWorkspace: null,
|
||||
workProducts: [],
|
||||
mentionedProjects: [],
|
||||
myLastTouchAt: recent(8),
|
||||
lastExternalCommentAt: recent(70),
|
||||
lastActivityAt: recent(3),
|
||||
isUnreadForMe: true,
|
||||
createdAt: recent(90),
|
||||
updatedAt: recent(3),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export const storybookIssues: Issue[] = [
|
||||
createIssue(),
|
||||
createIssue({
|
||||
id: "issue-storybook-2",
|
||||
title: "Add budget hard-stop incident review",
|
||||
status: "blocked",
|
||||
priority: "critical",
|
||||
assigneeAgentId: "agent-qa",
|
||||
checkoutRunId: null,
|
||||
executionRunId: null,
|
||||
executionLockedAt: null,
|
||||
startedAt: null,
|
||||
identifier: "PAP-1528",
|
||||
issueNumber: 1528,
|
||||
lastActivityAt: recent(18),
|
||||
}),
|
||||
createIssue({
|
||||
id: "issue-storybook-3",
|
||||
title: "QA invite flow on authenticated private mode",
|
||||
status: "in_review",
|
||||
priority: "medium",
|
||||
assigneeAgentId: null,
|
||||
assigneeUserId: "user-board",
|
||||
checkoutRunId: null,
|
||||
executionRunId: null,
|
||||
executionLockedAt: null,
|
||||
identifier: "PAP-1602",
|
||||
issueNumber: 1602,
|
||||
completedAt: null,
|
||||
lastActivityAt: recent(49),
|
||||
isUnreadForMe: false,
|
||||
}),
|
||||
];
|
||||
|
||||
export const storybookApprovals: Approval[] = [
|
||||
{
|
||||
id: "approval-hire-designer",
|
||||
companyId: "company-storybook",
|
||||
type: "hire_agent",
|
||||
requestedByAgentId: "agent-cto",
|
||||
requestedByUserId: null,
|
||||
status: "pending",
|
||||
payload: {
|
||||
name: "DesignSystemCoder",
|
||||
role: "designer",
|
||||
title: "Design System Engineer",
|
||||
icon: "sparkles",
|
||||
adapterType: "codex_local",
|
||||
capabilities: "Owns component quality, visual regression stories, and design-system consolidation.",
|
||||
desiredSkills: ["frontend-design", "web-design-guidelines"],
|
||||
},
|
||||
decisionNote: null,
|
||||
decidedByUserId: null,
|
||||
decidedAt: null,
|
||||
createdAt: recent(42),
|
||||
updatedAt: recent(42),
|
||||
},
|
||||
{
|
||||
id: "approval-budget",
|
||||
companyId: "company-storybook",
|
||||
type: "budget_override_required",
|
||||
requestedByAgentId: "agent-codex",
|
||||
requestedByUserId: null,
|
||||
status: "revision_requested",
|
||||
payload: {
|
||||
scopeName: "Paperclip App",
|
||||
scopeType: "project",
|
||||
windowKind: "calendar_month_utc",
|
||||
metric: "billed_cents",
|
||||
budgetAmount: 120_000,
|
||||
observedAmount: 131_400,
|
||||
guidance: "Raise the project budget only after current release smoke checks are green.",
|
||||
},
|
||||
decisionNote: "Need a tighter verification list before approving more spend.",
|
||||
decidedByUserId: "user-board",
|
||||
decidedAt: recent(12),
|
||||
createdAt: recent(75),
|
||||
updatedAt: recent(12),
|
||||
},
|
||||
{
|
||||
id: "approval-board",
|
||||
companyId: "company-storybook",
|
||||
type: "request_board_approval",
|
||||
requestedByAgentId: "agent-cto",
|
||||
requestedByUserId: null,
|
||||
status: "approved",
|
||||
payload: {
|
||||
title: "Publish the Storybook preview for design review",
|
||||
summary: "Build the static Storybook and attach the generated URL to the release issue.",
|
||||
recommendedAction: "Approve publishing the preview for internal board review.",
|
||||
nextActionOnApproval: "Run build-storybook, upload the static artifact, and request QA visual review.",
|
||||
risks: [
|
||||
"Stories may expose stale fixture copy if not maintained with UI changes.",
|
||||
"Preview screenshots can hide responsive regressions unless mobile viewports are checked.",
|
||||
],
|
||||
},
|
||||
decisionNote: "Approved for internal-only review.",
|
||||
decidedByUserId: "user-board",
|
||||
decidedAt: recent(4),
|
||||
createdAt: recent(55),
|
||||
updatedAt: recent(4),
|
||||
},
|
||||
];
|
||||
|
||||
export const storybookBudgetSummaries: BudgetPolicySummary[] = [
|
||||
{
|
||||
policyId: "budget-company-ok",
|
||||
companyId: "company-storybook",
|
||||
scopeType: "company",
|
||||
scopeId: "company-storybook",
|
||||
scopeName: "Paperclip Storybook",
|
||||
metric: "billed_cents",
|
||||
windowKind: "calendar_month_utc",
|
||||
amount: 250_000,
|
||||
observedAmount: 67_500,
|
||||
remainingAmount: 182_500,
|
||||
utilizationPercent: 27,
|
||||
warnPercent: 80,
|
||||
hardStopEnabled: true,
|
||||
notifyEnabled: true,
|
||||
isActive: true,
|
||||
status: "ok",
|
||||
paused: false,
|
||||
pauseReason: null,
|
||||
windowStart: new Date("2026-04-01T00:00:00.000Z"),
|
||||
windowEnd: new Date("2026-05-01T00:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
policyId: "budget-project-warning",
|
||||
companyId: "company-storybook",
|
||||
scopeType: "project",
|
||||
scopeId: "project-board-ui",
|
||||
scopeName: "Paperclip App",
|
||||
metric: "billed_cents",
|
||||
windowKind: "calendar_month_utc",
|
||||
amount: 120_000,
|
||||
observedAmount: 103_100,
|
||||
remainingAmount: 16_900,
|
||||
utilizationPercent: 86,
|
||||
warnPercent: 80,
|
||||
hardStopEnabled: true,
|
||||
notifyEnabled: true,
|
||||
isActive: true,
|
||||
status: "warning",
|
||||
paused: false,
|
||||
pauseReason: null,
|
||||
windowStart: new Date("2026-04-01T00:00:00.000Z"),
|
||||
windowEnd: new Date("2026-05-01T00:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
policyId: "budget-agent-hard-stop",
|
||||
companyId: "company-storybook",
|
||||
scopeType: "agent",
|
||||
scopeId: "agent-codex",
|
||||
scopeName: "CodexCoder",
|
||||
metric: "billed_cents",
|
||||
windowKind: "calendar_month_utc",
|
||||
amount: 40_000,
|
||||
observedAmount: 43_200,
|
||||
remainingAmount: 0,
|
||||
utilizationPercent: 108,
|
||||
warnPercent: 80,
|
||||
hardStopEnabled: true,
|
||||
notifyEnabled: true,
|
||||
isActive: true,
|
||||
status: "hard_stop",
|
||||
paused: true,
|
||||
pauseReason: "budget",
|
||||
windowStart: new Date("2026-04-01T00:00:00.000Z"),
|
||||
windowEnd: new Date("2026-05-01T00:00:00.000Z"),
|
||||
},
|
||||
];
|
||||
|
||||
export const storybookActivityEvents: ActivityEvent[] = [
|
||||
{
|
||||
id: "activity-1",
|
||||
companyId: "company-storybook",
|
||||
actorType: "agent",
|
||||
actorId: "agent-codex",
|
||||
action: "issue.status_changed",
|
||||
entityType: "issue",
|
||||
entityId: "issue-storybook-1",
|
||||
agentId: "agent-codex",
|
||||
runId: "run-storybook",
|
||||
details: { from: "todo", to: "in_progress" },
|
||||
createdAt: recent(28),
|
||||
},
|
||||
{
|
||||
id: "activity-2",
|
||||
companyId: "company-storybook",
|
||||
actorType: "user",
|
||||
actorId: "user-board",
|
||||
action: "approval.revision_requested",
|
||||
entityType: "approval",
|
||||
entityId: "approval-budget",
|
||||
agentId: null,
|
||||
runId: null,
|
||||
details: { type: "budget_override_required" },
|
||||
createdAt: recent(12),
|
||||
},
|
||||
{
|
||||
id: "activity-3",
|
||||
companyId: "company-storybook",
|
||||
actorType: "system",
|
||||
actorId: "system",
|
||||
action: "budget.hard_stop",
|
||||
entityType: "agent",
|
||||
entityId: "agent-codex",
|
||||
agentId: "agent-codex",
|
||||
runId: null,
|
||||
details: { observedAmount: 43_200, budgetAmount: 40_000 },
|
||||
createdAt: recent(7),
|
||||
},
|
||||
];
|
||||
|
||||
export const storybookEntityNameMap = new Map<string, string>([
|
||||
["issue:issue-storybook-1", "PAP-1641"],
|
||||
["issue:issue-storybook-2", "PAP-1528"],
|
||||
["issue:issue-storybook-3", "PAP-1602"],
|
||||
["approval:approval-budget", "Budget override"],
|
||||
["agent:agent-codex", "CodexCoder"],
|
||||
["agent:agent-qa", "QAChecker"],
|
||||
["agent:agent-cto", "CTO"],
|
||||
]);
|
||||
|
||||
export const storybookEntityTitleMap = new Map<string, string>([
|
||||
["issue:issue-storybook-1", "Create super-detailed storybooks for the project"],
|
||||
["issue:issue-storybook-2", "Add budget hard-stop incident review"],
|
||||
["issue:issue-storybook-3", "QA invite flow on authenticated private mode"],
|
||||
]);
|
||||
@@ -0,0 +1,266 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { AlertTriangle, CheckCircle2, Clock3, Eye, GitPullRequest, Inbox, WalletCards } from "lucide-react";
|
||||
import { ActivityRow } from "@/components/ActivityRow";
|
||||
import { ApprovalCard } from "@/components/ApprovalCard";
|
||||
import { BudgetPolicyCard } from "@/components/BudgetPolicyCard";
|
||||
import { Identity } from "@/components/Identity";
|
||||
import { IssueRow } from "@/components/IssueRow";
|
||||
import { PriorityIcon } from "@/components/PriorityIcon";
|
||||
import { StatusBadge } from "@/components/StatusBadge";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
storybookActivityEvents,
|
||||
storybookAgentMap,
|
||||
storybookAgents,
|
||||
storybookApprovals,
|
||||
storybookBudgetSummaries,
|
||||
storybookEntityNameMap,
|
||||
storybookEntityTitleMap,
|
||||
storybookIssues,
|
||||
} from "../fixtures/paperclipData";
|
||||
|
||||
function Section({
|
||||
eyebrow,
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="paperclip-story__frame overflow-hidden">
|
||||
<div className="border-b border-border px-5 py-4">
|
||||
<div className="paperclip-story__label">{eyebrow}</div>
|
||||
<h2 className="mt-1 text-xl font-semibold">{title}</h2>
|
||||
</div>
|
||||
<div className="p-5">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ControlPlaneSurfaces() {
|
||||
return (
|
||||
<div className="paperclip-story">
|
||||
<main className="paperclip-story__inner space-y-6">
|
||||
<section className="paperclip-story__frame p-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-5">
|
||||
<div>
|
||||
<div className="paperclip-story__label">Product surfaces</div>
|
||||
<h1 className="mt-2 text-3xl font-semibold tracking-tight">Control-plane boards and cards</h1>
|
||||
<p className="mt-3 max-w-3xl text-sm leading-6 text-muted-foreground">
|
||||
Paperclip's common board surfaces are deliberately dense: task rows, approvals, budget policy cards,
|
||||
and audit rows all need to scan quickly while preserving enough state to make autonomous work governable.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline">company scoped</Badge>
|
||||
<Badge variant="outline">single assignee</Badge>
|
||||
<Badge variant="outline">auditable</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Section eyebrow="Issues" title="Inbox/task rows across selection and unread states">
|
||||
<div className="overflow-hidden rounded-xl border border-border bg-background/70">
|
||||
{storybookIssues.map((issue, index) => (
|
||||
<IssueRow
|
||||
key={issue.id}
|
||||
issue={issue}
|
||||
selected={index === 0}
|
||||
unreadState={index === 0 ? "visible" : index === 1 ? "hidden" : null}
|
||||
onMarkRead={() => undefined}
|
||||
onArchive={() => undefined}
|
||||
desktopTrailing={
|
||||
<span className="hidden items-center gap-2 lg:inline-flex">
|
||||
<PriorityIcon priority={issue.priority} showLabel />
|
||||
{issue.assigneeAgentId ? (
|
||||
<Identity name={storybookAgentMap.get(issue.assigneeAgentId)?.name ?? "Unassigned"} size="sm" />
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">Board</span>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
trailingMeta={index === 0 ? "3m ago" : index === 1 ? "blocked by budget" : "review requested"}
|
||||
mobileMeta={<StatusBadge status={issue.status} />}
|
||||
titleSuffix={
|
||||
index === 0 ? (
|
||||
<span className="ml-2 inline-flex align-middle">
|
||||
<Badge variant="secondary">Storybook</Badge>
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section eyebrow="Approvals" title="Governance cards for pending, revision, and approved decisions">
|
||||
<div className="grid gap-5 xl:grid-cols-3">
|
||||
{storybookApprovals.map((approval) => (
|
||||
<ApprovalCard
|
||||
key={approval.id}
|
||||
approval={approval}
|
||||
requesterAgent={approval.requestedByAgentId ? storybookAgentMap.get(approval.requestedByAgentId) ?? null : null}
|
||||
onApprove={approval.status === "pending" ? () => undefined : undefined}
|
||||
onReject={approval.status === "pending" ? () => undefined : undefined}
|
||||
detailLink={`/approvals/${approval.id}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section eyebrow="Budgets" title="Healthy, warning, and hard-stop budget policy cards">
|
||||
<div className="grid gap-5 xl:grid-cols-3">
|
||||
{storybookBudgetSummaries.map((summary, index) => (
|
||||
<BudgetPolicyCard
|
||||
key={summary.policyId}
|
||||
summary={summary}
|
||||
compact={index === 0}
|
||||
onSave={index === 1 ? () => undefined : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-5 rounded-xl border border-border bg-background/70 p-5">
|
||||
<BudgetPolicyCard
|
||||
summary={storybookBudgetSummaries[2]!}
|
||||
variant="plain"
|
||||
onSave={() => undefined}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section eyebrow="Activity" title="Audit rows with agent, user, and system actors">
|
||||
<div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<div className="overflow-hidden rounded-xl border border-border bg-background/70">
|
||||
{storybookActivityEvents.map((event) => (
|
||||
<ActivityRow
|
||||
key={event.id}
|
||||
event={event}
|
||||
agentMap={storybookAgentMap}
|
||||
entityNameMap={storybookEntityNameMap}
|
||||
entityTitleMap={storybookEntityTitleMap}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Card className="shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Run summary card language</CardTitle>
|
||||
<CardDescription>Compact status treatments used around live work.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{[
|
||||
{ icon: Clock3, label: "Running", detail: "CodexCoder is editing Storybook fixtures", tone: "text-cyan-600" },
|
||||
{ icon: GitPullRequest, label: "Review", detail: "QAChecker requested browser screenshots", tone: "text-amber-600" },
|
||||
{ icon: CheckCircle2, label: "Verified", detail: "Vitest and static Storybook build passed", tone: "text-emerald-600" },
|
||||
{ icon: AlertTriangle, label: "Blocked", detail: "Budget hard stop paused a run", tone: "text-red-600" },
|
||||
].map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<div key={item.label} className="flex items-start gap-3 rounded-lg border border-border bg-background/70 p-3">
|
||||
<Icon className={`mt-0.5 h-4 w-4 ${item.tone}`} />
|
||||
<div>
|
||||
<div className="text-sm font-medium">{item.label}</div>
|
||||
<div className="text-xs leading-5 text-muted-foreground">{item.detail}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section eyebrow="Agents" title="Org snippets and quick scan identity">
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{storybookAgents.map((agent) => (
|
||||
<Card key={agent.id} className="shadow-none">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<Identity name={agent.name} size="lg" />
|
||||
<StatusBadge status={agent.status} />
|
||||
</div>
|
||||
<CardDescription>{agent.title}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm">
|
||||
<p className="leading-6 text-muted-foreground">{agent.capabilities}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline">{agent.role}</Badge>
|
||||
<Badge variant="outline">{agent.adapterType}</Badge>
|
||||
<Badge variant="outline" className="gap-1">
|
||||
<WalletCards className="h-3 w-3" />
|
||||
${(agent.spentMonthlyCents / 100).toFixed(0)} spent
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section eyebrow="Quicklook" title="Side-panel density reference">
|
||||
<div className="grid gap-4 lg:grid-cols-[320px_minmax(0,1fr)]">
|
||||
<Card className="shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Inbox className="h-4 w-4" />
|
||||
Inbox slice
|
||||
</CardTitle>
|
||||
<CardDescription>Small panels should keep controls reachable without nested cards.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span>Unread</span>
|
||||
<span className="font-mono">7</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span>Needs review</span>
|
||||
<span className="font-mono">3</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span>Blocked</span>
|
||||
<span className="font-mono">1</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="rounded-xl border border-border bg-background/70 p-5">
|
||||
<div className="mb-4 flex items-center gap-2 text-sm font-medium">
|
||||
<Eye className="h-4 w-4 text-muted-foreground" />
|
||||
Review target
|
||||
</div>
|
||||
<IssueRow
|
||||
issue={storybookIssues[0]!}
|
||||
selected
|
||||
unreadState="visible"
|
||||
onMarkRead={() => undefined}
|
||||
desktopTrailing={<PriorityIcon priority="high" showLabel />}
|
||||
trailingMeta="active run"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Product/Control Plane Surfaces",
|
||||
component: ControlPlaneSurfaces,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Product-surface stories exercise the board UI components that carry Paperclip's task, approval, budget, activity, and agent governance workflows.",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof ControlPlaneSurfaces>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const BoardStateMatrix: Story = {};
|
||||
@@ -0,0 +1,300 @@
|
||||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { AlertTriangle, ArrowRight, Check, Copy, Play, Plus, Save, Search, Settings } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ToggleSwitch } from "@/components/ui/toggle-switch";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
||||
const buttonVariants = ["default", "secondary", "outline", "ghost", "destructive", "link"] as const;
|
||||
const buttonSizes = ["xs", "sm", "default", "lg", "icon", "icon-sm"] as const;
|
||||
const badgeVariants = ["default", "secondary", "outline", "destructive", "ghost", "link"] as const;
|
||||
|
||||
function Section({
|
||||
eyebrow,
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="paperclip-story__frame overflow-hidden">
|
||||
<div className="border-b border-border px-5 py-4">
|
||||
<div className="paperclip-story__label">{eyebrow}</div>
|
||||
<h2 className="mt-1 text-xl font-semibold">{title}</h2>
|
||||
</div>
|
||||
<div className="p-5">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function FoundationsMatrix() {
|
||||
const [autoMode, setAutoMode] = useState(true);
|
||||
const [boardApproval, setBoardApproval] = useState(true);
|
||||
|
||||
return (
|
||||
<div className="paperclip-story">
|
||||
<main className="paperclip-story__inner space-y-6">
|
||||
<section className="paperclip-story__frame p-6">
|
||||
<div className="paperclip-story__label">Foundations</div>
|
||||
<h1 className="mt-2 text-3xl font-semibold tracking-tight">Primitives and interaction states</h1>
|
||||
<p className="mt-3 max-w-3xl text-sm leading-6 text-muted-foreground">
|
||||
A dense pass over the base controls that Paperclip pages use for operational actions, filtering,
|
||||
approvals, and settings.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<Section eyebrow="Actions" title="Button variants and sizes">
|
||||
<div className="space-y-6">
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{buttonVariants.map((variant) => (
|
||||
<div key={variant} className="rounded-lg border border-border bg-background/70 p-4">
|
||||
<div className="mb-3 text-xs font-medium capitalize text-muted-foreground">{variant}</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button variant={variant}>
|
||||
<Play className="h-4 w-4" />
|
||||
Invoke run
|
||||
</Button>
|
||||
<Button variant={variant} disabled>
|
||||
Disabled
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="rounded-lg border border-border bg-background/70 p-4">
|
||||
<div className="mb-3 text-xs font-medium text-muted-foreground">Sizes and icon-only actions</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{buttonSizes.map((size) => (
|
||||
<Button key={size} size={size} variant={size.startsWith("icon") ? "outline" : "secondary"}>
|
||||
{size.startsWith("icon") ? <Settings /> : size}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section eyebrow="Status labels" title="Badges and compact metadata">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{badgeVariants.map((variant) => (
|
||||
<Badge key={variant} variant={variant}>
|
||||
{variant === "destructive" ? <AlertTriangle /> : variant === "default" ? <Check /> : null}
|
||||
{variant}
|
||||
</Badge>
|
||||
))}
|
||||
<Badge variant="outline" className="font-mono">
|
||||
PAP-1641
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
<ArrowRight />
|
||||
in review
|
||||
</Badge>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section eyebrow="Inputs" title="Form controls with real Paperclip copy">
|
||||
<div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="story-title">Issue title</Label>
|
||||
<Input id="story-title" defaultValue="Create Storybook coverage for the board UI" />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="story-summary">Comment</Label>
|
||||
<Textarea
|
||||
id="story-summary"
|
||||
defaultValue={"Implemented the foundation stories.\nNext action: run static build verification."}
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label>Priority</Label>
|
||||
<Select defaultValue="high">
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Priority" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="critical">Critical</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Assignee</Label>
|
||||
<Select defaultValue="codexcoder">
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Assignee" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="codexcoder">CodexCoder</SelectItem>
|
||||
<SelectItem value="qachecker">QAChecker</SelectItem>
|
||||
<SelectItem value="board">Board</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Governed settings</CardTitle>
|
||||
<CardDescription>Switches, checkboxes, and validation copy in one compact panel.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between gap-4 rounded-lg border border-border p-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">Auto mode</div>
|
||||
<div className="text-xs text-muted-foreground">Let agents continue after review approval.</div>
|
||||
</div>
|
||||
<ToggleSwitch checked={autoMode} onCheckedChange={setAutoMode} />
|
||||
</div>
|
||||
<label className="flex items-start gap-3 rounded-lg border border-border p-3 text-sm">
|
||||
<Checkbox checked={boardApproval} onCheckedChange={(value) => setBoardApproval(value === true)} />
|
||||
<span>
|
||||
<span className="font-medium">Require board approval for new agents</span>
|
||||
<span className="mt-1 block text-xs text-muted-foreground">
|
||||
Mirrors the company-level governance control.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section eyebrow="Navigation" title="Tabs, overlays, and modal affordances">
|
||||
<div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_320px]">
|
||||
<Tabs defaultValue="details" className="rounded-lg border border-border bg-background/70 p-4">
|
||||
<TabsList variant="line">
|
||||
<TabsTrigger value="details">Details</TabsTrigger>
|
||||
<TabsTrigger value="activity">Activity</TabsTrigger>
|
||||
<TabsTrigger value="budget">Budget</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="details" className="pt-5 text-sm leading-6 text-muted-foreground">
|
||||
The line tab style is used on dense detail pages where the content, not the tab chrome, needs to dominate.
|
||||
</TabsContent>
|
||||
<TabsContent value="activity" className="pt-5 text-sm leading-6 text-muted-foreground">
|
||||
Activity copy stays compact and pairs with timestamped rows in the product stories.
|
||||
</TabsContent>
|
||||
<TabsContent value="budget" className="pt-5 text-sm leading-6 text-muted-foreground">
|
||||
Budget controls surface warning and hard-stop states in the control-plane stories.
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" className="w-full justify-start">
|
||||
<Search className="h-4 w-4" />
|
||||
Hover for tooltip
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Search issues, agents, projects, and approvals.</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="w-full justify-start">
|
||||
<Copy className="h-4 w-4" />
|
||||
Open popover
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" className="w-72">
|
||||
<div className="text-sm font-medium">Copy-safe detail</div>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
||||
Popovers should keep quick metadata close to the control that opened them.
|
||||
</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="w-full justify-start">
|
||||
<Plus className="h-4 w-4" />
|
||||
Open dialog
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create issue</DialogTitle>
|
||||
<DialogDescription>
|
||||
Dialogs should keep the primary decision and risk clear without leaving the current board context.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Separator />
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="dialog-title">Title</Label>
|
||||
<Input id="dialog-title" defaultValue="Review Storybook visual coverage" />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>
|
||||
<Save className="h-4 w-4" />
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Foundations/Primitive Matrix",
|
||||
component: FoundationsMatrix,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Foundation stories keep base shadcn/Radix primitives visible in every variant and key interaction state used by Paperclip.",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof FoundationsMatrix>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const AllPrimitives: Story = {};
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { BookOpen, CheckCircle2, FlaskConical, Layers3, Route, ShieldCheck } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
const storyGroups = [
|
||||
{
|
||||
title: "Foundations",
|
||||
icon: Layers3,
|
||||
stories: "Buttons, badges, form controls, tabs, cards, dialogs, overlays",
|
||||
why: "Baseline tokens and primitives before product components add state.",
|
||||
},
|
||||
{
|
||||
title: "Control Plane Surfaces",
|
||||
icon: ShieldCheck,
|
||||
stories: "Issue rows, approvals, budget cards, activity rows, metrics",
|
||||
why: "The V1 board UI depends on these dense operational patterns staying legible.",
|
||||
},
|
||||
{
|
||||
title: "UX Labs",
|
||||
icon: FlaskConical,
|
||||
stories: "Issue chat, run transcripts, invite/access flows",
|
||||
why: "The old `/tests/ux/*` pages are fixture-backed Storybook stories now.",
|
||||
},
|
||||
];
|
||||
|
||||
const coverageRows = [
|
||||
["System primitives", "Covered", "State matrix across size, variant, disabled, icon, and overlay behavior"],
|
||||
["Status language", "Covered", "Issue/agent lifecycle badges, priorities, quota thresholds, and empty states"],
|
||||
["Task surfaces", "Covered", "Inbox-style rows with unread, selected, archive, and trailing metadata states"],
|
||||
["Governance", "Covered", "Pending, revision-requested, approved, and budget-specific approval payloads"],
|
||||
["Budget controls", "Covered", "Healthy, warning, hard-stop, compact, plain, and editable card variants"],
|
||||
["Execution UX", "Covered", "Run transcript detail, live widget, dashboard card, streaming and settled views"],
|
||||
["Invite UX", "Covered", "Fixture-backed access roles, invite landing, pending, accepted, expired, and error states"],
|
||||
["Full app pages", "Deferred", "API-driven route stories should be added after page data loaders can be fixture-injected"],
|
||||
];
|
||||
|
||||
function StorybookGuide() {
|
||||
return (
|
||||
<div className="paperclip-story">
|
||||
<main className="paperclip-story__inner space-y-8">
|
||||
<section className="paperclip-story__frame overflow-hidden p-6 sm:p-8">
|
||||
<div className="flex flex-wrap items-start justify-between gap-6">
|
||||
<div className="max-w-3xl">
|
||||
<div className="paperclip-story__label flex items-center gap-2">
|
||||
<BookOpen className="h-4 w-4" />
|
||||
Paperclip Storybook
|
||||
</div>
|
||||
<h1 className="mt-3 text-3xl font-semibold tracking-tight sm:text-4xl">
|
||||
Board UI stories for real control-plane states
|
||||
</h1>
|
||||
<p className="mt-4 text-sm leading-6 text-muted-foreground sm:text-base">
|
||||
This Storybook is organized as a review workspace for Paperclip's operator UI: primitives first,
|
||||
product surfaces second, and the former UX test routes as isolated fixture-backed stories.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline">React 19</Badge>
|
||||
<Badge variant="outline">Vite</Badge>
|
||||
<Badge variant="outline">Tailwind 4</Badge>
|
||||
<Badge variant="outline">Fixture backed</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 md:grid-cols-3">
|
||||
{storyGroups.map((group) => {
|
||||
const Icon = group.icon;
|
||||
return (
|
||||
<Card key={group.title} className="paperclip-story__frame shadow-none">
|
||||
<CardHeader>
|
||||
<div className="flex h-10 w-10 items-center justify-center border border-border bg-background">
|
||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<CardTitle>{group.title}</CardTitle>
|
||||
<CardDescription>{group.stories}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm leading-6 text-muted-foreground">
|
||||
{group.why}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="paperclip-story__frame overflow-hidden">
|
||||
<div className="border-b border-border px-5 py-4">
|
||||
<div className="paperclip-story__label flex items-center gap-2">
|
||||
<Route className="h-4 w-4" />
|
||||
Coverage Map
|
||||
</div>
|
||||
</div>
|
||||
<div className="divide-y divide-border">
|
||||
{coverageRows.map(([area, status, detail]) => (
|
||||
<div key={area} className="grid gap-3 px-5 py-4 text-sm sm:grid-cols-[180px_120px_minmax(0,1fr)]">
|
||||
<div className="font-medium">{area}</div>
|
||||
<div>
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full border border-border bg-background px-2 py-0.5 text-xs text-muted-foreground">
|
||||
{status === "Covered" ? <CheckCircle2 className="h-3 w-3 text-emerald-500" /> : null}
|
||||
{status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground">{detail}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Overview/Storybook Guide",
|
||||
component: StorybookGuide,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"The overview story explains the local organization and the coverage contract for Paperclip's Storybook.",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof StorybookGuide>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const CoverageGuide: Story = {};
|
||||
@@ -0,0 +1,211 @@
|
||||
import { useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { AGENT_STATUSES, ISSUE_PRIORITIES, ISSUE_STATUSES } from "@paperclipai/shared";
|
||||
import { Bot, CheckCircle2, Clock3, DollarSign, FolderKanban, Inbox, MessageSquare, Users } from "lucide-react";
|
||||
import { CopyText } from "@/components/CopyText";
|
||||
import { EmptyState } from "@/components/EmptyState";
|
||||
import { Identity } from "@/components/Identity";
|
||||
import { MetricCard } from "@/components/MetricCard";
|
||||
import { PriorityIcon } from "@/components/PriorityIcon";
|
||||
import { QuotaBar } from "@/components/QuotaBar";
|
||||
import { StatusBadge } from "@/components/StatusBadge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
function Section({
|
||||
eyebrow,
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="paperclip-story__frame overflow-hidden">
|
||||
<div className="border-b border-border px-5 py-4">
|
||||
<div className="paperclip-story__label">{eyebrow}</div>
|
||||
<h2 className="mt-1 text-xl font-semibold">{title}</h2>
|
||||
</div>
|
||||
<div className="p-5">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusLanguage() {
|
||||
const [priority, setPriority] = useState("high");
|
||||
|
||||
return (
|
||||
<div className="paperclip-story">
|
||||
<main className="paperclip-story__inner space-y-6">
|
||||
<section className="paperclip-story__frame p-6">
|
||||
<div className="paperclip-story__label">Language</div>
|
||||
<h1 className="mt-2 text-3xl font-semibold tracking-tight">Status, priority, identity, and metrics</h1>
|
||||
<p className="mt-3 max-w-3xl text-sm leading-6 text-muted-foreground">
|
||||
These components carry the operational vocabulary of the board: who is acting, what state work is in,
|
||||
how urgent it is, and whether capacity or spend needs attention.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<Section eyebrow="Lifecycle" title="Issue and agent statuses">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card className="shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Issue statuses</CardTitle>
|
||||
<CardDescription>Every task transition state in the V1 issue lifecycle.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-wrap gap-2">
|
||||
{ISSUE_STATUSES.map((status) => (
|
||||
<StatusBadge key={status} status={status} />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Agent statuses</CardTitle>
|
||||
<CardDescription>Runtime and governance states shown in org, sidebar, and detail surfaces.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-wrap gap-2">
|
||||
{AGENT_STATUSES.map((status) => (
|
||||
<StatusBadge key={status} status={status} />
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section eyebrow="Priority" title="Static labels and editable popover trigger">
|
||||
<div className="grid gap-4 lg:grid-cols-[1fr_320px]">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{ISSUE_PRIORITIES.map((item) => (
|
||||
<div key={item} className="flex items-center justify-between rounded-lg border border-border bg-background/70 p-4">
|
||||
<PriorityIcon priority={item} showLabel />
|
||||
<span className="font-mono text-xs text-muted-foreground">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Card className="shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Editable priority</CardTitle>
|
||||
<CardDescription>Click the control to inspect the same popover used in issue rows.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PriorityIcon priority={priority} onChange={setPriority} showLabel />
|
||||
<div className="mt-3 text-xs text-muted-foreground">Current value: {priority}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section eyebrow="Identity" title="Agent and user chips">
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<Card className="shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>XS</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Identity name="CodexCoder" size="xs" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Small</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Identity name="Board User" size="sm" initials="BU" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Default</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Identity name="DesignSystemCoder" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Long label</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="max-w-[220px]">
|
||||
<Identity name="Senior Product Engineering Reviewer" size="lg" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section eyebrow="Dashboard" title="Metrics, quota bars, empty states, and copy affordances">
|
||||
<div className="grid gap-5 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<MetricCard icon={Users} value={8} label="Active agents" description="3 running right now" to="/agents/active" />
|
||||
<MetricCard icon={FolderKanban} value={27} label="Open issues" description="5 in review" to="/issues" />
|
||||
<MetricCard icon={DollarSign} value="$675" label="MTD spend" description="27% of budget" to="/costs" />
|
||||
<MetricCard icon={Clock3} value="14m" label="P95 run age" description="last 24 hours" />
|
||||
</div>
|
||||
|
||||
<Card className="shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Copyable identifiers</CardTitle>
|
||||
<CardDescription>Click values to exercise the status tooltip.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-muted-foreground">Issue</span>
|
||||
<CopyText text="PAP-1641" className="font-mono">PAP-1641</CopyText>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-muted-foreground">Run</span>
|
||||
<CopyText text="49442f05-f1c1-45c5-88d3-1e5b871dbb8b" className="font-mono">
|
||||
49442f05
|
||||
</CopyText>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-5 lg:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<Card className="shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Quota thresholds</CardTitle>
|
||||
<CardDescription>Green, warning, and hard-stop-adjacent progress treatments.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-5">
|
||||
<QuotaBar label="Company budget" percentUsed={27} leftLabel="$675 used" rightLabel="$2,500 cap" />
|
||||
<QuotaBar label="Project budget" percentUsed={86} leftLabel="$1,031 used" rightLabel="$1,200 cap" />
|
||||
<QuotaBar label="Agent budget" percentUsed={108} leftLabel="$432 used" rightLabel="$400 cap" showDeficitNotch />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>Empty state</CardTitle>
|
||||
<CardDescription>Used when a list has no meaningful rows yet.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<EmptyState icon={Inbox} message="No assigned work is waiting in this queue." action="Create issue" onAction={() => undefined} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</Section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Foundations/Status Language",
|
||||
component: StatusLanguage,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Status-language stories show the reusable operational labels, identity chips, metrics, and capacity indicators used throughout the board.",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof StatusLanguage>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const FullMatrix: Story = {};
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { IssueChatUxLab } from "@/pages/IssueChatUxLab";
|
||||
import { InviteUxLab } from "@/pages/InviteUxLab";
|
||||
import { RunTranscriptUxLab } from "@/pages/RunTranscriptUxLab";
|
||||
|
||||
function StoryFrame({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="paperclip-story">
|
||||
<main className="paperclip-story__inner">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "UX Labs/Converted Test Pages",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"The former in-app UX test routes are represented here as Storybook stories so fixture-backed review surfaces stay out of production routing.",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const IssueChatReviewSurface: Story = {
|
||||
name: "Issue Chat Review Surface",
|
||||
render: () => (
|
||||
<StoryFrame>
|
||||
<IssueChatUxLab />
|
||||
</StoryFrame>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Exercises assistant-ui issue chat states: timeline events, live run stream, queued message, feedback controls, submitting bubble, empty state, and disabled composer.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const RunTranscriptFixtures: Story = {
|
||||
name: "Run Transcript Fixtures",
|
||||
render: () => (
|
||||
<StoryFrame>
|
||||
<RunTranscriptUxLab />
|
||||
</StoryFrame>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Exercises run transcript presentation across the run detail page, issue live widget, and dashboard card density.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const InviteAndAccessFlow: Story = {
|
||||
name: "Invite And Access Flow",
|
||||
render: () => (
|
||||
<StoryFrame>
|
||||
<InviteUxLab />
|
||||
</StoryFrame>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Exercises invitation and access UX states with fixture-backed role choices, landing frames, history, and failure treatments.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
+1
-1
@@ -17,5 +17,5 @@
|
||||
"lexical": ["./node_modules/lexical/index.d.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src", "storybook"]
|
||||
}
|
||||
Reference in new issue
Block a user