@pilotiq/tiptap 3.10.4 → 3.10.6

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 (69) hide show
  1. package/CHANGELOG.md +745 -0
  2. package/boost/guidelines.md +268 -0
  3. package/boost/skills/pilotiq-tiptap-blocks/SKILL.md +48 -0
  4. package/boost/skills/pilotiq-tiptap-blocks/rules/custom-blocks.md +90 -0
  5. package/boost/skills/pilotiq-tiptap-blocks/rules/slash-menu-and-mentions.md +101 -0
  6. package/boost/skills/pilotiq-tiptap-blocks/rules/toolbar-and-extensibility.md +161 -0
  7. package/dist/react/CollabTextRenderer.d.ts.map +1 -1
  8. package/dist/react/CollabTextRenderer.js +4 -4
  9. package/dist/react/CollabTextRenderer.js.map +1 -1
  10. package/dist/react/MarkdownEditor.d.ts.map +1 -1
  11. package/dist/react/MarkdownEditor.js +4 -5
  12. package/dist/react/MarkdownEditor.js.map +1 -1
  13. package/dist/react/TiptapEditor.d.ts.map +1 -1
  14. package/dist/react/TiptapEditor.js +8 -7
  15. package/dist/react/TiptapEditor.js.map +1 -1
  16. package/package.json +6 -3
  17. package/dist/collabShapes.d.ts +0 -22
  18. package/dist/collabShapes.d.ts.map +0 -1
  19. package/dist/collabShapes.js +0 -2
  20. package/dist/collabShapes.js.map +0 -1
  21. package/src/Block.ts +0 -75
  22. package/src/MentionProvider.ts +0 -153
  23. package/src/PlainTextEditor.dom.test.ts +0 -111
  24. package/src/PlainTextEditor.test.ts +0 -158
  25. package/src/PlainTextEditor.ts +0 -229
  26. package/src/RichTextField.test.ts +0 -447
  27. package/src/RichTextField.ts +0 -508
  28. package/src/collabShapes.ts +0 -22
  29. package/src/extensions/AiInlineDiffExtension.ts +0 -286
  30. package/src/extensions/AiSuggestionExtension.test.ts +0 -141
  31. package/src/extensions/AiSuggestionExtension.ts +0 -522
  32. package/src/extensions/BlockNodeExtension.ts +0 -134
  33. package/src/extensions/DragHandleExtension.ts +0 -184
  34. package/src/extensions/GridExtension.test.ts +0 -31
  35. package/src/extensions/GridExtension.ts +0 -138
  36. package/src/extensions/MentionExtension.ts +0 -248
  37. package/src/extensions/MergeTagExtension.ts +0 -75
  38. package/src/extensions/SlashCommandExtension.test.ts +0 -147
  39. package/src/extensions/SlashCommandExtension.ts +0 -332
  40. package/src/extensions/TextSizeMarks.ts +0 -73
  41. package/src/index.ts +0 -62
  42. package/src/markdownExtension.ts +0 -19
  43. package/src/markdownStorage.ts +0 -49
  44. package/src/plugin.test.ts +0 -19
  45. package/src/plugin.ts +0 -26
  46. package/src/react/AiSuggestionBanner.tsx +0 -185
  47. package/src/react/BlockNodeView.tsx +0 -99
  48. package/src/react/BlockSidePanel.dom.test.tsx +0 -38
  49. package/src/react/BlockSidePanel.test.ts +0 -412
  50. package/src/react/BlockSidePanel.tsx +0 -451
  51. package/src/react/CollabTextRenderer.tsx +0 -230
  52. package/src/react/FloatingToolbar.tsx +0 -304
  53. package/src/react/MarkdownEditor.tsx +0 -606
  54. package/src/react/MentionMenu.tsx +0 -120
  55. package/src/react/Palette.tsx +0 -86
  56. package/src/react/SlashMenu.tsx +0 -129
  57. package/src/react/TableFloatingToolbar.tsx +0 -154
  58. package/src/react/TiptapEditor.dom.test.tsx +0 -112
  59. package/src/react/TiptapEditor.tsx +0 -776
  60. package/src/react/Toolbar.tsx +0 -438
  61. package/src/react/toolbarButtons.tsx +0 -579
  62. package/src/react/useAiInlineDiff.ts +0 -342
  63. package/src/react/useAiSuggestionBridge.ts +0 -223
  64. package/src/register.test.ts +0 -14
  65. package/src/register.ts +0 -42
  66. package/src/render.test.ts +0 -745
  67. package/src/render.ts +0 -480
  68. package/src/surgicalOps.ts +0 -205
  69. package/src/test/setup.ts +0 -64
@@ -1,579 +0,0 @@
1
- import type { ReactNode } from 'react'
2
- import type { Editor } from '@tiptap/core'
3
- import type { ToolbarButtonId } from '../RichTextField.js'
4
-
5
- /**
6
- * One toolbar button's behavior. The renderer wires `<button>` chrome — this
7
- * descriptor only knows the editor command, the active-state predicate, and
8
- * the icon.
9
- *
10
- * Buttons that depend on extensions registered in later phases (textColor,
11
- * highlight, attachFiles, table*) are registered with `available: false`
12
- * until that phase ships, so config that targets them today is silently
13
- * dropped instead of crashing.
14
- */
15
- export interface ToolbarButtonDef {
16
- id: ToolbarButtonId
17
- label: string
18
- shortcut?: string
19
- icon: ReactNode
20
- /** Phase-A buttons set this to `true`; later-phase placeholders set `false`. */
21
- available: boolean
22
- /** Whether the button reflects the editor's "currently active" state. */
23
- isActive?: (editor: Editor) => boolean
24
- /** Whether the button is disabled given the current selection. */
25
- isDisabled?: (editor: Editor) => boolean
26
- /** Run on click. */
27
- command: (editor: Editor) => void
28
- /**
29
- * Optional opt-in for buttons whose behavior is "open a UI" rather than
30
- * fire a chain — the editor surface keeps its own state for these and the
31
- * default click handler doesn't run a chain.
32
- */
33
- custom?: 'link' | 'textColor' | 'highlight' | 'attachFiles'
34
- }
35
-
36
- const ICON_PROPS = {
37
- width: 14, height: 14, viewBox: '0 0 24 24',
38
- fill: 'none', stroke: 'currentColor',
39
- strokeWidth: 2, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const,
40
- 'aria-hidden': 'true' as const,
41
- }
42
-
43
- // Inline SVGs (lucide.dev paths). Kept inline so the package doesn't pull
44
- // `lucide-react` as a peer dep.
45
- const Icons = {
46
- bold: (
47
- <svg {...ICON_PROPS} strokeWidth={2.25}>
48
- <path d="M6 12h9a4 4 0 0 1 0 8H6Z" />
49
- <path d="M6 4h7a4 4 0 0 1 0 8H6Z" />
50
- </svg>
51
- ),
52
- italic: (
53
- <svg {...ICON_PROPS}>
54
- <line x1="19" y1="4" x2="10" y2="4" />
55
- <line x1="14" y1="20" x2="5" y2="20" />
56
- <line x1="15" y1="4" x2="9" y2="20" />
57
- </svg>
58
- ),
59
- underline: (
60
- <svg {...ICON_PROPS}>
61
- <path d="M6 4v6a6 6 0 0 0 12 0V4" />
62
- <line x1="4" y1="20" x2="20" y2="20" />
63
- </svg>
64
- ),
65
- strike: (
66
- <svg {...ICON_PROPS}>
67
- <path d="M16 4H9a3 3 0 0 0-2.83 4" />
68
- <path d="M14 12a4 4 0 0 1 0 8H6" />
69
- <line x1="4" y1="12" x2="20" y2="12" />
70
- </svg>
71
- ),
72
- subscript: (
73
- <svg {...ICON_PROPS}>
74
- <path d="m4 5 8 10" />
75
- <path d="m12 5-8 10" />
76
- <path d="M20 21h-4c0-1.5.44-2 1.5-2.5S20 17.33 20 16c0-.47-.17-.93-.48-1.29a2.11 2.11 0 0 0-2.62-.44c-.42.24-.74.62-.9 1.07" />
77
- </svg>
78
- ),
79
- superscript: (
80
- <svg {...ICON_PROPS}>
81
- <path d="m4 19 8-10" />
82
- <path d="m12 19-8-10" />
83
- <path d="M20 11h-4c0-1.5.44-2 1.5-2.5S20 7.33 20 6c0-.47-.17-.93-.48-1.29a2.11 2.11 0 0 0-2.62-.44c-.42.24-.74.62-.9 1.07" />
84
- </svg>
85
- ),
86
- code: (
87
- <svg {...ICON_PROPS}>
88
- <polyline points="16 18 22 12 16 6" />
89
- <polyline points="8 6 2 12 8 18" />
90
- </svg>
91
- ),
92
- paragraph: (
93
- <svg {...ICON_PROPS}>
94
- <path d="M13 4v16" />
95
- <path d="M17 4v16" />
96
- <path d="M19 4H9.5a4.5 4.5 0 0 0 0 9H13" />
97
- </svg>
98
- ),
99
- h1: <span className="text-xs font-semibold leading-none">H1</span>,
100
- h2: <span className="text-xs font-semibold leading-none">H2</span>,
101
- h3: <span className="text-xs font-semibold leading-none">H3</span>,
102
- h4: <span className="text-xs font-semibold leading-none">H4</span>,
103
- h5: <span className="text-xs font-semibold leading-none">H5</span>,
104
- h6: <span className="text-xs font-semibold leading-none">H6</span>,
105
- // Sized text-string glyphs match the visual contrast of the marks they
106
- // toggle — lead is the larger of the pair, small is the slightly smaller.
107
- lead: <span className="text-sm font-semibold leading-none tracking-tight">P+</span>,
108
- small: <span className="text-[10px] font-semibold leading-none tracking-tight">P-</span>,
109
- alignStart: (
110
- <svg {...ICON_PROPS}>
111
- <line x1="3" y1="6" x2="21" y2="6" />
112
- <line x1="3" y1="12" x2="15" y2="12" />
113
- <line x1="3" y1="18" x2="18" y2="18" />
114
- </svg>
115
- ),
116
- alignCenter: (
117
- <svg {...ICON_PROPS}>
118
- <line x1="3" y1="6" x2="21" y2="6" />
119
- <line x1="6" y1="12" x2="18" y2="12" />
120
- <line x1="4" y1="18" x2="20" y2="18" />
121
- </svg>
122
- ),
123
- alignEnd: (
124
- <svg {...ICON_PROPS}>
125
- <line x1="3" y1="6" x2="21" y2="6" />
126
- <line x1="9" y1="12" x2="21" y2="12" />
127
- <line x1="6" y1="18" x2="21" y2="18" />
128
- </svg>
129
- ),
130
- alignJustify: (
131
- <svg {...ICON_PROPS}>
132
- <line x1="3" y1="6" x2="21" y2="6" />
133
- <line x1="3" y1="12" x2="21" y2="12" />
134
- <line x1="3" y1="18" x2="21" y2="18" />
135
- </svg>
136
- ),
137
- blockquote: (
138
- <svg {...ICON_PROPS}>
139
- <path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z" />
140
- <path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z" />
141
- </svg>
142
- ),
143
- codeBlock: (
144
- <svg {...ICON_PROPS}>
145
- <rect x="3" y="3" width="18" height="18" rx="2" />
146
- <polyline points="10 10 7 13 10 16" />
147
- <polyline points="14 10 17 13 14 16" />
148
- </svg>
149
- ),
150
- bulletList: (
151
- <svg {...ICON_PROPS}>
152
- <line x1="8" y1="6" x2="21" y2="6" />
153
- <line x1="8" y1="12" x2="21" y2="12" />
154
- <line x1="8" y1="18" x2="21" y2="18" />
155
- <circle cx="4" cy="6" r="1" fill="currentColor" />
156
- <circle cx="4" cy="12" r="1" fill="currentColor" />
157
- <circle cx="4" cy="18" r="1" fill="currentColor" />
158
- </svg>
159
- ),
160
- orderedList: (
161
- <svg {...ICON_PROPS}>
162
- <line x1="10" y1="6" x2="21" y2="6" />
163
- <line x1="10" y1="12" x2="21" y2="12" />
164
- <line x1="10" y1="18" x2="21" y2="18" />
165
- <path d="M4 6h1v4" />
166
- <path d="M4 10h2" />
167
- <path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1" />
168
- </svg>
169
- ),
170
- horizontalRule: (
171
- <svg {...ICON_PROPS}>
172
- <line x1="3" y1="12" x2="21" y2="12" />
173
- </svg>
174
- ),
175
- clearFormatting: (
176
- <svg {...ICON_PROPS}>
177
- <path d="M4 7V4h16v3" />
178
- <line x1="5" y1="20" x2="21" y2="20" />
179
- <line x1="15" y1="4" x2="9" y2="20" />
180
- <line x1="3" y1="3" x2="21" y2="21" />
181
- </svg>
182
- ),
183
- link: (
184
- <svg {...ICON_PROPS}>
185
- <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
186
- <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
187
- </svg>
188
- ),
189
- textColor: (
190
- <svg {...ICON_PROPS}>
191
- <path d="M5 21h14" />
192
- <path d="m6 18 6-13 6 13" />
193
- <path d="M9 13h6" />
194
- </svg>
195
- ),
196
- highlight: (
197
- <svg {...ICON_PROPS}>
198
- <path d="m9 11-6 6v3h3l6-6" />
199
- <path d="M22 12 12 2l-2 2 10 10z" />
200
- <path d="m14 6 6 6" />
201
- </svg>
202
- ),
203
- attachFiles: (
204
- <svg {...ICON_PROPS}>
205
- <rect x="3" y="3" width="18" height="18" rx="2" />
206
- <circle cx="9" cy="9" r="2" />
207
- <path d="m21 15-5-5L5 21" />
208
- </svg>
209
- ),
210
- table: (
211
- <svg {...ICON_PROPS}>
212
- <rect x="3" y="3" width="18" height="18" rx="2" />
213
- <line x1="3" y1="9" x2="21" y2="9" />
214
- <line x1="3" y1="15" x2="21" y2="15" />
215
- <line x1="9" y1="3" x2="9" y2="21" />
216
- <line x1="15" y1="3" x2="15" y2="21" />
217
- </svg>
218
- ),
219
- tableAddColumnBefore: (
220
- <svg {...ICON_PROPS}>
221
- <rect x="11" y="3" width="10" height="18" rx="1" />
222
- <line x1="6" y1="12" x2="2" y2="12" />
223
- <line x1="4" y1="10" x2="4" y2="14" />
224
- </svg>
225
- ),
226
- tableAddColumnAfter: (
227
- <svg {...ICON_PROPS}>
228
- <rect x="3" y="3" width="10" height="18" rx="1" />
229
- <line x1="18" y1="12" x2="22" y2="12" />
230
- <line x1="20" y1="10" x2="20" y2="14" />
231
- </svg>
232
- ),
233
- tableDeleteColumn: (
234
- <svg {...ICON_PROPS}>
235
- <rect x="3" y="3" width="18" height="18" rx="2" />
236
- <line x1="9" y1="3" x2="9" y2="21" />
237
- <line x1="15" y1="3" x2="15" y2="21" />
238
- <line x1="10" y1="9" x2="14" y2="13" />
239
- <line x1="14" y1="9" x2="10" y2="13" />
240
- </svg>
241
- ),
242
- tableAddRowBefore: (
243
- <svg {...ICON_PROPS}>
244
- <rect x="3" y="11" width="18" height="10" rx="1" />
245
- <line x1="12" y1="6" x2="12" y2="2" />
246
- <line x1="10" y1="4" x2="14" y2="4" />
247
- </svg>
248
- ),
249
- tableAddRowAfter: (
250
- <svg {...ICON_PROPS}>
251
- <rect x="3" y="3" width="18" height="10" rx="1" />
252
- <line x1="12" y1="18" x2="12" y2="22" />
253
- <line x1="10" y1="20" x2="14" y2="20" />
254
- </svg>
255
- ),
256
- tableDeleteRow: (
257
- <svg {...ICON_PROPS}>
258
- <rect x="3" y="3" width="18" height="18" rx="2" />
259
- <line x1="3" y1="9" x2="21" y2="9" />
260
- <line x1="3" y1="15" x2="21" y2="15" />
261
- <line x1="9" y1="11" x2="13" y2="13" />
262
- <line x1="13" y1="11" x2="9" y2="13" />
263
- </svg>
264
- ),
265
- tableMergeCells: (
266
- <svg {...ICON_PROPS}>
267
- <rect x="3" y="3" width="18" height="18" rx="2" />
268
- <line x1="3" y1="12" x2="9" y2="12" />
269
- <line x1="15" y1="12" x2="21" y2="12" />
270
- <line x1="12" y1="3" x2="12" y2="9" />
271
- <line x1="12" y1="15" x2="12" y2="21" />
272
- </svg>
273
- ),
274
- tableSplitCell: (
275
- <svg {...ICON_PROPS}>
276
- <rect x="3" y="3" width="18" height="18" rx="2" />
277
- <line x1="3" y1="12" x2="21" y2="12" />
278
- <line x1="12" y1="3" x2="12" y2="21" />
279
- </svg>
280
- ),
281
- tableToggleHeaderRow: (
282
- <svg {...ICON_PROPS}>
283
- <rect x="3" y="3" width="18" height="18" rx="2" />
284
- <rect x="3" y="3" width="18" height="6" fill="currentColor" opacity="0.25" stroke="none" />
285
- <line x1="3" y1="9" x2="21" y2="9" />
286
- <line x1="3" y1="15" x2="21" y2="15" />
287
- </svg>
288
- ),
289
- tableToggleHeaderCell: (
290
- <svg {...ICON_PROPS}>
291
- <rect x="3" y="3" width="18" height="18" rx="2" />
292
- <rect x="3" y="3" width="9" height="6" fill="currentColor" opacity="0.25" stroke="none" />
293
- <line x1="3" y1="9" x2="21" y2="9" />
294
- <line x1="3" y1="15" x2="21" y2="15" />
295
- <line x1="12" y1="3" x2="12" y2="21" />
296
- </svg>
297
- ),
298
- tableDelete: (
299
- <svg {...ICON_PROPS}>
300
- <rect x="3" y="3" width="18" height="18" rx="2" />
301
- <line x1="6" y1="6" x2="18" y2="18" />
302
- <line x1="18" y1="6" x2="6" y2="18" />
303
- </svg>
304
- ),
305
- undo: (
306
- <svg {...ICON_PROPS}>
307
- <path d="M3 7v6h6" />
308
- <path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13" />
309
- </svg>
310
- ),
311
- redo: (
312
- <svg {...ICON_PROPS}>
313
- <path d="M21 7v6h-6" />
314
- <path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3L21 13" />
315
- </svg>
316
- ),
317
- details: (
318
- <svg {...ICON_PROPS}>
319
- <polyline points="6 9 12 15 18 9" />
320
- <line x1="3" y1="4" x2="21" y2="4" />
321
- <line x1="3" y1="20" x2="21" y2="20" />
322
- </svg>
323
- ),
324
- grid: (
325
- <svg {...ICON_PROPS}>
326
- <rect x="3" y="4" width="8" height="16" rx="1" />
327
- <rect x="13" y="4" width="8" height="16" rx="1" />
328
- </svg>
329
- ),
330
- gridDelete: (
331
- <svg {...ICON_PROPS}>
332
- <rect x="3" y="4" width="8" height="16" rx="1" />
333
- <rect x="13" y="4" width="8" height="16" rx="1" />
334
- <line x1="2" y1="22" x2="22" y2="2" />
335
- </svg>
336
- ),
337
- }
338
-
339
- /** Lookup table for every recognized button id. */
340
- export const TOOLBAR_BUTTONS: Record<ToolbarButtonId, ToolbarButtonDef> = {
341
- bold: {
342
- id: 'bold', label: 'Bold', shortcut: 'B', available: true, icon: Icons.bold,
343
- isActive: (ed) => ed.isActive('bold'),
344
- command: (ed) => { ed.chain().focus().toggleBold().run() },
345
- },
346
- italic: {
347
- id: 'italic', label: 'Italic', shortcut: 'I', available: true, icon: Icons.italic,
348
- isActive: (ed) => ed.isActive('italic'),
349
- command: (ed) => { ed.chain().focus().toggleItalic().run() },
350
- },
351
- underline: {
352
- id: 'underline', label: 'Underline', shortcut: 'U', available: true, icon: Icons.underline,
353
- isActive: (ed) => ed.isActive('underline'),
354
- command: (ed) => { ed.chain().focus().toggleUnderline().run() },
355
- },
356
- strike: {
357
- id: 'strike', label: 'Strikethrough', shortcut: '⇧X', available: true, icon: Icons.strike,
358
- isActive: (ed) => ed.isActive('strike'),
359
- command: (ed) => { ed.chain().focus().toggleStrike().run() },
360
- },
361
- subscript: {
362
- id: 'subscript', label: 'Subscript', shortcut: ',', available: true, icon: Icons.subscript,
363
- isActive: (ed) => ed.isActive('subscript'),
364
- command: (ed) => { ed.chain().focus().toggleSubscript().run() },
365
- },
366
- superscript: {
367
- id: 'superscript', label: 'Superscript', shortcut: '.', available: true, icon: Icons.superscript,
368
- isActive: (ed) => ed.isActive('superscript'),
369
- command: (ed) => { ed.chain().focus().toggleSuperscript().run() },
370
- },
371
- code: {
372
- id: 'code', label: 'Inline code', shortcut: 'E', available: true, icon: Icons.code,
373
- isActive: (ed) => ed.isActive('code'),
374
- command: (ed) => { ed.chain().focus().toggleCode().run() },
375
- },
376
- lead: {
377
- id: 'lead', label: 'Lead', available: true, icon: Icons.lead,
378
- isActive: (ed) => ed.isActive('lead'),
379
- // `toggleMark` is the safest call — `chain().toggleLead()` would require
380
- // the command type to be visible to the chain at compile time, and the
381
- // marks ship from a sibling extension file the toolbar can't see typewise.
382
- command: (ed) => { ed.chain().focus().toggleMark('lead').run() },
383
- },
384
- small: {
385
- id: 'small', label: 'Small', available: true, icon: Icons.small,
386
- isActive: (ed) => ed.isActive('small'),
387
- command: (ed) => { ed.chain().focus().toggleMark('small').run() },
388
- },
389
- paragraph: {
390
- id: 'paragraph', label: 'Paragraph', available: true, icon: Icons.paragraph,
391
- isActive: (ed) => ed.isActive('paragraph'),
392
- command: (ed) => { ed.chain().focus().setParagraph().run() },
393
- },
394
- h1: makeHeading(1, Icons.h1),
395
- h2: makeHeading(2, Icons.h2),
396
- h3: makeHeading(3, Icons.h3),
397
- h4: makeHeading(4, Icons.h4),
398
- h5: makeHeading(5, Icons.h5),
399
- h6: makeHeading(6, Icons.h6),
400
- alignStart: makeAlign('left', 'Align left', Icons.alignStart),
401
- alignCenter: makeAlign('center', 'Align center', Icons.alignCenter),
402
- alignEnd: makeAlign('right', 'Align right', Icons.alignEnd),
403
- alignJustify: makeAlign('justify','Justify', Icons.alignJustify),
404
- blockquote: {
405
- id: 'blockquote', label: 'Quote', available: true, icon: Icons.blockquote,
406
- isActive: (ed) => ed.isActive('blockquote'),
407
- command: (ed) => { ed.chain().focus().toggleBlockquote().run() },
408
- },
409
- codeBlock: {
410
- id: 'codeBlock', label: 'Code block', available: true, icon: Icons.codeBlock,
411
- isActive: (ed) => ed.isActive('codeBlock'),
412
- command: (ed) => { ed.chain().focus().toggleCodeBlock().run() },
413
- },
414
- bulletList: {
415
- id: 'bulletList', label: 'Bullet list', available: true, icon: Icons.bulletList,
416
- isActive: (ed) => ed.isActive('bulletList'),
417
- command: (ed) => { ed.chain().focus().toggleBulletList().run() },
418
- },
419
- orderedList: {
420
- id: 'orderedList', label: 'Numbered list', available: true, icon: Icons.orderedList,
421
- isActive: (ed) => ed.isActive('orderedList'),
422
- command: (ed) => { ed.chain().focus().toggleOrderedList().run() },
423
- },
424
- horizontalRule: {
425
- id: 'horizontalRule', label: 'Divider', available: true, icon: Icons.horizontalRule,
426
- command: (ed) => { ed.chain().focus().setHorizontalRule().run() },
427
- },
428
- clearFormatting: {
429
- id: 'clearFormatting', label: 'Clear formatting', available: true, icon: Icons.clearFormatting,
430
- command: (ed) => { ed.chain().focus().clearNodes().unsetAllMarks().run() },
431
- },
432
- link: {
433
- id: 'link', label: 'Link', shortcut: 'K', available: true, icon: Icons.link, custom: 'link',
434
- isActive: (ed) => ed.isActive('link'),
435
- // Click-handling lives in the toolbar itself — the dialog needs React state.
436
- command: () => {},
437
- },
438
- undo: {
439
- id: 'undo', label: 'Undo', shortcut: 'Z', available: true, icon: Icons.undo,
440
- isDisabled: (ed) => !ed.can().undo(),
441
- command: (ed) => { ed.chain().focus().undo().run() },
442
- },
443
- redo: {
444
- id: 'redo', label: 'Redo', shortcut: '⇧Z', available: true, icon: Icons.redo,
445
- isDisabled: (ed) => !ed.can().redo(),
446
- command: (ed) => { ed.chain().focus().redo().run() },
447
- },
448
- textColor: {
449
- id: 'textColor', label: 'Text color', available: true, icon: Icons.textColor, custom: 'textColor',
450
- isActive: (ed) => Boolean(ed.getAttributes('textStyle')['color']),
451
- command: () => {},
452
- },
453
- highlight: {
454
- id: 'highlight', label: 'Highlight', available: true, icon: Icons.highlight, custom: 'highlight',
455
- isActive: (ed) => ed.isActive('highlight'),
456
- command: () => {},
457
- },
458
- attachFiles: {
459
- id: 'attachFiles', label: 'Attach files', available: true, icon: Icons.attachFiles, custom: 'attachFiles',
460
- // Click-handling lives in the toolbar — opens a Base UI dialog driven
461
- // by React state (file picker + alt text + upload progress).
462
- command: () => {},
463
- },
464
- // ---- Tables (Phase F). ----------------------------------------------------
465
- // The "insert table" button is always enabled (creates a table at the cursor);
466
- // every cell-action button is gated on `inTable` so the user can't run a
467
- // no-op command outside a table. Tiptap returns `false` from these commands
468
- // when the cursor is elsewhere — `can()` is the canonical check.
469
- table: {
470
- id: 'table', label: 'Insert table', available: true, icon: Icons.table,
471
- command: (ed) => {
472
- ed.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()
473
- },
474
- },
475
- tableAddColumnBefore: {
476
- id: 'tableAddColumnBefore', label: 'Add column before', available: true, icon: Icons.tableAddColumnBefore,
477
- isDisabled: (ed) => !ed.can().addColumnBefore(),
478
- command: (ed) => { ed.chain().focus().addColumnBefore().run() },
479
- },
480
- tableAddColumnAfter: {
481
- id: 'tableAddColumnAfter', label: 'Add column after', available: true, icon: Icons.tableAddColumnAfter,
482
- isDisabled: (ed) => !ed.can().addColumnAfter(),
483
- command: (ed) => { ed.chain().focus().addColumnAfter().run() },
484
- },
485
- tableDeleteColumn: {
486
- id: 'tableDeleteColumn', label: 'Delete column', available: true, icon: Icons.tableDeleteColumn,
487
- isDisabled: (ed) => !ed.can().deleteColumn(),
488
- command: (ed) => { ed.chain().focus().deleteColumn().run() },
489
- },
490
- tableAddRowBefore: {
491
- id: 'tableAddRowBefore', label: 'Add row before', available: true, icon: Icons.tableAddRowBefore,
492
- isDisabled: (ed) => !ed.can().addRowBefore(),
493
- command: (ed) => { ed.chain().focus().addRowBefore().run() },
494
- },
495
- tableAddRowAfter: {
496
- id: 'tableAddRowAfter', label: 'Add row after', available: true, icon: Icons.tableAddRowAfter,
497
- isDisabled: (ed) => !ed.can().addRowAfter(),
498
- command: (ed) => { ed.chain().focus().addRowAfter().run() },
499
- },
500
- tableDeleteRow: {
501
- id: 'tableDeleteRow', label: 'Delete row', available: true, icon: Icons.tableDeleteRow,
502
- isDisabled: (ed) => !ed.can().deleteRow(),
503
- command: (ed) => { ed.chain().focus().deleteRow().run() },
504
- },
505
- tableMergeCells: {
506
- id: 'tableMergeCells', label: 'Merge cells', available: true, icon: Icons.tableMergeCells,
507
- isDisabled: (ed) => !ed.can().mergeCells(),
508
- command: (ed) => { ed.chain().focus().mergeCells().run() },
509
- },
510
- tableSplitCell: {
511
- id: 'tableSplitCell', label: 'Split cell', available: true, icon: Icons.tableSplitCell,
512
- isDisabled: (ed) => !ed.can().splitCell(),
513
- command: (ed) => { ed.chain().focus().splitCell().run() },
514
- },
515
- tableToggleHeaderRow: {
516
- id: 'tableToggleHeaderRow', label: 'Toggle header row', available: true, icon: Icons.tableToggleHeaderRow,
517
- isDisabled: (ed) => !ed.can().toggleHeaderRow(),
518
- command: (ed) => { ed.chain().focus().toggleHeaderRow().run() },
519
- },
520
- tableToggleHeaderCell: {
521
- id: 'tableToggleHeaderCell', label: 'Toggle header cell', available: true, icon: Icons.tableToggleHeaderCell,
522
- isDisabled: (ed) => !ed.can().toggleHeaderCell(),
523
- isActive: (ed) => ed.isActive('tableHeader'),
524
- command: (ed) => { ed.chain().focus().toggleHeaderCell().run() },
525
- },
526
- tableDelete: {
527
- id: 'tableDelete', label: 'Delete table', available: true, icon: Icons.tableDelete,
528
- isDisabled: (ed) => !ed.can().deleteTable(),
529
- command: (ed) => { ed.chain().focus().deleteTable().run() },
530
- },
531
- // Wraps the current paragraph in a `<details>` node with an empty summary
532
- // and the existing content as the body. `setDetails` is shipped by the
533
- // `@tiptap/extension-details` package; `unsetDetails` (toggle off) sits
534
- // behind a separate slash entry to keep the toolbar surface compact.
535
- details: {
536
- id: 'details', label: 'Collapsible block', available: true, icon: Icons.details,
537
- isActive: (ed) => ed.isActive('details'),
538
- isDisabled: (ed) => !ed.can().setDetails(),
539
- command: (ed) => { ed.chain().focus().setDetails().run() },
540
- },
541
- // Multi-column grid layout. Toolbar button defaults to 2 columns — most-
542
- // common-case UX. Users wanting 3 columns reach for the slash menu's
543
- // dedicated entry. `gridDelete` unwraps the enclosing grid; greyed out
544
- // when the cursor isn't inside one.
545
- grid: {
546
- id: 'grid', label: '2-column grid', available: true, icon: Icons.grid,
547
- isActive: () => false,
548
- isDisabled: (ed) => !ed.can().setGrid({ columns: 2 }),
549
- command: (ed) => { ed.chain().focus().setGrid({ columns: 2 }).run() },
550
- },
551
- gridDelete: {
552
- id: 'gridDelete', label: 'Remove grid', available: true, icon: Icons.gridDelete,
553
- isActive: (ed) => ed.isActive('grid'),
554
- isDisabled: (ed) => !ed.can().unsetGrid(),
555
- command: (ed) => { ed.chain().focus().unsetGrid().run() },
556
- },
557
- }
558
-
559
- function makeHeading(level: 1 | 2 | 3 | 4 | 5 | 6, icon: ReactNode): ToolbarButtonDef {
560
- return {
561
- id: `h${level}` as ToolbarButtonId,
562
- label: `Heading ${level}`,
563
- available: true,
564
- icon,
565
- isActive: (ed) => ed.isActive('heading', { level }),
566
- command: (ed) => { ed.chain().focus().toggleHeading({ level }).run() },
567
- }
568
- }
569
-
570
- function makeAlign(value: 'left' | 'center' | 'right' | 'justify', label: string, icon: ReactNode): ToolbarButtonDef {
571
- return {
572
- id: `align${value === 'left' ? 'Start' : value === 'right' ? 'End' : value.charAt(0).toUpperCase() + value.slice(1)}` as ToolbarButtonId,
573
- label,
574
- available: true,
575
- icon,
576
- isActive: (ed) => ed.isActive({ textAlign: value }),
577
- command: (ed) => { ed.chain().focus().setTextAlign(value).run() },
578
- }
579
- }