@burdenoff/microfe-planmagnet 2026.930.24 → 2026.930.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/planmagnet/comments/CommentThread.d.ts +8 -1
  2. package/dist/planmagnet/comments/CommentThread.d.ts.map +1 -1
  3. package/dist/planmagnet/comments/CommentThread.js +188 -181
  4. package/dist/planmagnet/comments/CommentThread.js.map +1 -1
  5. package/dist/planmagnet/comments/TargetComments.d.ts +10 -2
  6. package/dist/planmagnet/comments/TargetComments.d.ts.map +1 -1
  7. package/dist/planmagnet/comments/TargetComments.js +10 -8
  8. package/dist/planmagnet/comments/TargetComments.js.map +1 -1
  9. package/dist/planmagnet/comments/WorkItemComments.d.ts +7 -1
  10. package/dist/planmagnet/comments/WorkItemComments.d.ts.map +1 -1
  11. package/dist/planmagnet/comments/WorkItemComments.js +5 -4
  12. package/dist/planmagnet/comments/WorkItemComments.js.map +1 -1
  13. package/dist/planmagnet/discussion/WorkItemDiscussionPanel.d.ts +8 -0
  14. package/dist/planmagnet/discussion/WorkItemDiscussionPanel.d.ts.map +1 -0
  15. package/dist/planmagnet/discussion/WorkItemDiscussionPanel.js +217 -0
  16. package/dist/planmagnet/discussion/WorkItemDiscussionPanel.js.map +1 -0
  17. package/dist/planmagnet/docs/DocDiffView.d.ts +26 -0
  18. package/dist/planmagnet/docs/DocDiffView.d.ts.map +1 -0
  19. package/dist/planmagnet/docs/DocDiffView.js +52 -0
  20. package/dist/planmagnet/docs/DocDiffView.js.map +1 -0
  21. package/dist/planmagnet/docs/DocSuggestionsPanel.d.ts +13 -0
  22. package/dist/planmagnet/docs/DocSuggestionsPanel.d.ts.map +1 -0
  23. package/dist/planmagnet/docs/DocSuggestionsPanel.js +308 -0
  24. package/dist/planmagnet/docs/DocSuggestionsPanel.js.map +1 -0
  25. package/dist/planmagnet/docs/DocVersionCompare.d.ts +7 -0
  26. package/dist/planmagnet/docs/DocVersionCompare.d.ts.map +1 -0
  27. package/dist/planmagnet/docs/DocVersionCompare.js +105 -0
  28. package/dist/planmagnet/docs/DocVersionCompare.js.map +1 -0
  29. package/dist/planmagnet/hooks/usePresence.d.ts +39 -0
  30. package/dist/planmagnet/hooks/usePresence.d.ts.map +1 -0
  31. package/dist/planmagnet/hooks/usePresence.js +146 -0
  32. package/dist/planmagnet/hooks/usePresence.js.map +1 -0
  33. package/dist/planmagnet/links/CodeLinksPanel.d.ts +22 -0
  34. package/dist/planmagnet/links/CodeLinksPanel.d.ts.map +1 -0
  35. package/dist/planmagnet/links/CodeLinksPanel.js +94 -0
  36. package/dist/planmagnet/links/CodeLinksPanel.js.map +1 -0
  37. package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
  38. package/dist/planmagnet/pages/DocsDocDetailPagePage.js +488 -217
  39. package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
  40. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
  41. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +136 -130
  42. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
  43. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
  44. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +205 -196
  45. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
  46. package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
  47. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +194 -160
  48. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
  49. package/dist/planmagnet/presence/PresenceAvatars.d.ts +16 -0
  50. package/dist/planmagnet/presence/PresenceAvatars.d.ts.map +1 -0
  51. package/dist/planmagnet/presence/PresenceAvatars.js +56 -0
  52. package/dist/planmagnet/presence/PresenceAvatars.js.map +1 -0
  53. package/dist/planmagnet/utils/graphqlErrors.d.ts +6 -0
  54. package/dist/planmagnet/utils/graphqlErrors.d.ts.map +1 -1
  55. package/dist/planmagnet/utils/graphqlErrors.js +5 -2
  56. package/dist/planmagnet/utils/graphqlErrors.js.map +1 -1
  57. package/dist/planmagnet/utils/lineDiff.d.ts +21 -0
  58. package/dist/planmagnet/utils/lineDiff.d.ts.map +1 -0
  59. package/dist/planmagnet/utils/lineDiff.js +36 -0
  60. package/dist/planmagnet/utils/lineDiff.js.map +1 -0
  61. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"TasksTaskDetailPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TasksTaskDetailPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { useState } from 'react';\nimport { throwOnError } from '../utils/mutationResult';\nimport { WorkItemComments } from '../comments/WorkItemComments';\nimport { WorkItemActivity } from '../comments/WorkItemActivity';\nimport { TaskTimePanel } from '../time/TaskTimePanel';\nimport { PersonName } from '../people/People';\nimport { DocSearchSelect } from '../controls/Pickers';\nimport { CustomFieldsPanel } from '../controls/CustomFieldValuesPanel';\nimport { DependencyPanel } from '../dependency/DependencyPanel';\nimport { IssueLinksPanel } from '../links/IssueLinksPanel';\nimport { WorkItemMilestoneSelect } from '../milestones/MilestoneControls';\nimport {\n WORK_ITEM_PRIORITIES,\n WorkItemStatusSelect,\n WorkItemAssigneeSelect,\n WorkItemEnumSelect,\n WorkItemHeaderActions,\n} from '../controls/WorkItemControls';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\n Button,\n Input,\n} from '@burdenoff/fe-libs/ui';\nimport { FileText, Link2, Plus, X } from 'lucide-react';\nimport { MarkdownEditor } from '#src/components/MarkdownEditor';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\n\nconst GET_WORK_ITEM = gql`\n query GetWorkItemDetail($workItemId: ID!) {\n planmagnetWorkItem(id: $workItemId) {\n id\n projectId\n title\n description\n type\n status\n priority\n key\n assigneeId\n reporterId\n createdBy\n sprintId\n milestoneId\n storyPoints\n estimateMinutes\n dueDate\n labels\n tags\n createdAt\n updatedAt\n customFieldValues {\n id\n definitionId\n value\n }\n }\n }\n`;\n\nconst GET_WORK_ITEM_DOCS = gql`\n query GetWorkItemDocs($workItemId: ID!) {\n planmagnetWorkItemDocs(workItemId: $workItemId) {\n id\n title\n status\n version\n updatedAt\n }\n }\n`;\n\nconst CREATE_DOC = gql`\n mutation CreateDocForWorkItem($input: PlanmagnetCreateDocInput!) {\n planmagnetCreateDoc(input: $input) {\n id\n }\n }\n`;\n\nconst LINK_DOC_TO_WORK_ITEM = gql`\n mutation LinkDocToWorkItem($docId: ID!, $workItemId: ID!) {\n planmagnetLinkDocToWorkItem(docId: $docId, workItemId: $workItemId) {\n id\n }\n }\n`;\n\nconst UNLINK_DOC_FROM_WORK_ITEM = gql`\n mutation UnlinkDocFromWorkItem($docId: ID!, $workItemId: ID!) {\n planmagnetUnlinkDocFromWorkItem(docId: $docId, workItemId: $workItemId)\n }\n`;\n\ninterface WorkItemNode {\n id: string;\n title: string;\n description: string | null;\n type: string;\n status: string;\n priority: string;\n key: string;\n assigneeId: string | null;\n reporterId: string | null;\n createdBy: string;\n sprintId: string | null;\n milestoneId: string | null;\n storyPoints: number | null;\n estimateMinutes: number | null;\n dueDate: string | null;\n labels: string[];\n tags: string[];\n createdAt: string;\n updatedAt: string;\n customFieldValues: { id: string; definitionId: string; value: unknown }[];\n}\n\ninterface WorkItemDocNode {\n id: string;\n title: string;\n status: string;\n version: number;\n updatedAt: string;\n}\n\nconst docStatusColor: Record<string, string> = {\n DRAFT: 'bg-gray-100 text-gray-800',\n PUBLISHED: 'bg-green-100 text-green-800',\n ARCHIVED: 'bg-gray-100 text-gray-500',\n};\n\n/**\n * Linked-docs panel (BOFF-4443): create a doc and link it to this work item\n * in one action, view currently-linked docs, and unlink one. Reads/writes go\n * through the real planmagnetWorkItemDocs / planmagnetCreateDoc /\n * planmagnetLinkDocToWorkItem / planmagnetUnlinkDocFromWorkItem GraphQL\n * fields — no fabricated data. Full doc editing (revisions, revert) lives on\n * a dedicated doc detail view; this panel is scoped to linking.\n */\nfunction LinkedDocsPanel({ workItemId }: { workItemId: string }) {\n const navigate = usePlanMagnetNavigate();\n const [isComposing, setIsComposing] = useState(false);\n // BOFF-7961: only \"New Doc\" existed, so a doc written on the Docs page could never be linked to a task.\n const [isLinking, setIsLinking] = useState(false);\n const [existingDocId, setExistingDocId] = useState('');\n const [newTitle, setNewTitle] = useState('');\n const [newContent, setNewContent] = useState('');\n\n const { data, loading, error, refetch } = useQuery<{\n planmagnetWorkItemDocs: WorkItemDocNode[];\n }>(GET_WORK_ITEM_DOCS, {\n variables: { workItemId },\n skip: !workItemId,\n });\n\n const [createDoc, { loading: creating }] = useMutation(CREATE_DOC);\n const [linkDoc, { loading: linking }] = useMutation(LINK_DOC_TO_WORK_ITEM);\n const [unlinkDoc, { loading: unlinking }] = useMutation(UNLINK_DOC_FROM_WORK_ITEM);\n // A refused mutation resolves with { error } (fe-libs errorPolicy 'all');\n // the composer used to clear itself as if the doc had been saved.\n const [actionError, setActionError] = useState<string | null>(null);\n\n const docs = data?.planmagnetWorkItemDocs ?? [];\n\n const handleCreateAndLink = async () => {\n if (!newTitle.trim() || !newContent.trim()) return;\n setActionError(null);\n try {\n const result = await throwOnError(\n createDoc({ variables: { input: { title: newTitle.trim(), content: newContent.trim() } } })\n );\n const docId = result.data?.planmagnetCreateDoc?.id;\n if (docId) {\n await throwOnError(linkDoc({ variables: { docId, workItemId } }));\n }\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'The doc could not be saved.');\n return;\n }\n setNewTitle('');\n setNewContent('');\n setIsComposing(false);\n void refetch();\n };\n\n const handleLinkExisting = async () => {\n if (!existingDocId) return;\n setActionError(null);\n try {\n await throwOnError(linkDoc({ variables: { docId: existingDocId, workItemId } }));\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'The doc could not be linked.');\n return;\n }\n setExistingDocId('');\n setIsLinking(false);\n void refetch();\n };\n\n const handleUnlink = async (docId: string) => {\n setActionError(null);\n try {\n await throwOnError(unlinkDoc({ variables: { docId, workItemId } }));\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'The doc could not be unlinked.');\n return;\n }\n void refetch();\n };\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle>Linked Docs</CardTitle>\n {!isComposing && !isLinking && (\n <div className=\"flex gap-2\">\n <Button size=\"sm\" variant=\"outline\" onClick={() => setIsLinking(true)}>\n <Link2 className=\"h-4 w-4 mr-1\" />\n Link existing doc\n </Button>\n <Button size=\"sm\" variant=\"outline\" onClick={() => setIsComposing(true)}>\n <Plus className=\"h-4 w-4 mr-1\" />\n New Doc\n </Button>\n </div>\n )}\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {isLinking && (\n <div className=\"space-y-2 rounded-md border border-input p-3\">\n <DocSearchSelect\n id=\"task-link-doc\"\n value={existingDocId}\n onChange={setExistingDocId}\n exclude={docs.map((d) => d.id)}\n />\n <div className=\"flex gap-2 justify-end\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => {\n setIsLinking(false);\n setExistingDocId('');\n }}\n >\n Cancel\n </Button>\n <Button size=\"sm\" disabled={!existingDocId || linking} onClick={handleLinkExisting}>\n {linking ? 'Linking...' : 'Link'}\n </Button>\n </div>\n </div>\n )}\n {isComposing && (\n <div className=\"space-y-3 rounded-md border border-input p-3\">\n <Input\n placeholder=\"Doc title\"\n value={newTitle}\n onChange={(e) => setNewTitle(e.target.value)}\n />\n <MarkdownEditor\n value={newContent}\n onChange={setNewContent}\n placeholder=\"Write the spec/doc content...\"\n minHeight={140}\n />\n <div className=\"flex gap-2 justify-end\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => {\n setIsComposing(false);\n setNewTitle('');\n setNewContent('');\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n disabled={creating || !newTitle.trim() || !newContent.trim()}\n onClick={handleCreateAndLink}\n >\n {creating ? 'Creating...' : 'Create & Link'}\n </Button>\n </div>\n </div>\n )}\n\n {loading && <p className=\"text-sm text-muted-foreground\">Loading docs...</p>}\n {error && <p className=\"text-sm text-red-500\">Error: {error.message}</p>}\n {actionError && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {actionError}\n </p>\n )}\n {!loading && !error && docs.length === 0 && !isComposing && !isLinking && (\n <p className=\"text-sm text-muted-foreground\">No docs linked to this work item yet.</p>\n )}\n {docs.map((doc) => (\n <div\n key={doc.id}\n className=\"flex items-center justify-between gap-3 rounded-md border border-input p-3\"\n >\n <div className=\"flex items-center gap-2 min-w-0\">\n <FileText className=\"h-4 w-4 text-muted-foreground shrink-0\" />\n <div className=\"min-w-0\">\n <button\n type=\"button\"\n className=\"block max-w-full truncate text-left text-sm font-medium hover:underline\"\n onClick={() =>\n navigate(`/docs/doc-detail-page?docId=${encodeURIComponent(doc.id)}`)\n }\n >\n {doc.title}\n </button>\n <p className=\"text-xs text-muted-foreground\">v{doc.version}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-2 shrink-0\">\n <span\n className={`text-xs px-2 py-0.5 rounded-full capitalize ${docStatusColor[doc.status] ?? ''}`}\n >\n {doc.status.toLowerCase()}\n </span>\n <Button\n size=\"icon\"\n variant=\"ghost\"\n disabled={unlinking}\n onClick={() => handleUnlink(doc.id)}\n aria-label={`Unlink ${doc.title}`}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n );\n}\n\nexport function TasksTaskDetailPagePage() {\n const { workspaceId } = usePlanMagnet();\n\n // In a real app, workItemId comes from route params.\n const workItemId = new URLSearchParams(window.location.search).get('taskId') ?? '';\n\n const { data, loading, error } = useQuery<{\n planmagnetWorkItem: WorkItemNode;\n }>(GET_WORK_ITEM, {\n variables: { workItemId },\n skip: !workspaceId || !workItemId,\n });\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-red-500\">Error: {error.message}</div>;\n }\n\n const task = data?.planmagnetWorkItem;\n if (!task) return <div className=\"p-6 text-muted-foreground\">Task not found.</div>;\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl font-semibold\">{task.title}</h1>\n <p className=\"text-sm text-muted-foreground\">\n {task.key} &middot; {task.type.toLowerCase()}\n </p>\n </div>\n <WorkItemHeaderActions workItemId={task.id} title={task.title} />\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Description</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm\">{task.description ?? 'No description provided.'}</p>\n </CardContent>\n </Card>\n <CustomFieldsPanel\n workItemId={task.id}\n projectId={task.projectId}\n values={task.customFieldValues ?? []}\n />\n <LinkedDocsPanel workItemId={task.id} />\n <TaskTimePanel\n workspaceId={workspaceId}\n workItemId={task.id}\n projectId={task.projectId}\n taskLabel={`${task.key} — ${task.title}`}\n />\n <DependencyPanel\n workspaceId={workspaceId ?? ''}\n workItemId={task.id}\n projectId={task.projectId}\n />\n {/* PDM-24: semantic links (blocks / duplicates / causes / relates to), not scheduling. */}\n <IssueLinksPanel workItemId={task.id} />\n <WorkItemComments workItemId={task.id} />\n <WorkItemActivity workItemId={task.id} />\n </div>\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Details</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3 text-sm\">\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-muted-foreground\">Status</span>\n <WorkItemStatusSelect\n workItemId={task.id}\n projectId={task.projectId}\n value={task.status}\n />\n </div>\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-muted-foreground\">Priority</span>\n <WorkItemEnumSelect\n workItemId={task.id}\n field=\"priority\"\n value={task.priority}\n options={WORK_ITEM_PRIORITIES}\n label=\"Priority\"\n />\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Type</span>\n <Badge variant=\"outline\" className=\"capitalize\">\n {task.type.toLowerCase()}\n </Badge>\n </div>\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-muted-foreground\">Milestone</span>\n {/* PDM-22: a milestone of this task's project, or none. */}\n <WorkItemMilestoneSelect\n workItemId={task.id}\n projectId={task.projectId}\n milestoneId={task.milestoneId}\n />\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Story Points</span>\n <span className=\"font-medium\">{task.storyPoints ?? 0}</span>\n </div>\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-muted-foreground\">Assignee</span>\n {/* Members by name (BOFF-7946); choosing one assigns the task (BOFF-7938). */}\n <WorkItemAssigneeSelect workItemId={task.id} assigneeId={task.assigneeId} />\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Reporter</span>\n {/* No PlanMagnet form sets reporterId, so a UI-created task has none; its creator reported it. */}\n <PersonName id={task.reporterId ?? task.createdBy} />\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Key</span>\n <span>{task.key}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Due Date</span>\n <span>{task.dueDate ?? 'Not set'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Created</span>\n <span>{task.createdAt}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Updated</span>\n <span>{task.updatedAt}</span>\n </div>\n <div>\n <span className=\"text-muted-foreground text-xs\">Labels</span>\n <div className=\"flex gap-1 mt-1 flex-wrap\">\n {(task.labels ?? []).map((l) => (\n <span key={l} className=\"text-xs px-2 py-0.5 rounded bg-muted\">\n {l}\n </span>\n ))}\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TasksTaskDetailPagePage.js","names":[],"sources":["../../../src/planmagnet/pages/TasksTaskDetailPagePage.tsx"],"sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\nimport { useState } from 'react';\nimport { throwOnError } from '../utils/mutationResult';\nimport { WorkItemComments } from '../comments/WorkItemComments';\nimport { WorkItemActivity } from '../comments/WorkItemActivity';\nimport { TaskTimePanel } from '../time/TaskTimePanel';\nimport { PersonName } from '../people/People';\nimport { DocSearchSelect } from '../controls/Pickers';\nimport { CustomFieldsPanel } from '../controls/CustomFieldValuesPanel';\nimport { DependencyPanel } from '../dependency/DependencyPanel';\nimport { IssueLinksPanel } from '../links/IssueLinksPanel';\nimport { WorkItemMilestoneSelect } from '../milestones/MilestoneControls';\nimport {\n WORK_ITEM_PRIORITIES,\n WorkItemStatusSelect,\n WorkItemAssigneeSelect,\n WorkItemEnumSelect,\n WorkItemHeaderActions,\n} from '../controls/WorkItemControls';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\n Button,\n Input,\n} from '@burdenoff/fe-libs/ui';\nimport { FileText, Link2, Plus, X } from 'lucide-react';\nimport { MarkdownEditor } from '#src/components/MarkdownEditor';\nimport { usePlanMagnet, usePlanMagnetNavigate } from '../providers/PlanMagnetProvider';\nimport { usePresence } from '../hooks/usePresence';\nimport { PresenceAvatars } from '../presence/PresenceAvatars';\nimport { WorkItemDiscussionPanel } from '../discussion/WorkItemDiscussionPanel';\nimport { CodeLinksPanel, type WorkItemCodeLink } from '../links/CodeLinksPanel';\n\nconst GET_WORK_ITEM = gql`\n query GetWorkItemDetail($workItemId: ID!) {\n planmagnetWorkItem(id: $workItemId) {\n id\n projectId\n title\n description\n type\n status\n priority\n key\n assigneeId\n reporterId\n createdBy\n sprintId\n milestoneId\n storyPoints\n estimateMinutes\n dueDate\n labels\n tags\n createdAt\n updatedAt\n customFieldValues {\n id\n definitionId\n value\n }\n discussionConversationId\n codeLinks {\n id\n kind\n title\n url\n state\n number\n authorLogin\n mergedAt\n ref\n }\n }\n }\n`;\n\nconst GET_WORK_ITEM_DOCS = gql`\n query GetWorkItemDocs($workItemId: ID!) {\n planmagnetWorkItemDocs(workItemId: $workItemId) {\n id\n title\n status\n version\n updatedAt\n }\n }\n`;\n\nconst CREATE_DOC = gql`\n mutation CreateDocForWorkItem($input: PlanmagnetCreateDocInput!) {\n planmagnetCreateDoc(input: $input) {\n id\n }\n }\n`;\n\nconst LINK_DOC_TO_WORK_ITEM = gql`\n mutation LinkDocToWorkItem($docId: ID!, $workItemId: ID!) {\n planmagnetLinkDocToWorkItem(docId: $docId, workItemId: $workItemId) {\n id\n }\n }\n`;\n\nconst UNLINK_DOC_FROM_WORK_ITEM = gql`\n mutation UnlinkDocFromWorkItem($docId: ID!, $workItemId: ID!) {\n planmagnetUnlinkDocFromWorkItem(docId: $docId, workItemId: $workItemId)\n }\n`;\n\ninterface WorkItemNode {\n id: string;\n title: string;\n description: string | null;\n type: string;\n status: string;\n priority: string;\n key: string;\n assigneeId: string | null;\n reporterId: string | null;\n createdBy: string;\n sprintId: string | null;\n milestoneId: string | null;\n storyPoints: number | null;\n estimateMinutes: number | null;\n dueDate: string | null;\n labels: string[];\n tags: string[];\n createdAt: string;\n updatedAt: string;\n customFieldValues: { id: string; definitionId: string; value: unknown }[];\n discussionConversationId: string | null;\n codeLinks: WorkItemCodeLink[];\n}\n\ninterface WorkItemDocNode {\n id: string;\n title: string;\n status: string;\n version: number;\n updatedAt: string;\n}\n\nconst docStatusColor: Record<string, string> = {\n DRAFT: 'bg-gray-100 text-gray-800',\n PUBLISHED: 'bg-green-100 text-green-800',\n ARCHIVED: 'bg-gray-100 text-gray-500',\n};\n\n/**\n * Linked-docs panel (BOFF-4443): create a doc and link it to this work item\n * in one action, view currently-linked docs, and unlink one. Reads/writes go\n * through the real planmagnetWorkItemDocs / planmagnetCreateDoc /\n * planmagnetLinkDocToWorkItem / planmagnetUnlinkDocFromWorkItem GraphQL\n * fields — no fabricated data. Full doc editing (revisions, revert) lives on\n * a dedicated doc detail view; this panel is scoped to linking.\n */\nfunction LinkedDocsPanel({ workItemId }: { workItemId: string }) {\n const navigate = usePlanMagnetNavigate();\n const [isComposing, setIsComposing] = useState(false);\n // BOFF-7961: only \"New Doc\" existed, so a doc written on the Docs page could never be linked to a task.\n const [isLinking, setIsLinking] = useState(false);\n const [existingDocId, setExistingDocId] = useState('');\n const [newTitle, setNewTitle] = useState('');\n const [newContent, setNewContent] = useState('');\n\n const { data, loading, error, refetch } = useQuery<{\n planmagnetWorkItemDocs: WorkItemDocNode[];\n }>(GET_WORK_ITEM_DOCS, {\n variables: { workItemId },\n skip: !workItemId,\n });\n\n const [createDoc, { loading: creating }] = useMutation(CREATE_DOC);\n const [linkDoc, { loading: linking }] = useMutation(LINK_DOC_TO_WORK_ITEM);\n const [unlinkDoc, { loading: unlinking }] = useMutation(UNLINK_DOC_FROM_WORK_ITEM);\n // A refused mutation resolves with { error } (fe-libs errorPolicy 'all');\n // the composer used to clear itself as if the doc had been saved.\n const [actionError, setActionError] = useState<string | null>(null);\n\n const docs = data?.planmagnetWorkItemDocs ?? [];\n\n const handleCreateAndLink = async () => {\n if (!newTitle.trim() || !newContent.trim()) return;\n setActionError(null);\n try {\n const result = await throwOnError(\n createDoc({ variables: { input: { title: newTitle.trim(), content: newContent.trim() } } })\n );\n const docId = result.data?.planmagnetCreateDoc?.id;\n if (docId) {\n await throwOnError(linkDoc({ variables: { docId, workItemId } }));\n }\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'The doc could not be saved.');\n return;\n }\n setNewTitle('');\n setNewContent('');\n setIsComposing(false);\n void refetch();\n };\n\n const handleLinkExisting = async () => {\n if (!existingDocId) return;\n setActionError(null);\n try {\n await throwOnError(linkDoc({ variables: { docId: existingDocId, workItemId } }));\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'The doc could not be linked.');\n return;\n }\n setExistingDocId('');\n setIsLinking(false);\n void refetch();\n };\n\n const handleUnlink = async (docId: string) => {\n setActionError(null);\n try {\n await throwOnError(unlinkDoc({ variables: { docId, workItemId } }));\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'The doc could not be unlinked.');\n return;\n }\n void refetch();\n };\n\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle>Linked Docs</CardTitle>\n {!isComposing && !isLinking && (\n <div className=\"flex gap-2\">\n <Button size=\"sm\" variant=\"outline\" onClick={() => setIsLinking(true)}>\n <Link2 className=\"h-4 w-4 mr-1\" />\n Link existing doc\n </Button>\n <Button size=\"sm\" variant=\"outline\" onClick={() => setIsComposing(true)}>\n <Plus className=\"h-4 w-4 mr-1\" />\n New Doc\n </Button>\n </div>\n )}\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {isLinking && (\n <div className=\"space-y-2 rounded-md border border-input p-3\">\n <DocSearchSelect\n id=\"task-link-doc\"\n value={existingDocId}\n onChange={setExistingDocId}\n exclude={docs.map((d) => d.id)}\n />\n <div className=\"flex gap-2 justify-end\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => {\n setIsLinking(false);\n setExistingDocId('');\n }}\n >\n Cancel\n </Button>\n <Button size=\"sm\" disabled={!existingDocId || linking} onClick={handleLinkExisting}>\n {linking ? 'Linking...' : 'Link'}\n </Button>\n </div>\n </div>\n )}\n {isComposing && (\n <div className=\"space-y-3 rounded-md border border-input p-3\">\n <Input\n placeholder=\"Doc title\"\n value={newTitle}\n onChange={(e) => setNewTitle(e.target.value)}\n />\n <MarkdownEditor\n value={newContent}\n onChange={setNewContent}\n placeholder=\"Write the spec/doc content...\"\n minHeight={140}\n />\n <div className=\"flex gap-2 justify-end\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => {\n setIsComposing(false);\n setNewTitle('');\n setNewContent('');\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n disabled={creating || !newTitle.trim() || !newContent.trim()}\n onClick={handleCreateAndLink}\n >\n {creating ? 'Creating...' : 'Create & Link'}\n </Button>\n </div>\n </div>\n )}\n\n {loading && <p className=\"text-sm text-muted-foreground\">Loading docs...</p>}\n {error && <p className=\"text-sm text-red-500\">Error: {error.message}</p>}\n {actionError && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {actionError}\n </p>\n )}\n {!loading && !error && docs.length === 0 && !isComposing && !isLinking && (\n <p className=\"text-sm text-muted-foreground\">No docs linked to this work item yet.</p>\n )}\n {docs.map((doc) => (\n <div\n key={doc.id}\n className=\"flex items-center justify-between gap-3 rounded-md border border-input p-3\"\n >\n <div className=\"flex items-center gap-2 min-w-0\">\n <FileText className=\"h-4 w-4 text-muted-foreground shrink-0\" />\n <div className=\"min-w-0\">\n <button\n type=\"button\"\n className=\"block max-w-full truncate text-left text-sm font-medium hover:underline\"\n onClick={() =>\n navigate(`/docs/doc-detail-page?docId=${encodeURIComponent(doc.id)}`)\n }\n >\n {doc.title}\n </button>\n <p className=\"text-xs text-muted-foreground\">v{doc.version}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-2 shrink-0\">\n <span\n className={`text-xs px-2 py-0.5 rounded-full capitalize ${docStatusColor[doc.status] ?? ''}`}\n >\n {doc.status.toLowerCase()}\n </span>\n <Button\n size=\"icon\"\n variant=\"ghost\"\n disabled={unlinking}\n onClick={() => handleUnlink(doc.id)}\n aria-label={`Unlink ${doc.title}`}\n >\n <X className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n );\n}\n\nexport function TasksTaskDetailPagePage() {\n const { workspaceId } = usePlanMagnet();\n\n // In a real app, workItemId comes from route params.\n const workItemId = new URLSearchParams(window.location.search).get('taskId') ?? '';\n\n const { data, loading, error } = useQuery<{\n planmagnetWorkItem: WorkItemNode;\n }>(GET_WORK_ITEM, {\n variables: { workItemId },\n skip: !workspaceId || !workItemId,\n });\n\n // CWF-24: who else is looking at this task right now, and who's mid-comment.\n const presence = usePresence('WORK_ITEM', workItemId || null);\n\n // Only before the first load: a refetch must not blank the page (it\n\n // unmounts every component on it and loses their state).\n\n if (loading && !data) {\n return (\n <div className=\"flex items-center justify-center p-12\">\n <div className=\"animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n );\n }\n\n if (error) {\n return <div className=\"p-6 text-red-500\">Error: {error.message}</div>;\n }\n\n const task = data?.planmagnetWorkItem;\n if (!task) return <div className=\"p-6 text-muted-foreground\">Task not found.</div>;\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl font-semibold\">{task.title}</h1>\n <p className=\"text-sm text-muted-foreground\">\n {task.key} &middot; {task.type.toLowerCase()}\n </p>\n <PresenceAvatars others={presence.others} />\n </div>\n <WorkItemHeaderActions workItemId={task.id} title={task.title} />\n </div>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2 space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Description</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm\">{task.description ?? 'No description provided.'}</p>\n </CardContent>\n </Card>\n <CustomFieldsPanel\n workItemId={task.id}\n projectId={task.projectId}\n values={task.customFieldValues ?? []}\n />\n <LinkedDocsPanel workItemId={task.id} />\n <TaskTimePanel\n workspaceId={workspaceId}\n workItemId={task.id}\n projectId={task.projectId}\n taskLabel={`${task.key} — ${task.title}`}\n />\n <DependencyPanel\n workspaceId={workspaceId ?? ''}\n workItemId={task.id}\n projectId={task.projectId}\n />\n {/* PDM-24: semantic links (blocks / duplicates / causes / relates to), not scheduling. */}\n <IssueLinksPanel workItemId={task.id} />\n {/* CWF-26: a real group conversation in Conversations, not a comment thread of its own. */}\n <WorkItemDiscussionPanel\n workItemId={task.id}\n workItemKey={task.key}\n workItemTitle={task.title}\n assigneeId={task.assigneeId}\n discussionConversationId={task.discussionConversationId}\n />\n {/* AUT-12: PRs/commits/releases GitHub sync discovered that mention this item's key. */}\n <CodeLinksPanel codeLinks={task.codeLinks ?? []} />\n <WorkItemComments\n workItemId={task.id}\n presence={{\n typingActorIds: presence.typingActorIds,\n notifyTyping: presence.notifyTyping,\n }}\n />\n <WorkItemActivity workItemId={task.id} />\n </div>\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Details</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3 text-sm\">\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-muted-foreground\">Status</span>\n <WorkItemStatusSelect\n workItemId={task.id}\n projectId={task.projectId}\n value={task.status}\n />\n </div>\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-muted-foreground\">Priority</span>\n <WorkItemEnumSelect\n workItemId={task.id}\n field=\"priority\"\n value={task.priority}\n options={WORK_ITEM_PRIORITIES}\n label=\"Priority\"\n />\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Type</span>\n <Badge variant=\"outline\" className=\"capitalize\">\n {task.type.toLowerCase()}\n </Badge>\n </div>\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-muted-foreground\">Milestone</span>\n {/* PDM-22: a milestone of this task's project, or none. */}\n <WorkItemMilestoneSelect\n workItemId={task.id}\n projectId={task.projectId}\n milestoneId={task.milestoneId}\n />\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Story Points</span>\n <span className=\"font-medium\">{task.storyPoints ?? 0}</span>\n </div>\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-muted-foreground\">Assignee</span>\n {/* Members by name (BOFF-7946); choosing one assigns the task (BOFF-7938). */}\n <WorkItemAssigneeSelect workItemId={task.id} assigneeId={task.assigneeId} />\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Reporter</span>\n {/* No PlanMagnet form sets reporterId, so a UI-created task has none; its creator reported it. */}\n <PersonName id={task.reporterId ?? task.createdBy} />\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Key</span>\n <span>{task.key}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Due Date</span>\n <span>{task.dueDate ?? 'Not set'}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Created</span>\n <span>{task.createdAt}</span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-muted-foreground\">Updated</span>\n <span>{task.updatedAt}</span>\n </div>\n <div>\n <span className=\"text-muted-foreground text-xs\">Labels</span>\n <div className=\"flex gap-1 mt-1 flex-wrap\">\n {(task.labels ?? []).map((l) => (\n <span key={l} className=\"text-xs px-2 py-0.5 rounded bg-muted\">\n {l}\n </span>\n ))}\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { PresenceOther } from '../hooks/usePresence';
2
+
3
+ export declare function PresenceAvatars({ others }: {
4
+ others: readonly PresenceOther[];
5
+ }): import("react/jsx-runtime").JSX.Element | null;
6
+ /**
7
+ * "Ada is typing…" (CWF-24), inside a comment thread. A live region (polite — a typing flip is not urgent enough
8
+ * to interrupt) so the line appearing/disappearing/changing names is announced without extra chrome. Renders an
9
+ * (invisible-to-sighted-users but still present) empty live region rather than nothing at all when no one is
10
+ * typing, so the region already exists in the accessibility tree before the first announcement — inserting a
11
+ * brand-new `aria-live` node right as it needs to speak is unreliable across screen readers.
12
+ */
13
+ export declare function TypingLine({ typingActorIds }: {
14
+ typingActorIds: readonly string[];
15
+ }): import("react/jsx-runtime").JSX.Element;
16
+ //# sourceMappingURL=PresenceAvatars.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PresenceAvatars.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/presence/PresenceAvatars.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAe1D,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,SAAS,aAAa,EAAE,CAAA;CAAE,kDA6B/E;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EAAE,cAAc,EAAE,EAAE;IAAE,cAAc,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,2CAsBnF"}
@@ -0,0 +1,56 @@
1
+ import { PersonAvatar as e, PersonName as t } from "../people/People.js";
2
+ import { Fragment as n } from "react";
3
+ import { jsx as r, jsxs as i } from "react/jsx-runtime";
4
+ //#region src/planmagnet/presence/PresenceAvatars.tsx
5
+ var a = 5;
6
+ function o({ others: t }) {
7
+ if (t.length === 0) return null;
8
+ let n = t.slice(0, a), o = t.length - n.length;
9
+ return /* @__PURE__ */ i("div", {
10
+ role: "group",
11
+ "aria-label": `Also here: ${t.length} other${t.length === 1 ? "" : "s"}`,
12
+ "data-testid": "presence-avatars",
13
+ className: "flex items-center gap-1.5",
14
+ children: [
15
+ /* @__PURE__ */ r("span", {
16
+ className: "text-xs text-muted-foreground",
17
+ children: "Also here:"
18
+ }),
19
+ /* @__PURE__ */ r("div", {
20
+ className: "flex -space-x-2",
21
+ children: n.map((t) => /* @__PURE__ */ r(e, {
22
+ id: t.actorId,
23
+ className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2 border-bg-surface bg-muted text-[10px] font-medium text-foreground"
24
+ }, t.actorId))
25
+ }),
26
+ o > 0 && /* @__PURE__ */ i("span", {
27
+ className: "text-xs text-muted-foreground",
28
+ "data-testid": "presence-avatars-overflow",
29
+ children: ["+", o]
30
+ })
31
+ ]
32
+ });
33
+ }
34
+ function s({ typingActorIds: e }) {
35
+ let [a, o] = e;
36
+ return /* @__PURE__ */ i("p", {
37
+ role: "status",
38
+ "aria-live": "polite",
39
+ "data-testid": "presence-typing",
40
+ className: "min-h-[1rem] text-xs italic text-muted-foreground",
41
+ children: [
42
+ e.length === 1 && /* @__PURE__ */ i(n, { children: [/* @__PURE__ */ r(t, { id: a }), " is typing…"] }),
43
+ e.length === 2 && /* @__PURE__ */ i(n, { children: [
44
+ /* @__PURE__ */ r(t, { id: a }),
45
+ " and ",
46
+ /* @__PURE__ */ r(t, { id: o }),
47
+ " are typing…"
48
+ ] }),
49
+ e.length > 2 && `${e.length} people are typing…`
50
+ ]
51
+ });
52
+ }
53
+ //#endregion
54
+ export { o as PresenceAvatars, s as TypingLine };
55
+
56
+ //# sourceMappingURL=PresenceAvatars.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PresenceAvatars.js","names":[],"sources":["../../../src/planmagnet/presence/PresenceAvatars.tsx"],"sourcesContent":["import { Fragment } from 'react';\nimport { PersonAvatar, PersonName } from '../people/People';\nimport type { PresenceOther } from '../hooks/usePresence';\n\n/**\n * \"Also here: …\" (CWF-24) — everyone else currently present on a task/doc/roadmap/project, up to 5 initials\n * bubbles plus a \"+N\" overflow count. `others` is expected to already exclude the caller — `usePresence` does\n * that — so this never needs its own id to filter against. Renders nothing when nobody else is here: an empty\n * \"Also here:\" row would be noise, not information.\n *\n * Each bubble is a `PersonAvatar`, which already carries its own resolved-name `aria-label`/tooltip (People.tsx),\n * so wrapping this in `role=\"group\"` with a visible label is enough for the set to read sensibly to a screen\n * reader — there is no second, separately-maintained list of names to keep in sync with the bubbles.\n */\n\nconst MAX_SHOWN = 5;\n\nexport function PresenceAvatars({ others }: { others: readonly PresenceOther[] }) {\n if (others.length === 0) return null;\n const shown = others.slice(0, MAX_SHOWN);\n const overflow = others.length - shown.length;\n\n return (\n <div\n role=\"group\"\n aria-label={`Also here: ${others.length} other${others.length === 1 ? '' : 's'}`}\n data-testid=\"presence-avatars\"\n className=\"flex items-center gap-1.5\"\n >\n <span className=\"text-xs text-muted-foreground\">Also here:</span>\n <div className=\"flex -space-x-2\">\n {shown.map((entry) => (\n <PersonAvatar\n key={entry.actorId}\n id={entry.actorId}\n className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2 border-bg-surface bg-muted text-[10px] font-medium text-foreground\"\n />\n ))}\n </div>\n {overflow > 0 && (\n <span className=\"text-xs text-muted-foreground\" data-testid=\"presence-avatars-overflow\">\n +{overflow}\n </span>\n )}\n </div>\n );\n}\n\n/**\n * \"Ada is typing…\" (CWF-24), inside a comment thread. A live region (polite — a typing flip is not urgent enough\n * to interrupt) so the line appearing/disappearing/changing names is announced without extra chrome. Renders an\n * (invisible-to-sighted-users but still present) empty live region rather than nothing at all when no one is\n * typing, so the region already exists in the accessibility tree before the first announcement — inserting a\n * brand-new `aria-live` node right as it needs to speak is unreliable across screen readers.\n */\nexport function TypingLine({ typingActorIds }: { typingActorIds: readonly string[] }) {\n const [first, second] = typingActorIds;\n return (\n <p\n role=\"status\"\n aria-live=\"polite\"\n data-testid=\"presence-typing\"\n className=\"min-h-[1rem] text-xs italic text-muted-foreground\"\n >\n {typingActorIds.length === 1 && (\n <Fragment>\n <PersonName id={first} /> is typing…\n </Fragment>\n )}\n {typingActorIds.length === 2 && (\n <Fragment>\n <PersonName id={first} /> and <PersonName id={second} /> are typing…\n </Fragment>\n )}\n {typingActorIds.length > 2 && `${typingActorIds.length} people are typing…`}\n </p>\n );\n}\n"],"mappings":";;;;AAeA,IAAM,IAAY;AAElB,SAAgB,EAAgB,EAAE,aAAgD;AAChF,KAAI,EAAO,WAAW,EAAG,QAAO;CAChC,IAAM,IAAQ,EAAO,MAAM,GAAG,EAAU,EAClC,IAAW,EAAO,SAAS,EAAM;AAEvC,QACE,kBAAC,OAAD;EACE,MAAK;EACL,cAAY,cAAc,EAAO,OAAO,QAAQ,EAAO,WAAW,IAAI,KAAK;EAC3E,eAAY;EACZ,WAAU;YAJZ;GAME,kBAAC,QAAD;IAAM,WAAU;cAAgC;IAAiB,CAAA;GACjE,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAM,KAAK,MACV,kBAAC,GAAD;KAEE,IAAI,EAAM;KACV,WAAU;KACV,EAHK,EAAM,QAGX,CACF;IACE,CAAA;GACL,IAAW,KACV,kBAAC,QAAD;IAAM,WAAU;IAAgC,eAAY;cAA5D,CAAwF,KACpF,EACG;;GAEL;;;AAWV,SAAgB,EAAW,EAAE,qBAAyD;CACpF,IAAM,CAAC,GAAO,KAAU;AACxB,QACE,kBAAC,KAAD;EACE,MAAK;EACL,aAAU;EACV,eAAY;EACZ,WAAU;YAJZ;GAMG,EAAe,WAAW,KACzB,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAY,IAAI,GAAS,CAAA,EAAA,cAChB,EAAA,CAAA;GAEZ,EAAe,WAAW,KACzB,kBAAC,GAAD,EAAA,UAAA;IACE,kBAAC,GAAD,EAAY,IAAI,GAAS,CAAA;;IAAK,kBAAC,GAAD,EAAY,IAAI,GAAU,CAAA;;IAC/C,EAAA,CAAA;GAEZ,EAAe,SAAS,KAAK,GAAG,EAAe,OAAO;GACrD"}
@@ -5,6 +5,12 @@
5
5
  * `src/planmagnet/releases/ReleaseReadinessCard.tsx`'s local `denied()`, generalized for reuse.
6
6
  */
7
7
  export declare function isForbidden(error: unknown): boolean;
8
+ /**
9
+ * Is this a write refused because the record moved on since the caller read it — a stale `expectedVersion`, a
10
+ * suggestion whose `baseVersion` the doc has passed, a checkout lock already held, a review already decided?
11
+ * `extensions.code: CONFLICT` the same way `isForbidden` reads `FORBIDDEN`, not a message-text guess.
12
+ */
13
+ export declare function isConflict(error: unknown): boolean;
8
14
  /**
9
15
  * A mutation refusal's display message, preferring the server's own field-tagged reason — a BAD_USER_INPUT
10
16
  * (e.g. confidence outside 0..100) carries `extensions.field` naming which input field was wrong — over a
@@ -1 +1 @@
1
- {"version":3,"file":"graphqlErrors.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/utils/graphqlErrors.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQnD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQ7E"}
1
+ {"version":3,"file":"graphqlErrors.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/utils/graphqlErrors.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQnD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQlD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQ7E"}
@@ -4,7 +4,10 @@ import { ServerError as t } from "../../node_modules/@apollo/client/errors/Serve
4
4
  function n(n) {
5
5
  return e.is(n) ? n.errors.some((e) => e.extensions?.code === "FORBIDDEN") : t.is(n) ? n.statusCode === 403 || /"code"\s*:\s*"FORBIDDEN"/.test(n.bodyText) : !1;
6
6
  }
7
- function r(t, n) {
7
+ function r(n) {
8
+ return e.is(n) ? n.errors.some((e) => e.extensions?.code === "CONFLICT") : t.is(n) ? n.statusCode === 409 || /"code"\s*:\s*"CONFLICT"/.test(n.bodyText) : !1;
9
+ }
10
+ function i(t, n) {
8
11
  if (e.is(t)) {
9
12
  let e = t.errors[0], n = e?.extensions?.field;
10
13
  if (e && typeof n == "string" && n) return `${n}: ${e.message}`;
@@ -13,6 +16,6 @@ function r(t, n) {
13
16
  return t instanceof Error ? t.message : n;
14
17
  }
15
18
  //#endregion
16
- export { n as isForbidden, r as mutationErrorMessage };
19
+ export { r as isConflict, n as isForbidden, i as mutationErrorMessage };
17
20
 
18
21
  //# sourceMappingURL=graphqlErrors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"graphqlErrors.js","names":[],"sources":["../../../src/planmagnet/utils/graphqlErrors.ts"],"sourcesContent":["import { CombinedGraphQLErrors, ServerError } from '@apollo/client/errors';\n\n/**\n * Is this the gateway refusing the viewer's role? The gateway answers an RBAC denial with HTTP 403 (400 inside\n * a batch) and `extensions.code: FORBIDDEN`. Depending on the fetch path Apollo surfaces that as GraphQL errors\n * or as a ServerError carrying the body, so both are read. Same detection as\n * `src/planmagnet/releases/ReleaseReadinessCard.tsx`'s local `denied()`, generalized for reuse.\n */\nexport function isForbidden(error: unknown): boolean {\n if (CombinedGraphQLErrors.is(error)) {\n return error.errors.some((e) => e.extensions?.code === 'FORBIDDEN');\n }\n if (ServerError.is(error)) {\n return error.statusCode === 403 || /\"code\"\\s*:\\s*\"FORBIDDEN\"/.test(error.bodyText);\n }\n return false;\n}\n\n/**\n * A mutation refusal's display message, preferring the server's own field-tagged reason — a BAD_USER_INPUT\n * (e.g. confidence outside 0..100) carries `extensions.field` naming which input field was wrong — over a\n * generic fallback.\n */\nexport function mutationErrorMessage(error: unknown, fallback: string): string {\n if (CombinedGraphQLErrors.is(error)) {\n const first = error.errors[0];\n const field = first?.extensions?.field;\n if (first && typeof field === 'string' && field) return `${field}: ${first.message}`;\n if (first?.message) return first.message;\n }\n return error instanceof Error ? error.message : fallback;\n}\n"],"mappings":";;;AAQA,SAAgB,EAAY,GAAyB;AAOnD,QANI,EAAsB,GAAG,EAAM,GAC1B,EAAM,OAAO,MAAM,MAAM,EAAE,YAAY,SAAS,YAAY,GAEjE,EAAY,GAAG,EAAM,GAChB,EAAM,eAAe,OAAO,2BAA2B,KAAK,EAAM,SAAS,GAE7E;;AAQT,SAAgB,EAAqB,GAAgB,GAA0B;AAC7E,KAAI,EAAsB,GAAG,EAAM,EAAE;EACnC,IAAM,IAAQ,EAAM,OAAO,IACrB,IAAQ,GAAO,YAAY;AACjC,MAAI,KAAS,OAAO,KAAU,YAAY,EAAO,QAAO,GAAG,EAAM,IAAI,EAAM;AAC3E,MAAI,GAAO,QAAS,QAAO,EAAM;;AAEnC,QAAO,aAAiB,QAAQ,EAAM,UAAU"}
1
+ {"version":3,"file":"graphqlErrors.js","names":[],"sources":["../../../src/planmagnet/utils/graphqlErrors.ts"],"sourcesContent":["import { CombinedGraphQLErrors, ServerError } from '@apollo/client/errors';\n\n/**\n * Is this the gateway refusing the viewer's role? The gateway answers an RBAC denial with HTTP 403 (400 inside\n * a batch) and `extensions.code: FORBIDDEN`. Depending on the fetch path Apollo surfaces that as GraphQL errors\n * or as a ServerError carrying the body, so both are read. Same detection as\n * `src/planmagnet/releases/ReleaseReadinessCard.tsx`'s local `denied()`, generalized for reuse.\n */\nexport function isForbidden(error: unknown): boolean {\n if (CombinedGraphQLErrors.is(error)) {\n return error.errors.some((e) => e.extensions?.code === 'FORBIDDEN');\n }\n if (ServerError.is(error)) {\n return error.statusCode === 403 || /\"code\"\\s*:\\s*\"FORBIDDEN\"/.test(error.bodyText);\n }\n return false;\n}\n\n/**\n * Is this a write refused because the record moved on since the caller read it — a stale `expectedVersion`, a\n * suggestion whose `baseVersion` the doc has passed, a checkout lock already held, a review already decided?\n * `extensions.code: CONFLICT` the same way `isForbidden` reads `FORBIDDEN`, not a message-text guess.\n */\nexport function isConflict(error: unknown): boolean {\n if (CombinedGraphQLErrors.is(error)) {\n return error.errors.some((e) => e.extensions?.code === 'CONFLICT');\n }\n if (ServerError.is(error)) {\n return error.statusCode === 409 || /\"code\"\\s*:\\s*\"CONFLICT\"/.test(error.bodyText);\n }\n return false;\n}\n\n/**\n * A mutation refusal's display message, preferring the server's own field-tagged reason — a BAD_USER_INPUT\n * (e.g. confidence outside 0..100) carries `extensions.field` naming which input field was wrong — over a\n * generic fallback.\n */\nexport function mutationErrorMessage(error: unknown, fallback: string): string {\n if (CombinedGraphQLErrors.is(error)) {\n const first = error.errors[0];\n const field = first?.extensions?.field;\n if (first && typeof field === 'string' && field) return `${field}: ${first.message}`;\n if (first?.message) return first.message;\n }\n return error instanceof Error ? error.message : fallback;\n}\n"],"mappings":";;;AAQA,SAAgB,EAAY,GAAyB;AAOnD,QANI,EAAsB,GAAG,EAAM,GAC1B,EAAM,OAAO,MAAM,MAAM,EAAE,YAAY,SAAS,YAAY,GAEjE,EAAY,GAAG,EAAM,GAChB,EAAM,eAAe,OAAO,2BAA2B,KAAK,EAAM,SAAS,GAE7E;;AAQT,SAAgB,EAAW,GAAyB;AAOlD,QANI,EAAsB,GAAG,EAAM,GAC1B,EAAM,OAAO,MAAM,MAAM,EAAE,YAAY,SAAS,WAAW,GAEhE,EAAY,GAAG,EAAM,GAChB,EAAM,eAAe,OAAO,0BAA0B,KAAK,EAAM,SAAS,GAE5E;;AAQT,SAAgB,EAAqB,GAAgB,GAA0B;AAC7E,KAAI,EAAsB,GAAG,EAAM,EAAE;EACnC,IAAM,IAAQ,EAAM,OAAO,IACrB,IAAQ,GAAO,YAAY;AACjC,MAAI,KAAS,OAAO,KAAU,YAAY,EAAO,QAAO,GAAG,EAAM,IAAI,EAAM;AAC3E,MAAI,GAAO,QAAS,QAAO,EAAM;;AAEnC,QAAO,aAAiB,QAAQ,EAAM,UAAU"}
@@ -0,0 +1,21 @@
1
+ export type LineDiffKind = 'ADDED' | 'REMOVED' | 'UNCHANGED';
2
+ export interface LineDiffHunk {
3
+ kind: LineDiffKind;
4
+ text: string;
5
+ }
6
+ export interface LineDiffResult {
7
+ hunks: LineDiffHunk[];
8
+ /** True when either side exceeded `LINE_CAP` — the hunks above are still a valid PREFIX of the real diff, same
9
+ * contract as the server's `PlanmagnetDocRevisionDiff.truncated`. */
10
+ truncated: boolean;
11
+ }
12
+ /**
13
+ * A line-based diff between two texts (CWF-18 §2's suggestion preview), computed entirely client-side with a
14
+ * small, dependency-free LCS — there is no server endpoint for "diff this UNSAVED proposed text against a
15
+ * revision" (the real `planmagnetDocRevisionDiff` only compares two already-SAVED versions of a doc, for the
16
+ * history panel's version compare — see `DocVersionCompare.tsx`, which uses that instead of this). Same hunk
17
+ * shape as the server's `PlanmagnetDocRevisionDiff` (`kind`/`text`, plus `truncated`) so both diffs render
18
+ * through the one shared `DocDiffHunks` component.
19
+ */
20
+ export declare function lineDiff(fromText: string, toText: string): LineDiffResult;
21
+ //# sourceMappingURL=lineDiff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lineDiff.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/utils/lineDiff.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;AAE7D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB;yEACqE;IACrE,SAAS,EAAE,OAAO,CAAC;CACpB;AASD;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,CA6CzE"}
@@ -0,0 +1,36 @@
1
+ //#region src/planmagnet/utils/lineDiff.ts
2
+ var e = 800;
3
+ function t(t, n) {
4
+ let r = t.split("\n"), i = n.split("\n"), a = r.length > e || i.length > e, o = r.slice(0, e), s = i.slice(0, e), c = o.length, l = s.length, u = Array.from({ length: c + 1 }, () => new Uint32Array(l + 1));
5
+ for (let e = c - 1; e >= 0; e--) {
6
+ let t = u[e], n = u[e + 1];
7
+ for (let r = l - 1; r >= 0; r--) t[r] = o[e] === s[r] ? n[r + 1] + 1 : Math.max(n[r], t[r + 1]);
8
+ }
9
+ let d = [], f = 0, p = 0;
10
+ for (; f < c && p < l;) o[f] === s[p] ? (d.push({
11
+ kind: "UNCHANGED",
12
+ text: o[f]
13
+ }), f++, p++) : u[f + 1][p] >= u[f][p + 1] ? (d.push({
14
+ kind: "REMOVED",
15
+ text: o[f]
16
+ }), f++) : (d.push({
17
+ kind: "ADDED",
18
+ text: s[p]
19
+ }), p++);
20
+ for (; f < c;) d.push({
21
+ kind: "REMOVED",
22
+ text: o[f]
23
+ }), f++;
24
+ for (; p < l;) d.push({
25
+ kind: "ADDED",
26
+ text: s[p]
27
+ }), p++;
28
+ return {
29
+ hunks: d,
30
+ truncated: a
31
+ };
32
+ }
33
+ //#endregion
34
+ export { t as lineDiff };
35
+
36
+ //# sourceMappingURL=lineDiff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lineDiff.js","names":[],"sources":["../../../src/planmagnet/utils/lineDiff.ts"],"sourcesContent":["export type LineDiffKind = 'ADDED' | 'REMOVED' | 'UNCHANGED';\n\nexport interface LineDiffHunk {\n kind: LineDiffKind;\n text: string;\n}\n\nexport interface LineDiffResult {\n hunks: LineDiffHunk[];\n /** True when either side exceeded `LINE_CAP` — the hunks above are still a valid PREFIX of the real diff, same\n * contract as the server's `PlanmagnetDocRevisionDiff.truncated`. */\n truncated: boolean;\n}\n\n/**\n * Lines beyond this many (per side) are dropped before diffing. Bounds the classic O(n·m) LCS table to at most\n * `(LINE_CAP + 1)^2` cells — comfortably interactive in a browser tab — while the diff on what is kept is still a\n * correct prefix of the real one.\n */\nconst LINE_CAP = 800;\n\n/**\n * A line-based diff between two texts (CWF-18 §2's suggestion preview), computed entirely client-side with a\n * small, dependency-free LCS — there is no server endpoint for \"diff this UNSAVED proposed text against a\n * revision\" (the real `planmagnetDocRevisionDiff` only compares two already-SAVED versions of a doc, for the\n * history panel's version compare — see `DocVersionCompare.tsx`, which uses that instead of this). Same hunk\n * shape as the server's `PlanmagnetDocRevisionDiff` (`kind`/`text`, plus `truncated`) so both diffs render\n * through the one shared `DocDiffHunks` component.\n */\nexport function lineDiff(fromText: string, toText: string): LineDiffResult {\n const fromAll = fromText.split('\\n');\n const toAll = toText.split('\\n');\n const truncated = fromAll.length > LINE_CAP || toAll.length > LINE_CAP;\n const from = fromAll.slice(0, LINE_CAP);\n const to = toAll.slice(0, LINE_CAP);\n\n const n = from.length;\n const m = to.length;\n // lcs[i][j] = length of the longest common subsequence of from[i..] and to[j..].\n const lcs: Uint32Array[] = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));\n for (let i = n - 1; i >= 0; i--) {\n const row = lcs[i];\n const nextRow = lcs[i + 1];\n for (let j = m - 1; j >= 0; j--) {\n row[j] = from[i] === to[j] ? nextRow[j + 1] + 1 : Math.max(nextRow[j], row[j + 1]);\n }\n }\n\n const hunks: LineDiffHunk[] = [];\n let i = 0;\n let j = 0;\n while (i < n && j < m) {\n if (from[i] === to[j]) {\n hunks.push({ kind: 'UNCHANGED', text: from[i] });\n i++;\n j++;\n } else if (lcs[i + 1][j] >= lcs[i][j + 1]) {\n hunks.push({ kind: 'REMOVED', text: from[i] });\n i++;\n } else {\n hunks.push({ kind: 'ADDED', text: to[j] });\n j++;\n }\n }\n while (i < n) {\n hunks.push({ kind: 'REMOVED', text: from[i] });\n i++;\n }\n while (j < m) {\n hunks.push({ kind: 'ADDED', text: to[j] });\n j++;\n }\n\n return { hunks, truncated };\n}\n"],"mappings":";AAmBA,IAAM,IAAW;AAUjB,SAAgB,EAAS,GAAkB,GAAgC;CACzE,IAAM,IAAU,EAAS,MAAM,KAAK,EAC9B,IAAQ,EAAO,MAAM,KAAK,EAC1B,IAAY,EAAQ,SAAS,KAAY,EAAM,SAAS,GACxD,IAAO,EAAQ,MAAM,GAAG,EAAS,EACjC,IAAK,EAAM,MAAM,GAAG,EAAS,EAE7B,IAAI,EAAK,QACT,IAAI,EAAG,QAEP,IAAqB,MAAM,KAAK,EAAE,QAAQ,IAAI,GAAG,QAAQ,IAAI,YAAY,IAAI,EAAE,CAAC;AACtF,MAAK,IAAI,IAAI,IAAI,GAAG,KAAK,GAAG,KAAK;EAC/B,IAAM,IAAM,EAAI,IACV,IAAU,EAAI,IAAI;AACxB,OAAK,IAAI,IAAI,IAAI,GAAG,KAAK,GAAG,IAC1B,GAAI,KAAK,EAAK,OAAO,EAAG,KAAK,EAAQ,IAAI,KAAK,IAAI,KAAK,IAAI,EAAQ,IAAI,EAAI,IAAI,GAAG;;CAItF,IAAM,IAAwB,EAAE,EAC5B,IAAI,GACJ,IAAI;AACR,QAAO,IAAI,KAAK,IAAI,GAClB,CAAI,EAAK,OAAO,EAAG,MACjB,EAAM,KAAK;EAAE,MAAM;EAAa,MAAM,EAAK;EAAI,CAAC,EAChD,KACA,OACS,EAAI,IAAI,GAAG,MAAM,EAAI,GAAG,IAAI,MACrC,EAAM,KAAK;EAAE,MAAM;EAAW,MAAM,EAAK;EAAI,CAAC,EAC9C,QAEA,EAAM,KAAK;EAAE,MAAM;EAAS,MAAM,EAAG;EAAI,CAAC,EAC1C;AAGJ,QAAO,IAAI,GAET,CADA,EAAM,KAAK;EAAE,MAAM;EAAW,MAAM,EAAK;EAAI,CAAC,EAC9C;AAEF,QAAO,IAAI,GAET,CADA,EAAM,KAAK;EAAE,MAAM;EAAS,MAAM,EAAG;EAAI,CAAC,EAC1C;AAGF,QAAO;EAAE;EAAO;EAAW"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-planmagnet",
3
- "version": "2026.930.24",
3
+ "version": "2026.930.25",
4
4
  "description": "Burdenoff PlanMagnet Microfrontend",
5
5
  "type": "module",
6
6
  "files": [