@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,98 @@
1
+ import { cmsApi } from './api.js';
2
+ /**
3
+ * Frozen tuple of every action the server accepts. Used both as a type-safe
4
+ * iteration source (bubble menu pills, slash-command registration) and by
5
+ * the runtime `isCoauthorAction` guard so callers don't drift from the
6
+ * server's allowlist.
7
+ */
8
+ export const COAUTHOR_ACTIONS = ['rewrite', 'expand', 'compress', 'proofread'];
9
+ const ACTION_LABELS = {
10
+ rewrite: 'Rewrite',
11
+ expand: 'Expand',
12
+ compress: 'Compress',
13
+ proofread: 'Proofread',
14
+ };
15
+ const ACTION_DESCRIPTIONS = {
16
+ rewrite: 'Rewrite the selection in a new tone or style',
17
+ expand: 'Expand the selection with more detail',
18
+ compress: 'Shorten the selection while preserving meaning',
19
+ proofread: 'Correct grammar, spelling, and punctuation',
20
+ };
21
+ /**
22
+ * Hard client-side ceiling that matches `AI_CONTEXT_MAX_CHARS` in cms-core.
23
+ * Catching this here avoids a round-trip just to get a 400 back when a user
24
+ * pastes a giant block into the bubble menu.
25
+ */
26
+ export const COAUTHOR_TEXT_MAX_CHARS = 50_000;
27
+ export function coauthorActionLabel(action) {
28
+ return ACTION_LABELS[action];
29
+ }
30
+ export function coauthorActionDescription(action) {
31
+ return ACTION_DESCRIPTIONS[action];
32
+ }
33
+ export function isCoauthorAction(value) {
34
+ return typeof value === 'string' && COAUTHOR_ACTIONS.includes(value);
35
+ }
36
+ /**
37
+ * Run a text-transformation micro-action against `/ai/coauthor`.
38
+ *
39
+ * Returns a discriminated outcome so callers can switch on `outcome.ok`
40
+ * without a try/catch — the underlying `cmsApi` already turns every
41
+ * non-2xx and every network failure into a structured error envelope.
42
+ *
43
+ * Pre-flight validation is intentionally cheap (whitespace + length) so the
44
+ * server stays the single source of truth for richer auth/rate-limit logic.
45
+ */
46
+ export async function coauthorText(action, text, opts = {}) {
47
+ if (!isCoauthorAction(action)) {
48
+ return { ok: false, error: `Unknown action "${String(action)}"`, status: 0 };
49
+ }
50
+ if (typeof text !== 'string' || text.trim().length === 0) {
51
+ return { ok: false, error: 'No text provided', status: 0 };
52
+ }
53
+ if (text.length > COAUTHOR_TEXT_MAX_CHARS) {
54
+ return {
55
+ ok: false,
56
+ error: `Text exceeds ${COAUTHOR_TEXT_MAX_CHARS.toLocaleString()} character limit`,
57
+ status: 0,
58
+ };
59
+ }
60
+ const body = { action, text };
61
+ if (opts.style !== undefined)
62
+ body.style = opts.style;
63
+ if (opts.tone !== undefined)
64
+ body.tone = opts.tone;
65
+ if (opts.instructions !== undefined)
66
+ body.instructions = opts.instructions;
67
+ if (opts.targetLength !== undefined)
68
+ body.targetLength = opts.targetLength;
69
+ const res = await cmsApi('/ai/coauthor', {
70
+ method: 'POST',
71
+ body: JSON.stringify(body),
72
+ });
73
+ if (res.error || !res.data) {
74
+ return {
75
+ ok: false,
76
+ error: res.error ?? `Request failed (${res.status})`,
77
+ status: res.status,
78
+ };
79
+ }
80
+ if (typeof res.data.text !== 'string') {
81
+ return {
82
+ ok: false,
83
+ error: 'Malformed response from /ai/coauthor (missing text)',
84
+ status: res.status,
85
+ };
86
+ }
87
+ return {
88
+ ok: true,
89
+ result: {
90
+ action: res.data.action ?? action,
91
+ text: res.data.text,
92
+ changes: action === 'proofread' && Array.isArray(res.data.changes)
93
+ ? res.data.changes
94
+ : undefined,
95
+ },
96
+ };
97
+ }
98
+ //# sourceMappingURL=coauthor-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coauthor-client.js","sourceRoot":"","sources":["../../src/lib/coauthor-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Browser-side Yjs collaboration provider for the TipTap editor.
3
+ *
4
+ * Wraps `y-websocket`'s `WebsocketProvider` with sensible defaults and a
5
+ * narrow public API. The wire protocol is byte-compatible with the
6
+ * cms-core realtime gateway shipped in PR #106, so the standard
7
+ * `WebsocketProvider` connects without any custom client code.
8
+ *
9
+ * Auth flows through the browser's cookie jar — `cms-admin` already uses
10
+ * cookie-based sessions, and the WebSocket upgrade request carries those
11
+ * cookies automatically against same-origin gateways. For cross-origin
12
+ * deployments, attach a query-string token via the `params` option.
13
+ */
14
+ import { Awareness } from 'y-protocols/awareness';
15
+ import { WebsocketProvider } from 'y-websocket';
16
+ import { IndexeddbPersistence } from 'y-indexeddb';
17
+ import * as Y from 'yjs';
18
+ /** Connection status reported via `onStatus` and surfaced in the editor UI. */
19
+ export type CollaborationStatus = 'connecting' | 'connected' | 'disconnected';
20
+ /**
21
+ * Offline-draft persistence status. Slice 6 layers a local IndexedDB
22
+ * mirror on top of the y-websocket transport so edits made while
23
+ * disconnected aren't lost. The lifecycle is:
24
+ *
25
+ * - `unsupported` — `indexedDB` is unavailable (Safari private mode,
26
+ * SSR, jsdom-without-fake-indexeddb).
27
+ * - `pending` — provider is being constructed; no local state yet.
28
+ * - `loading` — IndexedDB is replaying the last saved snapshot into
29
+ * the Y.Doc.
30
+ * - `ready` — the local store is initialised and every change is
31
+ * persisted locally before the WebSocket pushes it upstream.
32
+ * - `error` — IndexedDB threw; the editor still works in-memory but
33
+ * refresh will lose pending edits.
34
+ */
35
+ export type OfflinePersistenceStatus = 'unsupported' | 'pending' | 'loading' | 'ready' | 'error';
36
+ /** Per-user presence info that becomes the local awareness state. */
37
+ export interface CollaborationUser {
38
+ /** Display name (used for cursor labels and the presence chip strip). */
39
+ name: string;
40
+ /** CSS color value for the user's cursor / chip background. */
41
+ color: string;
42
+ /** Optional avatar URL (32-square recommended). Rendered if present. */
43
+ avatarUrl?: string;
44
+ /** Stable user id from the cms-core session (used as a dedup key). */
45
+ userId?: string;
46
+ }
47
+ export interface CollaborationOptions {
48
+ /** Logical document id (becomes the y-websocket "room name"). */
49
+ documentId: string;
50
+ /**
51
+ * Base URL of the realtime gateway, e.g. `wss://your.app/api/cms/realtime/sync`.
52
+ * The provider appends `/${documentId}` automatically (y-websocket convention).
53
+ * Use `ws://` for local dev.
54
+ */
55
+ url: string;
56
+ /** Presence info for the local user. */
57
+ user: CollaborationUser;
58
+ /**
59
+ * Extra query-string parameters (e.g. an auth token for cross-origin
60
+ * deployments). Forwarded to `WebsocketProvider`'s `params` option.
61
+ */
62
+ params?: Record<string, string>;
63
+ /** Called on every connection-state transition. */
64
+ onStatus?: (status: CollaborationStatus) => void;
65
+ /** Called on connection-level errors (e.g. 4401 auth rejection). */
66
+ onError?: (err: Error) => void;
67
+ /**
68
+ * If `false`, the helper does not connect on construction. Useful for
69
+ * tests and for editors that want a `Y.Doc` without network traffic.
70
+ * Defaults to `true`.
71
+ */
72
+ connect?: boolean;
73
+ /**
74
+ * Offline-draft persistence. When `true` (default), the provider
75
+ * mounts a `y-indexeddb` mirror so edits made while the WebSocket is
76
+ * disconnected survive a refresh. Pass `false` to skip the mirror
77
+ * (e.g. on shared kiosk machines where you don't want drafts in
78
+ * IndexedDB).
79
+ */
80
+ offline?: boolean;
81
+ /** Called on every offline-persistence status transition. */
82
+ onOfflineStatus?: (status: OfflinePersistenceStatus) => void;
83
+ }
84
+ export interface CollaborationContext {
85
+ /** Shared Y.Doc — bind this to the TipTap `Collaboration` extension. */
86
+ ydoc: Y.Doc;
87
+ /** The underlying y-websocket provider. */
88
+ provider: WebsocketProvider;
89
+ /** Awareness instance — bind to `CollaborationCursor` and the presence strip. */
90
+ awareness: Awareness;
91
+ /**
92
+ * Local IndexedDB mirror, when `offline` is enabled and the runtime
93
+ * supports it. `null` when persistence is disabled or unsupported
94
+ * (e.g. SSR / Safari private browsing).
95
+ */
96
+ offlinePersistence: IndexeddbPersistence | null;
97
+ /** Latest offline-persistence status. Mirrors `onOfflineStatus`. */
98
+ offlineStatus: OfflinePersistenceStatus;
99
+ /** Stop the provider, evict awareness, and destroy the Y.Doc. */
100
+ destroy: () => void;
101
+ }
102
+ /** Optional dependency-injection seam used by tests to avoid real WebSockets. */
103
+ export interface CollaborationDeps {
104
+ WebsocketProviderCtor?: typeof WebsocketProvider;
105
+ /**
106
+ * Override the IndexedDB persistence constructor (test seam). The
107
+ * default uses `y-indexeddb` which requires a browser-like
108
+ * `indexedDB` global; tests inject a no-op stub.
109
+ */
110
+ IndexeddbPersistenceCtor?: typeof IndexeddbPersistence;
111
+ /**
112
+ * Override the IndexedDB feature check. Defaults to inspecting
113
+ * `globalThis.indexedDB`. Tests inject `() => true` to exercise the
114
+ * persistence path under happy-dom.
115
+ */
116
+ isIndexedDbSupported?: () => boolean;
117
+ }
118
+ /**
119
+ * Open a collaboration session for `documentId`. Must be called in a browser
120
+ * context (jsdom is fine for tests when a fake provider is injected).
121
+ *
122
+ * The returned `ydoc` is the document the editor binds to via TipTap's
123
+ * `Collaboration` extension; `awareness` carries cursor + presence state.
124
+ * Always call `destroy()` on unmount to evict the local awareness state and
125
+ * close the socket.
126
+ */
127
+ export declare function createCollaborationProvider(opts: CollaborationOptions, deps?: CollaborationDeps): CollaborationContext;
128
+ /**
129
+ * Reduce an Awareness map into a deduplicated array of `CollaborationUser`s.
130
+ * Used by `PresenceChips` and any other UI that visualises co-editors.
131
+ */
132
+ export interface PresenceSnapshot {
133
+ /** Local user's client id (so UIs can omit themselves from the strip). */
134
+ selfClientId: number;
135
+ /** Connected users, deduplicated by `userId` when present, otherwise by clientId. */
136
+ users: Array<{
137
+ clientId: number;
138
+ user: CollaborationUser;
139
+ }>;
140
+ }
141
+ export declare function summarisePresence(awareness: Awareness): PresenceSnapshot;
142
+ //# sourceMappingURL=collaboration-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collaboration-provider.d.ts","sourceRoot":"","sources":["../../src/lib/collaboration-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,KAAK,CAAC,MAAM,KAAK,CAAA;AAExB,+EAA+E;AAC/E,MAAM,MAAM,mBAAmB,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,CAAA;AAE7E;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,wBAAwB,GAAG,aAAa,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;AAEhG,qEAAqE;AACrE,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAA;IACZ,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAA;IACb,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAA;IACX,wCAAwC;IACxC,IAAI,EAAE,iBAAiB,CAAA;IACvB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,mDAAmD;IACnD,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAA;IAChD,oEAAoE;IACpE,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;IAC9B;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,6DAA6D;IAC7D,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,wBAAwB,KAAK,IAAI,CAAA;CAC7D;AAED,MAAM,WAAW,oBAAoB;IACnC,wEAAwE;IACxE,IAAI,EAAE,CAAC,CAAC,GAAG,CAAA;IACX,2CAA2C;IAC3C,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,iFAAiF;IACjF,SAAS,EAAE,SAAS,CAAA;IACpB;;;;OAIG;IACH,kBAAkB,EAAE,oBAAoB,GAAG,IAAI,CAAA;IAC/C,oEAAoE;IACpE,aAAa,EAAE,wBAAwB,CAAA;IACvC,iEAAiE;IACjE,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED,iFAAiF;AACjF,MAAM,WAAW,iBAAiB;IAChC,qBAAqB,CAAC,EAAE,OAAO,iBAAiB,CAAA;IAChD;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,OAAO,oBAAoB,CAAA;IACtD;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,OAAO,CAAA;CACrC;AAED;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,oBAAoB,EAC1B,IAAI,GAAE,iBAAsB,GAC3B,oBAAoB,CAiItB;AAgBD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAA;IACpB,qFAAqF;IACrF,KAAK,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,iBAAiB,CAAA;KAAE,CAAC,CAAA;CAC5D;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,GAAG,gBAAgB,CAaxE"}
@@ -0,0 +1,168 @@
1
+ import { WebsocketProvider } from 'y-websocket';
2
+ import { IndexeddbPersistence } from 'y-indexeddb';
3
+ import * as Y from 'yjs';
4
+ /**
5
+ * Open a collaboration session for `documentId`. Must be called in a browser
6
+ * context (jsdom is fine for tests when a fake provider is injected).
7
+ *
8
+ * The returned `ydoc` is the document the editor binds to via TipTap's
9
+ * `Collaboration` extension; `awareness` carries cursor + presence state.
10
+ * Always call `destroy()` on unmount to evict the local awareness state and
11
+ * close the socket.
12
+ */
13
+ export function createCollaborationProvider(opts, deps = {}) {
14
+ if (typeof globalThis === 'undefined' ||
15
+ typeof globalThis.window === 'undefined') {
16
+ // Note: jsdom defines `window`, so this only trips on bare-Node SSR.
17
+ throw new Error('createCollaborationProvider can only be called in a browser context (window is undefined)');
18
+ }
19
+ if (!opts.documentId || !opts.url) {
20
+ throw new Error('createCollaborationProvider requires both documentId and url');
21
+ }
22
+ const Ctor = deps.WebsocketProviderCtor ?? WebsocketProvider;
23
+ const ydoc = new Y.Doc();
24
+ const provider = new Ctor(opts.url, opts.documentId, ydoc, {
25
+ connect: opts.connect ?? true,
26
+ params: opts.params,
27
+ });
28
+ // Layer y-indexeddb on top of the y-websocket transport for offline
29
+ // drafts. Both providers share the same Y.Doc, so they automatically
30
+ // converge: IndexedDB hydrates the doc on load, the websocket pushes
31
+ // pending updates to the gateway on reconnect.
32
+ const offlineEnabled = opts.offline !== false;
33
+ const offlineCheck = deps.isIndexedDbSupported ?? defaultIndexedDbSupportCheck;
34
+ let offlineStatus = 'pending';
35
+ const setOfflineStatus = (next) => {
36
+ offlineStatus = next;
37
+ opts.onOfflineStatus?.(next);
38
+ };
39
+ let offlinePersistence = null;
40
+ if (!offlineEnabled) {
41
+ setOfflineStatus('unsupported');
42
+ }
43
+ else if (!offlineCheck()) {
44
+ setOfflineStatus('unsupported');
45
+ }
46
+ else {
47
+ setOfflineStatus('loading');
48
+ const PersistenceCtor = deps.IndexeddbPersistenceCtor ?? IndexeddbPersistence;
49
+ try {
50
+ offlinePersistence = new PersistenceCtor(
51
+ // Namespace the IndexedDB key by documentId so multiple docs in
52
+ // the same admin session don't collide.
53
+ `actuate-cms:${opts.documentId}`, ydoc);
54
+ // `whenSynced` is the canonical "snapshot replay finished" hook
55
+ // from y-indexeddb (https://github.com/yjs/y-indexeddb).
56
+ offlinePersistence.whenSynced
57
+ .then(() => setOfflineStatus('ready'))
58
+ .catch((err) => {
59
+ opts.onError?.(err instanceof Error ? err : new Error('Offline persistence failed'));
60
+ setOfflineStatus('error');
61
+ });
62
+ }
63
+ catch (err) {
64
+ opts.onError?.(err instanceof Error ? err : new Error('Offline persistence failed'));
65
+ setOfflineStatus('error');
66
+ offlinePersistence = null;
67
+ }
68
+ }
69
+ // Seed local awareness with the user's presence info. The
70
+ // `CollaborationCursor` TipTap extension reads `user.name` + `user.color`
71
+ // out of awareness state and renders cursors accordingly.
72
+ provider.awareness.setLocalState({
73
+ user: {
74
+ name: opts.user.name,
75
+ color: opts.user.color,
76
+ avatarUrl: opts.user.avatarUrl,
77
+ userId: opts.user.userId,
78
+ },
79
+ });
80
+ if (opts.onStatus) {
81
+ const handler = (event) => {
82
+ const status = normaliseStatus(event.status);
83
+ if (status)
84
+ opts.onStatus(status);
85
+ };
86
+ provider.on('status', handler);
87
+ }
88
+ if (opts.onError) {
89
+ const handler = () => {
90
+ // y-websocket emits this on socket close + on protocol errors. We
91
+ // surface a generic Error so callers can render a toast without
92
+ // sniffing y-websocket internals.
93
+ opts.onError(new Error('Realtime connection error'));
94
+ };
95
+ provider.on('connection-error', handler);
96
+ }
97
+ const destroy = () => {
98
+ try {
99
+ provider.awareness.setLocalState(null);
100
+ }
101
+ catch {
102
+ /* awareness may already be torn down */
103
+ }
104
+ try {
105
+ provider.destroy();
106
+ }
107
+ catch {
108
+ /* idempotent */
109
+ }
110
+ if (offlinePersistence) {
111
+ try {
112
+ // y-indexeddb keeps the IDB connection open until `destroy()`;
113
+ // we don't drop the stored data (that's the point of offline
114
+ // drafts), but we close the handle so the runtime can GC.
115
+ offlinePersistence.destroy();
116
+ }
117
+ catch {
118
+ /* idempotent */
119
+ }
120
+ }
121
+ try {
122
+ ydoc.destroy();
123
+ }
124
+ catch {
125
+ /* idempotent */
126
+ }
127
+ };
128
+ return {
129
+ ydoc,
130
+ provider,
131
+ awareness: provider.awareness,
132
+ offlinePersistence,
133
+ get offlineStatus() {
134
+ return offlineStatus;
135
+ },
136
+ destroy,
137
+ };
138
+ }
139
+ function defaultIndexedDbSupportCheck() {
140
+ return (typeof globalThis !== 'undefined' &&
141
+ typeof globalThis.indexedDB !== 'undefined');
142
+ }
143
+ function normaliseStatus(value) {
144
+ if (value === 'connected')
145
+ return 'connected';
146
+ if (value === 'connecting')
147
+ return 'connecting';
148
+ if (value === 'disconnected')
149
+ return 'disconnected';
150
+ return null;
151
+ }
152
+ export function summarisePresence(awareness) {
153
+ const seenUserIds = new Set();
154
+ const users = [];
155
+ for (const [clientId, state] of awareness.getStates()) {
156
+ const user = state?.user;
157
+ if (!user || typeof user.name !== 'string')
158
+ continue;
159
+ if (user.userId) {
160
+ if (seenUserIds.has(user.userId))
161
+ continue;
162
+ seenUserIds.add(user.userId);
163
+ }
164
+ users.push({ clientId, user });
165
+ }
166
+ return { selfClientId: awareness.clientID, users };
167
+ }
168
+ //# sourceMappingURL=collaboration-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collaboration-provider.js","sourceRoot":"","sources":["../../src/lib/collaboration-provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Comment anchor helpers — convert between Yjs **relative positions** and
3
+ * the Base64 wire format the comments REST API speaks.
4
+ *
5
+ * Why relative positions: storing absolute offsets (e.g. char 42–58) is
6
+ * fatal under concurrent editing because someone inserting text before
7
+ * char 42 silently shifts the meaning of every saved anchor. Yjs ships
8
+ * `createRelativePositionFromTypeIndex` /
9
+ * `createAbsolutePositionFromRelativePosition` for exactly this case —
10
+ * they identify a slot in the CRDT graph that survives concurrent edits.
11
+ *
12
+ * The wire shape is `{ from: string; to: string }` where each end is the
13
+ * Base64 of `Y.encodeRelativePosition(rel)`. The slice 4 server treats
14
+ * the field as opaque; this module is the only place in the workspace
15
+ * that interprets it.
16
+ *
17
+ * ## Choosing the right Yjs type to anchor against
18
+ *
19
+ * `createRelativePositionFromTypeIndex(type, index)` interprets `index`
20
+ * differently depending on the type:
21
+ *
22
+ * - `Y.XmlText` / `Y.Text` — index is the character offset within the
23
+ * text. **This is what comment anchors use** because character offsets
24
+ * are the natural unit for a text selection.
25
+ * - `Y.XmlFragment` / `Y.XmlElement` — index counts *children*. Useful
26
+ * for "before/after this child" anchors but not what we want here.
27
+ *
28
+ * Slice 5's TipTap binding maps a PM selection to the underlying
29
+ * `Y.XmlText` instance + character offset before calling `encodeAnchor`,
30
+ * and inverts that mapping after `decodeAnchor` returns. The y-prosemirror
31
+ * plugin keeps a `mapping` for this conversion; consumers that aren't
32
+ * using y-prosemirror can talk to the Y types directly.
33
+ */
34
+ import * as Y from 'yjs';
35
+ import type { CommentAnchor } from './comments-client.js';
36
+ export interface CreateAnchorInput {
37
+ /** Live Yjs document the editor is bound to. */
38
+ doc: Y.Doc;
39
+ /**
40
+ * Yjs text type the selection lives in. Almost always a `Y.XmlText`
41
+ * for TipTap; we keep the bound a little loose so plain `Y.Text` works
42
+ * for non-rich-text editors.
43
+ */
44
+ yType: Y.XmlText | Y.Text;
45
+ /** Character offset (inclusive) of the selection start within `yType`. */
46
+ from: number;
47
+ /** Character offset (exclusive) of the selection end within `yType`. */
48
+ to: number;
49
+ }
50
+ /**
51
+ * Build a comment anchor from an editor selection. Returns `null` when
52
+ * the range is empty or inverted — the caller should not save anchors
53
+ * that would resolve to nothing.
54
+ */
55
+ export declare function encodeAnchor(input: CreateAnchorInput): CommentAnchor | null;
56
+ export interface ResolveAnchorInput {
57
+ doc: Y.Doc;
58
+ anchor: CommentAnchor;
59
+ }
60
+ export interface ResolvedAnchor {
61
+ /**
62
+ * The text type the relative position landed on. Comparing identity
63
+ * (`resolved.yType === editorBoundText`) lets the caller decide
64
+ * whether a comment is still attached to the expected text node.
65
+ */
66
+ yType: Y.AbstractType<unknown>;
67
+ /** Character offset of the saved selection start, after concurrent edits. */
68
+ from: number;
69
+ /** Character offset of the saved selection end. */
70
+ to: number;
71
+ }
72
+ /**
73
+ * Resolve a stored anchor back to live editor positions. Returns `null`
74
+ * when:
75
+ *
76
+ * - the anchor bytes are not valid base64 / not a Yjs relative position,
77
+ * - the relative position cannot be mapped (the anchored content was
78
+ * deleted entirely), or
79
+ * - the two ends collapse to the same offset (text between them was
80
+ * deleted, effectively detaching the comment).
81
+ *
82
+ * Slice 5 surfaces detached comments in the side panel with a small
83
+ * "anchor lost" badge so editors can still resolve / discuss them.
84
+ */
85
+ export declare function decodeAnchor(input: ResolveAnchorInput): ResolvedAnchor | null;
86
+ //# sourceMappingURL=comment-anchor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment-anchor.d.ts","sourceRoot":"","sources":["../../src/lib/comment-anchor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,CAAC,MAAM,KAAK,CAAA;AAExB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAEzD,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,GAAG,EAAE,CAAC,CAAC,GAAG,CAAA;IACV;;;;OAIG;IACH,KAAK,EAAE,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,IAAI,CAAA;IACzB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAA;IACZ,wEAAwE;IACxE,EAAE,EAAE,MAAM,CAAA;CACX;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,aAAa,GAAG,IAAI,CAe3E;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAA;IACV,MAAM,EAAE,aAAa,CAAA;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,KAAK,EAAE,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;IAC9B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAA;IACZ,mDAAmD;IACnD,EAAE,EAAE,MAAM,CAAA;CACX;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,kBAAkB,GAAG,cAAc,GAAG,IAAI,CAmC7E"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Comment anchor helpers — convert between Yjs **relative positions** and
3
+ * the Base64 wire format the comments REST API speaks.
4
+ *
5
+ * Why relative positions: storing absolute offsets (e.g. char 42–58) is
6
+ * fatal under concurrent editing because someone inserting text before
7
+ * char 42 silently shifts the meaning of every saved anchor. Yjs ships
8
+ * `createRelativePositionFromTypeIndex` /
9
+ * `createAbsolutePositionFromRelativePosition` for exactly this case —
10
+ * they identify a slot in the CRDT graph that survives concurrent edits.
11
+ *
12
+ * The wire shape is `{ from: string; to: string }` where each end is the
13
+ * Base64 of `Y.encodeRelativePosition(rel)`. The slice 4 server treats
14
+ * the field as opaque; this module is the only place in the workspace
15
+ * that interprets it.
16
+ *
17
+ * ## Choosing the right Yjs type to anchor against
18
+ *
19
+ * `createRelativePositionFromTypeIndex(type, index)` interprets `index`
20
+ * differently depending on the type:
21
+ *
22
+ * - `Y.XmlText` / `Y.Text` — index is the character offset within the
23
+ * text. **This is what comment anchors use** because character offsets
24
+ * are the natural unit for a text selection.
25
+ * - `Y.XmlFragment` / `Y.XmlElement` — index counts *children*. Useful
26
+ * for "before/after this child" anchors but not what we want here.
27
+ *
28
+ * Slice 5's TipTap binding maps a PM selection to the underlying
29
+ * `Y.XmlText` instance + character offset before calling `encodeAnchor`,
30
+ * and inverts that mapping after `decodeAnchor` returns. The y-prosemirror
31
+ * plugin keeps a `mapping` for this conversion; consumers that aren't
32
+ * using y-prosemirror can talk to the Y types directly.
33
+ */
34
+ import * as Y from 'yjs';
35
+ /**
36
+ * Build a comment anchor from an editor selection. Returns `null` when
37
+ * the range is empty or inverted — the caller should not save anchors
38
+ * that would resolve to nothing.
39
+ */
40
+ export function encodeAnchor(input) {
41
+ if (input.from >= input.to)
42
+ return null;
43
+ if (input.from < 0)
44
+ return null;
45
+ // `createRelativePositionFromTypeIndex` clamps `index` to `[0, type.length]`
46
+ // internally, but we surface an explicit failure when the *intent* is
47
+ // out of bounds so the caller can flag a stale selection.
48
+ if (input.to > input.yType.length)
49
+ return null;
50
+ const relFrom = Y.createRelativePositionFromTypeIndex(input.yType, input.from);
51
+ const relTo = Y.createRelativePositionFromTypeIndex(input.yType, input.to);
52
+ return {
53
+ from: encodeBase64(Y.encodeRelativePosition(relFrom)),
54
+ to: encodeBase64(Y.encodeRelativePosition(relTo)),
55
+ };
56
+ }
57
+ /**
58
+ * Resolve a stored anchor back to live editor positions. Returns `null`
59
+ * when:
60
+ *
61
+ * - the anchor bytes are not valid base64 / not a Yjs relative position,
62
+ * - the relative position cannot be mapped (the anchored content was
63
+ * deleted entirely), or
64
+ * - the two ends collapse to the same offset (text between them was
65
+ * deleted, effectively detaching the comment).
66
+ *
67
+ * Slice 5 surfaces detached comments in the side panel with a small
68
+ * "anchor lost" badge so editors can still resolve / discuss them.
69
+ */
70
+ export function decodeAnchor(input) {
71
+ let relFromBytes;
72
+ let relToBytes;
73
+ try {
74
+ relFromBytes = decodeBase64(input.anchor.from);
75
+ relToBytes = decodeBase64(input.anchor.to);
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ let relFrom;
81
+ let relTo;
82
+ try {
83
+ relFrom = Y.decodeRelativePosition(relFromBytes);
84
+ relTo = Y.decodeRelativePosition(relToBytes);
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ const absFrom = Y.createAbsolutePositionFromRelativePosition(relFrom, input.doc);
90
+ const absTo = Y.createAbsolutePositionFromRelativePosition(relTo, input.doc);
91
+ if (absFrom === null || absTo === null)
92
+ return null;
93
+ // Both ends must land on the same text node — otherwise the user's
94
+ // original selection has been split across nodes and we cannot paint
95
+ // a clean highlight without further work in slice 5.
96
+ if (absFrom.type !== absTo.type)
97
+ return null;
98
+ if (absFrom.index >= absTo.index)
99
+ return null;
100
+ return {
101
+ yType: absFrom.type,
102
+ from: absFrom.index,
103
+ to: absTo.index,
104
+ };
105
+ }
106
+ /**
107
+ * Base64 encode without depending on Node's `Buffer` (the admin bundle
108
+ * is browser-first). We hand-roll the encode because both `btoa` and
109
+ * `Buffer` have ergonomic mis-fits for `Uint8Array` payloads.
110
+ */
111
+ function encodeBase64(bytes) {
112
+ if (typeof btoa === 'function') {
113
+ let binary = '';
114
+ for (let i = 0; i < bytes.byteLength; i++) {
115
+ binary += String.fromCharCode(bytes[i]);
116
+ }
117
+ return btoa(binary);
118
+ }
119
+ const BufferCtor = globalThis.Buffer;
120
+ if (BufferCtor)
121
+ return BufferCtor.from(bytes).toString('base64');
122
+ throw new Error('No base64 encoder available');
123
+ }
124
+ function decodeBase64(s) {
125
+ if (typeof atob === 'function') {
126
+ const binary = atob(s);
127
+ const out = new Uint8Array(binary.length);
128
+ for (let i = 0; i < binary.length; i++) {
129
+ out[i] = binary.charCodeAt(i);
130
+ }
131
+ return out;
132
+ }
133
+ const BufferCtor = globalThis
134
+ .Buffer;
135
+ if (BufferCtor)
136
+ return new Uint8Array(BufferCtor.from(s, 'base64'));
137
+ throw new Error('No base64 decoder available');
138
+ }
139
+ //# sourceMappingURL=comment-anchor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment-anchor.js","sourceRoot":"","sources":["../../src/lib/comment-anchor.ts"],"names":[],"mappings":"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"}