2de893f624
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies. > - The board UI is the main operator surface, so its component and workflow coverage needs to stay reviewable as the product grows. > - This branch adds Storybook as a dedicated UI reference surface for core Paperclip screens and interaction patterns. > - That work spans Storybook infrastructure, app-level provider wiring, and a large fixture set that can render real control-plane states without a live backend. > - The branch also expands coverage across agents, budgets, issues, chat, dialogs, navigation, projects, and data visualization so future UI changes have a concrete visual baseline. > - This pull request packages that Storybook work on top of the latest `master`, excludes the lockfile from the final diff per repo policy, and fixes one fixture contract drift caught during verification. > - The benefit is a single reviewable PR that adds broad UI documentation and regression-surfacing coverage without losing the existing branch work. ## What Changed - Added Storybook 10 wiring for the UI package, including root scripts, UI package scripts, Storybook config, preview wrappers, Tailwind entrypoints, and setup docs. - Added a large fixture-backed data source for Storybook so complex board states can render without a live server. - Added story suites covering foundations, status language, control-plane surfaces, overview, UX labs, agent management, budget and finance, forms and editors, issue management, navigation and layout, chat and comments, data visualization, dialogs and modals, and projects/goals/workspaces. - Adjusted several UI components for Storybook parity so dialogs, menus, keyboard shortcuts, budget markers, markdown editing, and related surfaces render correctly in isolation. - Rebasing work for PR assembly: replayed the branch onto current `master`, removed `pnpm-lock.yaml` from the final PR diff, and aligned the dashboard fixture with the current `DashboardSummary.runActivity` API contract. ## Verification - `pnpm --filter @paperclipai/ui typecheck` - `pnpm --filter @paperclipai/ui build-storybook` - Manual diff audit after rebase: verified the PR no longer includes `pnpm-lock.yaml` and now cleanly targets current `master`. - Before/after UI note: before this branch there was no dedicated Storybook surface for these Paperclip views; after this branch the local Storybook build includes the new overview and domain story suites in `ui/storybook-static`. ## Risks - Large static fixture files can drift from shared types as dashboard and UI contracts evolve; this PR already needed one fixture correction for `runActivity`. - Storybook bundle output includes some large chunks, so future growth may need chunking work if build performance becomes an issue. - Several component tweaks were made for isolated rendering parity, so reviewers should spot-check key board surfaces against the live app behavior. ## Model Used - OpenAI Codex, GPT-5-based coding agent in the Paperclip harness; exact serving model ID is not exposed in-runtime to the agent. - Tool-assisted workflow with terminal execution, git operations, local typecheck/build verification, and GitHub CLI PR creation. - Context window/reasoning mode not surfaced by the harness. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
713 lines
26 KiB
TypeScript
713 lines
26 KiB
TypeScript
import { useMemo, useState, type ReactNode } from "react";
|
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
import type { Agent, CompanySecret, EnvBinding, Project, RoutineVariable } from "@paperclipai/shared";
|
|
import { Code2, FileText, ListPlus, RotateCcw, Table2 } from "lucide-react";
|
|
import { EnvVarEditor } from "@/components/EnvVarEditor";
|
|
import { ExecutionParticipantPicker } from "@/components/ExecutionParticipantPicker";
|
|
import { InlineEditor } from "@/components/InlineEditor";
|
|
import { InlineEntitySelector, type InlineEntityOption } from "@/components/InlineEntitySelector";
|
|
import { JsonSchemaForm, type JsonSchemaNode, getDefaultValues } from "@/components/JsonSchemaForm";
|
|
import { MarkdownBody } from "@/components/MarkdownBody";
|
|
import { MarkdownEditor, type MentionOption } from "@/components/MarkdownEditor";
|
|
import { ReportsToPicker } from "@/components/ReportsToPicker";
|
|
import {
|
|
RoutineRunVariablesDialog,
|
|
type RoutineRunDialogSubmitData,
|
|
} from "@/components/RoutineRunVariablesDialog";
|
|
import { RoutineVariablesEditor, RoutineVariablesHint } from "@/components/RoutineVariablesEditor";
|
|
import { ScheduleEditor, describeSchedule } from "@/components/ScheduleEditor";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { buildExecutionPolicy } from "@/lib/issue-execution-policy";
|
|
import { createIssue, storybookAgents } from "../fixtures/paperclipData";
|
|
|
|
function Section({
|
|
eyebrow,
|
|
title,
|
|
description,
|
|
children,
|
|
}: {
|
|
eyebrow: string;
|
|
title: string;
|
|
description?: string;
|
|
children: 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>
|
|
<div className="mt-1 flex flex-wrap items-end justify-between gap-3">
|
|
<div>
|
|
<h2 className="text-xl font-semibold">{title}</h2>
|
|
{description ? (
|
|
<p className="mt-2 max-w-3xl text-sm leading-6 text-muted-foreground">{description}</p>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="p-5">{children}</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function StatePanel({
|
|
label,
|
|
detail,
|
|
children,
|
|
disabled = false,
|
|
}: {
|
|
label: string;
|
|
detail?: string;
|
|
children: ReactNode;
|
|
disabled?: boolean;
|
|
}) {
|
|
return (
|
|
<div className="min-w-0 rounded-lg border border-border bg-background/70 p-4">
|
|
<div className="mb-3 flex min-h-6 flex-wrap items-start justify-between gap-2">
|
|
<div>
|
|
<div className="text-sm font-medium">{label}</div>
|
|
{detail ? <div className="mt-1 text-xs leading-5 text-muted-foreground">{detail}</div> : null}
|
|
</div>
|
|
{disabled ? <Badge variant="outline">disabled</Badge> : null}
|
|
</div>
|
|
<div className={disabled ? "pointer-events-none opacity-55" : undefined}>{children}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function StoryShell({ children }: { children: ReactNode }) {
|
|
return (
|
|
<div className="paperclip-story">
|
|
<main className="paperclip-story__inner space-y-6">{children}</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const reviewMarkdown = `# Release review
|
|
|
|
Ship criteria for the board UI refresh:
|
|
|
|
- [x] Preserve company-scoped routes
|
|
- [x] Keep comments and task updates auditable
|
|
- [ ] Attach screenshots after QA
|
|
|
|
| Surface | Owner | State |
|
|
| --- | --- | --- |
|
|
| Issues | CodexCoder | In progress |
|
|
| Approvals | CTO | Ready |
|
|
|
|
\`\`\`ts
|
|
const shouldRun = issue.status === "in_progress" && issue.companyId === company.id;
|
|
\`\`\`
|
|
|
|
See [the implementation notes](https://github.com/paperclipai/paperclip).`;
|
|
|
|
const editorMentions: MentionOption[] = [
|
|
{ id: "agent-codex", name: "CodexCoder", kind: "agent", agentId: "agent-codex", agentIcon: "code" },
|
|
{ id: "agent-qa", name: "QAChecker", kind: "agent", agentId: "agent-qa", agentIcon: "shield" },
|
|
{ id: "project-board-ui", name: "Board UI", kind: "project", projectId: "project-board-ui", projectColor: "#0f766e" },
|
|
{ id: "user-board", name: "Board Operator", kind: "user", userId: "user-board" },
|
|
];
|
|
|
|
const adapterSchema: JsonSchemaNode = {
|
|
type: "object",
|
|
required: ["adapterName", "apiKey", "concurrency"],
|
|
properties: {
|
|
adapterName: {
|
|
type: "string",
|
|
title: "Adapter name",
|
|
description: "Human-readable name shown in the adapter manager.",
|
|
minLength: 3,
|
|
default: "Codex local",
|
|
},
|
|
mode: {
|
|
type: "string",
|
|
title: "Run mode",
|
|
enum: ["review", "implementation", "maintenance"],
|
|
default: "implementation",
|
|
},
|
|
apiKey: {
|
|
type: "string",
|
|
title: "API key",
|
|
format: "secret-ref",
|
|
description: "Stored with the active Paperclip secret provider.",
|
|
},
|
|
concurrency: {
|
|
type: "integer",
|
|
title: "Max concurrent runs",
|
|
minimum: 1,
|
|
maximum: 6,
|
|
default: 2,
|
|
},
|
|
dryRun: {
|
|
type: "boolean",
|
|
title: "Dry run first",
|
|
description: "Require a preview run before mutating company data.",
|
|
default: true,
|
|
},
|
|
notes: {
|
|
type: "string",
|
|
title: "Operator notes",
|
|
format: "textarea",
|
|
maxLength: 500,
|
|
description: "Shown to the agent before checkout.",
|
|
},
|
|
allowedCommands: {
|
|
type: "array",
|
|
title: "Allowed commands",
|
|
description: "Commands this adapter can run without extra approval.",
|
|
items: { type: "string", default: "pnpm test" },
|
|
minItems: 1,
|
|
},
|
|
advanced: {
|
|
type: "object",
|
|
title: "Advanced guardrails",
|
|
properties: {
|
|
timeoutSeconds: { type: "integer", title: "Timeout seconds", minimum: 60, default: 900 },
|
|
requireApproval: { type: "boolean", title: "Require board approval", default: false },
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const validAdapterValues = {
|
|
...getDefaultValues(adapterSchema),
|
|
adapterName: "Codex local",
|
|
mode: "implementation",
|
|
apiKey: "secret:openai-api-key",
|
|
concurrency: 2,
|
|
dryRun: true,
|
|
notes: "Use the project worktree and post a concise task update before handoff.",
|
|
allowedCommands: ["pnpm --filter @paperclipai/ui typecheck", "pnpm build-storybook"],
|
|
advanced: { timeoutSeconds: 900, requireApproval: false },
|
|
};
|
|
|
|
const invalidAdapterValues = {
|
|
...validAdapterValues,
|
|
adapterName: "AI",
|
|
apiKey: "",
|
|
concurrency: 9,
|
|
};
|
|
|
|
const adapterErrors = {
|
|
"/adapterName": "Must be at least 3 characters",
|
|
"/apiKey": "This field is required",
|
|
"/concurrency": "Must be at most 6",
|
|
};
|
|
|
|
const storybookSecrets: CompanySecret[] = [
|
|
{
|
|
id: "secret-openai",
|
|
companyId: "company-storybook",
|
|
name: "OPENAI_API_KEY",
|
|
provider: "local_encrypted",
|
|
externalRef: null,
|
|
latestVersion: 3,
|
|
description: null,
|
|
createdByAgentId: null,
|
|
createdByUserId: "user-board",
|
|
createdAt: new Date("2026-04-18T10:00:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T10:00:00.000Z"),
|
|
},
|
|
{
|
|
id: "secret-github",
|
|
companyId: "company-storybook",
|
|
name: "GITHUB_TOKEN",
|
|
provider: "local_encrypted",
|
|
externalRef: null,
|
|
latestVersion: 1,
|
|
description: null,
|
|
createdByAgentId: null,
|
|
createdByUserId: "user-board",
|
|
createdAt: new Date("2026-04-19T10:00:00.000Z"),
|
|
updatedAt: new Date("2026-04-19T10:00:00.000Z"),
|
|
},
|
|
];
|
|
|
|
const filledEnv: Record<string, EnvBinding> = {
|
|
NODE_ENV: { type: "plain", value: "development" },
|
|
OPENAI_API_KEY: { type: "secret_ref", secretId: "secret-openai", version: "latest" },
|
|
};
|
|
|
|
const routineVariables: RoutineVariable[] = [
|
|
{
|
|
name: "repo",
|
|
label: "Repository",
|
|
type: "text",
|
|
defaultValue: "paperclipai/paperclip",
|
|
required: true,
|
|
options: [],
|
|
},
|
|
{
|
|
name: "priority",
|
|
label: "Priority",
|
|
type: "select",
|
|
defaultValue: "medium",
|
|
required: true,
|
|
options: ["low", "medium", "high"],
|
|
},
|
|
{
|
|
name: "include_browser",
|
|
label: "Include browser QA",
|
|
type: "boolean",
|
|
defaultValue: true,
|
|
required: false,
|
|
options: [],
|
|
},
|
|
{
|
|
name: "notes",
|
|
label: "Run notes",
|
|
type: "textarea",
|
|
defaultValue: "Capture any visible layout regressions.",
|
|
required: false,
|
|
options: [],
|
|
},
|
|
];
|
|
|
|
const storybookProject: Project = {
|
|
id: "project-board-ui",
|
|
companyId: "company-storybook",
|
|
urlKey: "board-ui",
|
|
goalId: "goal-company",
|
|
goalIds: ["goal-company"],
|
|
goals: [{ id: "goal-company", title: "We're building Paperclip" }],
|
|
name: "Board UI",
|
|
description: "Control-plane interface, Storybook review surfaces, and operator workflows.",
|
|
status: "in_progress",
|
|
leadAgentId: "agent-codex",
|
|
targetDate: null,
|
|
color: "#0f766e",
|
|
env: null,
|
|
pauseReason: null,
|
|
pausedAt: null,
|
|
executionWorkspacePolicy: null,
|
|
codebase: {
|
|
workspaceId: "workspace-board-ui",
|
|
repoUrl: "https://github.com/paperclipai/paperclip",
|
|
repoRef: "master",
|
|
defaultRef: "master",
|
|
repoName: "paperclip",
|
|
localFolder: "/Users/dotta/paperclip",
|
|
managedFolder: "paperclip",
|
|
effectiveLocalFolder: "/Users/dotta/paperclip",
|
|
origin: "local_folder",
|
|
},
|
|
workspaces: [],
|
|
primaryWorkspace: null,
|
|
archivedAt: null,
|
|
createdAt: new Date("2026-04-01T10:00:00.000Z"),
|
|
updatedAt: new Date("2026-04-20T10:00:00.000Z"),
|
|
};
|
|
|
|
const entityOptions: InlineEntityOption[] = [
|
|
{ id: "issue-1672", label: "Storybook forms and editors", searchText: "PAP-1672 ui story coverage" },
|
|
{ id: "project-board-ui", label: "Board UI", searchText: "project frontend Storybook" },
|
|
{ id: "agent-codex", label: "CodexCoder", searchText: "engineer implementation" },
|
|
];
|
|
|
|
function MarkdownEditorGallery() {
|
|
const [emptyMarkdown, setEmptyMarkdown] = useState("");
|
|
const [filledMarkdown, setFilledMarkdown] = useState(reviewMarkdown);
|
|
const [actionMarkdown, setActionMarkdown] = useState("Draft an update for @CodexCoder and /check-pr.");
|
|
|
|
return (
|
|
<Section
|
|
eyebrow="MarkdownEditor"
|
|
title="Composer states with content, read-only mode, and action buttons"
|
|
description="The editor is controlled in all examples so reviewers can type, trigger mentions, and see command insertion behavior."
|
|
>
|
|
<div className="grid gap-4 lg:grid-cols-2">
|
|
<StatePanel label="Empty" detail="Placeholder, border, and mention-ready empty state.">
|
|
<MarkdownEditor
|
|
value={emptyMarkdown}
|
|
onChange={setEmptyMarkdown}
|
|
placeholder="Write a task update..."
|
|
mentions={editorMentions}
|
|
/>
|
|
</StatePanel>
|
|
<StatePanel label="Filled" detail="Long-form markdown with a table and fenced code block.">
|
|
<MarkdownEditor value={filledMarkdown} onChange={setFilledMarkdown} mentions={editorMentions} />
|
|
</StatePanel>
|
|
<StatePanel label="Read-only" detail="Uses the editor rendering path without accepting edits." disabled>
|
|
<MarkdownEditor value={reviewMarkdown} onChange={() => undefined} readOnly mentions={editorMentions} />
|
|
</StatePanel>
|
|
<StatePanel label="Toolbar actions" detail="External controls exercise insertion actions around the editor.">
|
|
<div className="mb-3 flex flex-wrap gap-2">
|
|
<Button size="sm" variant="outline" onClick={() => setActionMarkdown((value) => `${value}\n\n## Next action\n`)}>
|
|
<FileText className="mr-2 h-4 w-4" />
|
|
Heading
|
|
</Button>
|
|
<Button size="sm" variant="outline" onClick={() => setActionMarkdown((value) => `${value}\n\n- Verify typecheck\n- Build Storybook\n`)}>
|
|
<ListPlus className="mr-2 h-4 w-4" />
|
|
List
|
|
</Button>
|
|
<Button size="sm" variant="outline" onClick={() => setActionMarkdown((value) => `${value}\n\n| Field | State |\n| --- | --- |\n| Forms | Ready |\n`)}>
|
|
<Table2 className="mr-2 h-4 w-4" />
|
|
Table
|
|
</Button>
|
|
<Button size="sm" variant="outline" onClick={() => setActionMarkdown((value) => `${value}\n\n\`\`\`sh\npnpm build-storybook\n\`\`\`\n`)}>
|
|
<Code2 className="mr-2 h-4 w-4" />
|
|
Code
|
|
</Button>
|
|
<Button size="sm" variant="ghost" onClick={() => setActionMarkdown("Draft an update for @CodexCoder and /check-pr.")}>
|
|
<RotateCcw className="mr-2 h-4 w-4" />
|
|
Reset
|
|
</Button>
|
|
</div>
|
|
<MarkdownEditor value={actionMarkdown} onChange={setActionMarkdown} mentions={editorMentions} />
|
|
</StatePanel>
|
|
</div>
|
|
</Section>
|
|
);
|
|
}
|
|
|
|
function MarkdownBodyGallery() {
|
|
return (
|
|
<Section
|
|
eyebrow="MarkdownBody"
|
|
title="Rendered markdown for task documents and comments"
|
|
description="GFM coverage includes headings, task lists, links, tables, and code blocks in the app's prose wrapper."
|
|
>
|
|
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_320px]">
|
|
<StatePanel label="Filled markdown" detail="Mixed document syntax with code and table overflow handling.">
|
|
<MarkdownBody linkIssueReferences={false}>{reviewMarkdown}</MarkdownBody>
|
|
</StatePanel>
|
|
<div className="space-y-4">
|
|
<StatePanel label="Empty">
|
|
<MarkdownBody>{""}</MarkdownBody>
|
|
<p className="text-sm text-muted-foreground">No markdown body content.</p>
|
|
</StatePanel>
|
|
<StatePanel label="Disabled container" disabled>
|
|
<MarkdownBody linkIssueReferences={false}>A read-only preview can be dimmed by the parent surface.</MarkdownBody>
|
|
</StatePanel>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
);
|
|
}
|
|
|
|
function JsonSchemaFormGallery() {
|
|
const [filledValues, setFilledValues] = useState<Record<string, unknown>>(validAdapterValues);
|
|
const [errorValues, setErrorValues] = useState<Record<string, unknown>>(invalidAdapterValues);
|
|
|
|
return (
|
|
<Section
|
|
eyebrow="JsonSchemaForm"
|
|
title="Generated adapter configuration forms"
|
|
description="The schema exercises strings, enums, secrets, numbers, booleans, arrays, objects, validation errors, and disabled controls."
|
|
>
|
|
<div className="grid gap-4 xl:grid-cols-2">
|
|
<StatePanel label="Filled">
|
|
<JsonSchemaForm schema={adapterSchema} values={filledValues} onChange={setFilledValues} />
|
|
</StatePanel>
|
|
<StatePanel label="Validation errors">
|
|
<JsonSchemaForm schema={adapterSchema} values={errorValues} onChange={setErrorValues} errors={adapterErrors} />
|
|
</StatePanel>
|
|
<StatePanel label="Empty schema">
|
|
<JsonSchemaForm schema={{ type: "object", properties: {} }} values={{}} onChange={() => undefined} />
|
|
</StatePanel>
|
|
<StatePanel label="Disabled" disabled>
|
|
<JsonSchemaForm schema={adapterSchema} values={filledValues} onChange={() => undefined} disabled />
|
|
</StatePanel>
|
|
</div>
|
|
</Section>
|
|
);
|
|
}
|
|
|
|
function InlineEditorGallery() {
|
|
const [title, setTitle] = useState("Storybook: Forms & Editors stories");
|
|
const [description, setDescription] = useState(
|
|
"Create fixture-backed editor stories for the board UI, then verify Storybook builds.",
|
|
);
|
|
const [emptyTitle, setEmptyTitle] = useState("");
|
|
|
|
return (
|
|
<Section eyebrow="InlineEditor" title="Inline title and description editing">
|
|
<div className="grid gap-4 lg:grid-cols-3">
|
|
<StatePanel label="Title editing" detail="Click the title to edit and press Enter to save.">
|
|
<InlineEditor value={title} onSave={setTitle} as="h2" className="text-2xl font-semibold" />
|
|
</StatePanel>
|
|
<StatePanel label="Description editing" detail="Multiline markdown editor with autosave affordance.">
|
|
<InlineEditor value={description} onSave={setDescription} as="p" multiline nullable />
|
|
</StatePanel>
|
|
<StatePanel label="Empty nullable title" detail="Placeholder state for optional inline fields.">
|
|
<InlineEditor value={emptyTitle} onSave={setEmptyTitle} as="h2" nullable placeholder="Untitled issue" />
|
|
</StatePanel>
|
|
</div>
|
|
</Section>
|
|
);
|
|
}
|
|
|
|
function EnvVarEditorGallery() {
|
|
const [emptyEnv, setEmptyEnv] = useState<Record<string, EnvBinding>>({});
|
|
const [env, setEnv] = useState<Record<string, EnvBinding>>(filledEnv);
|
|
const createSecret = async (name: string): Promise<CompanySecret> => ({
|
|
...storybookSecrets[0]!,
|
|
id: `secret-${name.toLowerCase()}`,
|
|
name,
|
|
latestVersion: 1,
|
|
});
|
|
|
|
return (
|
|
<Section eyebrow="EnvVarEditor" title="Runtime environment bindings">
|
|
<div className="grid gap-4 lg:grid-cols-3">
|
|
<StatePanel label="Empty add row" detail="Trailing blank row is the add state.">
|
|
<EnvVarEditor value={emptyEnv} secrets={storybookSecrets} onCreateSecret={createSecret} onChange={(next) => setEmptyEnv(next ?? {})} />
|
|
</StatePanel>
|
|
<StatePanel label="Plain and secret values" detail="Filled rows show edit, seal, secret select, and remove controls.">
|
|
<EnvVarEditor value={env} secrets={storybookSecrets} onCreateSecret={createSecret} onChange={(next) => setEnv(next ?? {})} />
|
|
</StatePanel>
|
|
<StatePanel label="Disabled shell" disabled>
|
|
<EnvVarEditor value={filledEnv} secrets={storybookSecrets} onCreateSecret={createSecret} onChange={() => undefined} />
|
|
</StatePanel>
|
|
</div>
|
|
</Section>
|
|
);
|
|
}
|
|
|
|
function ScheduleEditorGallery() {
|
|
const [emptyCron, setEmptyCron] = useState("");
|
|
const [weeklyCron, setWeeklyCron] = useState("30 9 * * 1");
|
|
const [customCron, setCustomCron] = useState("15 16 1 * *");
|
|
|
|
return (
|
|
<Section eyebrow="ScheduleEditor" title="Cron picker with human-readable previews">
|
|
<div className="grid gap-4 lg:grid-cols-3">
|
|
<StatePanel label="Empty default" detail={describeSchedule(emptyCron)}>
|
|
<ScheduleEditor value={emptyCron} onChange={setEmptyCron} />
|
|
</StatePanel>
|
|
<StatePanel label="Weekly filled" detail={describeSchedule(weeklyCron)}>
|
|
<ScheduleEditor value={weeklyCron} onChange={setWeeklyCron} />
|
|
</StatePanel>
|
|
<StatePanel label="Custom disabled preview" detail={describeSchedule(customCron)} disabled>
|
|
<ScheduleEditor value={customCron} onChange={setCustomCron} />
|
|
</StatePanel>
|
|
</div>
|
|
</Section>
|
|
);
|
|
}
|
|
|
|
function RoutineVariablesGallery() {
|
|
const [variables, setVariables] = useState<RoutineVariable[]>(routineVariables);
|
|
|
|
return (
|
|
<Section
|
|
eyebrow="RoutineVariablesEditor"
|
|
title="Detected runtime variable definitions"
|
|
description="Variable rows are synced from title and instructions placeholders, then configured with types, defaults, required flags, and select options."
|
|
>
|
|
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_320px]">
|
|
<StatePanel label="Detected variables">
|
|
<RoutineVariablesEditor
|
|
title="Review {{repo}} at {{priority}} priority"
|
|
description="Include browser QA: {{include_browser}}\n\nOperator notes: {{notes}}"
|
|
value={variables}
|
|
onChange={setVariables}
|
|
/>
|
|
</StatePanel>
|
|
<div className="space-y-4">
|
|
<StatePanel label="Empty hint">
|
|
<RoutineVariablesHint />
|
|
</StatePanel>
|
|
<StatePanel label="Disabled shell" disabled>
|
|
<RoutineVariablesEditor
|
|
title="Review {{repo}}"
|
|
description="Use {{priority}} priority"
|
|
value={variables.slice(0, 2)}
|
|
onChange={() => undefined}
|
|
/>
|
|
</StatePanel>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
);
|
|
}
|
|
|
|
function PickerGallery() {
|
|
const [issue, setIssue] = useState(() =>
|
|
createIssue({
|
|
executionPolicy: buildExecutionPolicy({
|
|
reviewerValues: ["agent:agent-qa"],
|
|
approverValues: ["user:user-board"],
|
|
}),
|
|
}),
|
|
);
|
|
const [manager, setManager] = useState<string | null>("agent-cto");
|
|
const [selectorValue, setSelectorValue] = useState("project-board-ui");
|
|
const agentsWithTerminated: Agent[] = useMemo(
|
|
() => [
|
|
...storybookAgents,
|
|
{
|
|
...storybookAgents[1]!,
|
|
id: "agent-legacy",
|
|
name: "LegacyReviewer",
|
|
status: "terminated",
|
|
reportsTo: null,
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
|
|
return (
|
|
<Section
|
|
eyebrow="Pickers"
|
|
title="Execution participants, reporting hierarchy, and inline entity selection"
|
|
description="Closed trigger states stay compact, while the dropdowns are interactive for search and selection review."
|
|
>
|
|
<div className="grid gap-4 xl:grid-cols-3">
|
|
<StatePanel label="ExecutionParticipantPicker" detail="Review and approval participants share the same policy object.">
|
|
<div className="flex flex-wrap gap-3">
|
|
<ExecutionParticipantPicker
|
|
issue={issue}
|
|
stageType="review"
|
|
agents={storybookAgents}
|
|
currentUserId="user-board"
|
|
onUpdate={(patch) => setIssue((current) => ({ ...current, ...patch }))}
|
|
/>
|
|
<ExecutionParticipantPicker
|
|
issue={issue}
|
|
stageType="approval"
|
|
agents={storybookAgents}
|
|
currentUserId="user-board"
|
|
onUpdate={(patch) => setIssue((current) => ({ ...current, ...patch }))}
|
|
/>
|
|
</div>
|
|
</StatePanel>
|
|
<StatePanel label="ReportsToPicker" detail="Selected manager, CEO disabled state, and filtered hierarchy choices.">
|
|
<div className="flex flex-wrap gap-3">
|
|
<ReportsToPicker agents={agentsWithTerminated} value={manager} onChange={setManager} excludeAgentIds={["agent-codex"]} />
|
|
<ReportsToPicker agents={agentsWithTerminated} value={null} onChange={() => undefined} disabled />
|
|
</div>
|
|
</StatePanel>
|
|
<StatePanel label="InlineEntitySelector" detail="Search/select dropdown for issue, project, and agent entities.">
|
|
<div className="flex flex-wrap gap-3">
|
|
<InlineEntitySelector
|
|
value={selectorValue}
|
|
options={entityOptions}
|
|
recentOptionIds={["issue-1672"]}
|
|
placeholder="Entity"
|
|
noneLabel="No entity"
|
|
searchPlaceholder="Search entities..."
|
|
emptyMessage="No matching entity."
|
|
onChange={setSelectorValue}
|
|
/>
|
|
<div className="pointer-events-none opacity-55">
|
|
<InlineEntitySelector
|
|
value=""
|
|
options={entityOptions}
|
|
placeholder="Entity"
|
|
noneLabel="No entity"
|
|
searchPlaceholder="Search entities..."
|
|
emptyMessage="No matching entity."
|
|
onChange={() => undefined}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</StatePanel>
|
|
</div>
|
|
</Section>
|
|
);
|
|
}
|
|
|
|
function FormsEditorsShowcase() {
|
|
return (
|
|
<StoryShell>
|
|
<section className="paperclip-story__frame p-6">
|
|
<div className="flex flex-wrap items-start justify-between gap-5">
|
|
<div>
|
|
<div className="paperclip-story__label">Forms and editors</div>
|
|
<h1 className="mt-2 text-3xl font-semibold tracking-tight">Paperclip form controls under realistic state</h1>
|
|
<p className="mt-3 max-w-3xl text-sm leading-6 text-muted-foreground">
|
|
Dense control-plane forms need to hold empty, filled, validation, and disabled states without losing scan
|
|
speed. These fixtures keep the components reviewable outside production routes.
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
<Badge variant="outline">empty</Badge>
|
|
<Badge variant="outline">filled</Badge>
|
|
<Badge variant="outline">validation</Badge>
|
|
<Badge variant="outline">disabled</Badge>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<MarkdownEditorGallery />
|
|
<MarkdownBodyGallery />
|
|
<JsonSchemaFormGallery />
|
|
<InlineEditorGallery />
|
|
<EnvVarEditorGallery />
|
|
<ScheduleEditorGallery />
|
|
<RoutineVariablesGallery />
|
|
<PickerGallery />
|
|
</StoryShell>
|
|
);
|
|
}
|
|
|
|
function RoutineRunDialogStory() {
|
|
const [open, setOpen] = useState(true);
|
|
const [submitted, setSubmitted] = useState<RoutineRunDialogSubmitData | null>(null);
|
|
|
|
return (
|
|
<StoryShell>
|
|
<Section
|
|
eyebrow="RoutineRunVariablesDialog"
|
|
title="Manual routine run configuration"
|
|
description="The dialog collects runtime variables, the target assignee, and optional project context before creating the run issue."
|
|
>
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<Button onClick={() => setOpen(true)}>Open run dialog</Button>
|
|
{submitted ? (
|
|
<pre className="max-w-full overflow-x-auto rounded-md border border-border bg-muted/40 px-3 py-2 text-xs">
|
|
{JSON.stringify(submitted, null, 2)}
|
|
</pre>
|
|
) : (
|
|
<span className="text-sm text-muted-foreground">Submit the dialog to inspect the payload.</span>
|
|
)}
|
|
</div>
|
|
</Section>
|
|
<RoutineRunVariablesDialog
|
|
open={open}
|
|
onOpenChange={setOpen}
|
|
companyId="company-storybook"
|
|
routineName="Weekly release review"
|
|
projects={[storybookProject]}
|
|
agents={storybookAgents}
|
|
defaultProjectId="project-board-ui"
|
|
defaultAssigneeAgentId="agent-codex"
|
|
variables={routineVariables}
|
|
isPending={false}
|
|
onSubmit={(data) => {
|
|
setSubmitted({ ...data });
|
|
setOpen(false);
|
|
}}
|
|
/>
|
|
</StoryShell>
|
|
);
|
|
}
|
|
|
|
const meta = {
|
|
title: "Components/Forms & Editors",
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
component:
|
|
"Fixture-backed stories for Paperclip form controls, markdown editors, inline editors, schedule controls, runtime-variable dialogs, and selection pickers.",
|
|
},
|
|
},
|
|
},
|
|
} satisfies Meta;
|
|
|
|
export default meta;
|
|
|
|
type Story = StoryObj<typeof meta>;
|
|
|
|
export const AllFormsAndEditors: Story = {
|
|
name: "All Forms And Editors",
|
|
render: () => <FormsEditorsShowcase />,
|
|
};
|
|
|
|
export const RoutineRunVariablesDialogOpen: Story = {
|
|
name: "Routine Run Variables Dialog",
|
|
render: () => <RoutineRunDialogStory />,
|
|
};
|