From ae627978fdb9752c4322ddaec6ef15f50615537b Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 20 Apr 2026 10:02:55 -0500 Subject: [PATCH] feat(ui): add chat-comments and data-viz-misc stories New story files for chat/comments (713 lines) and data visualization/ miscellaneous (750 lines) components. Updates fixtures and overview story to reflect expanded coverage. Co-Authored-By: Paperclip --- ui/storybook/fixtures/paperclipData.ts | 388 ++++++++- .../stories/chat-comments.stories.tsx | 713 +++++++++++++++++ .../stories/data-viz-misc.stories.tsx | 753 ++++++++++++++++++ ui/storybook/stories/overview.stories.tsx | 2 +- 4 files changed, 1819 insertions(+), 37 deletions(-) create mode 100644 ui/storybook/stories/chat-comments.stories.tsx create mode 100644 ui/storybook/stories/data-viz-misc.stories.tsx diff --git a/ui/storybook/fixtures/paperclipData.ts b/ui/storybook/fixtures/paperclipData.ts index 5f59901b6df..203d9c8400b 100644 --- a/ui/storybook/fixtures/paperclipData.ts +++ b/ui/storybook/fixtures/paperclipData.ts @@ -7,11 +7,13 @@ import type { Company, DashboardSummary, ExecutionWorkspace, + Goal, Issue, IssueDocument, IssueLabel, Project, SidebarBadges, + WorkspaceRuntimeService, } from "@paperclipai/shared"; import type { RunForIssue } from "@/api/activity"; import type { LiveRunForIssue } from "@/api/heartbeats"; @@ -219,7 +221,137 @@ function labelsFor(ids: string[]) { return ids.map((id) => storybookIssueLabelMap.get(id)).filter((label): label is IssueLabel => Boolean(label)); } -export const storybookProjectWorkspaces = [ +export const storybookGoals: Goal[] = [ + { + id: "goal-company", + companyId: "company-storybook", + title: "Build Paperclip", + description: "Make Paperclip the control plane operators trust for autonomous AI companies.", + level: "company", + status: "active", + parentId: null, + ownerAgentId: "agent-cto", + createdAt: recent(30_000), + updatedAt: recent(8), + }, + { + id: "goal-board-ux", + companyId: "company-storybook", + title: "Tighten board operator visibility", + description: "Every project, goal, and workspace surface should reveal ownership, progress, and runtime state at a glance.", + level: "team", + status: "active", + parentId: "goal-company", + ownerAgentId: "agent-codex", + createdAt: recent(19_000), + updatedAt: recent(18), + }, + { + id: "goal-agent-runtime", + companyId: "company-storybook", + title: "Stabilize agent runtime loops", + description: "Keep local and isolated workspaces predictable while preserving operator control.", + level: "team", + status: "planned", + parentId: "goal-company", + ownerAgentId: "agent-cto", + createdAt: recent(17_500), + updatedAt: recent(60), + }, + { + id: "goal-storybook", + companyId: "company-storybook", + title: "Complete Storybook review coverage", + description: "Capture dense board UI states in fixture-backed stories before release review.", + level: "task", + status: "active", + parentId: "goal-board-ux", + ownerAgentId: "agent-codex", + createdAt: recent(9_000), + updatedAt: recent(3), + }, + { + id: "goal-budget-safety", + companyId: "company-storybook", + title: "Enforce spend guardrails", + description: "Budget hard stops should be visible before they surprise operators.", + level: "agent", + status: "achieved", + parentId: "goal-agent-runtime", + ownerAgentId: "agent-qa", + createdAt: recent(12_000), + updatedAt: recent(120), + }, + { + id: "goal-archived-import", + companyId: "company-storybook", + title: "Retire old import wizard", + description: "Legacy import wizard work is preserved for audit only.", + level: "task", + status: "cancelled", + parentId: "goal-board-ux", + ownerAgentId: null, + createdAt: recent(24_000), + updatedAt: recent(2_500), + }, +]; + +function createRuntimeService( + overrides: Partial = {}, +): WorkspaceRuntimeService { + return { + id: overrides.id ?? "service-storybook", + companyId: overrides.companyId ?? "company-storybook", + projectId: overrides.projectId ?? "project-board-ui", + projectWorkspaceId: overrides.projectWorkspaceId ?? "workspace-board-ui", + executionWorkspaceId: overrides.executionWorkspaceId ?? "execution-workspace-storybook", + issueId: overrides.issueId ?? "issue-storybook-1", + scopeType: overrides.scopeType ?? "execution_workspace", + scopeId: overrides.scopeId ?? "execution-workspace-storybook", + serviceName: overrides.serviceName ?? "storybook", + status: overrides.status ?? "running", + lifecycle: overrides.lifecycle ?? "ephemeral", + reuseKey: overrides.reuseKey ?? "storybook", + command: overrides.command ?? "pnpm storybook", + cwd: overrides.cwd ?? "/Users/dotta/paperclip/ui", + port: overrides.port ?? 6006, + url: overrides.url ?? "http://localhost:6006", + provider: overrides.provider ?? "local_process", + providerRef: overrides.providerRef ?? null, + ownerAgentId: overrides.ownerAgentId ?? "agent-codex", + startedByRunId: overrides.startedByRunId ?? "run-storybook", + lastUsedAt: overrides.lastUsedAt ?? recent(4), + startedAt: overrides.startedAt ?? recent(16), + stoppedAt: overrides.stoppedAt ?? null, + stopPolicy: overrides.stopPolicy ?? null, + healthStatus: overrides.healthStatus ?? "healthy", + configIndex: overrides.configIndex ?? 0, + createdAt: overrides.createdAt ?? recent(16), + updatedAt: overrides.updatedAt ?? recent(4), + }; +} + +const storybookWorkspaceRuntime = { + commands: [ + { + id: "storybook", + name: "Storybook", + kind: "service", + command: "pnpm storybook", + cwd: "ui", + lifecycle: "shared", + }, + { + id: "typecheck-ui", + name: "UI typecheck", + kind: "job", + command: "pnpm --filter @paperclipai/ui typecheck", + cwd: ".", + }, + ], +}; + +export const storybookProjectWorkspaces: Project["workspaces"] = [ { id: "workspace-board-ui", companyId: "company-storybook", @@ -237,43 +369,114 @@ export const storybookProjectWorkspaces = [ remoteWorkspaceRef: null, sharedWorkspaceKey: "board-ui", metadata: null, - runtimeConfig: null, + runtimeConfig: { + workspaceRuntime: storybookWorkspaceRuntime, + desiredState: "running" as const, + serviceStates: { storybook: "running" as const }, + }, isPrimary: true, - runtimeServices: [ - { - id: "service-storybook", - companyId: "company-storybook", - projectId: "project-board-ui", - projectWorkspaceId: "workspace-board-ui", - executionWorkspaceId: "execution-workspace-storybook", - issueId: "issue-storybook-1", - scopeType: "execution_workspace" as const, - scopeId: "execution-workspace-storybook", - serviceName: "storybook", - status: "running" as const, - lifecycle: "ephemeral" as const, - reuseKey: "storybook", - command: "pnpm storybook", - cwd: "/Users/dotta/paperclip/ui", - port: 6006, - url: "http://localhost:6006", - provider: "local_process" as const, - providerRef: null, - ownerAgentId: "agent-codex", - startedByRunId: "run-storybook", - lastUsedAt: recent(4), - startedAt: recent(16), - stoppedAt: null, - stopPolicy: null, - healthStatus: "healthy" as const, - configIndex: 0, - createdAt: recent(16), - updatedAt: recent(4), - }, - ], + runtimeServices: [createRuntimeService()], createdAt: recent(20_000), updatedAt: recent(4), }, + { + id: "workspace-docs-remote", + companyId: "company-storybook", + projectId: "project-board-ui", + name: "Docs preview sandbox", + sourceType: "remote_managed", + cwd: null, + repoUrl: "https://github.com/paperclipai/paperclip", + repoRef: "preview/docs-workspaces", + defaultRef: "master", + visibility: "advanced", + setupCommand: "pnpm install", + cleanupCommand: "pnpm dev:stop", + remoteProvider: "vercel_sandbox", + remoteWorkspaceRef: "vcsb_storybook_docs_0420", + sharedWorkspaceKey: "docs-preview", + metadata: { region: "iad1", ttlHours: 8 }, + runtimeConfig: { + workspaceRuntime: { + commands: [ + { + id: "docs", + name: "Docs preview", + kind: "service", + command: "pnpm docs:dev", + cwd: ".", + lifecycle: "ephemeral", + }, + ], + }, + desiredState: "stopped", + serviceStates: { docs: "stopped" }, + }, + isPrimary: false, + runtimeServices: [ + createRuntimeService({ + id: "service-docs-preview", + projectWorkspaceId: "workspace-docs-remote", + executionWorkspaceId: null, + issueId: "issue-storybook-6", + scopeType: "project_workspace", + scopeId: "workspace-docs-remote", + serviceName: "docs", + status: "stopped", + lifecycle: "ephemeral", + reuseKey: "docs-preview", + command: "pnpm docs:dev", + cwd: null, + port: 4173, + url: "https://paperclip-docs-preview.vercel.app", + healthStatus: "unknown", + lastUsedAt: recent(48), + startedAt: recent(72), + stoppedAt: recent(46), + createdAt: recent(72), + updatedAt: recent(46), + }), + ], + createdAt: recent(2_000), + updatedAt: recent(46), + }, + { + id: "workspace-release-local", + companyId: "company-storybook", + projectId: "project-board-ui", + name: "Release smoke local checkout", + sourceType: "local_path", + cwd: "/Users/dotta/paperclip/.paperclip/workspaces/release-smoke", + repoUrl: "https://github.com/paperclipai/paperclip", + repoRef: "release/smoke-2026-04-20", + defaultRef: "master", + visibility: "advanced", + setupCommand: "pnpm install", + cleanupCommand: "pnpm dev:stop", + remoteProvider: null, + remoteWorkspaceRef: null, + sharedWorkspaceKey: "release-smoke", + metadata: null, + runtimeConfig: { + workspaceRuntime: { + commands: [ + { + id: "release-smoke", + name: "Release smoke", + kind: "job", + command: "pnpm test:release-smoke", + cwd: ".", + }, + ], + }, + desiredState: "stopped", + serviceStates: null, + }, + isPrimary: false, + runtimeServices: [], + createdAt: recent(7_200), + updatedAt: recent(90), + }, ]; export const storybookExecutionWorkspaces: ExecutionWorkspace[] = [ @@ -305,6 +508,57 @@ export const storybookExecutionWorkspaces: ExecutionWorkspace[] = [ createdAt: recent(90), updatedAt: recent(3), }, + { + id: "execution-workspace-cleanup", + companyId: "company-storybook", + projectId: "project-board-ui", + projectWorkspaceId: "workspace-release-local", + sourceIssueId: "issue-storybook-8", + mode: "isolated_workspace", + strategyType: "git_worktree", + name: "PAP-1608 release smoke cleanup", + status: "cleanup_failed", + cwd: "/Users/dotta/paperclip/.paperclip/worktrees/PAP-1608-release-smoke-cleanup", + repoUrl: "https://github.com/paperclipai/paperclip", + baseRef: "master", + branchName: "PAP-1608-release-smoke-cleanup", + providerType: "git_worktree", + providerRef: null, + derivedFromExecutionWorkspaceId: null, + lastUsedAt: recent(120), + openedAt: recent(260), + closedAt: recent(118), + cleanupEligibleAt: recent(115), + cleanupReason: "release smoke finished but the local worktree still has generated artifacts", + config: { + provisionCommand: null, + teardownCommand: "pnpm dev:stop && rm -rf ui/storybook-static", + cleanupCommand: null, + workspaceRuntime: storybookWorkspaceRuntime, + desiredState: "stopped", + serviceStates: { storybook: "stopped" }, + }, + metadata: { dirtyEntryCount: 3 }, + runtimeServices: [ + createRuntimeService({ + id: "service-cleanup-storybook", + projectWorkspaceId: "workspace-release-local", + executionWorkspaceId: "execution-workspace-cleanup", + issueId: "issue-storybook-8", + scopeId: "execution-workspace-cleanup", + status: "failed", + healthStatus: "unhealthy", + url: "http://localhost:6007", + port: 6007, + lastUsedAt: recent(130), + startedAt: recent(210), + stoppedAt: recent(125), + updatedAt: recent(120), + }), + ], + createdAt: recent(260), + updatedAt: recent(120), + }, ]; function createProject(overrides: Partial = {}): Project { @@ -314,8 +568,10 @@ function createProject(overrides: Partial = {}): Project { companyId: "company-storybook", urlKey: "board-ui", goalId: "goal-company", - goalIds: ["goal-company"], - goals: [{ id: "goal-company", title: "Build Paperclip" }], + goalIds: ["goal-company", "goal-board-ux"], + goals: storybookGoals + .filter((goal) => goal.id === "goal-company" || goal.id === "goal-board-ux") + .map(({ id, title }) => ({ id, title })), name: "Board UI", description: "Navigation, command, and operator layout polish.", status: "in_progress", @@ -388,6 +644,45 @@ export const storybookProjects: Project[] = [ pausedAt: recent(90), updatedAt: recent(90), }), + createProject({ + id: "project-archived-import", + urlKey: "archived-import-wizard", + name: "Archived Import Wizard", + description: "Preserved for audit after the import workflow moved into company packages.", + status: "cancelled", + leadAgentId: null, + goalId: "goal-archived-import", + goalIds: ["goal-archived-import"], + goals: [{ id: "goal-archived-import", title: "Retire old import wizard" }], + color: "#64748b", + workspaces: [], + primaryWorkspace: null, + codebase: { + workspaceId: null, + repoUrl: null, + repoRef: null, + defaultRef: null, + repoName: null, + localFolder: null, + managedFolder: "", + effectiveLocalFolder: "Not configured", + origin: "local_folder", + }, + executionWorkspacePolicy: { + enabled: false, + defaultMode: "shared_workspace", + allowIssueOverride: false, + defaultProjectWorkspaceId: null, + workspaceStrategy: null, + workspaceRuntime: null, + branchPolicy: null, + pullRequestPolicy: null, + runtimePolicy: null, + cleanupPolicy: null, + }, + archivedAt: recent(2_400), + updatedAt: recent(2_400), + }), ]; export function createIssue(overrides: Partial = {}): Issue { @@ -572,6 +867,27 @@ export const storybookIssues: Issue[] = [ lastActivityAt: recent(85), isUnreadForMe: false, }), + createIssue({ + id: "issue-storybook-8", + title: "Clean up release smoke worktree", + description: "Close the isolated release smoke workspace after static preview review.", + status: "blocked", + priority: "medium", + assigneeAgentId: "agent-codex", + checkoutRunId: null, + executionRunId: "run-release-smoke-cleanup", + executionLockedAt: null, + startedAt: recent(260), + identifier: "PAP-1608", + issueNumber: 1608, + projectId: "project-board-ui", + projectWorkspaceId: "workspace-release-local", + executionWorkspaceId: "execution-workspace-cleanup", + labelIds: ["label-ui", "label-risk"], + labels: labelsFor(["label-ui", "label-risk"]), + lastActivityAt: recent(120), + isUnreadForMe: false, + }), ]; export const storybookIssueDocuments: IssueDocument[] = [ diff --git a/ui/storybook/stories/chat-comments.stories.tsx b/ui/storybook/stories/chat-comments.stories.tsx new file mode 100644 index 00000000000..620b96c8816 --- /dev/null +++ b/ui/storybook/stories/chat-comments.stories.tsx @@ -0,0 +1,713 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { Agent, FeedbackVote, IssueComment } from "@paperclipai/shared"; +import type { TranscriptEntry } from "@/adapters"; +import type { LiveRunForIssue } from "@/api/heartbeats"; +import { CommentThread } from "@/components/CommentThread"; +import { IssueChatThread } from "@/components/IssueChatThread"; +import { RunChatSurface } from "@/components/RunChatSurface"; +import type { InlineEntityOption } from "@/components/InlineEntitySelector"; +import type { MentionOption } from "@/components/MarkdownEditor"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import type { + IssueChatComment, + IssueChatLinkedRun, + IssueChatTranscriptEntry, +} from "@/lib/issue-chat-messages"; +import type { IssueTimelineEvent } from "@/lib/issue-timeline-events"; +import { storybookAgentMap, storybookAgents } from "../fixtures/paperclipData"; + +const companyId = "company-storybook"; +const projectId = "project-board-ui"; +const issueId = "issue-chat-comments"; +const currentUserId = "user-board"; + +type StoryComment = IssueComment & { + runId?: string | null; + runAgentId?: string | null; + clientId?: string; + clientStatus?: "pending" | "queued"; + queueState?: "queued"; + queueTargetRunId?: string | null; +}; + +const codexAgent = storybookAgents.find((agent) => agent.id === "agent-codex") ?? storybookAgents[0]!; +const qaAgent = storybookAgents.find((agent) => agent.id === "agent-qa") ?? storybookAgents[1]!; +const ctoAgent = storybookAgents.find((agent) => agent.id === "agent-cto") ?? storybookAgents[2]!; + +const boardUserLabels = new Map([ + ["user-board", "Riley Board"], + ["user-product", "Mara Product"], +]); + +function Section({ + eyebrow, + title, + children, +}: { + eyebrow: string; + title: string; + children: React.ReactNode; +}) { + return ( +
+
+
+
{eyebrow}
+

{title}

+
+
+
{children}
+
+ ); +} + +function ScenarioCard({ + title, + description, + children, +}: { + title: string; + description: string; + children: React.ReactNode; +}) { + return ( + + + {title} + {description} + + {children} + + ); +} + +function createComment(overrides: Partial): StoryComment { + const createdAt = overrides.createdAt ?? new Date("2026-04-20T14:00:00.000Z"); + return { + id: "comment-default", + companyId, + issueId, + authorAgentId: null, + authorUserId: currentUserId, + body: "", + createdAt, + updatedAt: overrides.updatedAt ?? createdAt, + ...overrides, + }; +} + +function createSystemEvent(overrides: Partial): IssueTimelineEvent { + return { + id: "event-default", + createdAt: new Date("2026-04-20T14:00:00.000Z"), + actorType: "system", + actorId: "paperclip", + statusChange: { + from: "todo", + to: "in_progress", + }, + ...overrides, + }; +} + +const mentionOptions: MentionOption[] = [ + { + id: `agent:${codexAgent.id}`, + name: codexAgent.name, + kind: "agent", + agentId: codexAgent.id, + agentIcon: codexAgent.icon, + }, + { + id: `agent:${qaAgent.id}`, + name: qaAgent.name, + kind: "agent", + agentId: qaAgent.id, + agentIcon: qaAgent.icon, + }, + { + id: `project:${projectId}`, + name: "Board UI", + kind: "project", + projectId, + projectColor: "#0f766e", + }, +]; + +const reassignOptions: InlineEntityOption[] = [ + { + id: `agent:${codexAgent.id}`, + label: codexAgent.name, + searchText: `${codexAgent.name} engineer codex`, + }, + { + id: `agent:${qaAgent.id}`, + label: qaAgent.name, + searchText: `${qaAgent.name} qa browser review`, + }, + { + id: `agent:${ctoAgent.id}`, + label: ctoAgent.name, + searchText: `${ctoAgent.name} architecture review`, + }, + { + id: `user:${currentUserId}`, + label: "Riley Board", + searchText: "board operator", + }, +]; + +const singleComment = [ + createComment({ + id: "comment-single-board", + body: "Please make the issue chat states reviewable in Storybook before the next UI pass.", + createdAt: new Date("2026-04-20T13:12:00.000Z"), + }), +]; + +const longThreadComments = [ + createComment({ + id: "comment-long-board", + body: "The chat surface should show the operator request first, then agent progress, then review follow-up. Keep the density close to the issue page.", + createdAt: new Date("2026-04-20T13:02:00.000Z"), + }), + createComment({ + id: "comment-long-agent", + authorAgentId: codexAgent.id, + authorUserId: null, + body: "I found the existing `IssueChatThread` and `RunChatSurface` components and am building the stories around those props.", + createdAt: new Date("2026-04-20T13:08:00.000Z"), + runId: "run-comment-thread-01", + runAgentId: codexAgent.id, + }), + createComment({ + id: "comment-long-product", + authorUserId: "user-product", + body: "Also include the old comment timeline so we can compare it with the assistant-style issue chat.", + createdAt: new Date("2026-04-20T13:16:00.000Z"), + }), + createComment({ + id: "comment-long-qa", + authorAgentId: qaAgent.id, + authorUserId: null, + body: "QA note: the thread should stay readable with long markdown and when a queued operator reply is visible.", + createdAt: new Date("2026-04-20T13:24:00.000Z"), + runId: "run-comment-thread-02", + runAgentId: qaAgent.id, + }), +]; + +const markdownComments = [ + createComment({ + id: "comment-markdown-board", + body: [ + "Acceptance criteria:", + "", + "- Cover empty, single, and long comment states", + "- Show a code block in a comment", + "- Include a link to [the issue guide](/issues/PAP-1676)", + "", + "```ts", + "const success = stories.some((story) => story.includes(\"IssueChatThread\"));", + "```", + ].join("\n"), + createdAt: new Date("2026-04-20T13:28:00.000Z"), + }), + createComment({ + id: "comment-mentions-agent", + authorAgentId: codexAgent.id, + authorUserId: null, + body: "@QAChecker I added the fixture coverage. Please focus browser review on links, code blocks, and the queued comment treatment.", + createdAt: new Date("2026-04-20T13:35:00.000Z"), + runId: "run-markdown-01", + runAgentId: codexAgent.id, + }), +]; + +const queuedComment = createComment({ + id: "comment-queued-board", + body: "@CodexCoder after this run finishes, add a compact embedded variant too.", + createdAt: new Date("2026-04-20T13:39:00.000Z"), + clientId: "client-queued-storybook", + clientStatus: "queued", + queueState: "queued", + queueTargetRunId: "run-live-chat-01", +}); + +const commentTimelineEvents: IssueTimelineEvent[] = [ + createSystemEvent({ + id: "event-system-checkout", + createdAt: new Date("2026-04-20T13:04:00.000Z"), + actorType: "system", + actorId: "paperclip", + statusChange: { + from: "todo", + to: "in_progress", + }, + }), + createSystemEvent({ + id: "event-board-reassign", + createdAt: new Date("2026-04-20T13:18:00.000Z"), + actorType: "user", + actorId: currentUserId, + assigneeChange: { + from: { agentId: codexAgent.id, userId: null }, + to: { agentId: qaAgent.id, userId: null }, + }, + statusChange: undefined, + }), +]; + +const commentLinkedRuns = [ + { + runId: "run-comment-thread-01", + status: "succeeded", + agentId: codexAgent.id, + createdAt: new Date("2026-04-20T13:07:00.000Z"), + startedAt: new Date("2026-04-20T13:07:00.000Z"), + finishedAt: new Date("2026-04-20T13:11:00.000Z"), + }, + { + runId: "run-comment-thread-02", + status: "running", + agentId: qaAgent.id, + createdAt: new Date("2026-04-20T13:22:00.000Z"), + startedAt: new Date("2026-04-20T13:22:00.000Z"), + finishedAt: null, + }, +]; + +const feedbackVotes: FeedbackVote[] = [ + { + id: "feedback-chat-comment-01", + companyId, + issueId, + targetType: "issue_comment", + targetId: "comment-issue-agent", + authorUserId: currentUserId, + vote: "up", + reason: null, + sharedWithLabs: false, + sharedAt: null, + consentVersion: null, + redactionSummary: null, + createdAt: new Date("2026-04-20T13:52:00.000Z"), + updatedAt: new Date("2026-04-20T13:52:00.000Z"), + }, +]; + +const liveRun: LiveRunForIssue = { + id: "run-live-chat-01", + status: "running", + invocationSource: "manual", + triggerDetail: "comment", + createdAt: "2026-04-20T13:40:00.000Z", + startedAt: "2026-04-20T13:40:02.000Z", + finishedAt: null, + agentId: codexAgent.id, + agentName: codexAgent.name, + adapterType: "codex_local", + issueId, +}; + +const liveRunTranscript: TranscriptEntry[] = [ + { + kind: "assistant", + ts: "2026-04-20T13:40:08.000Z", + text: "I am wiring the chat and comments Storybook coverage now.", + }, + { + kind: "thinking", + ts: "2026-04-20T13:40:12.000Z", + text: "Need fixtures that exercise MarkdownBody, assistant-ui messages, and the embedded run transcript path without reaching the API.", + }, + { + kind: "tool_call", + ts: "2026-04-20T13:40:18.000Z", + name: "rg", + toolUseId: "tool-live-rg", + input: { + query: "IssueChatThread", + cwd: "ui/src", + }, + }, + { + kind: "tool_result", + ts: "2026-04-20T13:40:20.000Z", + toolUseId: "tool-live-rg", + content: "ui/src/components/IssueChatThread.tsx\nui/src/components/RunChatSurface.tsx", + isError: false, + }, + { + kind: "assistant", + ts: "2026-04-20T13:40:31.000Z", + text: [ + "The live run should render code blocks as part of the assistant response:", + "", + "```tsx", + "", + "```", + ].join("\n"), + }, + { + kind: "tool_call", + ts: "2026-04-20T13:40:44.000Z", + name: "apply_patch", + toolUseId: "tool-live-patch", + input: { + file: "ui/storybook/stories/chat-comments.stories.tsx", + action: "add fixtures", + }, + }, + { + kind: "tool_result", + ts: "2026-04-20T13:40:49.000Z", + toolUseId: "tool-live-patch", + content: "Added Storybook scenarios for comment thread, run chat, and issue chat.", + isError: false, + }, +]; + +const issueChatComments: IssueChatComment[] = [ + createComment({ + id: "comment-issue-board", + body: "Please turn the comment thread into a reviewable chat surface. I need to see operator messages, agent output, system events, and live run progress together.", + createdAt: new Date("2026-04-20T13:44:00.000Z"), + }), + createComment({ + id: "comment-issue-agent", + authorAgentId: codexAgent.id, + authorUserId: null, + body: "I kept the existing component contracts and added fixtures with realistic Paperclip work: checkout, comments, linked runs, and review feedback.", + createdAt: new Date("2026-04-20T13:50:00.000Z"), + runId: "run-issue-chat-01", + runAgentId: codexAgent.id, + }), + createComment({ + id: "comment-issue-queued", + body: "@QAChecker please do a quick visual pass after the Storybook build is green.", + createdAt: new Date("2026-04-20T13:56:00.000Z"), + clientId: "client-issue-queued", + clientStatus: "queued", + queueState: "queued", + queueTargetRunId: liveRun.id, + }), +]; + +const issueTimelineEvents: IssueTimelineEvent[] = [ + createSystemEvent({ + id: "event-issue-checkout", + createdAt: new Date("2026-04-20T13:42:00.000Z"), + actorType: "system", + actorId: "paperclip", + statusChange: { + from: "todo", + to: "in_progress", + }, + }), + createSystemEvent({ + id: "event-issue-assignee", + createdAt: new Date("2026-04-20T13:43:00.000Z"), + actorType: "user", + actorId: currentUserId, + statusChange: undefined, + assigneeChange: { + from: { agentId: null, userId: null }, + to: { agentId: codexAgent.id, userId: null }, + }, + }), +]; + +const issueLinkedRuns: IssueChatLinkedRun[] = [ + { + runId: "run-issue-chat-01", + status: "succeeded", + agentId: codexAgent.id, + agentName: codexAgent.name, + adapterType: "codex_local", + createdAt: new Date("2026-04-20T13:46:00.000Z"), + startedAt: new Date("2026-04-20T13:46:00.000Z"), + finishedAt: new Date("2026-04-20T13:51:00.000Z"), + hasStoredOutput: true, + }, +]; + +const issueTranscriptsByRunId = new Map([ + [ + "run-issue-chat-01", + [ + { + kind: "thinking", + ts: "2026-04-20T13:46:10.000Z", + text: "Checking the existing Storybook organization before adding a new product group.", + }, + { + kind: "tool_call", + ts: "2026-04-20T13:46:16.000Z", + name: "read_file", + toolUseId: "tool-issue-read", + input: { + path: "ui/storybook/stories/overview.stories.tsx", + }, + }, + { + kind: "tool_result", + ts: "2026-04-20T13:46:19.000Z", + toolUseId: "tool-issue-read", + content: "The coverage map already lists Chat & comments as a planned section.", + isError: false, + }, + { + kind: "assistant", + ts: "2026-04-20T13:49:00.000Z", + text: "Added the story file and kept every fixture local to the story so product data fixtures stay stable.", + }, + { + kind: "diff", + ts: "2026-04-20T13:49:04.000Z", + changeType: "file_header", + text: "diff --git a/ui/storybook/stories/chat-comments.stories.tsx b/ui/storybook/stories/chat-comments.stories.tsx", + }, + { + kind: "diff", + ts: "2026-04-20T13:49:05.000Z", + changeType: "add", + text: "+export const FullSurfaceMatrix: Story = {};", + }, + ], + ], + [liveRun.id, liveRunTranscript], +]); + +function ThreadProps({ + comments, + queuedComments = [], + timelineEvents = [], +}: { + comments: StoryComment[]; + queuedComments?: StoryComment[]; + timelineEvents?: IssueTimelineEvent[]; +}) { + return ( + {}} + enableReassign + reassignOptions={reassignOptions} + currentAssigneeValue={`agent:${codexAgent.id}`} + suggestedAssigneeValue={`agent:${codexAgent.id}`} + mentions={mentionOptions} + onInterruptQueued={async () => {}} + /> + ); +} + +function CommentThreadMatrix() { + return ( +
+
+ + + + + + + + + + + + +
+
+ ); +} + +function RunChatMatrix() { + return ( +
+
+
+ +
+ + + Run fixture shape + Streaming transcript entries mixed into the same chat renderer used by issue chat. + + +
+ Status + running +
+
+ Tool calls + rg, apply_patch +
+
+ Transcript entries + {liveRunTranscript.length} +
+
+
+
+
+ ); +} + +function IssueChatMatrix() { + return ( +
+
+
+ {}} + onVote={async () => {}} + onStopRun={async () => {}} + enableReassign + reassignOptions={reassignOptions} + currentAssigneeValue={`agent:${codexAgent.id}`} + suggestedAssigneeValue={`agent:${codexAgent.id}`} + mentions={mentionOptions} + enableLiveTranscriptPolling={false} + transcriptsByRunId={issueTranscriptsByRunId} + hasOutputForRun={(runId) => issueTranscriptsByRunId.has(runId)} + includeSucceededRunsWithoutOutput + onInterruptQueued={async () => {}} + onCancelQueued={() => undefined} + /> +
+
+ + {}} + enableLiveTranscriptPolling={false} + emptyMessage="No chat yet. The first operator note will start the issue conversation." + /> + + + {}} + showJumpToLatest={false} + enableLiveTranscriptPolling={false} + composerDisabledReason="This issue is in review. Request changes or approve it from the review controls." + /> + +
+
+
+ ); +} + +function ChatCommentsStories() { + return ( +
+
+
+
Chat & Comments
+

Threaded work conversations

+

+ Fixture-backed coverage for classic issue comments, embedded run chat, and the assistant-style issue chat + surface. The scenarios use Paperclip operational content with mixed authors, system timeline events, + markdown, code blocks, @mentions, links, queued comments, tool calls, and streaming run output. +

+
+ + + + +
+
+ ); +} + +const meta = { + title: "Product/Chat & Comments", + component: ChatCommentsStories, + parameters: { + docs: { + description: { + component: + "Chat and comments stories exercise CommentThread, RunChatSurface, and IssueChatThread across empty, single, long, markdown, mention, timeline, queued, linked-run, and streaming transcript states.", + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const FullSurfaceMatrix: Story = {}; + +export const CommentThreads: Story = { + render: () => ( +
+
+ +
+
+ ), +}; + +export const LiveRunChat: Story = { + render: () => ( +
+
+ +
+
+ ), +}; + +export const IssueChatWithTimeline: Story = { + render: () => ( +
+
+ +
+
+ ), +}; diff --git a/ui/storybook/stories/data-viz-misc.stories.tsx b/ui/storybook/stories/data-viz-misc.stories.tsx new file mode 100644 index 00000000000..47d310fb7ec --- /dev/null +++ b/ui/storybook/stories/data-viz-misc.stories.tsx @@ -0,0 +1,753 @@ +import { useEffect, useMemo, useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { HeartbeatRun, Issue } from "@paperclipai/shared"; +import { useQueryClient } from "@tanstack/react-query"; +import { + Archive, + Bot, + CheckCircle2, + Clock3, + FileCode2, + FolderKanban, + ListFilter, + Loader2, + Play, + ShieldCheck, + Sparkles, +} from "lucide-react"; +import { + ChartCard, + IssueStatusChart, + PriorityChart, + RunActivityChart, + SuccessRateChart, +} from "@/components/ActivityCharts"; +import { AsciiArtAnimation } from "@/components/AsciiArtAnimation"; +import { CompanyPatternIcon } from "@/components/CompanyPatternIcon"; +import { EntityRow } from "@/components/EntityRow"; +import { FilterBar, type FilterValue } from "@/components/FilterBar"; +import { KanbanBoard } from "@/components/KanbanBoard"; +import { LiveRunWidget } from "@/components/LiveRunWidget"; +import { OnboardingWizard } from "@/components/OnboardingWizard"; +import { + buildFileTree, + collectAllPaths, + countFiles, + PackageFileTree, + parseFrontmatter, + type FileTreeNode, +} from "@/components/PackageFileTree"; +import { PageSkeleton } from "@/components/PageSkeleton"; +import { StatusBadge } from "@/components/StatusBadge"; +import { SwipeToArchive } from "@/components/SwipeToArchive"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { useDialog } from "@/context/DialogContext"; +import { queryKeys } from "@/lib/queryKeys"; +import { + createIssue, + storybookAgents, + storybookIssues, + storybookLiveRuns, +} from "../fixtures/paperclipData"; + +const companyId = "company-storybook"; +const primaryIssueId = "issue-storybook-1"; + +function StoryShell({ children }: { children: React.ReactNode }) { + return ( +
+
{children}
+
+ ); +} + +function Section({ + eyebrow, + title, + children, +}: { + eyebrow: string; + title: string; + children: React.ReactNode; +}) { + return ( +
+
+
{eyebrow}
+

{title}

+
+
{children}
+
+ ); +} + +function daysAgo(days: number, hour = 12): Date { + const date = new Date(); + date.setHours(hour, 0, 0, 0); + date.setDate(date.getDate() - days); + return date; +} + +function makeHeartbeatRun(overrides: Partial): HeartbeatRun { + const createdAt = overrides.createdAt ?? daysAgo(1); + const run: HeartbeatRun = { + id: "run-fixture", + companyId, + agentId: "agent-codex", + invocationSource: "on_demand", + triggerDetail: "manual", + status: "succeeded", + startedAt: createdAt, + finishedAt: new Date(createdAt.getTime() + 11 * 60_000), + error: null, + wakeupRequestId: null, + exitCode: 0, + signal: null, + usageJson: null, + resultJson: null, + sessionIdBefore: null, + sessionIdAfter: null, + logStore: null, + logRef: null, + logBytes: 0, + logSha256: null, + logCompressed: false, + stdoutExcerpt: null, + stderrExcerpt: null, + errorCode: null, + externalRunId: null, + processPid: null, + processGroupId: null, + processStartedAt: createdAt, + retryOfRunId: null, + processLossRetryCount: 0, + livenessState: "completed", + livenessReason: null, + continuationAttempt: 0, + lastUsefulActionAt: null, + nextAction: null, + contextSnapshot: null, + updatedAt: createdAt, + ...overrides, + createdAt, + updatedAt: overrides.updatedAt ?? createdAt, + }; + return run; +} + +const activityRuns: HeartbeatRun[] = [ + makeHeartbeatRun({ id: "run-chart-1", status: "succeeded", createdAt: daysAgo(13), startedAt: daysAgo(13) }), + makeHeartbeatRun({ id: "run-chart-2", status: "succeeded", createdAt: daysAgo(10), startedAt: daysAgo(10) }), + makeHeartbeatRun({ id: "run-chart-3", status: "failed", createdAt: daysAgo(10), startedAt: daysAgo(10, 15), exitCode: 1 }), + makeHeartbeatRun({ id: "run-chart-4", status: "running", createdAt: daysAgo(7), startedAt: daysAgo(7), finishedAt: null }), + makeHeartbeatRun({ id: "run-chart-5", status: "succeeded", createdAt: daysAgo(5), startedAt: daysAgo(5) }), + makeHeartbeatRun({ id: "run-chart-6", status: "timed_out", createdAt: daysAgo(3), startedAt: daysAgo(3), errorCode: "timeout" }), + makeHeartbeatRun({ id: "run-chart-7", status: "succeeded", createdAt: daysAgo(1), startedAt: daysAgo(1) }), + makeHeartbeatRun({ id: "run-chart-8", status: "succeeded", createdAt: daysAgo(1, 16), startedAt: daysAgo(1, 16) }), +]; + +const activityIssues = [ + { priority: "high", status: "in_progress", createdAt: daysAgo(13) }, + { priority: "critical", status: "blocked", createdAt: daysAgo(11) }, + { priority: "medium", status: "todo", createdAt: daysAgo(9) }, + { priority: "medium", status: "in_review", createdAt: daysAgo(9, 16) }, + { priority: "low", status: "done", createdAt: daysAgo(6) }, + { priority: "high", status: "todo", createdAt: daysAgo(4) }, + { priority: "critical", status: "in_progress", createdAt: daysAgo(2) }, + { priority: "medium", status: "done", createdAt: daysAgo(1) }, +]; + +const kanbanIssues: Issue[] = [ + ...storybookIssues, + createIssue({ + id: "issue-kanban-backlog", + identifier: "PAP-1701", + issueNumber: 1701, + title: "Sketch company analytics dashboard", + status: "backlog", + priority: "low", + assigneeAgentId: "agent-cto", + }), + createIssue({ + id: "issue-kanban-cancelled", + identifier: "PAP-1702", + issueNumber: 1702, + title: "Remove obsolete color token migration", + status: "cancelled", + priority: "medium", + assigneeAgentId: null, + }), +]; + +const packageFiles: Record = { + "COMPANY.md": "---\nname: Paperclip Storybook\nkind: company\n---\nFixture company package for UI review.", + "agents/codexcoder/AGENTS.md": "---\nname: CodexCoder\nskills:\n - frontend-design\n - paperclip\n---\nShips product UI and verifies changes.", + "agents/qachecker/AGENTS.md": "---\nname: QAChecker\nskills:\n - web-design-guidelines\n---\nReviews browser behavior and acceptance criteria.", + "projects/board-ui/PROJECT.md": "---\ntitle: Board UI\nstatus: in_progress\n---\nStorybook and operator control-plane surfaces.", + "tasks/PAP-1641.md": "---\ntitle: Create super-detailed storybooks\npriority: high\n---\nParent issue for Storybook coverage.", + "tasks/PAP-1677.md": "---\ntitle: Data Visualization & Misc stories\npriority: medium\n---\nFixture task for this story file.", + "skills/frontend-design/SKILL.md": "---\nname: frontend-design\n---\nDesign quality guidance.", +}; + +const actionMap = new Map([ + ["COMPANY.md", "replace"], + ["agents/codexcoder/AGENTS.md", "update"], + ["agents/qachecker/AGENTS.md", "create"], + ["tasks/PAP-1677.md", "create"], +]); + +function ActivityChartsMatrix({ empty = false }: { empty?: boolean }) { + const runs = empty ? [] : activityRuns; + const issues = empty ? [] : activityIssues; + + return ( + +
+
+ + + + + + + + + + + + +
+
+
+ ); +} + +function KanbanBoardDemo({ empty = false }: { empty?: boolean }) { + const [issues, setIssues] = useState(empty ? [] : kanbanIssues); + const liveIssueIds = useMemo(() => new Set(["issue-storybook-1", "issue-kanban-backlog"]), []); + + return ( + +
+ { + setIssues((current) => + current.map((issue) => (issue.id === id ? { ...issue, ...data } : issue)), + ); + }} + /> +
+
+ ); +} + +function FilterBarDemo({ empty = false }: { empty?: boolean }) { + const [filters, setFilters] = useState( + empty + ? [] + : [ + { key: "status", label: "Status", value: "In progress" }, + { key: "assignee", label: "Assignee", value: "CodexCoder" }, + { key: "priority", label: "Priority", value: "High" }, + { key: "project", label: "Project", value: "Board UI" }, + ], + ); + + return ( + +
+
+ setFilters((current) => current.filter((filter) => filter.key !== key))} + onClear={() => setFilters([])} + /> + {filters.length === 0 && ( +
+ + No filters are active. +
+ )} +
+
+
+ ); +} + +function LiveRunWidgetStory({ empty = false, loading = false }: { empty?: boolean; loading?: boolean }) { + const queryClient = useQueryClient(); + + useEffect(() => { + if (loading) return; + queryClient.setQueryData(queryKeys.issues.liveRuns(primaryIssueId), empty ? [] : storybookLiveRuns); + queryClient.setQueryData(queryKeys.issues.activeRun(primaryIssueId), empty ? null : storybookLiveRuns[0]); + }, [empty, loading, queryClient]); + + if (loading) { + return ( + +
+
+ + Waiting for the first run poll. +
+
+
+ ); + } + + return ( + +
+ + {empty && ( +
+ + The widget renders no panel when the issue has no live runs. +
+ )} +
+
+ ); +} + +function OpenOnboardingOnMount({ initialStep }: { initialStep: 1 | 2 }) { + const { openOnboarding } = useDialog(); + + useEffect(() => { + openOnboarding(initialStep === 1 ? { initialStep } : { initialStep, companyId }); + }, [initialStep, openOnboarding]); + + return ; +} + +function PackageFileTreeDemo({ empty = false }: { empty?: boolean }) { + const nodes = useMemo( + () => (empty ? [] : buildFileTree(packageFiles, actionMap)), + [empty], + ); + const allFilePaths = useMemo(() => collectAllPaths(nodes, "file"), [nodes]); + const [expandedDirs, setExpandedDirs] = useState(() => collectAllPaths(nodes, "dir")); + const [checkedFiles, setCheckedFiles] = useState(() => allFilePaths); + const [selectedFile, setSelectedFile] = useState(empty ? null : "tasks/PAP-1677.md"); + + useEffect(() => { + setExpandedDirs(collectAllPaths(nodes, "dir")); + setCheckedFiles(allFilePaths); + setSelectedFile(empty ? null : "tasks/PAP-1677.md"); + }, [allFilePaths, empty, nodes]); + + const selectedContent = selectedFile ? packageFiles[selectedFile] ?? "" : ""; + const frontmatter = selectedContent ? parseFrontmatter(selectedContent) : null; + + function toggleDir(path: string) { + setExpandedDirs((current) => { + const next = new Set(current); + if (next.has(path)) next.delete(path); + else next.add(path); + return next; + }); + } + + function toggleCheck(path: string, kind: "file" | "dir") { + setCheckedFiles((current) => { + const next = new Set(current); + const paths = + kind === "file" + ? [path] + : [...collectAllPaths(findNode(nodes, path)?.children ?? [], "file")]; + const shouldCheck = paths.some((candidate) => !next.has(candidate)); + for (const candidate of paths) { + if (shouldCheck) next.add(candidate); + else next.delete(candidate); + } + return next; + }); + } + + return ( + +
+ {empty ? ( +
+ No files are included in this package preview. +
+ ) : ( +
+
+
+ Package contents + {countFiles(nodes)} files +
+ + node.action ? ( + + {node.action} + + ) : null + } + /> +
+ + + + + {selectedFile} + + Frontmatter and markdown body parsed from the selected package file. + + + {frontmatter ? ( +
+ {Object.entries(frontmatter.data).map(([key, value]) => ( +
+
{key}
+
{Array.isArray(value) ? value.join(", ") : value}
+
+ ))} +
+ ) : null} +
+                  {frontmatter?.body.trim() || selectedContent}
+                
+
+
+
+ )} +
+
+ ); +} + +function findNode(nodes: FileTreeNode[], path: string): FileTreeNode | null { + for (const node of nodes) { + if (node.path === path) return node; + const child = findNode(node.children, path); + if (child) return child; + } + return null; +} + +function EntityRowsDemo({ empty = false }: { empty?: boolean }) { + const rows = empty + ? [] + : [ + { + id: "agent", + leading: , + identifier: "agent", + title: "CodexCoder", + subtitle: "Senior Product Engineer · active in Storybook worktree", + trailing: , + selected: true, + }, + { + id: "issue", + leading: , + identifier: "PAP-1677", + title: "Storybook: Data Visualization & Misc stories", + subtitle: "Medium priority · Board UI project", + trailing: UI, + }, + { + id: "approval", + leading: , + identifier: "approval", + title: "Publish Storybook preview", + subtitle: "Approved for internal design review", + trailing: , + }, + ]; + + return ( + +
+
+ {rows.map((row) => ( + + ))} + {rows.length === 0 && ( +
No entities match this view.
+ )} +
+
+
+ ); +} + +function SwipeToArchiveDemo({ disabled = false }: { disabled?: boolean }) { + const [archived, setArchived] = useState(false); + + return ( + +
+
+
+ Inbox +
+ {archived ? ( +
+ + Archived +
+ ) : ( + setArchived(true)} + > + } + identifier="PAP-1677" + title="Storybook: Data Visualization & Misc stories" + subtitle={disabled ? "Gesture disabled while review is locked" : "Swipe left on touch devices to archive"} + trailing={mobile} + /> + + )} + +
+
+
+ ); +} + +function CompanyPatternIconMatrix() { + const companies = [ + { name: "Paperclip Storybook", color: "#0f766e" }, + { name: "Research Bureau", color: "#2563eb" }, + { name: "Launch Ops", color: "#c2410c" }, + { name: "Atlas Finance", color: "#7c3aed" }, + ]; + const sizes = ["h-8 w-8 text-xs", "h-11 w-11 text-base", "h-16 w-16 text-xl", "h-24 w-24 text-3xl"]; + + return ( + +
+
+ {companies.map((company) => ( + + + {company.name} + {company.color} + + + {sizes.map((size) => ( + + ))} + + + ))} +
+
+
+ ); +} + +function AsciiArtAnimationDemo({ loading = false }: { loading?: boolean }) { + return ( + +
+
+ {loading ? ( +
+ + Preparing animation canvas +
+ ) : ( + + )} +
+
+
+ ); +} + +function PageSkeletonMatrix() { + const variants = [ + "list", + "issues-list", + "detail", + "dashboard", + "approvals", + "costs", + "inbox", + "org-chart", + ] as const; + + return ( + +
+
+ {variants.map((variant) => ( + + + {variant} + + +
+ +
+
+
+ ))} +
+
+
+ ); +} + +const meta = { + title: "Product/Data Visualization & Misc", + parameters: { + docs: { + description: { + component: + "Fixture-backed stories for charting, board, filtering, live run, onboarding, package preview, entity row, mobile gesture, generated icon, ASCII animation, and skeleton states.", + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const ActivityChartsPopulated: Story = { + name: "ActivityCharts / Populated", + render: () => , +}; + +export const ActivityChartsEmpty: Story = { + name: "ActivityCharts / Empty", + render: () => , +}; + +export const KanbanBoardPopulated: Story = { + name: "KanbanBoard / Populated", + render: () => , +}; + +export const KanbanBoardEmpty: Story = { + name: "KanbanBoard / Empty", + render: () => , +}; + +export const FilterBarPopulated: Story = { + name: "FilterBar / Populated", + render: () => , +}; + +export const FilterBarEmpty: Story = { + name: "FilterBar / Empty", + render: () => , +}; + +export const LiveRunWidgetPopulated: Story = { + name: "LiveRunWidget / Populated", + render: () => , +}; + +export const LiveRunWidgetLoading: Story = { + name: "LiveRunWidget / Loading", + render: () => , +}; + +export const LiveRunWidgetEmpty: Story = { + name: "LiveRunWidget / Empty", + render: () => , +}; + +export const OnboardingWizardCompanyStep: Story = { + name: "OnboardingWizard / Company Step", + render: () => , +}; + +export const OnboardingWizardAgentStep: Story = { + name: "OnboardingWizard / Agent Step", + render: () => , +}; + +export const PackageFileTreePopulated: Story = { + name: "PackageFileTree / Populated", + render: () => , +}; + +export const PackageFileTreeEmpty: Story = { + name: "PackageFileTree / Empty", + render: () => , +}; + +export const EntityRowPopulated: Story = { + name: "EntityRow / Populated", + render: () => , +}; + +export const EntityRowEmpty: Story = { + name: "EntityRow / Empty", + render: () => , +}; + +export const SwipeToArchiveMobile: Story = { + name: "SwipeToArchive / Mobile", + render: () => , +}; + +export const SwipeToArchiveDisabled: Story = { + name: "SwipeToArchive / Disabled", + render: () => , +}; + +export const CompanyPatternIconSizes: Story = { + name: "CompanyPatternIcon / Sizes", + render: () => , +}; + +export const AsciiArtAnimationPopulated: Story = { + name: "AsciiArtAnimation / Populated", + render: () => , +}; + +export const AsciiArtAnimationLoading: Story = { + name: "AsciiArtAnimation / Loading", + render: () => , +}; + +export const PageSkeletonLayouts: Story = { + name: "PageSkeleton / Layouts", + render: () => , +}; diff --git a/ui/storybook/stories/overview.stories.tsx b/ui/storybook/stories/overview.stories.tsx index f56cf4239ef..c95b2126a69 100644 --- a/ui/storybook/stories/overview.stories.tsx +++ b/ui/storybook/stories/overview.stories.tsx @@ -102,7 +102,7 @@ const coverageRows = [ ["Budget & finance", "Planned", "Incident cards, provider quotas, biller spend, subscription panels"], ["Dialogs & modals", "Planned", "New issue/agent/goal/project dialogs, diff modal, image gallery"], ["Projects & goals", "Planned", "Project properties, workspace cards, goal trees, runtime controls"], - ["Chat & comments", "Planned", "Comment threads, run chat, issue chat with timeline events"], + ["Chat & comments", "Covered", "Comment threads, run chat, issue chat with timeline events"], ["Data viz & misc", "Planned", "Activity charts, kanban, filter bar, live widget, onboarding, skeletons"], ["Full app pages", "Deferred", "API-driven route stories after page data loaders can be fixture-injected"], ];