mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-13 17:40:38 +09:00
## Thinking Path > - Paperclip orchestrates AI-agent companies through issues, documents, runs, and durable company-scoped state. > - Issue documents are where agents and operators capture plans, handoffs, and work products. > - Before this change, document collaboration could only happen through whole-document edits and detached issue comments. > - Inline document annotations need stable anchors, revision-aware persistence, and UI affordances that do not break existing document editing. > - This pull request adds company-scoped document annotation threads, comments, anchor snapshots, API routes, and board UI. > - The benefit is that operators and agents can discuss specific document passages without losing context as documents evolve. ## What Changed - Added document annotation tables, schema exports, shared types, validators, anchor hashing, and text-anchor helpers. - Added server-side document annotation services and issue routes for listing, creating, commenting, resolving, and reopening annotation threads. - Included annotation summaries in relevant issue document reads and backup/recovery document workspace behavior. - Added React UI for inline document highlights, comment panels, mobile sheet behavior, deep-link focus, and resolved/open filtering. - Added annotation design artifacts, Storybook coverage, screenshots, and a screenshot helper script. - Rebased the branch onto current `paperclipai/paperclip` `master` and renumbered the annotation migration from `0085_old_swarm` to `0091_old_swarm`; the SQL uses `IF NOT EXISTS` guards so environments that previously applied the old migration number can safely apply the new one. - Adjusted the new annotation UI tests to use a local async flush helper because this workspace's React 19.2.4 export does not expose `React.act`. ## Verification - `pnpm run preflight:workspace-links && pnpm exec vitest run packages/shared/src/document-anchors.test.ts server/src/__tests__/document-annotation-routes.test.ts server/src/__tests__/document-annotations-service.test.ts ui/src/components/DocumentAnnotationLayer.test.tsx ui/src/components/IssueDocumentAnnotations.test.tsx ui/src/lib/document-annotation-hash.test.ts ui/src/lib/document-annotation-selection.test.ts` - Confirmed `git diff --check` passes. - Confirmed no `pnpm-lock.yaml` or `.github/workflows/*` files are included in the PR diff. ## Risks - Medium risk: this adds new persisted annotation tables and routes across db/shared/server/ui. - Migration risk is reduced by moving the branch migration to `0091_old_swarm` after upstream `0090_resource_memberships` and keeping the SQL idempotent for old `0085_old_swarm` adopters. - UI risk is mostly around text range anchoring and panel positioning across long documents, folded content, and mobile layouts; the PR includes focused unit coverage and design screenshots. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, GPT-5 coding agent, tool-using software engineering mode. Context window size is not exposed in this Paperclip runtime. ## 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 - [x] 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>
571 lines
20 KiB
TypeScript
571 lines
20 KiB
TypeScript
import { useMemo, useRef, useState } from "react";
|
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import type {
|
|
Agent,
|
|
DocumentAnnotationThreadWithComments,
|
|
Issue,
|
|
IssueDocument,
|
|
} from "@paperclipai/shared";
|
|
import { DocumentAnnotationPanel } from "@/components/DocumentAnnotationPanel";
|
|
import { DocumentAnnotationLayer, type PendingAnchor } from "@/components/DocumentAnnotationLayer";
|
|
import {
|
|
DocumentAnnotationsCountChip,
|
|
IssueDocumentAnnotations,
|
|
} from "@/components/IssueDocumentAnnotations";
|
|
import { IssueDocumentsSection } from "@/components/IssueDocumentsSection";
|
|
import { MarkdownBody } from "@/components/MarkdownBody";
|
|
import { MarkdownEditor } from "@/components/MarkdownEditor";
|
|
import { queryKeys } from "@/lib/queryKeys";
|
|
import type { CompanyUserProfile } from "@/lib/company-members";
|
|
|
|
const sampleMarkdown = `# Plan: Document Highlights And Comment Threads
|
|
|
|
We should **keep** the current markdown document stack for the first version.
|
|
The existing editor is MDXEditor on top of Lexical, and the current code already uses Lexical-level customization.
|
|
|
|
## Reader And Goal
|
|
|
|
Reader: board reviewer, CTO, and implementing engineers.
|
|
|
|
## Anchor Strategy
|
|
|
|
Do not insert comment markers into markdown. The markdown document body must
|
|
remain portable and readable.
|
|
|
|
Use a sidecar anchor made from two selectors:
|
|
|
|
- Text quote selector: exact selected text plus prefix/suffix context.
|
|
- Text position selector: normalized rendered-text offsets plus markdown source offsets.
|
|
|
|
## Future Work
|
|
|
|
Phase 5 covers QA validation across desktop and mobile.`;
|
|
|
|
function makeThread(
|
|
overrides: Partial<DocumentAnnotationThreadWithComments> = {},
|
|
): DocumentAnnotationThreadWithComments {
|
|
const id = overrides.id ?? "thread-1";
|
|
return {
|
|
id,
|
|
companyId: "co-1",
|
|
issueId: "issue-1",
|
|
documentId: "doc-1",
|
|
documentKey: "plan",
|
|
status: "open",
|
|
anchorState: "active",
|
|
anchorConfidence: "exact",
|
|
originalRevisionId: "rev-4",
|
|
originalRevisionNumber: 4,
|
|
currentRevisionId: "rev-4",
|
|
currentRevisionNumber: 4,
|
|
selectedText: "keep the current markdown document stack",
|
|
prefixText: "We should ",
|
|
suffixText: " for the first version",
|
|
normalizedStart: 0,
|
|
normalizedEnd: 40,
|
|
markdownStart: 0,
|
|
markdownEnd: 40,
|
|
anchorSelector: {
|
|
quote: {
|
|
exact: "keep the current markdown document stack",
|
|
prefix: "We should ",
|
|
suffix: " for the first version",
|
|
},
|
|
position: { normalizedStart: 0, normalizedEnd: 40, markdownStart: 0, markdownEnd: 40 },
|
|
},
|
|
createdByAgentId: null,
|
|
createdByUserId: "user-1",
|
|
resolvedByAgentId: null,
|
|
resolvedByUserId: null,
|
|
resolvedAt: null,
|
|
createdAt: new Date("2026-05-12T10:00:00Z"),
|
|
updatedAt: new Date("2026-05-12T10:01:00Z"),
|
|
comments: [
|
|
{
|
|
id: "comment-1",
|
|
companyId: "co-1",
|
|
threadId: id,
|
|
issueId: "issue-1",
|
|
documentId: "doc-1",
|
|
body: "Could we benchmark the editor against a CRDT alternative before committing?",
|
|
authorType: "user",
|
|
authorAgentId: null,
|
|
authorUserId: "user-1",
|
|
createdByRunId: null,
|
|
createdAt: new Date("2026-05-12T10:00:00Z"),
|
|
updatedAt: new Date("2026-05-12T10:00:00Z"),
|
|
},
|
|
{
|
|
id: "comment-2",
|
|
companyId: "co-1",
|
|
threadId: id,
|
|
issueId: "issue-1",
|
|
documentId: "doc-1",
|
|
body: "We did a small spike — happy to share results in the plan.",
|
|
authorType: "agent",
|
|
authorAgentId: "agent-uxdesigner",
|
|
authorUserId: null,
|
|
createdByRunId: "run-1",
|
|
createdAt: new Date("2026-05-12T10:01:00Z"),
|
|
updatedAt: new Date("2026-05-12T10:01:00Z"),
|
|
},
|
|
],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
const baseThreads: DocumentAnnotationThreadWithComments[] = [
|
|
makeThread({ id: "open-1" }),
|
|
makeThread({
|
|
id: "stale-1",
|
|
anchorState: "stale",
|
|
anchorConfidence: "fuzzy",
|
|
selectedText: "two selectors",
|
|
prefixText: "anchor made from ",
|
|
suffixText: ":",
|
|
comments: [
|
|
{
|
|
id: "comment-stale",
|
|
companyId: "co-1",
|
|
threadId: "stale-1",
|
|
issueId: "issue-1",
|
|
documentId: "doc-1",
|
|
body: "Original wording was slightly different — re-anchor when convenient.",
|
|
authorType: "user",
|
|
authorAgentId: null,
|
|
authorUserId: "user-1",
|
|
createdByRunId: null,
|
|
createdAt: new Date("2026-05-12T11:00:00Z"),
|
|
updatedAt: new Date("2026-05-12T11:00:00Z"),
|
|
},
|
|
],
|
|
}),
|
|
makeThread({
|
|
id: "resolved-1",
|
|
status: "resolved",
|
|
selectedText: "Reader: board reviewer, CTO, and implementing engineers",
|
|
comments: [
|
|
{
|
|
id: "comment-resolved",
|
|
companyId: "co-1",
|
|
threadId: "resolved-1",
|
|
issueId: "issue-1",
|
|
documentId: "doc-1",
|
|
body: "Updated reader list to add the security lead.",
|
|
authorType: "agent",
|
|
authorAgentId: "agent-uxdesigner",
|
|
authorUserId: null,
|
|
createdByRunId: "run-1",
|
|
createdAt: new Date("2026-05-12T12:00:00Z"),
|
|
updatedAt: new Date("2026-05-12T12:00:00Z"),
|
|
},
|
|
],
|
|
}),
|
|
makeThread({
|
|
id: "orphan-1",
|
|
anchorState: "orphaned",
|
|
selectedText: "an earlier paragraph that has been rewritten",
|
|
comments: [
|
|
{
|
|
id: "comment-orphan",
|
|
companyId: "co-1",
|
|
threadId: "orphan-1",
|
|
issueId: "issue-1",
|
|
documentId: "doc-1",
|
|
body: "This anchor lost its location after the rewrite. Original quote preserved.",
|
|
authorType: "user",
|
|
authorAgentId: null,
|
|
authorUserId: "user-1",
|
|
createdByRunId: null,
|
|
createdAt: new Date("2026-05-12T13:00:00Z"),
|
|
updatedAt: new Date("2026-05-12T13:00:00Z"),
|
|
},
|
|
],
|
|
}),
|
|
];
|
|
|
|
const integratedAgentMap: ReadonlyMap<string, Pick<Agent, "id" | "name">> = new Map([
|
|
["agent-uxdesigner", { id: "agent-uxdesigner", name: "UXDesigner" }],
|
|
]);
|
|
const integratedUserProfileMap: ReadonlyMap<string, CompanyUserProfile> = new Map([
|
|
["user-1", { label: "Dotta", image: null }],
|
|
]);
|
|
|
|
function makeClient() {
|
|
return new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false, staleTime: Number.POSITIVE_INFINITY },
|
|
mutations: { retry: false },
|
|
},
|
|
});
|
|
}
|
|
|
|
const integratedIssueId = "issue-storybook-1";
|
|
const integratedDoc: IssueDocument = {
|
|
id: "doc-storybook-1",
|
|
companyId: "co-1",
|
|
issueId: integratedIssueId,
|
|
key: "plan",
|
|
title: "Plan",
|
|
format: "markdown",
|
|
body: sampleMarkdown,
|
|
latestRevisionId: "rev-4",
|
|
latestRevisionNumber: 4,
|
|
createdByAgentId: null,
|
|
createdByUserId: "user-1",
|
|
updatedByAgentId: null,
|
|
updatedByUserId: "user-1",
|
|
lockedAt: null,
|
|
lockedByAgentId: null,
|
|
lockedByUserId: null,
|
|
createdAt: new Date("2026-05-12T09:00:00Z"),
|
|
updatedAt: new Date("2026-05-12T10:01:00Z"),
|
|
};
|
|
|
|
function makeIntegratedIssue(): Issue {
|
|
return {
|
|
id: integratedIssueId,
|
|
companyId: "co-1",
|
|
projectId: null,
|
|
projectWorkspaceId: null,
|
|
goalId: null,
|
|
parentId: null,
|
|
title: "Highlighting and comments on documents",
|
|
description: null,
|
|
status: "in_progress",
|
|
workMode: "standard",
|
|
priority: "medium",
|
|
assigneeAgentId: null,
|
|
assigneeUserId: null,
|
|
checkoutRunId: null,
|
|
executionRunId: null,
|
|
executionAgentNameKey: null,
|
|
executionLockedAt: null,
|
|
createdByAgentId: null,
|
|
createdByUserId: "user-1",
|
|
issueNumber: 9402,
|
|
identifier: "PAP-9402",
|
|
requestDepth: 0,
|
|
billingCode: null,
|
|
assigneeAdapterOverrides: null,
|
|
executionWorkspaceId: null,
|
|
executionWorkspacePreference: null,
|
|
executionWorkspaceSettings: null,
|
|
startedAt: null,
|
|
completedAt: null,
|
|
cancelledAt: null,
|
|
hiddenAt: null,
|
|
documentSummaries: [
|
|
{
|
|
id: integratedDoc.id,
|
|
companyId: integratedDoc.companyId,
|
|
issueId: integratedIssueId,
|
|
key: integratedDoc.key,
|
|
title: integratedDoc.title,
|
|
format: integratedDoc.format,
|
|
latestRevisionId: integratedDoc.latestRevisionId,
|
|
latestRevisionNumber: integratedDoc.latestRevisionNumber,
|
|
createdByAgentId: null,
|
|
createdByUserId: "user-1",
|
|
updatedByAgentId: null,
|
|
updatedByUserId: "user-1",
|
|
lockedAt: integratedDoc.lockedAt,
|
|
lockedByAgentId: integratedDoc.lockedByAgentId,
|
|
lockedByUserId: integratedDoc.lockedByUserId,
|
|
createdAt: integratedDoc.createdAt,
|
|
updatedAt: integratedDoc.updatedAt,
|
|
},
|
|
],
|
|
legacyPlanDocument: null,
|
|
planDocument: integratedDoc,
|
|
createdAt: new Date("2026-05-10T00:00:00Z"),
|
|
updatedAt: new Date("2026-05-12T10:01:00Z"),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Storybook fetch stub for the integrated stories. The annotation surface is
|
|
* driven by prefilled React Query data, but MarkdownEditor in edit mode can
|
|
* fire an autosave PUT on first onChange. Without this stub the cell would
|
|
* render a "Request failed: 404" string from the section's error state — which
|
|
* defeats the purpose of the integrated capture.
|
|
*/
|
|
function useIntegratedFetchStub(issueId: string, doc: IssueDocument) {
|
|
// Install once per mount; the cleanup restores the previous fetch.
|
|
// The preview's global fetch fixture is still in place — we only intercept
|
|
// the document mutation URL pattern for this issue.
|
|
useMemo(() => {
|
|
if (typeof window === "undefined") return;
|
|
const upsertUrlPath = `/api/issues/${issueId}/documents/${doc.key}`;
|
|
const original = window.fetch.bind(window);
|
|
const wrapped: typeof window.fetch = async (input, init) => {
|
|
const rawUrl = typeof input === "string"
|
|
? input
|
|
: input instanceof URL
|
|
? input.href
|
|
: input.url;
|
|
const method = (init?.method ?? (typeof input === "object" && "method" in input ? (input as Request).method : "GET")).toUpperCase();
|
|
const url = new URL(rawUrl, window.location.origin);
|
|
if (url.pathname === upsertUrlPath && (method === "PUT" || method === "GET")) {
|
|
return Response.json({ ...doc, latestRevisionNumber: doc.latestRevisionNumber + 1 });
|
|
}
|
|
return original(input, init);
|
|
};
|
|
window.fetch = wrapped;
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [issueId, doc.key]);
|
|
}
|
|
|
|
function IntegratedSurface({
|
|
threads = baseThreads,
|
|
focusedThreadId = "open-1",
|
|
initialPanelOpen = true,
|
|
beginEditOnMount = false,
|
|
}: {
|
|
threads?: DocumentAnnotationThreadWithComments[];
|
|
focusedThreadId?: string | null;
|
|
initialPanelOpen?: boolean;
|
|
beginEditOnMount?: boolean;
|
|
}) {
|
|
const issue = useMemo(makeIntegratedIssue, []);
|
|
useIntegratedFetchStub(issue.id, integratedDoc);
|
|
const queryClient = useMemo(() => {
|
|
const client = makeClient();
|
|
// Prefill documents + annotations cache so React Query renders without hitting the network.
|
|
client.setQueryData(queryKeys.issues.documents(issue.id), [integratedDoc]);
|
|
client.setQueryData(
|
|
queryKeys.issues.documentAnnotations(issue.id, integratedDoc.key, "all"),
|
|
threads,
|
|
);
|
|
return client;
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [issue.id]);
|
|
|
|
const panelKeys = initialPanelOpen ? [integratedDoc.key] : [];
|
|
const focusedThreadIds = focusedThreadId ? { [integratedDoc.key]: focusedThreadId } : undefined;
|
|
const editKey = beginEditOnMount ? integratedDoc.key : null;
|
|
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
<div className="paperclip-doc-annotation-integrated mx-auto max-w-[1320px] p-4">
|
|
<div className="rounded-lg border border-border bg-background p-4">
|
|
<IssueDocumentsSection
|
|
issue={issue}
|
|
canDeleteDocuments={false}
|
|
agentMap={integratedAgentMap}
|
|
userProfileMap={integratedUserProfileMap}
|
|
defaultAnnotationPanelOpenKeys={panelKeys}
|
|
defaultAnnotationFocusedThreadIds={focusedThreadIds}
|
|
forceEditDocumentKey={editKey}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</QueryClientProvider>
|
|
);
|
|
}
|
|
|
|
function DirtyDraftWithIntegratedHeader() {
|
|
const issue = useMemo(makeIntegratedIssue, []);
|
|
useIntegratedFetchStub(issue.id, integratedDoc);
|
|
const queryClient = useMemo(() => {
|
|
const client = makeClient();
|
|
client.setQueryData(queryKeys.issues.documents(issue.id), [integratedDoc]);
|
|
client.setQueryData(
|
|
queryKeys.issues.documentAnnotations(issue.id, integratedDoc.key, "all"),
|
|
baseThreads,
|
|
);
|
|
return client;
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [issue.id]);
|
|
const [panelOpen, setPanelOpen] = useState(true);
|
|
const [draftBody, setDraftBody] = useState(`${sampleMarkdown}\n\nA work-in-progress edit that is unsaved.`);
|
|
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
<div className="paperclip-doc-annotation-integrated mx-auto max-w-[1320px] p-4">
|
|
<div className="rounded-lg border border-border bg-background p-4">
|
|
<div className="space-y-3">
|
|
<div className="flex flex-wrap items-center gap-2 min-w-0">
|
|
<h3 className="w-full text-sm font-medium text-muted-foreground shrink-0 sm:w-auto">Documents</h3>
|
|
</div>
|
|
<div className="rounded-lg border border-border p-3">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
<span className="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground">▾</span>
|
|
<span className="shrink-0 rounded-full border border-border px-2 py-0.5 font-mono text-[10px] uppercase tracking-[0.16em] text-muted-foreground">
|
|
plan
|
|
</span>
|
|
<span className="text-[11px] text-muted-foreground">rev 4 ▾</span>
|
|
<span className="truncate text-[11px] text-muted-foreground">updated 2h ago</span>
|
|
<DocumentAnnotationsCountChip
|
|
issueId={issue.id}
|
|
docKey={integratedDoc.key}
|
|
panelOpen={panelOpen}
|
|
onToggle={() => setPanelOpen((current) => !current)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="mt-3 space-y-3">
|
|
<IssueDocumentAnnotations
|
|
issueId={issue.id}
|
|
doc={integratedDoc}
|
|
bodyMarkdown={draftBody}
|
|
draftDirty
|
|
draftConflicted={false}
|
|
historicalPreview={false}
|
|
locationHash=""
|
|
panelOpen={panelOpen}
|
|
onPanelOpenChange={setPanelOpen}
|
|
agentMap={integratedAgentMap}
|
|
userProfileMap={integratedUserProfileMap}
|
|
defaultFocusedThreadId="open-1"
|
|
>
|
|
<MarkdownEditor
|
|
value={draftBody}
|
|
onChange={(body) => setDraftBody(body)}
|
|
placeholder="Markdown body"
|
|
bordered={false}
|
|
className="bg-transparent"
|
|
contentClassName="paperclip-edit-in-place-content min-h-[220px] text-[15px] leading-7"
|
|
/>
|
|
</IssueDocumentAnnotations>
|
|
<div className="flex min-h-4 items-center justify-end px-1">
|
|
<span className="text-[11px] text-amber-300">Autosaving…</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</QueryClientProvider>
|
|
);
|
|
}
|
|
|
|
function StatesShowcase({ focusedThreadId = "open-1" }: { focusedThreadId?: string }) {
|
|
const queryClient = useMemo(() => makeClient(), []);
|
|
const bodyRef = useRef<HTMLElement | null>(null);
|
|
const [pendingAnchor, setPendingAnchor] = useState<PendingAnchor | null>(null);
|
|
const [focused, setFocused] = useState<string | null>(focusedThreadId);
|
|
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
<div className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_360px]">
|
|
<div className="relative rounded-lg border border-border bg-card p-4">
|
|
<section
|
|
ref={(element) => {
|
|
bodyRef.current = element;
|
|
}}
|
|
className="relative"
|
|
>
|
|
<MarkdownBody className="text-[15px] leading-7">{sampleMarkdown}</MarkdownBody>
|
|
<DocumentAnnotationLayer
|
|
containerRef={bodyRef}
|
|
markdown={sampleMarkdown}
|
|
threads={baseThreads.map((thread) => ({
|
|
id: thread.id,
|
|
selectedText: thread.selectedText,
|
|
status: thread.status,
|
|
anchorState: thread.anchorState,
|
|
}))}
|
|
focusedThreadId={focused}
|
|
onThreadFocus={(id) => setFocused(id)}
|
|
pendingAnchor={pendingAnchor}
|
|
onPendingAnchorChange={setPendingAnchor}
|
|
onRequestComment={() => {}}
|
|
hideResolved={false}
|
|
/>
|
|
</section>
|
|
</div>
|
|
<DocumentAnnotationPanel
|
|
open
|
|
onOpenChange={() => {}}
|
|
issueId="issue-1"
|
|
documentKey="plan"
|
|
documentRevisionNumber={4}
|
|
baseRevisionId="rev-4"
|
|
baseRevisionNumber={4}
|
|
threads={baseThreads}
|
|
focusedThreadId={focused}
|
|
focusedCommentId={null}
|
|
onFocusThread={(id) => setFocused(id)}
|
|
pendingAnchor={null}
|
|
onClearPendingAnchor={() => setPendingAnchor(null)}
|
|
agentMap={integratedAgentMap}
|
|
userProfileMap={integratedUserProfileMap}
|
|
/>
|
|
</div>
|
|
</QueryClientProvider>
|
|
);
|
|
}
|
|
|
|
const meta = {
|
|
title: "Product/Documents/Annotations",
|
|
component: StatesShowcase,
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
component:
|
|
"Document annotation surface for issue documents. Stories under 'Integrated' render the real IssueDocumentsSection chrome (count chip in header, panel + body in their actual layout). Stories under 'States' isolate the panel/layer for unit-level visual debugging.",
|
|
},
|
|
},
|
|
},
|
|
} satisfies Meta<typeof StatesShowcase>;
|
|
|
|
export default meta;
|
|
|
|
type Story = StoryObj<typeof meta>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Integrated stories — render IssueDocumentsSection with all chrome.
|
|
// These are the captures the UX gate requires.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const IntegratedDesktopOpen: Story = {
|
|
parameters: { viewport: { defaultViewport: "responsive" } },
|
|
render: () => <IntegratedSurface focusedThreadId="open-1" initialPanelOpen />,
|
|
};
|
|
|
|
export const IntegratedDesktopZeroComments: Story = {
|
|
parameters: { viewport: { defaultViewport: "responsive" } },
|
|
render: () => <IntegratedSurface threads={[]} initialPanelOpen={false} focusedThreadId={null} />,
|
|
};
|
|
|
|
export const IntegratedDesktopEditMode: Story = {
|
|
parameters: { viewport: { defaultViewport: "responsive" } },
|
|
render: () => (
|
|
<IntegratedSurface focusedThreadId="open-1" initialPanelOpen beginEditOnMount />
|
|
),
|
|
};
|
|
|
|
export const IntegratedDesktopDirtyDraft: Story = {
|
|
parameters: { viewport: { defaultViewport: "responsive" } },
|
|
render: () => <DirtyDraftWithIntegratedHeader />,
|
|
};
|
|
|
|
export const IntegratedMobileBottomSheet: Story = {
|
|
parameters: { viewport: { defaultViewport: "mobile1" } },
|
|
render: () => <IntegratedSurface focusedThreadId="open-1" initialPanelOpen />,
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Isolated state stories (kept for unit-level visual debugging).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const DesktopOpenFocused: Story = {
|
|
render: () => <StatesShowcase focusedThreadId="open-1" />,
|
|
};
|
|
|
|
export const DesktopResolvedFocused: Story = {
|
|
render: () => <StatesShowcase focusedThreadId="resolved-1" />,
|
|
};
|
|
|
|
export const DesktopStaleFocused: Story = {
|
|
render: () => <StatesShowcase focusedThreadId="stale-1" />,
|
|
};
|
|
|
|
export const DesktopOrphanedFocused: Story = {
|
|
render: () => <StatesShowcase focusedThreadId="orphan-1" />,
|
|
};
|