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.
- package/dist/cli.js +96 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +160 -1
- package/dist/index.js +283 -126
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +172 -2
- package/dist/server.js +581 -1
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +252 -2
- package/dist/worker.js +588 -1
- package/dist/worker.js.map +1 -1
- package/features/admin-page/feature.json +29 -0
- package/features/admin-page/src/AdminPage.tsx +292 -0
- package/features/admin-page/src/admin-schema.ts +20 -0
- package/features/ai-chat/feature.json +41 -0
- package/features/ai-chat/src/AiChatPage.tsx +692 -0
- package/features/ai-chat/src/ChatPanel.tsx +1098 -0
- package/features/ai-chat/src/ai-chat-schema.ts +11 -0
- package/features/canvas/feature.json +45 -0
- package/features/canvas/src/CanvasEditorPage.tsx +87 -0
- package/features/canvas/src/CanvasListPage.tsx +202 -0
- package/features/canvas/src/canvas-schema.ts +33 -0
- package/features/canvas/src/components/CanvasToolbar.tsx +170 -0
- package/features/canvas/src/components/CanvasView.tsx +369 -0
- package/features/canvas/src/components/ShapeRenderer.tsx +114 -0
- package/features/cron/feature.json +33 -0
- package/features/cron/src/CronLogPage.tsx +100 -0
- package/features/cron/src/cron.ts +25 -0
- package/features/docs/feature.json +96 -0
- package/features/docs/src/DocsEditorPage.tsx +1012 -0
- package/features/docs/src/DocsListPage.tsx +951 -0
- package/features/docs/src/DocsOutlinePanel.tsx +68 -0
- package/features/docs/src/DocsPresence.tsx +138 -0
- package/features/docs/src/InviteDialog.tsx +336 -0
- package/features/docs/src/LibrarySidebar.tsx +464 -0
- package/features/docs/src/docs-favorites.ts +18 -0
- package/features/docs/src/docs-library-types.ts +15 -0
- package/features/docs/src/docs-schema.ts +80 -0
- package/features/docs/src/docs-ui.css +309 -0
- package/features/docs/src/editor/DocEditorSurface.tsx +257 -0
- package/features/docs/src/editor/DocsTiptapToolbar.tsx +560 -0
- package/features/docs/src/editor/useDocEditor.ts +206 -0
- package/features/docs/src/use-yjs-room-with-awareness.ts +285 -0
- package/features/file-attachments/feature.json +42 -0
- package/features/file-attachments/src/AttachmentsPage.tsx +864 -0
- package/features/file-attachments/src/attachments-constants.ts +9 -0
- package/features/file-attachments/src/attachments-schema.ts +45 -0
- package/features/file-attachments/src/useFilePreview.ts +471 -0
- package/features/file-attachments/src/useMimeTypeDetection.ts +138 -0
- package/features/file-manager/feature.json +24 -0
- package/features/file-manager/src/FileManagerPage.tsx +330 -0
- package/features/integration-test/feature.json +18 -0
- package/features/integration-test/src/IntegrationTestPage.tsx +423 -0
- package/features/integration-test/src/integration.spec.ts +42 -0
- package/features/items/feature.json +33 -0
- package/features/items/src/ItemsPage.tsx +325 -0
- package/features/items/src/items-constants.ts +10 -0
- package/features/items/src/items-schema.ts +27 -0
- package/features/kanban/feature.json +18 -0
- package/features/kanban/src/Kanban.tsx +283 -0
- package/features/landing/feature.json +62 -0
- package/features/landing/src/LandingPage.tsx +627 -0
- package/features/landing/src/primitives.tsx +389 -0
- package/features/landing/src/sections/FAQSection.tsx +110 -0
- package/features/landing/src/sections/FeaturesGridSection.tsx +311 -0
- package/features/landing/src/sections/HowItWorksSection.tsx +62 -0
- package/features/landing/src/sections/LogoCloudSection.tsx +58 -0
- package/features/landing/src/sections/ShowcaseSection.tsx +63 -0
- package/features/landing/src/sections/StatsSection.tsx +31 -0
- package/features/landing/src/sections/TeamSection.tsx +86 -0
- package/features/landing/src/sections/TestimonialsSection.tsx +68 -0
- package/features/landing/src/sections/VideoSection.tsx +71 -0
- package/features/leaderboard/feature.json +33 -0
- package/features/leaderboard/src/LeaderboardPage.tsx +372 -0
- package/features/leaderboard/src/leaderboard-constants.ts +21 -0
- package/features/leaderboard/src/leaderboard-schema.ts +36 -0
- package/features/messaging/feature.json +111 -0
- package/features/messaging/src/ChatMultiPage.tsx +300 -0
- package/features/messaging/src/ChatPage.tsx +75 -0
- package/features/messaging/src/components/chat/ChatHeader.tsx +76 -0
- package/features/messaging/src/components/chat/MessageActionSheet.tsx +168 -0
- package/features/messaging/src/components/chat/MessageInput.tsx +65 -0
- package/features/messaging/src/components/chat/MessageItem.tsx +352 -0
- package/features/messaging/src/components/chat/MessageList.tsx +248 -0
- package/features/messaging/src/components/chat/ReactionPicker.tsx +2 -0
- package/features/messaging/src/components/chat/ThreadPanel.tsx +107 -0
- package/features/messaging/src/components/chat/UserProfilePopover.tsx +158 -0
- package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +188 -0
- package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +163 -0
- package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +147 -0
- package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +344 -0
- package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +460 -0
- package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +190 -0
- package/features/messaging/src/components/chat-multi/InvitationList.tsx +149 -0
- package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +159 -0
- package/features/messaging/src/hooks/useChatChannel.ts +64 -0
- package/features/messaging/src/hooks/useLongPress.ts +74 -0
- package/features/messaging/src/hooks/useMultiChannel.ts +142 -0
- package/features/messaging/src/messaging-schema.ts +25 -0
- package/features/presence-test/feature.json +21 -0
- package/features/presence-test/src/PresenceTestPage.tsx +120 -0
- package/features/sidebar/feature.json +19 -0
- package/features/sidebar/src/AppSidebar.tsx +558 -0
- package/features/tasks/feature.json +33 -0
- package/features/tasks/src/TasksPage.tsx +598 -0
- package/features/tasks/src/tasks-constants.ts +43 -0
- package/features/tasks/src/tasks-schema.ts +56 -0
- package/features/testing/feature.json +49 -0
- package/features/testing/src/TestPage.tsx +145 -0
- package/features/testing/src/cpu-burn-action.ts +51 -0
- package/features/testing/src/subrequest-burn-action.ts +53 -0
- package/features/testing/src/test-schema.ts +23 -0
- package/features/topbar/feature.json +18 -0
- package/features/topbar/src/Topbar.tsx +353 -0
- package/features/tree/feature.json +26 -0
- package/features/tree/src/TreeSidebar.tsx +607 -0
- package/features/tree/src/tree-constants.ts +68 -0
- package/features/tree/src/tree-hooks.ts +258 -0
- package/package.json +4 -2
- 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
|
+
}
|