@pilotiq/tiptap 3.10.5 → 3.10.7

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 (135) hide show
  1. package/CHANGELOG.md +751 -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/markdownExtension.js +259 -164
  8. package/dist/react/BlockNodeView.d.ts +1 -1
  9. package/dist/react/FloatingToolbar.d.ts +1 -1
  10. package/dist/react/MentionMenu.d.ts +1 -1
  11. package/dist/react/Palette.d.ts +1 -1
  12. package/dist/react/SlashMenu.d.ts +1 -1
  13. package/dist/react/TableFloatingToolbar.d.ts +1 -1
  14. package/dist/react/TiptapEditor.d.ts +1 -1
  15. package/dist/react/Toolbar.d.ts +2 -2
  16. package/package.json +6 -4
  17. package/dist/Block.d.ts.map +0 -1
  18. package/dist/Block.js.map +0 -1
  19. package/dist/MentionProvider.d.ts.map +0 -1
  20. package/dist/MentionProvider.js.map +0 -1
  21. package/dist/PlainTextEditor.d.ts.map +0 -1
  22. package/dist/PlainTextEditor.js.map +0 -1
  23. package/dist/RichTextField.d.ts.map +0 -1
  24. package/dist/RichTextField.js.map +0 -1
  25. package/dist/extensions/AiInlineDiffExtension.d.ts.map +0 -1
  26. package/dist/extensions/AiInlineDiffExtension.js.map +0 -1
  27. package/dist/extensions/AiSuggestionExtension.d.ts.map +0 -1
  28. package/dist/extensions/AiSuggestionExtension.js.map +0 -1
  29. package/dist/extensions/BlockNodeExtension.d.ts.map +0 -1
  30. package/dist/extensions/BlockNodeExtension.js.map +0 -1
  31. package/dist/extensions/DragHandleExtension.d.ts.map +0 -1
  32. package/dist/extensions/DragHandleExtension.js.map +0 -1
  33. package/dist/extensions/GridExtension.d.ts.map +0 -1
  34. package/dist/extensions/GridExtension.js.map +0 -1
  35. package/dist/extensions/MentionExtension.d.ts.map +0 -1
  36. package/dist/extensions/MentionExtension.js.map +0 -1
  37. package/dist/extensions/MergeTagExtension.d.ts.map +0 -1
  38. package/dist/extensions/MergeTagExtension.js.map +0 -1
  39. package/dist/extensions/SlashCommandExtension.d.ts.map +0 -1
  40. package/dist/extensions/SlashCommandExtension.js.map +0 -1
  41. package/dist/extensions/TextSizeMarks.d.ts.map +0 -1
  42. package/dist/extensions/TextSizeMarks.js.map +0 -1
  43. package/dist/index.d.ts.map +0 -1
  44. package/dist/index.js.map +0 -1
  45. package/dist/markdownExtension.d.ts.map +0 -1
  46. package/dist/markdownStorage.d.ts.map +0 -1
  47. package/dist/markdownStorage.js.map +0 -1
  48. package/dist/plugin.d.ts.map +0 -1
  49. package/dist/plugin.js.map +0 -1
  50. package/dist/react/AiSuggestionBanner.d.ts.map +0 -1
  51. package/dist/react/AiSuggestionBanner.js.map +0 -1
  52. package/dist/react/BlockNodeView.d.ts.map +0 -1
  53. package/dist/react/BlockNodeView.js.map +0 -1
  54. package/dist/react/BlockSidePanel.d.ts.map +0 -1
  55. package/dist/react/BlockSidePanel.js.map +0 -1
  56. package/dist/react/CollabTextRenderer.d.ts.map +0 -1
  57. package/dist/react/CollabTextRenderer.js.map +0 -1
  58. package/dist/react/FloatingToolbar.d.ts.map +0 -1
  59. package/dist/react/FloatingToolbar.js.map +0 -1
  60. package/dist/react/MarkdownEditor.d.ts.map +0 -1
  61. package/dist/react/MarkdownEditor.js.map +0 -1
  62. package/dist/react/MentionMenu.d.ts.map +0 -1
  63. package/dist/react/MentionMenu.js.map +0 -1
  64. package/dist/react/Palette.d.ts.map +0 -1
  65. package/dist/react/Palette.js.map +0 -1
  66. package/dist/react/SlashMenu.d.ts.map +0 -1
  67. package/dist/react/SlashMenu.js.map +0 -1
  68. package/dist/react/TableFloatingToolbar.d.ts.map +0 -1
  69. package/dist/react/TableFloatingToolbar.js.map +0 -1
  70. package/dist/react/TiptapEditor.d.ts.map +0 -1
  71. package/dist/react/TiptapEditor.js.map +0 -1
  72. package/dist/react/Toolbar.d.ts.map +0 -1
  73. package/dist/react/Toolbar.js.map +0 -1
  74. package/dist/react/toolbarButtons.d.ts.map +0 -1
  75. package/dist/react/toolbarButtons.js.map +0 -1
  76. package/dist/react/useAiInlineDiff.d.ts.map +0 -1
  77. package/dist/react/useAiInlineDiff.js.map +0 -1
  78. package/dist/react/useAiSuggestionBridge.d.ts.map +0 -1
  79. package/dist/react/useAiSuggestionBridge.js.map +0 -1
  80. package/dist/register.d.ts.map +0 -1
  81. package/dist/register.js.map +0 -1
  82. package/dist/render.d.ts.map +0 -1
  83. package/dist/render.js.map +0 -1
  84. package/dist/surgicalOps.d.ts.map +0 -1
  85. package/dist/surgicalOps.js.map +0 -1
  86. package/dist/test/setup.d.ts.map +0 -1
  87. package/dist/test/setup.js.map +0 -1
  88. package/src/Block.ts +0 -75
  89. package/src/MentionProvider.ts +0 -153
  90. package/src/PlainTextEditor.dom.test.ts +0 -111
  91. package/src/PlainTextEditor.test.ts +0 -158
  92. package/src/PlainTextEditor.ts +0 -229
  93. package/src/RichTextField.test.ts +0 -447
  94. package/src/RichTextField.ts +0 -508
  95. package/src/extensions/AiInlineDiffExtension.ts +0 -286
  96. package/src/extensions/AiSuggestionExtension.test.ts +0 -141
  97. package/src/extensions/AiSuggestionExtension.ts +0 -522
  98. package/src/extensions/BlockNodeExtension.ts +0 -134
  99. package/src/extensions/DragHandleExtension.ts +0 -184
  100. package/src/extensions/GridExtension.test.ts +0 -31
  101. package/src/extensions/GridExtension.ts +0 -138
  102. package/src/extensions/MentionExtension.ts +0 -248
  103. package/src/extensions/MergeTagExtension.ts +0 -75
  104. package/src/extensions/SlashCommandExtension.test.ts +0 -147
  105. package/src/extensions/SlashCommandExtension.ts +0 -332
  106. package/src/extensions/TextSizeMarks.ts +0 -73
  107. package/src/index.ts +0 -62
  108. package/src/markdownExtension.ts +0 -19
  109. package/src/markdownStorage.ts +0 -49
  110. package/src/plugin.test.ts +0 -19
  111. package/src/plugin.ts +0 -26
  112. package/src/react/AiSuggestionBanner.tsx +0 -185
  113. package/src/react/BlockNodeView.tsx +0 -99
  114. package/src/react/BlockSidePanel.dom.test.tsx +0 -38
  115. package/src/react/BlockSidePanel.test.ts +0 -412
  116. package/src/react/BlockSidePanel.tsx +0 -451
  117. package/src/react/CollabTextRenderer.tsx +0 -228
  118. package/src/react/FloatingToolbar.tsx +0 -304
  119. package/src/react/MarkdownEditor.tsx +0 -603
  120. package/src/react/MentionMenu.tsx +0 -120
  121. package/src/react/Palette.tsx +0 -86
  122. package/src/react/SlashMenu.tsx +0 -129
  123. package/src/react/TableFloatingToolbar.tsx +0 -154
  124. package/src/react/TiptapEditor.dom.test.tsx +0 -112
  125. package/src/react/TiptapEditor.tsx +0 -777
  126. package/src/react/Toolbar.tsx +0 -438
  127. package/src/react/toolbarButtons.tsx +0 -579
  128. package/src/react/useAiInlineDiff.ts +0 -342
  129. package/src/react/useAiSuggestionBridge.ts +0 -223
  130. package/src/register.test.ts +0 -14
  131. package/src/register.ts +0 -42
  132. package/src/render.test.ts +0 -745
  133. package/src/render.ts +0 -480
  134. package/src/surgicalOps.ts +0 -205
  135. 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
- }