deepspace 0.3.9 → 0.3.11

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 (120) hide show
  1. package/dist/cli.js +96 -18
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.d.ts +160 -1
  4. package/dist/index.js +283 -126
  5. package/dist/index.js.map +1 -1
  6. package/dist/server.d.ts +172 -2
  7. package/dist/server.js +581 -1
  8. package/dist/server.js.map +1 -1
  9. package/dist/worker.d.ts +252 -2
  10. package/dist/worker.js +588 -1
  11. package/dist/worker.js.map +1 -1
  12. package/features/admin-page/feature.json +29 -0
  13. package/features/admin-page/src/AdminPage.tsx +292 -0
  14. package/features/admin-page/src/admin-schema.ts +20 -0
  15. package/features/ai-chat/feature.json +41 -0
  16. package/features/ai-chat/src/AiChatPage.tsx +692 -0
  17. package/features/ai-chat/src/ChatPanel.tsx +1098 -0
  18. package/features/ai-chat/src/ai-chat-schema.ts +11 -0
  19. package/features/canvas/feature.json +45 -0
  20. package/features/canvas/src/CanvasEditorPage.tsx +87 -0
  21. package/features/canvas/src/CanvasListPage.tsx +202 -0
  22. package/features/canvas/src/canvas-schema.ts +33 -0
  23. package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
  24. package/features/canvas/src/components/CanvasView.tsx +369 -0
  25. package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
  26. package/features/cron/feature.json +33 -0
  27. package/features/cron/src/CronLogPage.tsx +100 -0
  28. package/features/cron/src/cron.ts +25 -0
  29. package/features/docs/feature.json +96 -0
  30. package/features/docs/src/DocsEditorPage.tsx +1012 -0
  31. package/features/docs/src/DocsListPage.tsx +951 -0
  32. package/features/docs/src/DocsOutlinePanel.tsx +68 -0
  33. package/features/docs/src/DocsPresence.tsx +138 -0
  34. package/features/docs/src/InviteDialog.tsx +336 -0
  35. package/features/docs/src/LibrarySidebar.tsx +464 -0
  36. package/features/docs/src/docs-favorites.ts +18 -0
  37. package/features/docs/src/docs-library-types.ts +15 -0
  38. package/features/docs/src/docs-schema.ts +80 -0
  39. package/features/docs/src/docs-ui.css +309 -0
  40. package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
  41. package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
  42. package/features/docs/src/editor/useDocEditor.ts +206 -0
  43. package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
  44. package/features/file-attachments/feature.json +42 -0
  45. package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
  46. package/features/file-attachments/src/attachments-constants.ts +9 -0
  47. package/features/file-attachments/src/attachments-schema.ts +45 -0
  48. package/features/file-attachments/src/useFilePreview.ts +471 -0
  49. package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
  50. package/features/file-manager/feature.json +24 -0
  51. package/features/file-manager/src/FileManagerPage.tsx +330 -0
  52. package/features/integration-test/feature.json +18 -0
  53. package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
  54. package/features/integration-test/src/integration.spec.ts +42 -0
  55. package/features/items/feature.json +33 -0
  56. package/features/items/src/ItemsPage.tsx +325 -0
  57. package/features/items/src/items-constants.ts +10 -0
  58. package/features/items/src/items-schema.ts +27 -0
  59. package/features/kanban/feature.json +18 -0
  60. package/features/kanban/src/Kanban.tsx +283 -0
  61. package/features/landing/feature.json +62 -0
  62. package/features/landing/src/LandingPage.tsx +627 -0
  63. package/features/landing/src/primitives.tsx +389 -0
  64. package/features/landing/src/sections/FAQSection.tsx +110 -0
  65. package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
  66. package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
  67. package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
  68. package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
  69. package/features/landing/src/sections/StatsSection.tsx +31 -0
  70. package/features/landing/src/sections/TeamSection.tsx +86 -0
  71. package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
  72. package/features/landing/src/sections/VideoSection.tsx +71 -0
  73. package/features/leaderboard/feature.json +33 -0
  74. package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
  75. package/features/leaderboard/src/leaderboard-constants.ts +21 -0
  76. package/features/leaderboard/src/leaderboard-schema.ts +36 -0
  77. package/features/messaging/feature.json +111 -0
  78. package/features/messaging/src/ChatMultiPage.tsx +300 -0
  79. package/features/messaging/src/ChatPage.tsx +75 -0
  80. package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
  81. package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
  82. package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
  83. package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
  84. package/features/messaging/src/components/chat/MessageList.tsx +248 -0
  85. package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
  86. package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
  87. package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
  88. package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
  89. package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
  90. package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
  91. package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
  92. package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
  93. package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
  94. package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
  95. package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
  96. package/features/messaging/src/hooks/useChatChannel.ts +64 -0
  97. package/features/messaging/src/hooks/useLongPress.ts +74 -0
  98. package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
  99. package/features/messaging/src/messaging-schema.ts +25 -0
  100. package/features/presence-test/feature.json +21 -0
  101. package/features/presence-test/src/PresenceTestPage.tsx +120 -0
  102. package/features/sidebar/feature.json +19 -0
  103. package/features/sidebar/src/AppSidebar.tsx +558 -0
  104. package/features/tasks/feature.json +33 -0
  105. package/features/tasks/src/TasksPage.tsx +598 -0
  106. package/features/tasks/src/tasks-constants.ts +43 -0
  107. package/features/tasks/src/tasks-schema.ts +56 -0
  108. package/features/testing/feature.json +49 -0
  109. package/features/testing/src/TestPage.tsx +145 -0
  110. package/features/testing/src/cpu-burn-action.ts +51 -0
  111. package/features/testing/src/subrequest-burn-action.ts +53 -0
  112. package/features/testing/src/test-schema.ts +23 -0
  113. package/features/topbar/feature.json +18 -0
  114. package/features/topbar/src/Topbar.tsx +353 -0
  115. package/features/tree/feature.json +26 -0
  116. package/features/tree/src/TreeSidebar.tsx +607 -0
  117. package/features/tree/src/tree-constants.ts +68 -0
  118. package/features/tree/src/tree-hooks.ts +258 -0
  119. package/package.json +4 -2
  120. package/scripts/add-feature.cjs +654 -0
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Docs feature UI — derives palette from the host app's design tokens (--color-* on
3
+ * shadcn/Tailwind starters, --theme-* / --ui-* when DeepSpaceThemeProvider applies a theme).
4
+ * No separate light/dark block: inherits whatever the root provides (e.g. [data-theme] changes).
5
+ */
6
+
7
+ .docs-feature-scope {
8
+ --docs-el-bg: var(--color-background, var(--theme-background, #fafafa));
9
+ --docs-el-surface: var(
10
+ --color-card,
11
+ var(--theme-library-card-bg, var(--theme-panel-bg, #ffffff))
12
+ );
13
+ --docs-el-text: var(--color-foreground, var(--theme-text, #1d1d1f));
14
+ --docs-el-muted: var(
15
+ --color-muted-foreground,
16
+ var(--theme-text-secondary, var(--theme-text-tertiary, #86868b))
17
+ );
18
+ --docs-el-accent: var(--color-primary, var(--theme-accent, var(--ui-accent, #3b82f6)));
19
+ --docs-el-line: var(--color-border, var(--theme-border, rgba(15, 23, 42, 0.12)));
20
+
21
+ --docs-el-selection: var(
22
+ --color-primary-muted,
23
+ color-mix(in srgb, var(--docs-el-accent) 22%, transparent)
24
+ );
25
+
26
+ --docs-editor-desk: var(
27
+ --color-muted,
28
+ var(
29
+ --theme-library-surface,
30
+ var(--theme-surface-solid, var(--theme-surface, var(--docs-el-bg)))
31
+ )
32
+ );
33
+ --docs-editor-paper: #ffffff;
34
+ --docs-editor-paper-text: #111111;
35
+ --docs-editor-paper-muted: #6b7280;
36
+ --docs-editor-paper-border: var(--docs-el-line);
37
+ --docs-editor-paper-shadow: var(
38
+ --shadow-card,
39
+ var(--ui-shadow-md, 0 4px 12px rgba(15, 23, 42, 0.08))
40
+ );
41
+ }
42
+
43
+ /*
44
+ * Editor text selection ("cursor highlight") is intentionally NOT themed:
45
+ * - Background uses a stable blue so the selection always reads as a
46
+ * selection — themes whose accent is yellow/red don't make selection
47
+ * look like a highlighter mark.
48
+ * - `color: inherit` is REQUIRED, not optional. Browsers (Chromium, Safari,
49
+ * Firefox) all repaint selected text in their own default contrast color
50
+ * — usually white — when you set a `::selection` background without an
51
+ * explicit `color`. That's what made selected text turn white. Forcing
52
+ * `color: inherit` keeps the underlying text in its own color (default
53
+ * black on the paper, or whatever the user picked via the text-color
54
+ * toolbar).
55
+ * - `-webkit-text-fill-color: inherit` is the same fix for Safari, which
56
+ * would otherwise still override `color` inside `::selection`.
57
+ */
58
+ .docs-paged-editor .ProseMirror ::selection,
59
+ .docs-paged-editor .ProseMirror::selection {
60
+ background-color: #6aa8f3;
61
+ color: inherit;
62
+ -webkit-text-fill-color: inherit;
63
+ }
64
+
65
+ .docs-editor-paper {
66
+ background-color: var(--docs-editor-paper);
67
+ border: 1px solid var(--docs-editor-paper-border);
68
+ box-shadow: var(--docs-editor-paper-shadow);
69
+ }
70
+
71
+ .docs-paged-editor-canvas {
72
+ background-color: var(--docs-editor-desk);
73
+ }
74
+
75
+ .docs-presence-avatar,
76
+ .docs-presence-more {
77
+ align-items: center;
78
+ background-color: color-mix(in srgb, var(--docs-el-accent) 14%, var(--docs-el-surface));
79
+ border: 2px solid var(--docs-el-surface);
80
+ border-radius: 9999px;
81
+ color: var(--docs-el-text);
82
+ display: inline-flex;
83
+ flex: none;
84
+ font-size: 11px;
85
+ font-weight: 700;
86
+ height: 30px;
87
+ justify-content: center;
88
+ line-height: 1;
89
+ overflow: hidden;
90
+ width: 30px;
91
+ }
92
+
93
+ .docs-presence-more {
94
+ cursor: pointer;
95
+ transition:
96
+ background-color 160ms ease,
97
+ color 160ms ease;
98
+ }
99
+
100
+ .docs-presence-more:hover {
101
+ background-color: color-mix(in srgb, var(--docs-el-accent) 22%, var(--docs-el-surface));
102
+ color: var(--docs-el-accent);
103
+ }
104
+
105
+ .docs-paged-page-padding {
106
+ --docs-pad-v: 0.42in;
107
+ --docs-pad-h: 0.38in;
108
+ padding: var(--docs-pad-v) var(--docs-pad-h);
109
+ }
110
+
111
+ @media (min-width: 640px) {
112
+ .docs-paged-page-padding {
113
+ --docs-pad-v: 0.55in;
114
+ --docs-pad-h: 0.65in;
115
+ }
116
+ }
117
+
118
+ @media (min-width: 1024px) {
119
+ .docs-paged-page-padding {
120
+ --docs-pad-v: 0.62in;
121
+ --docs-pad-h: 0.75in;
122
+ }
123
+ }
124
+
125
+ .docs-paged-editor-font {
126
+ font-size: 11pt;
127
+ line-height: 1.4;
128
+ color: var(--docs-editor-paper-text);
129
+ }
130
+
131
+ @media (max-width: 640px) {
132
+ .docs-paged-editor-font {
133
+ font-size: 16px;
134
+ line-height: 1.6;
135
+ }
136
+ }
137
+
138
+ /* Tiptap ProseMirror canvas */
139
+ .docs-paged-editor .ProseMirror,
140
+ .docs-paged-editor .tiptap.ProseMirror,
141
+ .docs-paged-editor .docs-tiptap-editor {
142
+ outline: none;
143
+ min-height: 1.6em;
144
+ caret-color: #000;
145
+ color: var(--docs-editor-paper-text);
146
+ }
147
+
148
+ .docs-paged-editor .ProseMirror p,
149
+ .docs-paged-editor .ProseMirror li,
150
+ .docs-paged-editor .ProseMirror h1,
151
+ .docs-paged-editor .ProseMirror h2,
152
+ .docs-paged-editor .ProseMirror h3,
153
+ .docs-paged-editor .ProseMirror h4,
154
+ .docs-paged-editor .ProseMirror h5,
155
+ .docs-paged-editor .ProseMirror h6 {
156
+ margin: 0 0 0.45em;
157
+ }
158
+
159
+ .docs-paged-editor .ProseMirror h1 {
160
+ font-size: 1.9em;
161
+ font-weight: 700;
162
+ line-height: 1.2;
163
+ margin-top: 0.2em;
164
+ }
165
+ .docs-paged-editor .ProseMirror h2 {
166
+ font-size: 1.5em;
167
+ font-weight: 700;
168
+ line-height: 1.25;
169
+ margin-top: 0.15em;
170
+ }
171
+ .docs-paged-editor .ProseMirror h3 {
172
+ font-size: 1.25em;
173
+ font-weight: 600;
174
+ line-height: 1.3;
175
+ }
176
+
177
+ .docs-paged-editor .ProseMirror blockquote {
178
+ border-left: 3px solid var(--docs-el-line);
179
+ margin: 0.5em 0;
180
+ padding: 0.1em 0.85em;
181
+ color: var(--docs-editor-paper-muted);
182
+ }
183
+
184
+ .docs-paged-editor .ProseMirror ul,
185
+ .docs-paged-editor .ProseMirror ol {
186
+ padding-left: 1.4em;
187
+ margin: 0 0 0.45em;
188
+ }
189
+
190
+ .docs-paged-editor .ProseMirror ul[data-type='taskList'] {
191
+ list-style: none;
192
+ padding: 0;
193
+ }
194
+
195
+ .docs-paged-editor .ProseMirror ul[data-type='taskList'] li {
196
+ align-items: flex-start;
197
+ display: flex;
198
+ gap: 0.5em;
199
+ }
200
+
201
+ .docs-paged-editor .ProseMirror ul[data-type='taskList'] li > label {
202
+ flex: none;
203
+ margin-top: 0.25em;
204
+ }
205
+
206
+ .docs-paged-editor .ProseMirror hr {
207
+ border: none;
208
+ border-top: 1px solid var(--docs-el-line);
209
+ margin: 1em 0;
210
+ }
211
+
212
+ .docs-paged-editor .ProseMirror a {
213
+ color: var(--docs-el-accent);
214
+ text-decoration: underline;
215
+ text-underline-offset: 2px;
216
+ }
217
+
218
+ .docs-paged-editor .ProseMirror code {
219
+ background-color: color-mix(in srgb, var(--docs-el-muted) 14%, transparent);
220
+ border-radius: 3px;
221
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
222
+ font-size: 0.9em;
223
+ padding: 0.1em 0.3em;
224
+ }
225
+
226
+ .docs-paged-editor .ProseMirror mark,
227
+ .docs-paged-editor .ProseMirror .docs-text-highlight {
228
+ background-color: #bfdbfe;
229
+ color: var(--docs-editor-paper-text);
230
+ }
231
+
232
+ .docs-paged-editor .ProseMirror mark *,
233
+ .docs-paged-editor .ProseMirror .docs-text-highlight * {
234
+ color: var(--docs-editor-paper-text) !important;
235
+ }
236
+
237
+ /* Placeholder (from @tiptap/extension-placeholder) — applied to the first empty paragraph. */
238
+ .docs-paged-editor .ProseMirror p.is-editor-empty:first-child::before {
239
+ color: var(--docs-editor-paper-muted);
240
+ content: attr(data-placeholder);
241
+ float: left;
242
+ height: 0;
243
+ pointer-events: none;
244
+ }
245
+
246
+ /* Soft page break: decoration spacer that fills the rest of the current page. */
247
+ .docs-soft-page-break {
248
+ display: block;
249
+ flex: none;
250
+ width: 100%;
251
+ pointer-events: none;
252
+ user-select: none;
253
+ }
254
+
255
+ @media print {
256
+ .docs-soft-page-break {
257
+ display: none !important;
258
+ }
259
+ }
260
+
261
+ /* CollaborationCaret — remote cursor & label rendered by the extension. */
262
+ .collaboration-carets__caret {
263
+ border-left: 1px solid var(--collaboration-caret-color, #2563eb);
264
+ border-right: 1px solid var(--collaboration-caret-color, #2563eb);
265
+ margin-left: -1px;
266
+ margin-right: -1px;
267
+ pointer-events: none;
268
+ position: relative;
269
+ word-break: normal;
270
+ }
271
+
272
+ .collaboration-carets__label {
273
+ border-radius: 3px;
274
+ color: #111111;
275
+ font-size: 11px;
276
+ font-weight: 600;
277
+ font-style: normal;
278
+ left: -1px;
279
+ letter-spacing: 0.01em;
280
+ line-height: 1;
281
+ padding: 0.1em 0.35em;
282
+ position: absolute;
283
+ top: -1.4em;
284
+ user-select: none;
285
+ white-space: nowrap;
286
+ }
287
+
288
+ .ProseMirror-yjs-selection,
289
+ .collaboration-carets__selection {
290
+ border-radius: 1px;
291
+ }
292
+
293
+ /*
294
+ * Print: the on-screen editor inherits the host theme's foreground color via
295
+ * --docs-el-text (often a light gray on dark themes), but the print canvas is
296
+ * forced to white. Without this override, the printed page comes out blank.
297
+ */
298
+ @media print {
299
+ .docs-paged-page-face {
300
+ background-color: #ffffff !important;
301
+ }
302
+ .docs-paged-editor {
303
+ color: var(--docs-editor-paper-text) !important;
304
+ }
305
+ .collaboration-carets__caret,
306
+ .collaboration-carets__label {
307
+ display: none !important;
308
+ }
309
+ }
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Paged editor surface — Letter-sized, decoration-based pagination.
3
+ *
4
+ * The ProseMirror document stays as one collaborative document. Pagination is
5
+ * a view-only layer: we measure rendered text, insert ProseMirror Decoration
6
+ * widgets at the calculated page boundaries (spacer divs filling the rest of
7
+ * each sheet), and render absolutely-positioned "page face" backgrounds
8
+ * behind the content. The Yjs document is never touched.
9
+ *
10
+ * Decorations are a no-op for the CRDT, so adding/removing page breaks
11
+ * never produces a Yjs update, never re-syncs, and never moves a remote
12
+ * collaborator's caret.
13
+ */
14
+
15
+ import { useCallback, useLayoutEffect, useRef, useState } from 'react'
16
+ import { EditorContent, type Editor } from '@tiptap/react'
17
+ import { Decoration, DecorationSet } from '@tiptap/pm/view'
18
+
19
+ const PX_PER_IN = 96
20
+ const PAGE_W_IN = 8.5
21
+ const PAGE_H_IN = 11
22
+ export const PAGE_HEIGHT_PX = Math.round(PAGE_H_IN * PX_PER_IN)
23
+ export const PAGE_WIDTH_PX = Math.round(PAGE_W_IN * PX_PER_IN)
24
+ export const GAP_PX = 24
25
+ const PAGE_STRIDE_PX = PAGE_HEIGHT_PX + GAP_PX
26
+ const PAGE_EPSILON_PX = 3
27
+ const MAX_SOFT_PAGE_BREAKS = 80
28
+ const PAGINATION_UPDATE_DELAY_MS = 180
29
+
30
+ export const TYPICAL_WORDS_PER_PAGE = 480
31
+
32
+ type SoftPageBreak = {
33
+ pos: number
34
+ height: number
35
+ }
36
+
37
+ function pageCountForBodyHeight(bodyHeight: number, pageBodyHeight: number) {
38
+ let pages = 1
39
+ while (bodyHeight > (pages - 1) * PAGE_STRIDE_PX + pageBodyHeight + PAGE_EPSILON_PX) {
40
+ pages += 1
41
+ }
42
+ return pages
43
+ }
44
+
45
+ function makeSoftPageBreakDecorations(editor: Editor, breaks: SoftPageBreak[]) {
46
+ if (breaks.length === 0) return DecorationSet.empty
47
+
48
+ const docSize = editor.state.doc.content.size
49
+ return DecorationSet.create(
50
+ editor.state.doc,
51
+ breaks.map((pageBreak, index) =>
52
+ Decoration.widget(
53
+ Math.min(pageBreak.pos, docSize),
54
+ () => {
55
+ const spacer = document.createElement('span')
56
+ spacer.className = 'docs-soft-page-break'
57
+ spacer.style.height = `${pageBreak.height}px`
58
+ spacer.contentEditable = 'false'
59
+ spacer.setAttribute('aria-hidden', 'true')
60
+ return spacer
61
+ },
62
+ {
63
+ key: `docs-page-break-${index}-${pageBreak.pos}-${Math.round(pageBreak.height)}`,
64
+ side: 1,
65
+ ignoreSelection: true,
66
+ },
67
+ ),
68
+ ),
69
+ )
70
+ }
71
+
72
+ type DocEditorSurfaceProps = {
73
+ editor: Editor
74
+ onPageCountChange?: (n: number) => void
75
+ }
76
+
77
+ export function DocEditorSurface({ editor, onPageCountChange }: DocEditorSurfaceProps) {
78
+ const [pageCount, setPageCount] = useState(1)
79
+ const pageBreaksRef = useRef<SoftPageBreak[]>([])
80
+ const rafRef = useRef<number | undefined>(undefined)
81
+ const recalcTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
82
+
83
+ const report = useCallback(
84
+ (n: number) => {
85
+ onPageCountChange?.(n)
86
+ setPageCount((prev) => (prev === n ? prev : n))
87
+ },
88
+ [onPageCountChange],
89
+ )
90
+
91
+ useLayoutEffect(() => {
92
+ const view = editor.view
93
+ const dom = view?.dom
94
+ if (!dom) return
95
+
96
+ /**
97
+ * `useEditor` destroys+recreates the Tiptap editor whenever the
98
+ * `extensions` array identity changes — notably when the owner toggles
99
+ * this peer between editor and viewer, since the placeholder string
100
+ * flips on `showReadOnlyDocUx`. The destroy nulls the ProseMirror
101
+ * `docView` internally; any rAF / timeout / ResizeObserver that fires
102
+ * afterwards and still calls `view.dispatch` or `view.setProps` lands
103
+ * in `updateStateInner` and throws
104
+ * `Cannot read properties of null (reading 'matchesNode')`, killing
105
+ * the route. `editor.isDestroyed` is the supported signal — set the
106
+ * moment Tiptap tears the editor down — so we use it to short-circuit
107
+ * every async callback rooted in this effect.
108
+ */
109
+ const viewIsAlive = () => !editor.isDestroyed
110
+
111
+ const previousDecorations = view.props.decorations
112
+ const refreshDecorations = () => {
113
+ if (!viewIsAlive()) return
114
+ view.dispatch(
115
+ view.state.tr.setMeta('addToHistory', false).setMeta('docsPagination', true),
116
+ )
117
+ }
118
+
119
+ view.setProps({
120
+ decorations: (state) => {
121
+ const externalDecorations =
122
+ typeof previousDecorations === 'function'
123
+ ? previousDecorations(state)
124
+ : previousDecorations
125
+ const pageDecorations = makeSoftPageBreakDecorations(editor, pageBreaksRef.current)
126
+
127
+ if (!externalDecorations) return pageDecorations
128
+ const externalDecorationItems: Decoration[] = []
129
+ externalDecorations.forEachSet((set) => {
130
+ externalDecorationItems.push(...set.find())
131
+ })
132
+ return DecorationSet.create(state.doc, [
133
+ ...externalDecorationItems,
134
+ ...pageDecorations.find(),
135
+ ])
136
+ },
137
+ })
138
+
139
+ const recalc = () => {
140
+ if (recalcTimerRef.current !== undefined) {
141
+ clearTimeout(recalcTimerRef.current)
142
+ recalcTimerRef.current = undefined
143
+ }
144
+ if (rafRef.current !== undefined) cancelAnimationFrame(rafRef.current)
145
+ rafRef.current = requestAnimationFrame(() => {
146
+ rafRef.current = undefined
147
+ if (!viewIsAlive()) return
148
+ const shell = dom.closest<HTMLElement>('.docs-paged-content')
149
+ if (!shell) return
150
+
151
+ const shellStyle = window.getComputedStyle(shell)
152
+ const paddingTop = Number.parseFloat(shellStyle.paddingTop) || 0
153
+ const paddingBottom = Number.parseFloat(shellStyle.paddingBottom) || 0
154
+ const pageBodyHeight = PAGE_HEIGHT_PX - paddingTop - paddingBottom
155
+
156
+ pageBreaksRef.current = []
157
+ refreshDecorations()
158
+
159
+ let lastBreakPos = -1
160
+ for (let pageIndex = 0; pageIndex < MAX_SOFT_PAGE_BREAKS; pageIndex += 1) {
161
+ if (!viewIsAlive()) return
162
+ const bodyHeight = dom.scrollHeight
163
+ const pageBottom = pageIndex * PAGE_STRIDE_PX + pageBodyHeight
164
+ if (bodyHeight <= pageBottom + PAGE_EPSILON_PX) break
165
+
166
+ const domRect = dom.getBoundingClientRect()
167
+ const posAtBoundary = view.posAtCoords({
168
+ left: domRect.right - 2,
169
+ top: domRect.top + pageBottom - 2,
170
+ })
171
+
172
+ if (!posAtBoundary || posAtBoundary.pos <= lastBreakPos) break
173
+
174
+ const coords = view.coordsAtPos(posAtBoundary.pos)
175
+ const breakY = Math.max(coords.bottom - domRect.top, pageIndex * PAGE_STRIDE_PX)
176
+ const breakHeight = Math.max(0, (pageIndex + 1) * PAGE_STRIDE_PX - breakY)
177
+
178
+ if (breakHeight <= PAGE_EPSILON_PX) break
179
+
180
+ lastBreakPos = posAtBoundary.pos
181
+ pageBreaksRef.current = [
182
+ ...pageBreaksRef.current,
183
+ { pos: posAtBoundary.pos, height: breakHeight },
184
+ ]
185
+ refreshDecorations()
186
+ }
187
+
188
+ if (!viewIsAlive()) return
189
+ const pagedBodyHeight = dom.scrollHeight
190
+ const measuredPages = pageCountForBodyHeight(pagedBodyHeight, pageBodyHeight)
191
+ report(measuredPages)
192
+ })
193
+ }
194
+ const scheduleRecalc = () => {
195
+ if (recalcTimerRef.current !== undefined) clearTimeout(recalcTimerRef.current)
196
+ recalcTimerRef.current = setTimeout(() => {
197
+ recalcTimerRef.current = undefined
198
+ if (!viewIsAlive()) return
199
+ recalc()
200
+ }, PAGINATION_UPDATE_DELAY_MS)
201
+ }
202
+
203
+ const ro = new ResizeObserver(() => {
204
+ if (!viewIsAlive()) return
205
+ recalc()
206
+ })
207
+ ro.observe(dom)
208
+ recalc()
209
+ editor.on('update', scheduleRecalc)
210
+
211
+ return () => {
212
+ if (recalcTimerRef.current !== undefined) clearTimeout(recalcTimerRef.current)
213
+ if (rafRef.current !== undefined) cancelAnimationFrame(rafRef.current)
214
+ recalcTimerRef.current = undefined
215
+ rafRef.current = undefined
216
+ ro.disconnect()
217
+ editor.off('update', scheduleRecalc)
218
+ pageBreaksRef.current = []
219
+ if (!viewIsAlive()) return
220
+ view.setProps({ decorations: previousDecorations })
221
+ refreshDecorations()
222
+ }
223
+ }, [editor, report])
224
+
225
+ const stackHeight = pageCount * PAGE_HEIGHT_PX + Math.max(0, pageCount - 1) * GAP_PX
226
+
227
+ return (
228
+ <div className="docs-paged-editor-canvas h-full min-h-0 w-full flex-1 overflow-y-auto overflow-x-hidden overscroll-y-contain print:block print:!h-auto print:!overflow-visible print:!bg-white">
229
+ <div className="docs-paged-inner pointer-events-auto mx-auto w-full max-w-full px-2 py-3 sm:px-3 sm:py-4 md:py-5">
230
+ <div
231
+ className="docs-paged-page-frame relative mx-auto"
232
+ style={{ width: `${PAGE_W_IN}in`, maxWidth: '100%' }}
233
+ >
234
+ <div className="docs-paged-page-stack relative" style={{ minHeight: `${stackHeight}px` }}>
235
+ {Array.from({ length: pageCount }, (_, index) => (
236
+ <div
237
+ key={index}
238
+ className="docs-paged-page-face docs-editor-paper pointer-events-none absolute left-0 top-0 h-[11in] w-full rounded-sm print:static print:h-auto print:shadow-none print:border-0"
239
+ style={{ transform: `translateY(${index * PAGE_STRIDE_PX}px)` }}
240
+ />
241
+ ))}
242
+ <div
243
+ className="docs-paged-content docs-paged-page-padding relative z-10 box-border"
244
+ style={{ maxWidth: '100%', minHeight: `${stackHeight}px` }}
245
+ >
246
+ <EditorContent
247
+ editor={editor}
248
+ className="docs-paged-editor docs-paged-editor-font tiptap"
249
+ data-testid="editor-content"
250
+ />
251
+ </div>
252
+ </div>
253
+ </div>
254
+ </div>
255
+ </div>
256
+ )
257
+ }