@actuate-media/cms-admin 0.12.0 → 0.13.0

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 (157) hide show
  1. package/dist/__tests__/components/comment-mark.test.d.ts +2 -0
  2. package/dist/__tests__/components/comment-mark.test.d.ts.map +1 -0
  3. package/dist/__tests__/components/comment-mark.test.js +89 -0
  4. package/dist/__tests__/components/comment-mark.test.js.map +1 -0
  5. package/dist/__tests__/components/comment-side-panel.test.d.ts +2 -0
  6. package/dist/__tests__/components/comment-side-panel.test.d.ts.map +1 -0
  7. package/dist/__tests__/components/comment-side-panel.test.js +291 -0
  8. package/dist/__tests__/components/comment-side-panel.test.js.map +1 -0
  9. package/dist/__tests__/components/notification-bell.test.d.ts +2 -0
  10. package/dist/__tests__/components/notification-bell.test.d.ts.map +1 -0
  11. package/dist/__tests__/components/notification-bell.test.js +346 -0
  12. package/dist/__tests__/components/notification-bell.test.js.map +1 -0
  13. package/dist/__tests__/components/offline-status.test.d.ts +2 -0
  14. package/dist/__tests__/components/offline-status.test.d.ts.map +1 -0
  15. package/dist/__tests__/components/offline-status.test.js +56 -0
  16. package/dist/__tests__/components/offline-status.test.js.map +1 -0
  17. package/dist/__tests__/components/presence-chips-helpers.test.d.ts +2 -0
  18. package/dist/__tests__/components/presence-chips-helpers.test.d.ts.map +1 -0
  19. package/dist/__tests__/components/presence-chips-helpers.test.js +35 -0
  20. package/dist/__tests__/components/presence-chips-helpers.test.js.map +1 -0
  21. package/dist/__tests__/lib/active-editor.test.d.ts +2 -0
  22. package/dist/__tests__/lib/active-editor.test.d.ts.map +1 -0
  23. package/dist/__tests__/lib/active-editor.test.js +57 -0
  24. package/dist/__tests__/lib/active-editor.test.js.map +1 -0
  25. package/dist/__tests__/lib/coauthor-client.test.d.ts +2 -0
  26. package/dist/__tests__/lib/coauthor-client.test.d.ts.map +1 -0
  27. package/dist/__tests__/lib/coauthor-client.test.js +173 -0
  28. package/dist/__tests__/lib/coauthor-client.test.js.map +1 -0
  29. package/dist/__tests__/lib/collaboration-provider.test.d.ts +2 -0
  30. package/dist/__tests__/lib/collaboration-provider.test.d.ts.map +1 -0
  31. package/dist/__tests__/lib/collaboration-provider.test.js +335 -0
  32. package/dist/__tests__/lib/collaboration-provider.test.js.map +1 -0
  33. package/dist/__tests__/lib/comment-anchor.test.d.ts +2 -0
  34. package/dist/__tests__/lib/comment-anchor.test.d.ts.map +1 -0
  35. package/dist/__tests__/lib/comment-anchor.test.js +173 -0
  36. package/dist/__tests__/lib/comment-anchor.test.js.map +1 -0
  37. package/dist/__tests__/lib/comments-client.test.d.ts +2 -0
  38. package/dist/__tests__/lib/comments-client.test.d.ts.map +1 -0
  39. package/dist/__tests__/lib/comments-client.test.js +202 -0
  40. package/dist/__tests__/lib/comments-client.test.js.map +1 -0
  41. package/dist/__tests__/lib/notifications-client.test.d.ts +2 -0
  42. package/dist/__tests__/lib/notifications-client.test.d.ts.map +1 -0
  43. package/dist/__tests__/lib/notifications-client.test.js +283 -0
  44. package/dist/__tests__/lib/notifications-client.test.js.map +1 -0
  45. package/dist/__tests__/lib/slash-command.test.d.ts +2 -0
  46. package/dist/__tests__/lib/slash-command.test.d.ts.map +1 -0
  47. package/dist/__tests__/lib/slash-command.test.js +123 -0
  48. package/dist/__tests__/lib/slash-command.test.js.map +1 -0
  49. package/dist/actuate-admin.css +1 -1
  50. package/dist/components/AICoauthorBubbleMenu.d.ts +18 -0
  51. package/dist/components/AICoauthorBubbleMenu.d.ts.map +1 -0
  52. package/dist/components/AICoauthorBubbleMenu.js +113 -0
  53. package/dist/components/AICoauthorBubbleMenu.js.map +1 -0
  54. package/dist/components/CoauthorResultPopover.d.ts +28 -0
  55. package/dist/components/CoauthorResultPopover.d.ts.map +1 -0
  56. package/dist/components/CoauthorResultPopover.js +42 -0
  57. package/dist/components/CoauthorResultPopover.js.map +1 -0
  58. package/dist/components/CommandPalette.d.ts +1 -1
  59. package/dist/components/CommandPalette.d.ts.map +1 -1
  60. package/dist/components/CommandPalette.js +138 -40
  61. package/dist/components/CommandPalette.js.map +1 -1
  62. package/dist/components/CommentMark.d.ts +54 -0
  63. package/dist/components/CommentMark.d.ts.map +1 -0
  64. package/dist/components/CommentMark.js +104 -0
  65. package/dist/components/CommentMark.js.map +1 -0
  66. package/dist/components/CommentSidePanel.d.ts +86 -0
  67. package/dist/components/CommentSidePanel.d.ts.map +1 -0
  68. package/dist/components/CommentSidePanel.js +165 -0
  69. package/dist/components/CommentSidePanel.js.map +1 -0
  70. package/dist/components/LivePreview.d.ts.map +1 -1
  71. package/dist/components/LivePreview.js +80 -16
  72. package/dist/components/LivePreview.js.map +1 -1
  73. package/dist/components/NotificationBell.d.ts +66 -0
  74. package/dist/components/NotificationBell.d.ts.map +1 -0
  75. package/dist/components/NotificationBell.js +142 -0
  76. package/dist/components/NotificationBell.js.map +1 -0
  77. package/dist/components/OfflineStatus.d.ts +39 -0
  78. package/dist/components/OfflineStatus.d.ts.map +1 -0
  79. package/dist/components/OfflineStatus.js +47 -0
  80. package/dist/components/OfflineStatus.js.map +1 -0
  81. package/dist/components/PresenceChips.d.ts +31 -0
  82. package/dist/components/PresenceChips.d.ts.map +1 -0
  83. package/dist/components/PresenceChips.js +58 -0
  84. package/dist/components/PresenceChips.js.map +1 -0
  85. package/dist/components/TipTapEditor.d.ts +61 -1
  86. package/dist/components/TipTapEditor.d.ts.map +1 -1
  87. package/dist/components/TipTapEditor.js +211 -8
  88. package/dist/components/TipTapEditor.js.map +1 -1
  89. package/dist/components/ui/Badge.d.ts +1 -1
  90. package/dist/index.d.ts +8 -0
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +4 -0
  93. package/dist/index.js.map +1 -1
  94. package/dist/lib/active-editor.d.ts +6 -0
  95. package/dist/lib/active-editor.d.ts.map +1 -0
  96. package/dist/lib/active-editor.js +35 -0
  97. package/dist/lib/active-editor.js.map +1 -0
  98. package/dist/lib/api.d.ts +8 -0
  99. package/dist/lib/api.d.ts.map +1 -1
  100. package/dist/lib/api.js +10 -0
  101. package/dist/lib/api.js.map +1 -1
  102. package/dist/lib/coauthor-client.d.ts +72 -0
  103. package/dist/lib/coauthor-client.d.ts.map +1 -0
  104. package/dist/lib/coauthor-client.js +98 -0
  105. package/dist/lib/coauthor-client.js.map +1 -0
  106. package/dist/lib/collaboration-provider.d.ts +142 -0
  107. package/dist/lib/collaboration-provider.d.ts.map +1 -0
  108. package/dist/lib/collaboration-provider.js +168 -0
  109. package/dist/lib/collaboration-provider.js.map +1 -0
  110. package/dist/lib/comment-anchor.d.ts +86 -0
  111. package/dist/lib/comment-anchor.d.ts.map +1 -0
  112. package/dist/lib/comment-anchor.js +139 -0
  113. package/dist/lib/comment-anchor.js.map +1 -0
  114. package/dist/lib/comments-client.d.ts +106 -0
  115. package/dist/lib/comments-client.d.ts.map +1 -0
  116. package/dist/lib/comments-client.js +138 -0
  117. package/dist/lib/comments-client.js.map +1 -0
  118. package/dist/lib/notifications-client.d.ts +121 -0
  119. package/dist/lib/notifications-client.d.ts.map +1 -0
  120. package/dist/lib/notifications-client.js +173 -0
  121. package/dist/lib/notifications-client.js.map +1 -0
  122. package/dist/lib/slash-command.d.ts +39 -0
  123. package/dist/lib/slash-command.d.ts.map +1 -0
  124. package/dist/lib/slash-command.js +50 -0
  125. package/dist/lib/slash-command.js.map +1 -0
  126. package/package.json +12 -3
  127. package/src/__tests__/components/comment-mark.test.ts +99 -0
  128. package/src/__tests__/components/comment-side-panel.test.tsx +343 -0
  129. package/src/__tests__/components/notification-bell.test.tsx +379 -0
  130. package/src/__tests__/components/offline-status.test.tsx +66 -0
  131. package/src/__tests__/components/presence-chips-helpers.test.ts +44 -0
  132. package/src/__tests__/lib/active-editor.test.ts +70 -0
  133. package/src/__tests__/lib/coauthor-client.test.ts +213 -0
  134. package/src/__tests__/lib/collaboration-provider.test.ts +425 -0
  135. package/src/__tests__/lib/comment-anchor.test.ts +188 -0
  136. package/src/__tests__/lib/comments-client.test.ts +239 -0
  137. package/src/__tests__/lib/notifications-client.test.ts +339 -0
  138. package/src/__tests__/lib/slash-command.test.ts +146 -0
  139. package/src/components/AICoauthorBubbleMenu.tsx +187 -0
  140. package/src/components/CoauthorResultPopover.tsx +193 -0
  141. package/src/components/CommandPalette.tsx +397 -209
  142. package/src/components/CommentMark.ts +138 -0
  143. package/src/components/CommentSidePanel.tsx +521 -0
  144. package/src/components/LivePreview.tsx +117 -11
  145. package/src/components/NotificationBell.tsx +314 -0
  146. package/src/components/OfflineStatus.tsx +90 -0
  147. package/src/components/PresenceChips.tsx +150 -0
  148. package/src/components/TipTapEditor.tsx +310 -12
  149. package/src/index.ts +20 -0
  150. package/src/lib/active-editor.ts +39 -0
  151. package/src/lib/api.ts +11 -0
  152. package/src/lib/coauthor-client.ts +160 -0
  153. package/src/lib/collaboration-provider.ts +306 -0
  154. package/src/lib/comment-anchor.ts +179 -0
  155. package/src/lib/comments-client.ts +208 -0
  156. package/src/lib/notifications-client.ts +292 -0
  157. package/src/lib/slash-command.ts +67 -0
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Comments REST client — thin, typed wrapper around the
3
+ * `/documents/:id/comments` + `/comments/:id` endpoints shipped in slice 4
4
+ * of Phase 3 (see `packages/cms-core/src/realtime/comments.ts`).
5
+ *
6
+ * The client lives in `cms-admin` so the side panel, the bubble-menu
7
+ * "comment on selection" action, and any future MCP/AI hooks all share one
8
+ * request path, error model, and DTO shape.
9
+ *
10
+ * Mirrors the server contract:
11
+ * POST /documents/:documentId/comments -> { body, anchor?, parentId? }
12
+ * GET /documents/:documentId/comments?… -> { data: CommentDTO[] }
13
+ * PATCH /comments/:id -> { body }
14
+ * POST /comments/:id/resolve -> 200 with updated DTO
15
+ * POST /comments/:id/reopen -> 200 with updated DTO
16
+ * DELETE /comments/:id -> 200 with soft-deleted DTO
17
+ */
18
+ /**
19
+ * Yjs relative-position anchor returned by the server. The encoding is
20
+ * intentionally opaque on the wire (Base64 of `Y.encodeRelativePosition`);
21
+ * `lib/comment-anchor.ts` handles the round-trip when slice 5 paints a
22
+ * highlight in the editor.
23
+ */
24
+ export interface CommentAnchor {
25
+ from: string;
26
+ to: string;
27
+ }
28
+ /**
29
+ * Public DTO returned by every comments endpoint. Mirrors `CommentDTO` in
30
+ * `@actuate-media/cms-core/realtime`; we duplicate the shape here so
31
+ * `cms-admin` doesn't take a hard dependency on the server-side service
32
+ * layer (which carries Prisma types we don't want pulled into the bundle).
33
+ */
34
+ export interface Comment {
35
+ id: string;
36
+ documentId: string;
37
+ parentId: string | null;
38
+ userId: string | null;
39
+ body: string;
40
+ anchor: CommentAnchor | null;
41
+ resolvedAt: string | null;
42
+ resolvedById: string | null;
43
+ deletedAt: string | null;
44
+ createdAt: string;
45
+ updatedAt: string;
46
+ isResolved: boolean;
47
+ }
48
+ export type CommentOutcome<T> = {
49
+ ok: true;
50
+ result: T;
51
+ } | {
52
+ ok: false;
53
+ error: string;
54
+ status: number;
55
+ };
56
+ export interface CreateCommentInput {
57
+ documentId: string;
58
+ body: string;
59
+ anchor?: CommentAnchor | null;
60
+ parentId?: string | null;
61
+ }
62
+ export interface ListCommentsOptions {
63
+ /** Default: false. Resolved threads are filtered out unless requested. */
64
+ includeResolved?: boolean;
65
+ /** Default: false. Admin-only; the server rejects non-admin callers. */
66
+ includeDeleted?: boolean;
67
+ /** Default: server-side cap (100). Hard upper bound is 200. */
68
+ pageSize?: number;
69
+ }
70
+ /**
71
+ * Create a top-level comment or a reply. Pass `parentId` to thread the
72
+ * comment under an existing one — the server enforces single-level
73
+ * threading (replies cannot themselves be parents).
74
+ */
75
+ export declare function createComment(input: CreateCommentInput): Promise<CommentOutcome<Comment>>;
76
+ /**
77
+ * List comments for a document. Active comments (not resolved, not deleted)
78
+ * are returned by default; pass `includeResolved` to keep resolved threads
79
+ * in the response. The server rejects `includeDeleted` for non-admins.
80
+ */
81
+ export declare function listComments(documentId: string, opts?: ListCommentsOptions): Promise<CommentOutcome<Comment[]>>;
82
+ /** Edit a comment body. Only the author or an admin is permitted. */
83
+ export declare function updateComment(id: string, body: string): Promise<CommentOutcome<Comment>>;
84
+ /** Resolve a thread. Write-role or the author may resolve. */
85
+ export declare function resolveComment(id: string): Promise<CommentOutcome<Comment>>;
86
+ /** Re-open a resolved thread. Same permission as resolve. */
87
+ export declare function reopenComment(id: string): Promise<CommentOutcome<Comment>>;
88
+ /**
89
+ * Soft-delete a comment. The row is preserved so its replies don't lose
90
+ * their thread root — the server simply stops returning it from the list
91
+ * endpoint unless an admin asks for `includeDeleted`.
92
+ */
93
+ export declare function deleteComment(id: string): Promise<CommentOutcome<Comment>>;
94
+ /**
95
+ * Sort comments into thread groups suitable for rendering. Roots are
96
+ * returned in chronological order; replies are ordered the same way and
97
+ * attached to their root. Replies whose root is missing (e.g. the root was
98
+ * filtered out by `includeResolved=false`) are dropped — the UI can fetch
99
+ * them explicitly if it needs to render orphaned replies.
100
+ */
101
+ export interface CommentThread {
102
+ root: Comment;
103
+ replies: Comment[];
104
+ }
105
+ export declare function groupCommentsIntoThreads(comments: readonly Comment[]): CommentThread[];
106
+ //# sourceMappingURL=comments-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comments-client.d.ts","sourceRoot":"","sources":["../../src/lib/comments-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;CACX;AAED;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,OAAO,CAAA;CACpB;AAED,MAAM,MAAM,cAAc,CAAC,CAAC,IACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,CAAC,CAAA;CAAE,GACvB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhD,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,aAAa,GAAG,IAAI,CAAA;IAC7B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,mBAAmB;IAClC,0EAA0E;IAC1E,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,wEAAwE;IACxE,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAsBD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAiB/F;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,mBAAwB,GAC7B,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC,CAAC,CAOpC;AAED,qEAAqE;AACrE,wBAAsB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAU9F;AAED,8DAA8D;AAC9D,wBAAsB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAOjF;AAED,6DAA6D;AAC7D,wBAAsB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAOhF;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAMhF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,OAAO,EAAE,CAAA;CACnB;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,aAAa,EAAE,CAgBtF"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Comments REST client — thin, typed wrapper around the
3
+ * `/documents/:id/comments` + `/comments/:id` endpoints shipped in slice 4
4
+ * of Phase 3 (see `packages/cms-core/src/realtime/comments.ts`).
5
+ *
6
+ * The client lives in `cms-admin` so the side panel, the bubble-menu
7
+ * "comment on selection" action, and any future MCP/AI hooks all share one
8
+ * request path, error model, and DTO shape.
9
+ *
10
+ * Mirrors the server contract:
11
+ * POST /documents/:documentId/comments -> { body, anchor?, parentId? }
12
+ * GET /documents/:documentId/comments?… -> { data: CommentDTO[] }
13
+ * PATCH /comments/:id -> { body }
14
+ * POST /comments/:id/resolve -> 200 with updated DTO
15
+ * POST /comments/:id/reopen -> 200 with updated DTO
16
+ * DELETE /comments/:id -> 200 with soft-deleted DTO
17
+ */
18
+ import { cmsApi } from './api.js';
19
+ function encodeOptions(opts = {}) {
20
+ const params = new URLSearchParams();
21
+ if (opts.includeResolved)
22
+ params.set('includeResolved', 'true');
23
+ if (opts.includeDeleted)
24
+ params.set('includeDeleted', 'true');
25
+ if (opts.pageSize !== undefined)
26
+ params.set('pageSize', String(opts.pageSize));
27
+ const qs = params.toString();
28
+ return qs ? `?${qs}` : '';
29
+ }
30
+ function failure(error, status) {
31
+ return { ok: false, error, status };
32
+ }
33
+ function envelope(res) {
34
+ if (res.error || res.data === undefined) {
35
+ return failure(res.error ?? `Request failed (${res.status})`, res.status);
36
+ }
37
+ return { ok: true, result: res.data };
38
+ }
39
+ /**
40
+ * Create a top-level comment or a reply. Pass `parentId` to thread the
41
+ * comment under an existing one — the server enforces single-level
42
+ * threading (replies cannot themselves be parents).
43
+ */
44
+ export async function createComment(input) {
45
+ if (typeof input.documentId !== 'string' || input.documentId.length === 0) {
46
+ return failure('documentId is required', 0);
47
+ }
48
+ if (typeof input.body !== 'string' || input.body.trim().length === 0) {
49
+ return failure('Comment cannot be empty', 0);
50
+ }
51
+ const body = { body: input.body.trim() };
52
+ if (input.anchor)
53
+ body.anchor = input.anchor;
54
+ if (input.parentId)
55
+ body.parentId = input.parentId;
56
+ const res = await cmsApi(`/documents/${encodeURIComponent(input.documentId)}/comments`, {
57
+ method: 'POST',
58
+ body: JSON.stringify(body),
59
+ });
60
+ return envelope(res);
61
+ }
62
+ /**
63
+ * List comments for a document. Active comments (not resolved, not deleted)
64
+ * are returned by default; pass `includeResolved` to keep resolved threads
65
+ * in the response. The server rejects `includeDeleted` for non-admins.
66
+ */
67
+ export async function listComments(documentId, opts = {}) {
68
+ if (typeof documentId !== 'string' || documentId.length === 0) {
69
+ return failure('documentId is required', 0);
70
+ }
71
+ const path = `/documents/${encodeURIComponent(documentId)}/comments${encodeOptions(opts)}`;
72
+ const res = await cmsApi(path, { method: 'GET' });
73
+ return envelope(res);
74
+ }
75
+ /** Edit a comment body. Only the author or an admin is permitted. */
76
+ export async function updateComment(id, body) {
77
+ if (!id)
78
+ return failure('comment id is required', 0);
79
+ if (typeof body !== 'string' || body.trim().length === 0) {
80
+ return failure('Comment cannot be empty', 0);
81
+ }
82
+ const res = await cmsApi(`/comments/${encodeURIComponent(id)}`, {
83
+ method: 'PATCH',
84
+ body: JSON.stringify({ body: body.trim() }),
85
+ });
86
+ return envelope(res);
87
+ }
88
+ /** Resolve a thread. Write-role or the author may resolve. */
89
+ export async function resolveComment(id) {
90
+ if (!id)
91
+ return failure('comment id is required', 0);
92
+ const res = await cmsApi(`/comments/${encodeURIComponent(id)}/resolve`, {
93
+ method: 'POST',
94
+ body: '{}',
95
+ });
96
+ return envelope(res);
97
+ }
98
+ /** Re-open a resolved thread. Same permission as resolve. */
99
+ export async function reopenComment(id) {
100
+ if (!id)
101
+ return failure('comment id is required', 0);
102
+ const res = await cmsApi(`/comments/${encodeURIComponent(id)}/reopen`, {
103
+ method: 'POST',
104
+ body: '{}',
105
+ });
106
+ return envelope(res);
107
+ }
108
+ /**
109
+ * Soft-delete a comment. The row is preserved so its replies don't lose
110
+ * their thread root — the server simply stops returning it from the list
111
+ * endpoint unless an admin asks for `includeDeleted`.
112
+ */
113
+ export async function deleteComment(id) {
114
+ if (!id)
115
+ return failure('comment id is required', 0);
116
+ const res = await cmsApi(`/comments/${encodeURIComponent(id)}`, {
117
+ method: 'DELETE',
118
+ });
119
+ return envelope(res);
120
+ }
121
+ export function groupCommentsIntoThreads(comments) {
122
+ const roots = new Map();
123
+ const sorted = [...comments].sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
124
+ for (const c of sorted) {
125
+ if (c.parentId === null) {
126
+ roots.set(c.id, { root: c, replies: [] });
127
+ }
128
+ }
129
+ for (const c of sorted) {
130
+ if (c.parentId !== null) {
131
+ const root = roots.get(c.parentId);
132
+ if (root)
133
+ root.replies.push(c);
134
+ }
135
+ }
136
+ return Array.from(roots.values());
137
+ }
138
+ //# sourceMappingURL=comments-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comments-client.js","sourceRoot":"","sources":["../../src/lib/comments-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Notifications REST client — typed wrapper around the slice-6 endpoints
3
+ * (`/notifications`, `/notifications/unread-count`,
4
+ * `/notifications/:id/read`, `/notifications/read-all`).
5
+ *
6
+ * Lives in `cms-admin` so the bell, the toast layer, and any future
7
+ * background sync agent share one request path, error model, and DTO
8
+ * shape. The wire format mirrors `NotificationDTO` in
9
+ * `@actuate-media/cms-core/realtime/notifications.ts`; we duplicate the
10
+ * shape here to avoid taking a hard dependency on the server module
11
+ * (which carries Prisma types that don't belong in the admin bundle).
12
+ */
13
+ export type NotificationKind = 'comment_reply' | 'comment_resolved' | 'comment_mention';
14
+ export interface CommentReplyPayload {
15
+ kind: 'comment_reply';
16
+ actorId: string | null;
17
+ actorName: string | null;
18
+ excerpt: string;
19
+ }
20
+ export interface CommentResolvedPayload {
21
+ kind: 'comment_resolved';
22
+ actorId: string | null;
23
+ actorName: string | null;
24
+ rootExcerpt: string;
25
+ }
26
+ export interface CommentMentionPayload {
27
+ kind: 'comment_mention';
28
+ actorId: string | null;
29
+ actorName: string | null;
30
+ excerpt: string;
31
+ }
32
+ export type NotificationPayload = CommentReplyPayload | CommentResolvedPayload | CommentMentionPayload;
33
+ export interface Notification {
34
+ id: string;
35
+ userId: string;
36
+ kind: NotificationKind;
37
+ documentId: string | null;
38
+ commentId: string | null;
39
+ payload: NotificationPayload;
40
+ readAt: string | null;
41
+ createdAt: string;
42
+ }
43
+ export type NotificationOutcome<T> = {
44
+ ok: true;
45
+ result: T;
46
+ } | {
47
+ ok: false;
48
+ error: string;
49
+ status: number;
50
+ };
51
+ export interface ListNotificationsOptions {
52
+ /** Default: false. When true the response includes already-read rows. */
53
+ includeRead?: boolean;
54
+ /** Default: server-side cap. Hard upper bound is 200. */
55
+ pageSize?: number;
56
+ }
57
+ export declare function listNotifications(opts?: ListNotificationsOptions): Promise<NotificationOutcome<Notification[]>>;
58
+ export declare function unreadCount(): Promise<NotificationOutcome<number>>;
59
+ export declare function markNotificationRead(id: string): Promise<NotificationOutcome<Notification>>;
60
+ export declare function markAllNotificationsRead(): Promise<NotificationOutcome<number>>;
61
+ /**
62
+ * Subscribe to the per-user SSE push channel that ships notifications
63
+ * the instant they're persisted server-side. Replaces the 30-second
64
+ * poll loop in the notification bell.
65
+ *
66
+ * The browser's native `EventSource` handles reconnection for us — on
67
+ * a server-initiated close (the stream caps at ~4 minutes by design),
68
+ * the connection re-opens automatically. We DO NOT abort the poll
69
+ * fallback in callers: SSE is a latency-win, the poll is what keeps
70
+ * multi-node deployments correct (each node only pushes to its own
71
+ * subscribers).
72
+ *
73
+ * Returns a `close()` function that callers MUST invoke on unmount —
74
+ * otherwise the browser keeps the connection open and we leak event
75
+ * listeners.
76
+ *
77
+ * `EventSource` itself isn't available in the JSDOM test environment;
78
+ * callers can inject a `factory` to substitute a test double. In
79
+ * production we fall back to the global `EventSource` when no factory
80
+ * is supplied.
81
+ */
82
+ export interface NotificationStreamOptions {
83
+ /** Called for every `event: notification` frame. */
84
+ onNotification: (n: Notification) => void;
85
+ /** Called once on the `event: ready` frame. */
86
+ onReady?: (payload: {
87
+ userId: string;
88
+ }) => void;
89
+ /** Called on connection errors. Useful for falling back to the poll loop. */
90
+ onError?: (event: Event) => void;
91
+ /**
92
+ * Optional factory so unit tests can inject a fake `EventSource`,
93
+ * or a null sentinel when the environment lacks one (SSR, JSDOM
94
+ * without a polyfill). Production code can rely on the default
95
+ * (global `EventSource`).
96
+ */
97
+ factory?: (url: string) => EventSourceLike | null;
98
+ }
99
+ /**
100
+ * Minimal subset of the `EventSource` interface our subscriber relies
101
+ * on. Browsers ship the full DOM type; the admin only needs add/remove
102
+ * event listeners plus `close()`.
103
+ */
104
+ export interface EventSourceLike {
105
+ addEventListener: (type: string, listener: (event: MessageEvent | Event) => void) => void;
106
+ removeEventListener?: (type: string, listener: (event: MessageEvent | Event) => void) => void;
107
+ close: () => void;
108
+ }
109
+ export interface NotificationStreamHandle {
110
+ /** Tear down the EventSource and stop receiving events. Idempotent. */
111
+ close: () => void;
112
+ }
113
+ export declare function subscribeNotificationStream(opts: NotificationStreamOptions): NotificationStreamHandle;
114
+ /**
115
+ * Cheap single-line summary for the bell list — the server gives us a
116
+ * structured payload, but the dropdown wants one human-readable string
117
+ * per row. Falls back to `Notification` when an unknown kind sneaks in
118
+ * (defensive against future server-side additions).
119
+ */
120
+ export declare function notificationSummary(notification: Notification): string;
121
+ //# sourceMappingURL=notifications-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications-client.d.ts","sourceRoot":"","sources":["../../src/lib/notifications-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Notifications REST client — typed wrapper around the slice-6 endpoints
3
+ * (`/notifications`, `/notifications/unread-count`,
4
+ * `/notifications/:id/read`, `/notifications/read-all`).
5
+ *
6
+ * Lives in `cms-admin` so the bell, the toast layer, and any future
7
+ * background sync agent share one request path, error model, and DTO
8
+ * shape. The wire format mirrors `NotificationDTO` in
9
+ * `@actuate-media/cms-core/realtime/notifications.ts`; we duplicate the
10
+ * shape here to avoid taking a hard dependency on the server module
11
+ * (which carries Prisma types that don't belong in the admin bundle).
12
+ */
13
+ import { cmsApi, getApiBase } from './api.js';
14
+ function failure(error, status) {
15
+ return { ok: false, error, status };
16
+ }
17
+ function envelope(res) {
18
+ if (res.error || res.data === undefined) {
19
+ return failure(res.error ?? `Request failed (${res.status})`, res.status);
20
+ }
21
+ return { ok: true, result: res.data };
22
+ }
23
+ function buildQuery(opts = {}) {
24
+ const params = new URLSearchParams();
25
+ if (opts.includeRead)
26
+ params.set('includeRead', 'true');
27
+ if (opts.pageSize !== undefined)
28
+ params.set('pageSize', String(opts.pageSize));
29
+ const qs = params.toString();
30
+ return qs ? `?${qs}` : '';
31
+ }
32
+ export async function listNotifications(opts = {}) {
33
+ const res = await cmsApi(`/notifications${buildQuery(opts)}`, {
34
+ method: 'GET',
35
+ });
36
+ return envelope(res);
37
+ }
38
+ export async function unreadCount() {
39
+ const res = await cmsApi(`/notifications/unread-count`, {
40
+ method: 'GET',
41
+ });
42
+ if (res.error || !res.data) {
43
+ return failure(res.error ?? `Request failed (${res.status})`, res.status);
44
+ }
45
+ return { ok: true, result: res.data.count };
46
+ }
47
+ export async function markNotificationRead(id) {
48
+ if (!id)
49
+ return failure('notification id is required', 0);
50
+ const res = await cmsApi(`/notifications/${encodeURIComponent(id)}/read`, {
51
+ method: 'POST',
52
+ body: '{}',
53
+ });
54
+ return envelope(res);
55
+ }
56
+ export async function markAllNotificationsRead() {
57
+ const res = await cmsApi(`/notifications/read-all`, {
58
+ method: 'POST',
59
+ body: '{}',
60
+ });
61
+ if (res.error || !res.data) {
62
+ return failure(res.error ?? `Request failed (${res.status})`, res.status);
63
+ }
64
+ return { ok: true, result: res.data.count };
65
+ }
66
+ export function subscribeNotificationStream(opts) {
67
+ // The `cmsApi()` helper handles base-path / origin resolution for
68
+ // fetch calls, but EventSource takes a raw URL. We mirror its
69
+ // logic locally — `/api/cms/notifications/stream` is correct for
70
+ // both the local dev server and any production deploy that mounts
71
+ // the CMS API at the default prefix. Custom prefixes are handled by
72
+ // the consumer override (factory).
73
+ // Resolve against `setApiBase()` so a consumer mounting the CMS at
74
+ // a non-default prefix (e.g. `/cms-api`) still hits the right SSE
75
+ // endpoint — `EventSource` doesn't go through `cmsApi()`, so we
76
+ // can't rely on its url-construction logic.
77
+ const url = `${getApiBase()}/notifications/stream`;
78
+ const make = opts.factory ?? defaultEventSourceFactory;
79
+ const source = make(url);
80
+ if (!source) {
81
+ // No EventSource available (SSR? JSDOM without polyfill? Or the
82
+ // injected factory deliberately returns null in tests). Return a
83
+ // no-op handle so callers can compose without conditional logic.
84
+ return { close: () => { } };
85
+ }
86
+ let closed = false;
87
+ const onReady = (event) => {
88
+ if (!opts.onReady)
89
+ return;
90
+ const data = parseEventData(event);
91
+ if (data && typeof data.userId === 'string') {
92
+ opts.onReady({ userId: data.userId });
93
+ }
94
+ };
95
+ const onNotification = (event) => {
96
+ const data = parseEventData(event);
97
+ if (data && isNotification(data)) {
98
+ opts.onNotification(data);
99
+ }
100
+ };
101
+ const onError = (event) => {
102
+ opts.onError?.(event);
103
+ };
104
+ source.addEventListener('ready', onReady);
105
+ source.addEventListener('notification', onNotification);
106
+ source.addEventListener('error', onError);
107
+ return {
108
+ close: () => {
109
+ if (closed)
110
+ return;
111
+ closed = true;
112
+ source.removeEventListener?.('ready', onReady);
113
+ source.removeEventListener?.('notification', onNotification);
114
+ source.removeEventListener?.('error', onError);
115
+ source.close();
116
+ },
117
+ };
118
+ }
119
+ function defaultEventSourceFactory(url) {
120
+ if (typeof EventSource === 'undefined')
121
+ return null;
122
+ // `withCredentials: true` so the admin session cookie travels with
123
+ // the connection upgrade — every notification stream requires auth
124
+ // and the same-origin cookie is the only credential we trust here.
125
+ return new EventSource(url, { withCredentials: true });
126
+ }
127
+ function parseEventData(event) {
128
+ // EventSource emits MessageEvents with `data: string`. Plain
129
+ // `Event` (e.g. the `error` event) has no payload; ignore.
130
+ const data = event.data;
131
+ if (typeof data !== 'string')
132
+ return null;
133
+ try {
134
+ return JSON.parse(data);
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
140
+ function isNotification(value) {
141
+ if (!value || typeof value !== 'object')
142
+ return false;
143
+ const v = value;
144
+ // Defensive: don't trust the wire blindly — verify the discriminant
145
+ // properties our consumers will read. A drift in the server-side
146
+ // wire format must surface as "missing fields" in the test suite,
147
+ // not as runtime errors deep in the bell's render path.
148
+ return (typeof v.id === 'string' &&
149
+ typeof v.userId === 'string' &&
150
+ typeof v.createdAt === 'string' &&
151
+ !!v.payload &&
152
+ typeof v.payload.kind === 'string');
153
+ }
154
+ /**
155
+ * Cheap single-line summary for the bell list — the server gives us a
156
+ * structured payload, but the dropdown wants one human-readable string
157
+ * per row. Falls back to `Notification` when an unknown kind sneaks in
158
+ * (defensive against future server-side additions).
159
+ */
160
+ export function notificationSummary(notification) {
161
+ const actor = notification.payload.actorName ?? 'Someone';
162
+ switch (notification.payload.kind) {
163
+ case 'comment_reply':
164
+ return `${actor} replied: "${notification.payload.excerpt}"`;
165
+ case 'comment_resolved':
166
+ return `${actor} resolved your thread: "${notification.payload.rootExcerpt}"`;
167
+ case 'comment_mention':
168
+ return `${actor} mentioned you: "${notification.payload.excerpt}"`;
169
+ default:
170
+ return 'Notification';
171
+ }
172
+ }
173
+ //# sourceMappingURL=notifications-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications-client.js","sourceRoot":"","sources":["../../src/lib/notifications-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import { type CoauthorAction } from './coauthor-client.js';
2
+ /**
3
+ * Cmd+K slash-command parsing for the AI co-author.
4
+ *
5
+ * Recognized syntax (case-insensitive, leading whitespace tolerated):
6
+ *
7
+ * /ai <action> [text...]
8
+ *
9
+ * `<action>` is one of `COAUTHOR_ACTIONS`. `text` is the payload handed to
10
+ * `/ai/coauthor`; it may be empty while the user is still typing.
11
+ *
12
+ * The parser is pure so we can unit-test every edge case (case folding,
13
+ * trailing whitespace, partial input, unknown actions) without spinning up
14
+ * `cmdk` or the rest of the palette.
15
+ */
16
+ export interface AiSlashCommand {
17
+ action: CoauthorAction;
18
+ text: string;
19
+ }
20
+ /**
21
+ * Returns the matched `{ action, text }` when `input` is a complete
22
+ * slash command (`/ai rewrite ...`), or `null` otherwise. `text` may be
23
+ * empty when the user has typed `/ai rewrite` with no payload yet.
24
+ */
25
+ export declare function parseAiSlashCommand(input: string): AiSlashCommand | null;
26
+ /**
27
+ * Returns true when `input` looks like the start of an AI slash command
28
+ * — even when the action isn't typed yet (`/`, `/a`, `/ai`, `/ai re`).
29
+ * The palette uses this to switch into "AI mode" (hide nav results,
30
+ * show the four action suggestions).
31
+ */
32
+ export declare function isAiSlashCommandInput(input: string): boolean;
33
+ /**
34
+ * Ordered list of actions to show in AI mode. When the user has typed a
35
+ * valid command, that command's action is hoisted to the top so it gets
36
+ * the default keyboard focus (and Enter runs it).
37
+ */
38
+ export declare function orderedAiActions(input: string): CoauthorAction[];
39
+ //# sourceMappingURL=slash-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slash-command.d.ts","sourceRoot":"","sources":["../../src/lib/slash-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE9F;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,cAAc,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;CACb;AAID;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAYxE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAM5D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAIhE"}
@@ -0,0 +1,50 @@
1
+ import { COAUTHOR_ACTIONS, isCoauthorAction } from './coauthor-client.js';
2
+ const SLASH_HEAD = /^\s*\/\s*ai(?:\s+|$)/i;
3
+ /**
4
+ * Returns the matched `{ action, text }` when `input` is a complete
5
+ * slash command (`/ai rewrite ...`), or `null` otherwise. `text` may be
6
+ * empty when the user has typed `/ai rewrite` with no payload yet.
7
+ */
8
+ export function parseAiSlashCommand(input) {
9
+ if (typeof input !== 'string')
10
+ return null;
11
+ if (!SLASH_HEAD.test(input))
12
+ return null;
13
+ const after = input.replace(SLASH_HEAD, '');
14
+ if (after.length === 0)
15
+ return null;
16
+ const firstWhitespace = after.search(/\s/);
17
+ const head = (firstWhitespace === -1 ? after : after.slice(0, firstWhitespace)).toLowerCase();
18
+ if (!isCoauthorAction(head))
19
+ return null;
20
+ const tail = firstWhitespace === -1 ? '' : after.slice(firstWhitespace + 1);
21
+ return { action: head, text: tail.trim() };
22
+ }
23
+ /**
24
+ * Returns true when `input` looks like the start of an AI slash command
25
+ * — even when the action isn't typed yet (`/`, `/a`, `/ai`, `/ai re`).
26
+ * The palette uses this to switch into "AI mode" (hide nav results,
27
+ * show the four action suggestions).
28
+ */
29
+ export function isAiSlashCommandInput(input) {
30
+ if (typeof input !== 'string')
31
+ return false;
32
+ const trimmed = input.trim();
33
+ if (trimmed.length === 0)
34
+ return false;
35
+ if (trimmed === '/')
36
+ return true;
37
+ return /^\/\s*a(i\b|i?[a-z]*|)?(\s.*)?$/i.test(trimmed);
38
+ }
39
+ /**
40
+ * Ordered list of actions to show in AI mode. When the user has typed a
41
+ * valid command, that command's action is hoisted to the top so it gets
42
+ * the default keyboard focus (and Enter runs it).
43
+ */
44
+ export function orderedAiActions(input) {
45
+ const parsed = parseAiSlashCommand(input);
46
+ if (!parsed)
47
+ return [...COAUTHOR_ACTIONS];
48
+ return [parsed.action, ...COAUTHOR_ACTIONS.filter((a) => a !== parsed.action)];
49
+ }
50
+ //# sourceMappingURL=slash-command.js.map