@burdenoff/microfe-planmagnet 2026.930.23 → 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 (111) hide show
  1. package/dist/index.js +6 -5
  2. package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
  3. package/dist/planmagnet/PlanMagnetRoot.js +154 -147
  4. package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
  5. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  6. package/dist/planmagnet/PlanMagnetRoutes.js +66 -62
  7. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  8. package/dist/planmagnet/comments/CommentThread.d.ts +8 -1
  9. package/dist/planmagnet/comments/CommentThread.d.ts.map +1 -1
  10. package/dist/planmagnet/comments/CommentThread.js +188 -181
  11. package/dist/planmagnet/comments/CommentThread.js.map +1 -1
  12. package/dist/planmagnet/comments/TargetComments.d.ts +10 -2
  13. package/dist/planmagnet/comments/TargetComments.d.ts.map +1 -1
  14. package/dist/planmagnet/comments/TargetComments.js +10 -8
  15. package/dist/planmagnet/comments/TargetComments.js.map +1 -1
  16. package/dist/planmagnet/comments/WorkItemComments.d.ts +7 -1
  17. package/dist/planmagnet/comments/WorkItemComments.d.ts.map +1 -1
  18. package/dist/planmagnet/comments/WorkItemComments.js +5 -4
  19. package/dist/planmagnet/comments/WorkItemComments.js.map +1 -1
  20. package/dist/planmagnet/controls/FeatureControls.d.ts +11 -0
  21. package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
  22. package/dist/planmagnet/controls/FeatureControls.js +305 -135
  23. package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
  24. package/dist/planmagnet/controls/Pickers.d.ts +18 -0
  25. package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
  26. package/dist/planmagnet/controls/Pickers.js +78 -1
  27. package/dist/planmagnet/controls/Pickers.js.map +1 -1
  28. package/dist/planmagnet/controls/PrioritizationControls.d.ts +72 -0
  29. package/dist/planmagnet/controls/PrioritizationControls.d.ts.map +1 -0
  30. package/dist/planmagnet/controls/PrioritizationControls.js +246 -0
  31. package/dist/planmagnet/controls/PrioritizationControls.js.map +1 -0
  32. package/dist/planmagnet/discussion/WorkItemDiscussionPanel.d.ts +8 -0
  33. package/dist/planmagnet/discussion/WorkItemDiscussionPanel.d.ts.map +1 -0
  34. package/dist/planmagnet/discussion/WorkItemDiscussionPanel.js +217 -0
  35. package/dist/planmagnet/discussion/WorkItemDiscussionPanel.js.map +1 -0
  36. package/dist/planmagnet/docs/DocDiffView.d.ts +26 -0
  37. package/dist/planmagnet/docs/DocDiffView.d.ts.map +1 -0
  38. package/dist/planmagnet/docs/DocDiffView.js +52 -0
  39. package/dist/planmagnet/docs/DocDiffView.js.map +1 -0
  40. package/dist/planmagnet/docs/DocSuggestionsPanel.d.ts +13 -0
  41. package/dist/planmagnet/docs/DocSuggestionsPanel.d.ts.map +1 -0
  42. package/dist/planmagnet/docs/DocSuggestionsPanel.js +308 -0
  43. package/dist/planmagnet/docs/DocSuggestionsPanel.js.map +1 -0
  44. package/dist/planmagnet/docs/DocVersionCompare.d.ts +7 -0
  45. package/dist/planmagnet/docs/DocVersionCompare.d.ts.map +1 -0
  46. package/dist/planmagnet/docs/DocVersionCompare.js +105 -0
  47. package/dist/planmagnet/docs/DocVersionCompare.js.map +1 -0
  48. package/dist/planmagnet/hooks/usePresence.d.ts +39 -0
  49. package/dist/planmagnet/hooks/usePresence.d.ts.map +1 -0
  50. package/dist/planmagnet/hooks/usePresence.js +146 -0
  51. package/dist/planmagnet/hooks/usePresence.js.map +1 -0
  52. package/dist/planmagnet/index.d.ts +1 -0
  53. package/dist/planmagnet/index.d.ts.map +1 -1
  54. package/dist/planmagnet/index.js +1 -0
  55. package/dist/planmagnet/links/CodeLinksPanel.d.ts +22 -0
  56. package/dist/planmagnet/links/CodeLinksPanel.d.ts.map +1 -0
  57. package/dist/planmagnet/links/CodeLinksPanel.js +94 -0
  58. package/dist/planmagnet/links/CodeLinksPanel.js.map +1 -0
  59. package/dist/planmagnet/pages/DocsDocDetailPagePage.d.ts.map +1 -1
  60. package/dist/planmagnet/pages/DocsDocDetailPagePage.js +488 -217
  61. package/dist/planmagnet/pages/DocsDocDetailPagePage.js.map +1 -1
  62. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
  63. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +131 -125
  64. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
  65. package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
  66. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +183 -99
  67. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
  68. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
  69. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +113 -97
  70. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
  71. package/dist/planmagnet/pages/ProductsPrioritizationPagePage.d.ts +2 -0
  72. package/dist/planmagnet/pages/ProductsPrioritizationPagePage.d.ts.map +1 -0
  73. package/dist/planmagnet/pages/ProductsPrioritizationPagePage.js +363 -0
  74. package/dist/planmagnet/pages/ProductsPrioritizationPagePage.js.map +1 -0
  75. package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
  76. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +96 -79
  77. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
  78. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
  79. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +144 -128
  80. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
  81. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.d.ts.map +1 -1
  82. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js +205 -196
  83. package/dist/planmagnet/pages/ProjectsProjectDetailPagePage.js.map +1 -1
  84. package/dist/planmagnet/pages/TasksTaskDetailPagePage.d.ts.map +1 -1
  85. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js +194 -160
  86. package/dist/planmagnet/pages/TasksTaskDetailPagePage.js.map +1 -1
  87. package/dist/planmagnet/presence/PresenceAvatars.d.ts +16 -0
  88. package/dist/planmagnet/presence/PresenceAvatars.d.ts.map +1 -0
  89. package/dist/planmagnet/presence/PresenceAvatars.js +56 -0
  90. package/dist/planmagnet/presence/PresenceAvatars.js.map +1 -0
  91. package/dist/planmagnet/product/ProductControls.d.ts +33 -0
  92. package/dist/planmagnet/product/ProductControls.d.ts.map +1 -1
  93. package/dist/planmagnet/product/ProductControls.js +593 -268
  94. package/dist/planmagnet/product/ProductControls.js.map +1 -1
  95. package/dist/planmagnet/product/routes.d.ts +10 -0
  96. package/dist/planmagnet/product/routes.d.ts.map +1 -0
  97. package/dist/planmagnet/product/routes.js +6 -0
  98. package/dist/planmagnet/product/routes.js.map +1 -0
  99. package/dist/planmagnet/roadmaps/RoadmapControls.d.ts +10 -0
  100. package/dist/planmagnet/roadmaps/RoadmapControls.d.ts.map +1 -1
  101. package/dist/planmagnet/roadmaps/RoadmapControls.js +235 -184
  102. package/dist/planmagnet/roadmaps/RoadmapControls.js.map +1 -1
  103. package/dist/planmagnet/utils/graphqlErrors.d.ts +6 -0
  104. package/dist/planmagnet/utils/graphqlErrors.d.ts.map +1 -1
  105. package/dist/planmagnet/utils/graphqlErrors.js +5 -2
  106. package/dist/planmagnet/utils/graphqlErrors.js.map +1 -1
  107. package/dist/planmagnet/utils/lineDiff.d.ts +21 -0
  108. package/dist/planmagnet/utils/lineDiff.d.ts.map +1 -0
  109. package/dist/planmagnet/utils/lineDiff.js +36 -0
  110. package/dist/planmagnet/utils/lineDiff.js.map +1 -0
  111. package/package.json +1 -1
@@ -0,0 +1,308 @@
1
+ import { PersonName as e } from "../people/People.js";
2
+ import { throwOnError as t } from "../utils/mutationResult.js";
3
+ import { ConfirmButton as n, ErrorLine as r } from "../controls/ConfirmButton.js";
4
+ import { isConflict as i, isForbidden as a, mutationErrorMessage as o } from "../utils/graphqlErrors.js";
5
+ import { lineDiff as s } from "../utils/lineDiff.js";
6
+ import { DocDiffHunks as c } from "./DocDiffView.js";
7
+ import { useState as l } from "react";
8
+ import { FileEdit as u, Undo2 as d } from "lucide-react";
9
+ import { gql as f } from "@apollo/client";
10
+ import { useMutation as p, useQuery as m } from "@apollo/client/react";
11
+ import { Fragment as h, jsx as g, jsxs as _ } from "react/jsx-runtime";
12
+ import { Button as v, Card as y, CardContent as b, CardHeader as x, CardTitle as S, Tabs as C, TabsContent as w, TabsList as T, TabsTrigger as E } from "@burdenoff/fe-libs/ui";
13
+ //#region src/planmagnet/docs/DocSuggestionsPanel.tsx
14
+ var D = 10, O = "DocSuggestionsPanel", k = f`
15
+ query DocSuggestionsPanel(
16
+ $docId: ID!
17
+ $status: PlanmagnetDocSuggestionStatus
18
+ $pagination: PlanmagnetPaginationInput
19
+ ) {
20
+ planmagnetDoc(id: $docId) {
21
+ id
22
+ suggestions(status: $status, pagination: $pagination) {
23
+ totalCount
24
+ pageInfo {
25
+ hasNextPage
26
+ endCursor
27
+ }
28
+ items {
29
+ id
30
+ authorId
31
+ baseVersion
32
+ proposedTitle
33
+ proposedContent
34
+ note
35
+ status
36
+ createdAt
37
+ decidedAt
38
+ decidedBy
39
+ }
40
+ }
41
+ }
42
+ }
43
+ `, A = f`
44
+ mutation PlanmagnetDecideDocSuggestionDecide($id: ID!, $accept: Boolean!) {
45
+ planmagnetDecideDocSuggestion(id: $id, accept: $accept) {
46
+ id
47
+ status
48
+ }
49
+ }
50
+ `, j = f`
51
+ mutation PlanmagnetWithdrawDocSuggestionWithdraw($id: ID!) {
52
+ planmagnetWithdrawDocSuggestion(id: $id) {
53
+ id
54
+ status
55
+ }
56
+ }
57
+ `, M = (e) => new Date(e).toLocaleString();
58
+ function N({ suggestion: u, docTitle: f, docContent: m, revisions: y, currentUserId: b }) {
59
+ let [x, S] = l(!1), [C, w] = l(null), [T, E] = p(A, {
60
+ refetchQueries: [O, "DocDetail"],
61
+ awaitRefetchQueries: !0
62
+ }), [D, k] = p(j, {
63
+ refetchQueries: [O],
64
+ awaitRefetchQueries: !0
65
+ }), N = y.find((e) => e.version === u.baseVersion), P = N?.content ?? m, F = N?.title ?? f, I = s(P, u.proposedContent), L = u.proposedTitle !== null && u.proposedTitle !== F, R = u.authorId === b, z = async (e, n) => {
66
+ w(null);
67
+ try {
68
+ await t(e());
69
+ } catch (e) {
70
+ w(a(e) ? "Your role cannot decide this suggestion." : i(e) ? "The doc changed since this was suggested." : o(e, n));
71
+ }
72
+ };
73
+ return /* @__PURE__ */ _("li", {
74
+ className: "space-y-2 rounded-md border border-border-default p-3",
75
+ "data-testid": "doc-suggestion",
76
+ children: [
77
+ /* @__PURE__ */ _("div", {
78
+ className: "flex flex-wrap items-start justify-between gap-2",
79
+ children: [/* @__PURE__ */ _("div", {
80
+ className: "min-w-0",
81
+ children: [
82
+ /* @__PURE__ */ _("p", {
83
+ className: "text-sm",
84
+ children: [
85
+ /* @__PURE__ */ g("span", {
86
+ className: "font-medium",
87
+ children: /* @__PURE__ */ g(e, { id: u.authorId })
88
+ }),
89
+ " ",
90
+ "proposed ",
91
+ M(u.createdAt),
92
+ " against version ",
93
+ u.baseVersion
94
+ ]
95
+ }),
96
+ u.note && /* @__PURE__ */ g("p", {
97
+ className: "mt-0.5 text-xs text-muted-foreground",
98
+ children: u.note
99
+ }),
100
+ u.status !== "OPEN" && /* @__PURE__ */ _("p", {
101
+ className: "mt-0.5 text-xs text-muted-foreground",
102
+ children: [
103
+ u.status === "ACCEPTED" ? "Accepted" : "Rejected",
104
+ u.decidedBy ? /* @__PURE__ */ _(h, { children: [
105
+ " ",
106
+ "by ",
107
+ /* @__PURE__ */ g(e, { id: u.decidedBy })
108
+ ] }) : null,
109
+ u.decidedAt ? ` · ${M(u.decidedAt)}` : ""
110
+ ]
111
+ })
112
+ ]
113
+ }), /* @__PURE__ */ g(v, {
114
+ size: "sm",
115
+ variant: "ghost",
116
+ className: "h-7 shrink-0 text-xs",
117
+ onClick: () => S((e) => !e),
118
+ "aria-expanded": x,
119
+ children: x ? "Hide diff" : "Show diff"
120
+ })]
121
+ }),
122
+ C && /* @__PURE__ */ g(r, { message: C }),
123
+ x && /* @__PURE__ */ _("div", {
124
+ className: "space-y-1",
125
+ children: [L && /* @__PURE__ */ _("p", {
126
+ className: "text-xs text-muted-foreground",
127
+ children: [
128
+ "Title: ",
129
+ /* @__PURE__ */ g("span", {
130
+ className: "line-through",
131
+ children: F
132
+ }),
133
+ " →",
134
+ " ",
135
+ /* @__PURE__ */ g("span", {
136
+ className: "font-medium text-foreground",
137
+ children: u.proposedTitle
138
+ })
139
+ ]
140
+ }), /* @__PURE__ */ g(c, {
141
+ hunks: I.hunks,
142
+ truncated: I.truncated,
143
+ emptyMessage: "No content differences.",
144
+ testId: `doc-suggestion-diff-${u.id}`
145
+ })]
146
+ }),
147
+ u.status === "OPEN" && /* @__PURE__ */ _("div", {
148
+ className: "flex flex-wrap justify-end gap-2 border-t border-border-default pt-2",
149
+ children: [
150
+ R && /* @__PURE__ */ g(n, {
151
+ label: "Withdraw",
152
+ icon: /* @__PURE__ */ g(d, {
153
+ className: "h-3.5 w-3.5",
154
+ "aria-hidden": "true"
155
+ }),
156
+ title: "Withdraw this suggestion?",
157
+ description: "It moves to Withdrawn and can no longer be accepted or rejected.",
158
+ confirmLabel: "Withdraw",
159
+ variant: "outline",
160
+ disabled: k.loading,
161
+ onConfirm: () => void z(() => D({ variables: { id: u.id } }), "The suggestion could not be withdrawn."),
162
+ testId: "doc-suggestion-withdraw"
163
+ }),
164
+ /* @__PURE__ */ g(v, {
165
+ size: "sm",
166
+ variant: "outline",
167
+ disabled: E.loading,
168
+ "data-testid": "doc-suggestion-reject",
169
+ onClick: () => void z(() => T({ variables: {
170
+ id: u.id,
171
+ accept: !1
172
+ } }), "The suggestion could not be rejected."),
173
+ children: "Reject"
174
+ }),
175
+ /* @__PURE__ */ g(v, {
176
+ size: "sm",
177
+ disabled: E.loading,
178
+ "data-testid": "doc-suggestion-accept",
179
+ onClick: () => void z(() => T({ variables: {
180
+ id: u.id,
181
+ accept: !0
182
+ } }), "The suggestion could not be accepted."),
183
+ children: "Accept"
184
+ })
185
+ ]
186
+ })
187
+ ]
188
+ });
189
+ }
190
+ function P({ docId: e, status: t, docTitle: n, docContent: i, revisions: o, currentUserId: s }) {
191
+ let [c, u] = l(!1), { data: d, loading: f, error: p, fetchMore: h } = m(k, {
192
+ variables: {
193
+ docId: e,
194
+ status: t,
195
+ pagination: { first: D }
196
+ },
197
+ skip: !e
198
+ }), y = d?.planmagnetDoc?.suggestions, b = y?.items ?? [], x = async () => {
199
+ let n = y?.pageInfo.endCursor;
200
+ if (n) {
201
+ u(!0);
202
+ try {
203
+ await h({
204
+ variables: {
205
+ docId: e,
206
+ status: t,
207
+ pagination: {
208
+ first: D,
209
+ after: n
210
+ }
211
+ },
212
+ updateQuery: (e, { fetchMoreResult: t }) => !e.planmagnetDoc || !t.planmagnetDoc ? e : { planmagnetDoc: {
213
+ ...t.planmagnetDoc,
214
+ suggestions: {
215
+ ...t.planmagnetDoc.suggestions,
216
+ items: [...e.planmagnetDoc.suggestions.items, ...t.planmagnetDoc.suggestions.items]
217
+ }
218
+ } }
219
+ });
220
+ } finally {
221
+ u(!1);
222
+ }
223
+ }
224
+ };
225
+ return /* @__PURE__ */ _("div", {
226
+ className: "space-y-2",
227
+ children: [
228
+ f && !d && /* @__PURE__ */ g("p", {
229
+ className: "text-sm text-muted-foreground",
230
+ children: "Loading…"
231
+ }),
232
+ p && /* @__PURE__ */ g(r, { message: a(p) ? "Your role cannot see this doc’s suggestions." : `Suggestions could not be loaded: ${p.message}` }),
233
+ y && b.length === 0 && /* @__PURE__ */ g("p", {
234
+ className: "text-sm text-muted-foreground",
235
+ children: t === "OPEN" ? "No open suggestions." : t === "ACCEPTED" ? "None accepted yet." : "None rejected yet."
236
+ }),
237
+ /* @__PURE__ */ g("ul", {
238
+ className: "space-y-2",
239
+ children: b.map((e) => /* @__PURE__ */ g(N, {
240
+ suggestion: e,
241
+ docTitle: n,
242
+ docContent: i,
243
+ revisions: o,
244
+ currentUserId: s
245
+ }, e.id))
246
+ }),
247
+ y?.pageInfo.hasNextPage && /* @__PURE__ */ g(v, {
248
+ size: "sm",
249
+ variant: "outline",
250
+ onClick: () => void x(),
251
+ disabled: c,
252
+ children: c ? "Loading…" : `Show more (${b.length} of ${y.totalCount})`
253
+ })
254
+ ]
255
+ });
256
+ }
257
+ function F({ docId: e, docTitle: t, docContent: n, revisions: r, currentUserId: i }) {
258
+ let [a, o] = l("OPEN");
259
+ return /* @__PURE__ */ _(y, {
260
+ "data-testid": "doc-suggestions",
261
+ children: [/* @__PURE__ */ g(x, { children: /* @__PURE__ */ _(S, {
262
+ className: "flex items-center gap-2",
263
+ children: [/* @__PURE__ */ g(u, {
264
+ className: "h-4 w-4",
265
+ "aria-hidden": "true"
266
+ }), "Suggestions"]
267
+ }) }), /* @__PURE__ */ g(b, { children: /* @__PURE__ */ _(C, {
268
+ value: a,
269
+ onValueChange: (e) => o(e),
270
+ children: [/* @__PURE__ */ _(T, { children: [
271
+ /* @__PURE__ */ g(E, {
272
+ value: "OPEN",
273
+ "data-testid": "doc-suggestions-tab-open",
274
+ children: "Open"
275
+ }),
276
+ /* @__PURE__ */ g(E, {
277
+ value: "ACCEPTED",
278
+ "data-testid": "doc-suggestions-tab-accepted",
279
+ children: "Accepted"
280
+ }),
281
+ /* @__PURE__ */ g(E, {
282
+ value: "REJECTED",
283
+ "data-testid": "doc-suggestions-tab-rejected",
284
+ children: "Rejected"
285
+ })
286
+ ] }), [
287
+ "OPEN",
288
+ "ACCEPTED",
289
+ "REJECTED"
290
+ ].map((a) => /* @__PURE__ */ g(w, {
291
+ value: a,
292
+ className: "mt-3",
293
+ children: /* @__PURE__ */ g(P, {
294
+ docId: e,
295
+ status: a,
296
+ docTitle: t,
297
+ docContent: n,
298
+ revisions: r,
299
+ currentUserId: i
300
+ })
301
+ }, a))]
302
+ }) })]
303
+ });
304
+ }
305
+ //#endregion
306
+ export { F as DocSuggestionsPanel };
307
+
308
+ //# sourceMappingURL=DocSuggestionsPanel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DocSuggestionsPanel.js","names":[],"sources":["../../../src/planmagnet/docs/DocSuggestionsPanel.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport {\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { FileEdit, Undo2 } from 'lucide-react';\nimport { PersonName } from '../people/People';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { throwOnError } from '../utils/mutationResult';\nimport { isConflict, isForbidden, mutationErrorMessage } from '../utils/graphqlErrors';\nimport { lineDiff } from '../utils/lineDiff';\nimport { DocDiffHunks } from './DocDiffView';\n\n/**\n * Proposed edits against a doc (CWF-18 §2) that have not been applied: Open / Accepted / Rejected, each its own\n * tab and its own (paginated) `planmagnetDoc.suggestions(status: …)` read — Withdrawn ones are never shown here,\n * matching the three tabs this panel was scoped to (a withdrawn suggestion is, by design, gone from view — its\n * author already said it no longer applies).\n *\n * The diff preview is computed HERE, client-side (`lineDiff`): `planmagnetDocRevisionDiff` only compares two\n * already-saved revisions, and a suggestion's `proposedContent` was never saved as one — so the base side comes\n * from whichever loaded `PlanmagnetDocRevision` matches `baseVersion` (falling back to the live doc when the\n * suggestion is against the CURRENT version, which has no revision row of its own yet).\n *\n * Accept/Reject are shown to every viewer (the resolver is the real gate, same \"show it, let the server refuse\n * it\" house rule as every other write-tier control in this MFE) except Withdraw, which IS client-gated to the\n * suggestion's own author — `planmagnetWithdrawDocSuggestion`'s own schema doc says it is author-only, refused\n * NOT_FOUND (not FORBIDDEN) for anyone else, so showing it to a non-author would just be a button that always\n * fails.\n */\n\nconst PAGE = 10;\nconst SUGGESTION_QUERY_NAME = 'DocSuggestionsPanel';\n\nconst DOC_SUGGESTIONS = gql`\n query DocSuggestionsPanel(\n $docId: ID!\n $status: PlanmagnetDocSuggestionStatus\n $pagination: PlanmagnetPaginationInput\n ) {\n planmagnetDoc(id: $docId) {\n id\n suggestions(status: $status, pagination: $pagination) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n items {\n id\n authorId\n baseVersion\n proposedTitle\n proposedContent\n note\n status\n createdAt\n decidedAt\n decidedBy\n }\n }\n }\n }\n`;\n\nconst DECIDE = gql`\n mutation PlanmagnetDecideDocSuggestionDecide($id: ID!, $accept: Boolean!) {\n planmagnetDecideDocSuggestion(id: $id, accept: $accept) {\n id\n status\n }\n }\n`;\n\nconst WITHDRAW = gql`\n mutation PlanmagnetWithdrawDocSuggestionWithdraw($id: ID!) {\n planmagnetWithdrawDocSuggestion(id: $id) {\n id\n status\n }\n }\n`;\n\ntype SuggestionStatus = 'OPEN' | 'ACCEPTED' | 'REJECTED';\n\ninterface Suggestion {\n id: string;\n authorId: string;\n baseVersion: number;\n proposedTitle: string | null;\n proposedContent: string;\n note: string | null;\n status: 'OPEN' | 'ACCEPTED' | 'REJECTED' | 'WITHDRAWN';\n createdAt: string;\n decidedAt: string | null;\n decidedBy: string | null;\n}\n\nexport interface DocRevisionForDiff {\n version: number;\n title: string;\n content: string;\n}\n\nconst when = (iso: string) => new Date(iso).toLocaleString();\n\nfunction SuggestionRow({\n suggestion,\n docTitle,\n docContent,\n revisions,\n currentUserId,\n}: {\n suggestion: Suggestion;\n docTitle: string;\n docContent: string;\n revisions: readonly DocRevisionForDiff[];\n currentUserId: string | undefined;\n}) {\n const [showDiff, setShowDiff] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const decideRefetch = {\n refetchQueries: [SUGGESTION_QUERY_NAME, 'DocDetail'],\n awaitRefetchQueries: true,\n };\n const [decide, decideState] = useMutation(DECIDE, decideRefetch);\n const [withdraw, withdrawState] = useMutation(WITHDRAW, {\n refetchQueries: [SUGGESTION_QUERY_NAME],\n awaitRefetchQueries: true,\n });\n\n // The suggestion's base version: a saved revision when the doc has moved on since, otherwise the live doc\n // itself (a suggestion against the CURRENT version has no PlanmagnetDocRevision row yet — only a save creates\n // one).\n const baseRevision = revisions.find((r) => r.version === suggestion.baseVersion);\n const baseContent = baseRevision?.content ?? docContent;\n const baseTitle = baseRevision?.title ?? docTitle;\n const diff = lineDiff(baseContent, suggestion.proposedContent);\n const titleChanged = suggestion.proposedTitle !== null && suggestion.proposedTitle !== baseTitle;\n const mine = suggestion.authorId === currentUserId;\n\n const act = async (action: () => Promise<{ error?: unknown }>, fallback: string) => {\n setError(null);\n try {\n await throwOnError(action());\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role cannot decide this suggestion.'\n : isConflict(e)\n ? 'The doc changed since this was suggested.'\n : mutationErrorMessage(e, fallback)\n );\n }\n };\n\n return (\n <li\n className=\"space-y-2 rounded-md border border-border-default p-3\"\n data-testid=\"doc-suggestion\"\n >\n <div className=\"flex flex-wrap items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <p className=\"text-sm\">\n <span className=\"font-medium\">\n <PersonName id={suggestion.authorId} />\n </span>{' '}\n proposed {when(suggestion.createdAt)} against version {suggestion.baseVersion}\n </p>\n {suggestion.note && (\n <p className=\"mt-0.5 text-xs text-muted-foreground\">{suggestion.note}</p>\n )}\n {suggestion.status !== 'OPEN' && (\n <p className=\"mt-0.5 text-xs text-muted-foreground\">\n {suggestion.status === 'ACCEPTED' ? 'Accepted' : 'Rejected'}\n {suggestion.decidedBy ? (\n <>\n {' '}\n by <PersonName id={suggestion.decidedBy} />\n </>\n ) : null}\n {suggestion.decidedAt ? ` · ${when(suggestion.decidedAt)}` : ''}\n </p>\n )}\n </div>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-7 shrink-0 text-xs\"\n onClick={() => setShowDiff((v) => !v)}\n aria-expanded={showDiff}\n >\n {showDiff ? 'Hide diff' : 'Show diff'}\n </Button>\n </div>\n {error && <ErrorLine message={error} />}\n {showDiff && (\n <div className=\"space-y-1\">\n {titleChanged && (\n <p className=\"text-xs text-muted-foreground\">\n Title: <span className=\"line-through\">{baseTitle}</span> →{' '}\n <span className=\"font-medium text-foreground\">{suggestion.proposedTitle}</span>\n </p>\n )}\n <DocDiffHunks\n hunks={diff.hunks}\n truncated={diff.truncated}\n emptyMessage=\"No content differences.\"\n testId={`doc-suggestion-diff-${suggestion.id}`}\n />\n </div>\n )}\n {suggestion.status === 'OPEN' && (\n <div className=\"flex flex-wrap justify-end gap-2 border-t border-border-default pt-2\">\n {mine && (\n <ConfirmButton\n label=\"Withdraw\"\n icon={<Undo2 className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />}\n title=\"Withdraw this suggestion?\"\n description=\"It moves to Withdrawn and can no longer be accepted or rejected.\"\n confirmLabel=\"Withdraw\"\n variant=\"outline\"\n disabled={withdrawState.loading}\n onConfirm={() =>\n void act(\n () => withdraw({ variables: { id: suggestion.id } }),\n 'The suggestion could not be withdrawn.'\n )\n }\n testId=\"doc-suggestion-withdraw\"\n />\n )}\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={decideState.loading}\n data-testid=\"doc-suggestion-reject\"\n onClick={() =>\n void act(\n () => decide({ variables: { id: suggestion.id, accept: false } }),\n 'The suggestion could not be rejected.'\n )\n }\n >\n Reject\n </Button>\n <Button\n size=\"sm\"\n disabled={decideState.loading}\n data-testid=\"doc-suggestion-accept\"\n onClick={() =>\n void act(\n () => decide({ variables: { id: suggestion.id, accept: true } }),\n 'The suggestion could not be accepted.'\n )\n }\n >\n Accept\n </Button>\n </div>\n )}\n </li>\n );\n}\n\nfunction SuggestionList({\n docId,\n status,\n docTitle,\n docContent,\n revisions,\n currentUserId,\n}: {\n docId: string;\n status: SuggestionStatus;\n docTitle: string;\n docContent: string;\n revisions: readonly DocRevisionForDiff[];\n currentUserId: string | undefined;\n}) {\n const [loadingMore, setLoadingMore] = useState(false);\n const { data, loading, error, fetchMore } = useQuery<{\n planmagnetDoc: {\n id: string;\n suggestions: {\n totalCount: number;\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n items: Suggestion[];\n };\n } | null;\n }>(DOC_SUGGESTIONS, {\n variables: { docId, status, pagination: { first: PAGE } },\n skip: !docId,\n });\n\n const list = data?.planmagnetDoc?.suggestions;\n const items = list?.items ?? [];\n\n const loadMore = async () => {\n const after = list?.pageInfo.endCursor;\n if (!after) return;\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { docId, status, pagination: { first: PAGE, after } },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!prev.planmagnetDoc || !fetchMoreResult.planmagnetDoc) return prev;\n return {\n planmagnetDoc: {\n ...fetchMoreResult.planmagnetDoc,\n suggestions: {\n ...fetchMoreResult.planmagnetDoc.suggestions,\n items: [\n ...prev.planmagnetDoc.suggestions.items,\n ...fetchMoreResult.planmagnetDoc.suggestions.items,\n ],\n },\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n return (\n <div className=\"space-y-2\">\n {loading && !data && <p className=\"text-sm text-muted-foreground\">Loading…</p>}\n {error && (\n <ErrorLine\n message={\n isForbidden(error)\n ? 'Your role cannot see this doc’s suggestions.'\n : `Suggestions could not be loaded: ${error.message}`\n }\n />\n )}\n {list && items.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">\n {status === 'OPEN'\n ? 'No open suggestions.'\n : status === 'ACCEPTED'\n ? 'None accepted yet.'\n : 'None rejected yet.'}\n </p>\n )}\n <ul className=\"space-y-2\">\n {items.map((s) => (\n <SuggestionRow\n key={s.id}\n suggestion={s}\n docTitle={docTitle}\n docContent={docContent}\n revisions={revisions}\n currentUserId={currentUserId}\n />\n ))}\n </ul>\n {list?.pageInfo.hasNextPage && (\n <Button size=\"sm\" variant=\"outline\" onClick={() => void loadMore()} disabled={loadingMore}>\n {loadingMore ? 'Loading…' : `Show more (${items.length} of ${list.totalCount})`}\n </Button>\n )}\n </div>\n );\n}\n\nexport function DocSuggestionsPanel({\n docId,\n docTitle,\n docContent,\n revisions,\n currentUserId,\n}: {\n docId: string;\n docTitle: string;\n docContent: string;\n revisions: readonly DocRevisionForDiff[];\n currentUserId: string | undefined;\n}) {\n const [tab, setTab] = useState<SuggestionStatus>('OPEN');\n return (\n <Card data-testid=\"doc-suggestions\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <FileEdit className=\"h-4 w-4\" aria-hidden=\"true\" />\n Suggestions\n </CardTitle>\n </CardHeader>\n <CardContent>\n <Tabs value={tab} onValueChange={(v) => setTab(v as SuggestionStatus)}>\n <TabsList>\n <TabsTrigger value=\"OPEN\" data-testid=\"doc-suggestions-tab-open\">\n Open\n </TabsTrigger>\n <TabsTrigger value=\"ACCEPTED\" data-testid=\"doc-suggestions-tab-accepted\">\n Accepted\n </TabsTrigger>\n <TabsTrigger value=\"REJECTED\" data-testid=\"doc-suggestions-tab-rejected\">\n Rejected\n </TabsTrigger>\n </TabsList>\n {(['OPEN', 'ACCEPTED', 'REJECTED'] as const).map((status) => (\n <TabsContent key={status} value={status} className=\"mt-3\">\n <SuggestionList\n docId={docId}\n status={status}\n docTitle={docTitle}\n docContent={docContent}\n revisions={revisions}\n currentUserId={currentUserId}\n />\n </TabsContent>\n ))}\n </Tabs>\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export declare function DocVersionCompare({ docId, versions, }: {
2
+ docId: string;
3
+ versions: readonly {
4
+ version: number;
5
+ }[];
6
+ }): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=DocVersionCompare.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DocVersionCompare.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/docs/DocVersionCompare.tsx"],"names":[],"mappings":"AAwCA,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,SAAS;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC1C,2CAwGA"}
@@ -0,0 +1,105 @@
1
+ import { ErrorLine as e } from "../controls/ConfirmButton.js";
2
+ import { isForbidden as t } from "../utils/graphqlErrors.js";
3
+ import { DocDiffHunks as n } from "./DocDiffView.js";
4
+ import { useState as r } from "react";
5
+ import { GitCompare as i } from "lucide-react";
6
+ import { gql as a } from "@apollo/client";
7
+ import { useLazyQuery as o } from "@apollo/client/react";
8
+ import { Fragment as s, jsx as c, jsxs as l } from "react/jsx-runtime";
9
+ import { Button as u } from "@burdenoff/fe-libs/ui";
10
+ //#region src/planmagnet/docs/DocVersionCompare.tsx
11
+ var d = a`
12
+ query PlanmagnetDocRevisionDiffPanel($docId: ID!, $fromVersion: Int!, $toVersion: Int) {
13
+ planmagnetDocRevisionDiff(docId: $docId, fromVersion: $fromVersion, toVersion: $toVersion) {
14
+ docId
15
+ fromVersion
16
+ toVersion
17
+ truncated
18
+ hunks {
19
+ kind
20
+ text
21
+ }
22
+ }
23
+ }
24
+ `;
25
+ function f({ docId: a, versions: f }) {
26
+ let p = [...f].sort((e, t) => t.version - e.version), [m, h] = r(""), [g, _] = r(""), [v, y] = o(d), b = () => {
27
+ m && v({ variables: {
28
+ docId: a,
29
+ fromVersion: Number(m),
30
+ toVersion: g ? Number(g) : void 0
31
+ } });
32
+ }, x = y.data?.planmagnetDocRevisionDiff ?? null;
33
+ return /* @__PURE__ */ l("div", {
34
+ className: "space-y-3 border-t border-border-default pt-3",
35
+ "data-testid": "doc-version-compare",
36
+ children: [/* @__PURE__ */ l("p", {
37
+ className: "flex items-center gap-1 text-xs font-medium text-muted-foreground",
38
+ children: [/* @__PURE__ */ c(i, {
39
+ className: "h-3.5 w-3.5",
40
+ "aria-hidden": "true"
41
+ }), "Compare versions"]
42
+ }), p.length < 2 ? /* @__PURE__ */ c("p", {
43
+ className: "text-xs text-muted-foreground",
44
+ children: "Needs at least two versions to compare."
45
+ }) : /* @__PURE__ */ l(s, { children: [
46
+ /* @__PURE__ */ l("div", {
47
+ className: "grid grid-cols-2 gap-2",
48
+ children: [/* @__PURE__ */ l("div", { children: [/* @__PURE__ */ c("label", {
49
+ htmlFor: "doc-diff-from",
50
+ className: "text-xs font-medium",
51
+ children: "From"
52
+ }), /* @__PURE__ */ l("select", {
53
+ id: "doc-diff-from",
54
+ className: "mt-1 w-full rounded-md border border-input bg-background px-2 py-1.5 text-sm",
55
+ value: m,
56
+ onChange: (e) => {
57
+ let t = e.target.value;
58
+ h(t), _((e) => e === t ? "" : e);
59
+ },
60
+ children: [/* @__PURE__ */ c("option", {
61
+ value: "",
62
+ children: "Select a version"
63
+ }), p.map((e) => /* @__PURE__ */ l("option", {
64
+ value: e.version,
65
+ children: ["Version ", e.version]
66
+ }, e.version))]
67
+ })] }), /* @__PURE__ */ l("div", { children: [/* @__PURE__ */ c("label", {
68
+ htmlFor: "doc-diff-to",
69
+ className: "text-xs font-medium",
70
+ children: "To"
71
+ }), /* @__PURE__ */ l("select", {
72
+ id: "doc-diff-to",
73
+ className: "mt-1 w-full rounded-md border border-input bg-background px-2 py-1.5 text-sm",
74
+ value: g,
75
+ onChange: (e) => _(e.target.value),
76
+ children: [/* @__PURE__ */ c("option", {
77
+ value: "",
78
+ children: "Current"
79
+ }), p.filter((e) => String(e.version) !== m).map((e) => /* @__PURE__ */ l("option", {
80
+ value: e.version,
81
+ children: ["Version ", e.version]
82
+ }, e.version))]
83
+ })] })]
84
+ }),
85
+ /* @__PURE__ */ c(u, {
86
+ size: "sm",
87
+ variant: "outline",
88
+ onClick: b,
89
+ disabled: !m || y.loading,
90
+ "data-testid": "doc-version-compare-run",
91
+ children: y.loading ? "Comparing…" : "Compare"
92
+ }),
93
+ y.error && /* @__PURE__ */ c(e, { message: t(y.error) ? "Your role cannot compare versions of this doc." : `The comparison could not be loaded: ${y.error.message}` }),
94
+ x && /* @__PURE__ */ c(n, {
95
+ hunks: x.hunks,
96
+ truncated: x.truncated,
97
+ testId: "doc-version-diff"
98
+ })
99
+ ] })]
100
+ });
101
+ }
102
+ //#endregion
103
+ export { f as DocVersionCompare };
104
+
105
+ //# sourceMappingURL=DocVersionCompare.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DocVersionCompare.js","names":[],"sources":["../../../src/planmagnet/docs/DocVersionCompare.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useLazyQuery } from '@apollo/client/react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { GitCompare } from 'lucide-react';\nimport { ErrorLine } from '../controls/ConfirmButton';\nimport { isForbidden } from '../utils/graphqlErrors';\nimport { DocDiffHunks, type DocDiffHunkLike } from './DocDiffView';\n\n/**\n * Version compare in the doc's history panel (CWF-18 §3): pick two SAVED versions and diff them through the\n * server's `planmagnetDocRevisionDiff` (line-based, bounded, same hunk shape `DocDiffHunks` already renders for\n * a suggestion preview). \"To\" left as \"Current\" diffs against the doc's live version, matching the query's own\n * \"omit toVersion = against current\" contract.\n */\n\nconst DOC_REVISION_DIFF = gql`\n query PlanmagnetDocRevisionDiffPanel($docId: ID!, $fromVersion: Int!, $toVersion: Int) {\n planmagnetDocRevisionDiff(docId: $docId, fromVersion: $fromVersion, toVersion: $toVersion) {\n docId\n fromVersion\n toVersion\n truncated\n hunks {\n kind\n text\n }\n }\n }\n`;\n\ninterface RevisionDiffData {\n planmagnetDocRevisionDiff: {\n fromVersion: number;\n toVersion: number;\n truncated: boolean;\n hunks: DocDiffHunkLike[];\n };\n}\n\nexport function DocVersionCompare({\n docId,\n versions,\n}: {\n docId: string;\n versions: readonly { version: number }[];\n}) {\n const sorted = [...versions].sort((a, b) => b.version - a.version);\n const [fromVersion, setFromVersion] = useState('');\n const [toVersion, setToVersion] = useState('');\n const [runDiff, diffState] = useLazyQuery<RevisionDiffData>(DOC_REVISION_DIFF);\n\n const compare = () => {\n if (!fromVersion) return;\n void runDiff({\n variables: {\n docId,\n fromVersion: Number(fromVersion),\n toVersion: toVersion ? Number(toVersion) : undefined,\n },\n });\n };\n\n const diff = diffState.data?.planmagnetDocRevisionDiff ?? null;\n\n return (\n <div\n className=\"space-y-3 border-t border-border-default pt-3\"\n data-testid=\"doc-version-compare\"\n >\n <p className=\"flex items-center gap-1 text-xs font-medium text-muted-foreground\">\n <GitCompare className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n Compare versions\n </p>\n {sorted.length < 2 ? (\n <p className=\"text-xs text-muted-foreground\">Needs at least two versions to compare.</p>\n ) : (\n <>\n <div className=\"grid grid-cols-2 gap-2\">\n <div>\n <label htmlFor=\"doc-diff-from\" className=\"text-xs font-medium\">\n From\n </label>\n <select\n id=\"doc-diff-from\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-2 py-1.5 text-sm\"\n value={fromVersion}\n onChange={(e) => {\n const next = e.target.value;\n setFromVersion(next);\n // Same \"To\" cannot equal \"From\" rule RoadmapVersionsPanel's compare section uses: picking a\n // version as \"From\" that is already selected as \"To\" silently falls the visible \"To\" back to\n // \"Current\" — reset the state to match what a Compare click would actually send.\n setToVersion((prevTo) => (prevTo === next ? '' : prevTo));\n }}\n >\n <option value=\"\">Select a version</option>\n {sorted.map((v) => (\n <option key={v.version} value={v.version}>\n Version {v.version}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"doc-diff-to\" className=\"text-xs font-medium\">\n To\n </label>\n <select\n id=\"doc-diff-to\"\n className=\"mt-1 w-full rounded-md border border-input bg-background px-2 py-1.5 text-sm\"\n value={toVersion}\n onChange={(e) => setToVersion(e.target.value)}\n >\n <option value=\"\">Current</option>\n {sorted\n .filter((v) => String(v.version) !== fromVersion)\n .map((v) => (\n <option key={v.version} value={v.version}>\n Version {v.version}\n </option>\n ))}\n </select>\n </div>\n </div>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={compare}\n disabled={!fromVersion || diffState.loading}\n data-testid=\"doc-version-compare-run\"\n >\n {diffState.loading ? 'Comparing…' : 'Compare'}\n </Button>\n {diffState.error && (\n <ErrorLine\n message={\n isForbidden(diffState.error)\n ? 'Your role cannot compare versions of this doc.'\n : `The comparison could not be loaded: ${diffState.error.message}`\n }\n />\n )}\n {diff && (\n <DocDiffHunks hunks={diff.hunks} truncated={diff.truncated} testId=\"doc-version-diff\" />\n )}\n </>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AAgBA,IAAM,IAAoB,CAAG;;;;;;;;;;;;;;AAwB7B,SAAgB,EAAkB,EAChC,UACA,eAIC;CACD,IAAM,IAAS,CAAC,GAAG,EAAS,CAAC,MAAM,GAAG,MAAM,EAAE,UAAU,EAAE,QAAQ,EAC5D,CAAC,GAAa,KAAkB,EAAS,GAAG,EAC5C,CAAC,GAAW,KAAgB,EAAS,GAAG,EACxC,CAAC,GAAS,KAAa,EAA+B,EAAkB,EAExE,UAAgB;AACf,OACA,EAAQ,EACX,WAAW;GACT;GACA,aAAa,OAAO,EAAY;GAChC,WAAW,IAAY,OAAO,EAAU,GAAG,KAAA;GAC5C,EACF,CAAC;IAGE,IAAO,EAAU,MAAM,6BAA6B;AAE1D,QACE,kBAAC,OAAD;EACE,WAAU;EACV,eAAY;YAFd,CAIE,kBAAC,KAAD;GAAG,WAAU;aAAb,CACE,kBAAC,GAAD;IAAY,WAAU;IAAc,eAAY;IAAS,CAAA,EAAA,mBAEvD;MACH,EAAO,SAAS,IACf,kBAAC,KAAD;GAAG,WAAU;aAAgC;GAA2C,CAAA,GAExF,kBAAA,GAAA,EAAA,UAAA;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;KAAO,SAAQ;KAAgB,WAAU;eAAsB;KAEvD,CAAA,EACR,kBAAC,UAAD;KACE,IAAG;KACH,WAAU;KACV,OAAO;KACP,WAAW,MAAM;MACf,IAAM,IAAO,EAAE,OAAO;AAKtB,MAJA,EAAe,EAAK,EAIpB,GAAc,MAAY,MAAW,IAAO,KAAK,EAAQ;;eAV7D,CAaE,kBAAC,UAAD;MAAQ,OAAM;gBAAG;MAAyB,CAAA,EACzC,EAAO,KAAK,MACX,kBAAC,UAAD;MAAwB,OAAO,EAAE;gBAAjC,CAA0C,YAC/B,EAAE,QACJ;QAFI,EAAE,QAEN,CACT,CACK;OACL,EAAA,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;KAAO,SAAQ;KAAc,WAAU;eAAsB;KAErD,CAAA,EACR,kBAAC,UAAD;KACE,IAAG;KACH,WAAU;KACV,OAAO;KACP,WAAW,MAAM,EAAa,EAAE,OAAO,MAAM;eAJ/C,CAME,kBAAC,UAAD;MAAQ,OAAM;gBAAG;MAAgB,CAAA,EAChC,EACE,QAAQ,MAAM,OAAO,EAAE,QAAQ,KAAK,EAAY,CAChD,KAAK,MACJ,kBAAC,UAAD;MAAwB,OAAO,EAAE;gBAAjC,CAA0C,YAC/B,EAAE,QACJ;QAFI,EAAE,QAEN,CACT,CACG;OACL,EAAA,CAAA,CACF;;GACN,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACR,SAAS;IACT,UAAU,CAAC,KAAe,EAAU;IACpC,eAAY;cAEX,EAAU,UAAU,eAAe;IAC7B,CAAA;GACR,EAAU,SACT,kBAAC,GAAD,EACE,SACE,EAAY,EAAU,MAAM,GACxB,mDACA,uCAAuC,EAAU,MAAM,WAE7D,CAAA;GAEH,KACC,kBAAC,GAAD;IAAc,OAAO,EAAK;IAAO,WAAW,EAAK;IAAW,QAAO;IAAqB,CAAA;GAEzF,EAAA,CAAA,CAED"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Live presence + typing on one target (CWF-24): who else is here, and who of them is typing right now.
3
+ *
4
+ * Heartbeat every 15 s while the tab is visible (paused while hidden — no point telling the server "still here"
5
+ * from a backgrounded tab, and it would only cost quota for nothing anyone can see), an explicit
6
+ * `planmagnetPresenceLeave` on unmount or when the target itself changes (closing this view, not merely going
7
+ * idle — same distinction the mutation's own doc comment draws), and a typing heartbeat `notifyTyping()` callers
8
+ * fire on every keystroke, throttled here to at most one per 5 s so a whole paragraph does not turn into a burst
9
+ * of writes (the server also rate-limits to one heartbeat per 5s per actor+target, so this throttle is a client-
10
+ * side courtesy, not the actual gate). There is no explicit "stopped typing" call — `PlanmagnetPresenceEntry.typing`
11
+ * already goes stale 8 s after the last `typing: true` heartbeat (see that field's own schema doc comment), so a
12
+ * caller that stops typing simply stops calling `notifyTyping()` and the flag lapses on its own.
13
+ *
14
+ * The live list comes from `planmagnetPresence`, kept fresh by `planmagnetOnPresenceChanged` — a THIN event (kind
15
+ * + actorId only, CWF-24's own house rule against ever trusting a real-time payload as data) that triggers a
16
+ * re-read under the caller's own permissions, same discipline `useLiveRefresh` uses elsewhere in this MFE. If the
17
+ * subscription errors (including the expected "no SSE transport" case in a standalone/Storybook build — see
18
+ * `useLiveRefresh`'s note on `PlanMagnetProvider`), this degrades to a 30 s poll rather than going silent.
19
+ *
20
+ * Heartbeat/leave/typing calls are fire-and-forget: presence is a nice-to-have, never something a refusal here
21
+ * should surface as a page error, so failures are swallowed rather than thrown (mirroring how `useLiveRefresh`
22
+ * treats a degraded subscription as expected, not exceptional).
23
+ */
24
+ export type PlanmagnetPresenceTargetType = 'WORK_ITEM' | 'DOC' | 'ROADMAP' | 'PROJECT';
25
+ export interface PresenceOther {
26
+ actorId: string;
27
+ lastSeenAt: string;
28
+ typing: boolean;
29
+ }
30
+ export interface UsePresenceResult {
31
+ /** Other actors present on this target within the last 30 s — the caller (self) is already excluded. */
32
+ others: PresenceOther[];
33
+ /** Ids of OTHER actors currently typing (fresh within the last 8 s, per the server) — self excluded. */
34
+ typingActorIds: string[];
35
+ /** Call on every keystroke in a composer for this target; throttled internally to one heartbeat per 5 s. */
36
+ notifyTyping: () => void;
37
+ }
38
+ export declare function usePresence(targetType: PlanmagnetPresenceTargetType, targetId: string | null | undefined): UsePresenceResult;
39
+ //# sourceMappingURL=usePresence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePresence.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/hooks/usePresence.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,MAAM,4BAA4B,GAAG,WAAW,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,CAAC;AAEvF,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,wGAAwG;IACxG,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,wGAAwG;IACxG,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,4GAA4G;IAC5G,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B;AA2DD,wBAAgB,WAAW,CACzB,UAAU,EAAE,4BAA4B,EACxC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAClC,iBAAiB,CAwGnB"}