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,560 @@
1
+ /**
2
+ * Editor toolbar — issues Tiptap chain commands (no execCommand).
3
+ *
4
+ * Active state is read from the live editor via `useEditorState`, so toggles
5
+ * accurately reflect the current selection without manually re-rendering.
6
+ */
7
+
8
+ import { useState, type ReactNode } from 'react'
9
+ import { type Editor, useEditorState } from '@tiptap/react'
10
+ import {
11
+ AlignCenter,
12
+ AlignLeft,
13
+ AlignRight,
14
+ Bold,
15
+ Heading1,
16
+ Heading2,
17
+ Heading3,
18
+ Highlighter,
19
+ Indent,
20
+ Italic,
21
+ Link as LinkIcon,
22
+ List,
23
+ ListOrdered,
24
+ Minus,
25
+ Outdent,
26
+ Palette,
27
+ Quote,
28
+ Redo2,
29
+ RemoveFormatting,
30
+ Strikethrough,
31
+ Underline as UnderlineIcon,
32
+ Undo2,
33
+ } from 'lucide-react'
34
+
35
+ const ZOOM_PRESETS = [
36
+ { label: '75%', value: 0.75 },
37
+ { label: '100%', value: 1 },
38
+ { label: '125%', value: 1.25 },
39
+ { label: '150%', value: 1.5 },
40
+ ]
41
+
42
+ const TEXT_COLOR_OPTIONS = [
43
+ { label: 'Default', value: '' },
44
+ { label: 'Gray', value: '#6b7280' },
45
+ { label: 'Red', value: '#dc2626' },
46
+ { label: 'Orange', value: '#ea580c' },
47
+ { label: 'Yellow', value: '#ca8a04' },
48
+ { label: 'Green', value: '#16a34a' },
49
+ { label: 'Blue', value: '#2563eb' },
50
+ { label: 'Purple', value: '#7c3aed' },
51
+ ]
52
+
53
+ const HIGHLIGHT_OPTIONS = [
54
+ { label: 'None', value: '' },
55
+ { label: 'Blue', value: '#bfdbfe' },
56
+ { label: 'Yellow', value: '#fde68a' },
57
+ { label: 'Green', value: '#bbf7d0' },
58
+ { label: 'Pink', value: '#fbcfe8' },
59
+ ]
60
+
61
+ function Divider() {
62
+ return (
63
+ <div className="mx-1 h-6 w-px self-center" style={{ backgroundColor: 'var(--docs-el-line)' }} />
64
+ )
65
+ }
66
+
67
+ function ToolbarButton({
68
+ onClick,
69
+ disabled,
70
+ active,
71
+ title,
72
+ children,
73
+ }: {
74
+ onClick: () => void
75
+ disabled?: boolean
76
+ active?: boolean
77
+ title: string
78
+ children: ReactNode
79
+ }) {
80
+ return (
81
+ <button
82
+ type="button"
83
+ onMouseDown={(e) => e.preventDefault()}
84
+ onClick={onClick}
85
+ disabled={disabled}
86
+ title={title}
87
+ className={`rounded p-1.5 transition-colors ${
88
+ disabled
89
+ ? 'cursor-not-allowed opacity-40'
90
+ : 'hover:bg-black/[0.06] dark:hover:bg-white/[0.08]'
91
+ }`}
92
+ style={{
93
+ color: active ? 'var(--docs-el-accent)' : 'var(--docs-el-muted)',
94
+ backgroundColor: active
95
+ ? 'color-mix(in srgb, var(--docs-el-accent) 14%, transparent)'
96
+ : undefined,
97
+ }}
98
+ >
99
+ {children}
100
+ </button>
101
+ )
102
+ }
103
+
104
+ export interface DocsTiptapToolbarProps {
105
+ editor: Editor | null
106
+ disabled?: boolean
107
+ canvasZoom: number
108
+ onCanvasZoomChange: (z: number) => void
109
+ }
110
+
111
+ const EMPTY_TOOLBAR_STATE = {
112
+ canUndo: false,
113
+ canRedo: false,
114
+ isBold: false,
115
+ isItalic: false,
116
+ isUnderline: false,
117
+ isStrike: false,
118
+ isH1: false,
119
+ isH2: false,
120
+ isH3: false,
121
+ isBlockquote: false,
122
+ isBulletList: false,
123
+ isOrderedList: false,
124
+ alignLeft: false,
125
+ alignCenter: false,
126
+ alignRight: false,
127
+ }
128
+
129
+ export function DocsTiptapToolbar({
130
+ editor,
131
+ disabled,
132
+ canvasZoom,
133
+ onCanvasZoomChange,
134
+ }: DocsTiptapToolbarProps) {
135
+ const [zoomOpen, setZoomOpen] = useState(false)
136
+ const [colorOpen, setColorOpen] = useState(false)
137
+ const [highlightOpen, setHighlightOpen] = useState(false)
138
+ const iconSize = 'h-4 w-4'
139
+
140
+ /**
141
+ * Read live editor state so toggles reflect the actual selection. Falls
142
+ * back to no-op flags when `editor` is null (initial render before the
143
+ * Tiptap instance is ready) or when the underlying ProseMirror view has
144
+ * been torn down — that happens transiently during StrictMode double-mount
145
+ * in dev and when `docId` changes (which swaps the Y.Doc and forces the
146
+ * editor to be re-created). Calling `e.can()` on a destroyed editor throws
147
+ * "Cannot read properties of null (reading 'can')" inside Tiptap.
148
+ */
149
+ const state =
150
+ useEditorState({
151
+ editor,
152
+ selector: ({ editor: e }) => {
153
+ if (!e || e.isDestroyed || !e.view) {
154
+ return EMPTY_TOOLBAR_STATE
155
+ }
156
+ try {
157
+ return {
158
+ canUndo: e.can().chain().undo().run(),
159
+ canRedo: e.can().chain().redo().run(),
160
+ isBold: e.isActive('bold'),
161
+ isItalic: e.isActive('italic'),
162
+ isUnderline: e.isActive('underline'),
163
+ isStrike: e.isActive('strike'),
164
+ isH1: e.isActive('heading', { level: 1 }),
165
+ isH2: e.isActive('heading', { level: 2 }),
166
+ isH3: e.isActive('heading', { level: 3 }),
167
+ isBlockquote: e.isActive('blockquote'),
168
+ isBulletList: e.isActive('bulletList'),
169
+ isOrderedList: e.isActive('orderedList'),
170
+ alignLeft: e.isActive({ textAlign: 'left' }),
171
+ alignCenter: e.isActive({ textAlign: 'center' }),
172
+ alignRight: e.isActive({ textAlign: 'right' }),
173
+ }
174
+ } catch {
175
+ return EMPTY_TOOLBAR_STATE
176
+ }
177
+ },
178
+ }) ?? EMPTY_TOOLBAR_STATE
179
+
180
+ /** Formatting actions require a mounted editor and an enabled connection/role. */
181
+ const formattingLocked = disabled || !editor
182
+
183
+ const run = (fn: (chain: ReturnType<Editor['chain']>) => unknown) => {
184
+ if (!editor || disabled) return
185
+ fn(editor.chain().focus())
186
+ }
187
+
188
+ const normalizedZoom =
189
+ ZOOM_PRESETS.find((p) => Math.abs(p.value - canvasZoom) < 0.001)?.label ??
190
+ `${Math.round(canvasZoom * 100)}%`
191
+
192
+ return (
193
+ <div
194
+ data-testid="editor-toolbar"
195
+ className="sticky top-0 z-10 flex flex-wrap items-center gap-0.5 border-b px-3 py-1.5 print:hidden"
196
+ style={{
197
+ borderColor: 'var(--docs-el-line)',
198
+ backgroundColor: 'color-mix(in srgb, var(--docs-el-surface) 96%, transparent)',
199
+ }}
200
+ >
201
+ <ToolbarButton
202
+ onClick={() => run((c) => c.undo().run())}
203
+ disabled={formattingLocked || !state.canUndo}
204
+ title="Undo (⌘Z)"
205
+ >
206
+ <Undo2 className={iconSize} />
207
+ </ToolbarButton>
208
+ <ToolbarButton
209
+ onClick={() => run((c) => c.redo().run())}
210
+ disabled={formattingLocked || !state.canRedo}
211
+ title="Redo (⌘⇧Z)"
212
+ >
213
+ <Redo2 className={iconSize} />
214
+ </ToolbarButton>
215
+
216
+ <Divider />
217
+
218
+ <ToolbarButton
219
+ onClick={() => run((c) => c.toggleHeading({ level: 1 }).run())}
220
+ disabled={formattingLocked}
221
+ active={state.isH1}
222
+ title="Heading 1"
223
+ >
224
+ <Heading1 className={iconSize} />
225
+ </ToolbarButton>
226
+ <ToolbarButton
227
+ onClick={() => run((c) => c.toggleHeading({ level: 2 }).run())}
228
+ disabled={formattingLocked}
229
+ active={state.isH2}
230
+ title="Heading 2"
231
+ >
232
+ <Heading2 className={iconSize} />
233
+ </ToolbarButton>
234
+ <ToolbarButton
235
+ onClick={() => run((c) => c.toggleHeading({ level: 3 }).run())}
236
+ disabled={formattingLocked}
237
+ active={state.isH3}
238
+ title="Heading 3"
239
+ >
240
+ <Heading3 className={iconSize} />
241
+ </ToolbarButton>
242
+
243
+ <Divider />
244
+
245
+ <ToolbarButton
246
+ onClick={() => run((c) => c.toggleBold().run())}
247
+ disabled={formattingLocked}
248
+ active={state.isBold}
249
+ title="Bold (⌘B)"
250
+ >
251
+ <Bold className={iconSize} />
252
+ </ToolbarButton>
253
+ <ToolbarButton
254
+ onClick={() => run((c) => c.toggleItalic().run())}
255
+ disabled={formattingLocked}
256
+ active={state.isItalic}
257
+ title="Italic (⌘I)"
258
+ >
259
+ <Italic className={iconSize} />
260
+ </ToolbarButton>
261
+ <ToolbarButton
262
+ onClick={() => run((c) => c.toggleUnderline().run())}
263
+ disabled={formattingLocked}
264
+ active={state.isUnderline}
265
+ title="Underline (⌘U)"
266
+ >
267
+ <UnderlineIcon className={iconSize} />
268
+ </ToolbarButton>
269
+ <ToolbarButton
270
+ onClick={() => run((c) => c.toggleStrike().run())}
271
+ disabled={formattingLocked}
272
+ active={state.isStrike}
273
+ title="Strikethrough"
274
+ >
275
+ <Strikethrough className={iconSize} />
276
+ </ToolbarButton>
277
+
278
+ <Divider />
279
+
280
+ <div className="relative">
281
+ <button
282
+ type="button"
283
+ onMouseDown={(e) => e.preventDefault()}
284
+ onClick={() => {
285
+ setColorOpen((o) => !o)
286
+ setHighlightOpen(false)
287
+ setZoomOpen(false)
288
+ }}
289
+ disabled={formattingLocked}
290
+ className="flex items-center gap-1 rounded px-2 py-1 text-sm transition-colors hover:bg-black/[0.05] disabled:cursor-not-allowed disabled:opacity-40 dark:hover:bg-white/[0.06]"
291
+ style={{ color: 'var(--docs-el-muted)' }}
292
+ title="Text color"
293
+ >
294
+ <Palette className="h-3.5 w-3.5" />
295
+ <svg className="h-3 w-3 shrink-0" viewBox="0 0 12 12" fill="currentColor" aria-hidden>
296
+ <path d="M3 5l3 3 3-3" />
297
+ </svg>
298
+ </button>
299
+ {colorOpen && !formattingLocked ? (
300
+ <>
301
+ <button
302
+ type="button"
303
+ className="fixed inset-0 z-40 cursor-default bg-transparent"
304
+ aria-label="Close color menu"
305
+ onClick={() => setColorOpen(false)}
306
+ />
307
+ <div
308
+ className="absolute left-0 top-full z-50 mt-1 grid w-[9.5rem] grid-cols-4 gap-1 rounded-lg border p-2 shadow-lg"
309
+ style={{
310
+ borderColor: 'var(--docs-el-line)',
311
+ backgroundColor: 'var(--docs-el-surface)',
312
+ }}
313
+ >
314
+ {TEXT_COLOR_OPTIONS.map(({ label, value }) => (
315
+ <button
316
+ key={label}
317
+ type="button"
318
+ onMouseDown={(e) => e.preventDefault()}
319
+ onClick={() => {
320
+ if (value) run((c) => c.setColor(value).run())
321
+ else run((c) => c.unsetColor().run())
322
+ setColorOpen(false)
323
+ }}
324
+ title={label}
325
+ aria-label={label}
326
+ className="h-7 w-7 rounded-full border transition-transform hover:scale-105"
327
+ style={{
328
+ backgroundColor: value || 'transparent',
329
+ borderColor: 'var(--docs-el-line)',
330
+ }}
331
+ />
332
+ ))}
333
+ </div>
334
+ </>
335
+ ) : null}
336
+ </div>
337
+
338
+ <div className="relative">
339
+ <button
340
+ type="button"
341
+ onMouseDown={(e) => e.preventDefault()}
342
+ onClick={() => {
343
+ setHighlightOpen((o) => !o)
344
+ setColorOpen(false)
345
+ setZoomOpen(false)
346
+ }}
347
+ disabled={formattingLocked}
348
+ className="flex items-center gap-1 rounded px-2 py-1 text-sm transition-colors hover:bg-black/[0.05] disabled:cursor-not-allowed disabled:opacity-40 dark:hover:bg-white/[0.06]"
349
+ style={{ color: 'var(--docs-el-muted)' }}
350
+ title="Highlight"
351
+ >
352
+ <Highlighter className="h-3.5 w-3.5" />
353
+ <svg className="h-3 w-3 shrink-0" viewBox="0 0 12 12" fill="currentColor" aria-hidden>
354
+ <path d="M3 5l3 3 3-3" />
355
+ </svg>
356
+ </button>
357
+ {highlightOpen && !formattingLocked ? (
358
+ <>
359
+ <button
360
+ type="button"
361
+ className="fixed inset-0 z-40 cursor-default bg-transparent"
362
+ aria-label="Close highlight menu"
363
+ onClick={() => setHighlightOpen(false)}
364
+ />
365
+ <div
366
+ className="absolute left-0 top-full z-50 mt-1 grid w-[9.5rem] grid-cols-4 gap-1 rounded-lg border p-2 shadow-lg"
367
+ style={{
368
+ borderColor: 'var(--docs-el-line)',
369
+ backgroundColor: 'var(--docs-el-surface)',
370
+ }}
371
+ >
372
+ {HIGHLIGHT_OPTIONS.map(({ label, value }) => (
373
+ <button
374
+ key={label}
375
+ type="button"
376
+ onMouseDown={(e) => e.preventDefault()}
377
+ onClick={() => {
378
+ if (value) run((c) => c.toggleHighlight({ color: value }).run())
379
+ else run((c) => c.unsetHighlight().run())
380
+ setHighlightOpen(false)
381
+ }}
382
+ title={label}
383
+ aria-label={label}
384
+ className="h-7 w-7 rounded-full border transition-transform hover:scale-105"
385
+ style={{
386
+ backgroundColor: value || 'transparent',
387
+ borderColor: 'var(--docs-el-line)',
388
+ }}
389
+ />
390
+ ))}
391
+ </div>
392
+ </>
393
+ ) : null}
394
+ </div>
395
+
396
+ <Divider />
397
+
398
+ <ToolbarButton
399
+ onClick={() => run((c) => c.toggleBlockquote().run())}
400
+ disabled={formattingLocked}
401
+ active={state.isBlockquote}
402
+ title="Block quote"
403
+ >
404
+ <Quote className={iconSize} />
405
+ </ToolbarButton>
406
+
407
+ <Divider />
408
+
409
+ <ToolbarButton
410
+ onClick={() => run((c) => c.setTextAlign('left').run())}
411
+ disabled={formattingLocked}
412
+ active={state.alignLeft}
413
+ title="Align left"
414
+ >
415
+ <AlignLeft className={iconSize} />
416
+ </ToolbarButton>
417
+ <ToolbarButton
418
+ onClick={() => run((c) => c.setTextAlign('center').run())}
419
+ disabled={formattingLocked}
420
+ active={state.alignCenter}
421
+ title="Align center"
422
+ >
423
+ <AlignCenter className={iconSize} />
424
+ </ToolbarButton>
425
+ <ToolbarButton
426
+ onClick={() => run((c) => c.setTextAlign('right').run())}
427
+ disabled={formattingLocked}
428
+ active={state.alignRight}
429
+ title="Align right"
430
+ >
431
+ <AlignRight className={iconSize} />
432
+ </ToolbarButton>
433
+
434
+ <Divider />
435
+
436
+ <ToolbarButton
437
+ onClick={() => run((c) => c.toggleBulletList().run())}
438
+ disabled={formattingLocked}
439
+ active={state.isBulletList}
440
+ title="Bulleted list"
441
+ >
442
+ <List className={iconSize} />
443
+ </ToolbarButton>
444
+ <ToolbarButton
445
+ onClick={() => run((c) => c.toggleOrderedList().run())}
446
+ disabled={formattingLocked}
447
+ active={state.isOrderedList}
448
+ title="Numbered list"
449
+ >
450
+ <ListOrdered className={iconSize} />
451
+ </ToolbarButton>
452
+
453
+ <Divider />
454
+
455
+ <ToolbarButton
456
+ onClick={() => {
457
+ if (formattingLocked || !editor) return
458
+ const previousUrl = editor.getAttributes('link').href as string | undefined
459
+ const url = window.prompt('Link URL', previousUrl ?? '')
460
+ if (url === null) return
461
+ if (url === '') {
462
+ run((c) => c.extendMarkRange('link').unsetLink().run())
463
+ return
464
+ }
465
+ run((c) => c.extendMarkRange('link').setLink({ href: url }).run())
466
+ }}
467
+ disabled={formattingLocked}
468
+ title="Link"
469
+ >
470
+ <LinkIcon className={iconSize} />
471
+ </ToolbarButton>
472
+ <ToolbarButton
473
+ onClick={() => run((c) => c.setHorizontalRule().run())}
474
+ disabled={formattingLocked}
475
+ title="Horizontal rule"
476
+ >
477
+ <Minus className={iconSize} />
478
+ </ToolbarButton>
479
+
480
+ <Divider />
481
+
482
+ <ToolbarButton
483
+ onClick={() => run((c) => c.sinkListItem('listItem').run())}
484
+ disabled={formattingLocked}
485
+ title="Indent list item"
486
+ >
487
+ <Indent className={iconSize} />
488
+ </ToolbarButton>
489
+ <ToolbarButton
490
+ onClick={() => run((c) => c.liftListItem('listItem').run())}
491
+ disabled={formattingLocked}
492
+ title="Outdent list item"
493
+ >
494
+ <Outdent className={iconSize} />
495
+ </ToolbarButton>
496
+
497
+ <Divider />
498
+
499
+ <ToolbarButton
500
+ onClick={() => run((c) => c.unsetAllMarks().clearNodes().run())}
501
+ disabled={formattingLocked}
502
+ title="Clear formatting"
503
+ >
504
+ <RemoveFormatting className={iconSize} />
505
+ </ToolbarButton>
506
+
507
+ <div className="ml-auto relative">
508
+ <button
509
+ type="button"
510
+ onClick={() => setZoomOpen(!zoomOpen)}
511
+ className="flex min-w-[3.25rem] items-center gap-1 rounded px-2 py-1 text-sm tabular-nums transition-colors hover:bg-black/[0.05] dark:hover:bg-white/[0.06]"
512
+ style={{ color: 'var(--docs-el-muted)' }}
513
+ >
514
+ {normalizedZoom}
515
+ <svg className="h-3 w-3 shrink-0" viewBox="0 0 12 12" fill="currentColor" aria-hidden>
516
+ <path d="M3 5l3 3 3-3" />
517
+ </svg>
518
+ </button>
519
+ {zoomOpen && (
520
+ <>
521
+ <button
522
+ type="button"
523
+ className="fixed inset-0 z-40 cursor-default bg-transparent"
524
+ aria-label="Close"
525
+ onClick={() => setZoomOpen(false)}
526
+ />
527
+ <div
528
+ className="absolute right-0 top-full z-50 mt-1 min-w-[7rem] overflow-y-auto rounded-lg border py-1 shadow-lg"
529
+ style={{
530
+ borderColor: 'var(--docs-el-line)',
531
+ backgroundColor: 'var(--docs-el-surface)',
532
+ }}
533
+ >
534
+ {ZOOM_PRESETS.map(({ label, value }) => (
535
+ <button
536
+ key={label}
537
+ type="button"
538
+ onClick={() => {
539
+ onCanvasZoomChange(value)
540
+ setZoomOpen(false)
541
+ }}
542
+ className="w-full px-3 py-1.5 text-left text-sm tabular-nums transition-colors hover:bg-black/[0.04] dark:hover:bg-white/[0.06]"
543
+ style={{
544
+ color:
545
+ Math.abs(canvasZoom - value) < 0.001
546
+ ? 'var(--docs-el-accent)'
547
+ : 'var(--docs-el-text)',
548
+ fontWeight: Math.abs(canvasZoom - value) < 0.001 ? 600 : 400,
549
+ }}
550
+ >
551
+ {label}
552
+ </button>
553
+ ))}
554
+ </div>
555
+ </>
556
+ )}
557
+ </div>
558
+ </div>
559
+ )
560
+ }