@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,447 +0,0 @@
1
- import { describe, it } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { TextField, TextareaField } from '@pilotiq/pilotiq'
4
-
5
- import {
6
- RichTextField,
7
- DEFAULT_TOOLBAR_GROUPS,
8
- DEFAULT_TEXT_COLORS,
9
- DEFAULT_HIGHLIGHT_COLORS,
10
- } from './RichTextField.js'
11
- import { Block } from './Block.js'
12
- import { MentionProvider } from './MentionProvider.js'
13
-
14
- describe('RichTextField.toMeta', () => {
15
- it('emits fieldType=richtext with empty defaults', () => {
16
- const meta = RichTextField.make('body').toMeta()
17
- assert.equal(meta.fieldType, 'richtext')
18
- assert.equal(meta.name, 'body')
19
- assert.deepEqual(meta.blocks, [])
20
- assert.equal(meta.slashCommand, true)
21
- assert.equal(meta.floatingToolbar, true)
22
- assert.equal(meta.storage, 'json')
23
- assert.deepEqual(meta.toolbarGroups, DEFAULT_TOOLBAR_GROUPS)
24
- })
25
-
26
- it('serializes blocks via Block.toMeta()', () => {
27
- const meta = RichTextField.make('body').blocks([
28
- Block.make('callout').label('Callout').icon('💡').schema([
29
- TextField.make('title'),
30
- TextareaField.make('content').required(),
31
- ]),
32
- ]).toMeta()
33
-
34
- assert.equal(meta.blocks.length, 1)
35
- const block = meta.blocks[0]!
36
- assert.equal(block.name, 'callout')
37
- assert.equal(block.label, 'Callout')
38
- assert.equal(block.icon, '💡')
39
- assert.equal(block.schema.length, 2)
40
- assert.equal(block.schema[0]!.name, 'title')
41
- assert.equal(block.schema[0]!.fieldType, 'text')
42
- assert.equal(block.schema[1]!.name, 'content')
43
- assert.equal(block.schema[1]!.fieldType, 'textarea')
44
- assert.equal(block.schema[1]!.required, true)
45
- })
46
-
47
- it('honors slashCommand(false)', () => {
48
- const meta = RichTextField.make('body').slashCommand(false).toMeta()
49
- assert.equal(meta.slashCommand, false)
50
- })
51
-
52
- it('inherits required + placeholder from base Field', () => {
53
- const meta = RichTextField.make('body')
54
- .label('Article body')
55
- .placeholder('Start writing…')
56
- .required()
57
- .toMeta()
58
-
59
- assert.equal(meta.label, 'Article body')
60
- assert.equal(meta.placeholder, 'Start writing…')
61
- assert.equal(meta.required, true)
62
- })
63
- })
64
-
65
- describe('RichTextField toolbar API', () => {
66
- it('toolbar(false) hides the top-level toolbar', () => {
67
- const meta = RichTextField.make('body').toolbar(false).toMeta()
68
- assert.equal(meta.toolbarGroups, null)
69
- })
70
-
71
- it('toolbar(true) restores the default after toolbar(false)', () => {
72
- const meta = RichTextField.make('body').toolbar(false).toolbar(true).toMeta()
73
- assert.deepEqual(meta.toolbarGroups, DEFAULT_TOOLBAR_GROUPS)
74
- })
75
-
76
- it('floatingToolbar(false) toggles the selection toolbar', () => {
77
- const meta = RichTextField.make('body').floatingToolbar(false).toMeta()
78
- assert.equal(meta.floatingToolbar, false)
79
- })
80
-
81
- it('toolbarButtons([groups]) replaces the default layout', () => {
82
- const meta = RichTextField.make('body')
83
- .toolbarButtons([
84
- ['bold', 'italic'],
85
- ['undo', 'redo'],
86
- ])
87
- .toMeta()
88
- assert.deepEqual(meta.toolbarGroups, [
89
- ['bold', 'italic'],
90
- ['undo', 'redo'],
91
- ])
92
- })
93
-
94
- it('toolbarButtons(null) hides the toolbar', () => {
95
- const meta = RichTextField.make('body').toolbarButtons(null).toMeta()
96
- assert.equal(meta.toolbarGroups, null)
97
- })
98
-
99
- it('disableToolbarButtons removes ids from every group', () => {
100
- const meta = RichTextField.make('body')
101
- .disableToolbarButtons(['italic', 'undo', 'redo'])
102
- .toMeta()
103
- const flat = (meta.toolbarGroups ?? []).flat()
104
- assert.equal(flat.includes('italic'), false)
105
- assert.equal(flat.includes('undo'), false)
106
- assert.equal(flat.includes('redo'), false)
107
- assert.equal(flat.includes('bold'), true)
108
- })
109
-
110
- it('disableToolbarButtons drops a group when it empties out', () => {
111
- const meta = RichTextField.make('body')
112
- .toolbarButtons([
113
- ['bold'],
114
- ['italic'],
115
- ])
116
- .disableToolbarButtons(['italic'])
117
- .toMeta()
118
- assert.deepEqual(meta.toolbarGroups, [['bold']])
119
- })
120
-
121
- it('enableToolbarButtons appends to the last group', () => {
122
- const meta = RichTextField.make('body')
123
- .toolbarButtons([
124
- ['bold', 'italic'],
125
- ['undo', 'redo'],
126
- ])
127
- .enableToolbarButtons(['horizontalRule'])
128
- .toMeta()
129
- assert.deepEqual(meta.toolbarGroups, [
130
- ['bold', 'italic'],
131
- ['undo', 'redo', 'horizontalRule'],
132
- ])
133
- })
134
-
135
- it('enableToolbarButtons obeys disable list', () => {
136
- const meta = RichTextField.make('body')
137
- .toolbarButtons([['bold']])
138
- .enableToolbarButtons(['italic', 'underline'])
139
- .disableToolbarButtons(['underline'])
140
- .toMeta()
141
- assert.deepEqual(meta.toolbarGroups, [['bold', 'italic']])
142
- })
143
-
144
- it('accepts lead + small in custom toolbar groups', () => {
145
- const meta = RichTextField.make('body')
146
- .toolbarButtons([
147
- ['bold', 'italic'],
148
- ['lead', 'small'],
149
- ])
150
- .toMeta()
151
- assert.deepEqual(meta.toolbarGroups, [
152
- ['bold', 'italic'],
153
- ['lead', 'small'],
154
- ])
155
- })
156
- })
157
-
158
- describe('RichTextField color palettes', () => {
159
- it('defaults to the bundled text-color palette', () => {
160
- const meta = RichTextField.make('body').toMeta()
161
- assert.deepEqual(meta.textColors, DEFAULT_TEXT_COLORS)
162
- assert.deepEqual(meta.highlightColors, DEFAULT_HIGHLIGHT_COLORS)
163
- assert.equal(meta.customTextColors, false)
164
- })
165
-
166
- it('textColors([...]) replaces the palette', () => {
167
- const palette = [
168
- { value: '#1e293b', label: 'Slate' },
169
- { value: '#dc2626', label: 'Red', dark: '#fca5a5' },
170
- ]
171
- const meta = RichTextField.make('body').textColors(palette).toMeta()
172
- assert.deepEqual(meta.textColors, palette)
173
- })
174
-
175
- it('customTextColors() opts in to the free-form picker', () => {
176
- const meta = RichTextField.make('body').customTextColors().toMeta()
177
- assert.equal(meta.customTextColors, true)
178
- })
179
-
180
- it('highlightColors([...]) replaces the highlight palette', () => {
181
- const palette = [{ value: '#fef08a', label: 'Yellow' }]
182
- const meta = RichTextField.make('body').highlightColors(palette).toMeta()
183
- assert.deepEqual(meta.highlightColors, palette)
184
- })
185
-
186
- it('passing null restores the defaults', () => {
187
- const meta = RichTextField.make('body')
188
- .textColors([{ value: '#000', label: 'Black' }])
189
- .textColors(null)
190
- .toMeta()
191
- assert.deepEqual(meta.textColors, DEFAULT_TEXT_COLORS)
192
- })
193
- })
194
-
195
- describe('RichTextField file attachments', () => {
196
- it('defaults: resizableImages=false; no attachment options; toolbar strips attachFiles when no adapter', () => {
197
- const meta = RichTextField.make('body')
198
- .toolbarButtons([['bold', 'attachFiles']])
199
- .toMeta()
200
- assert.equal(meta.resizableImages, false)
201
- assert.equal('fileAttachmentsAcceptedFileTypes' in meta, false)
202
- assert.equal('fileAttachmentsMaxSize' in meta, false)
203
- assert.equal('fileAttachmentsDirectory' in meta, false)
204
- assert.equal('fileAttachmentsVisibility' in meta, false)
205
- assert.equal('uploadUrl' in meta, false)
206
- // attachFiles stripped from the resolved groups when no adapter is wired.
207
- assert.deepEqual(meta.toolbarGroups, [['bold']])
208
- })
209
-
210
- it('preserves attachFiles + stamps uploadUrl when adapter is wired', () => {
211
- const meta = RichTextField.make('body')
212
- .toolbarButtons([['bold', 'attachFiles']])
213
- .toMeta({ uploadUrl: '/admin/_uploads', hasUploadAdapter: true })
214
- assert.deepEqual(meta.toolbarGroups, [['bold', 'attachFiles']])
215
- assert.equal(meta.uploadUrl, '/admin/_uploads')
216
- })
217
-
218
- it('drops a toolbar group entirely when attachFiles was its only button', () => {
219
- const meta = RichTextField.make('body')
220
- .toolbarButtons([['bold'], ['attachFiles']])
221
- .toMeta()
222
- assert.deepEqual(meta.toolbarGroups, [['bold']])
223
- })
224
-
225
- it('exposes resize + size + accept + directory + visibility', () => {
226
- const meta = RichTextField.make('body')
227
- .resizableImages()
228
- .fileAttachmentsAcceptedFileTypes(['image/*'])
229
- .fileAttachmentsMaxSize(2_000_000)
230
- .fileAttachmentsDirectory('articles')
231
- .fileAttachmentsVisibility('private')
232
- .toMeta()
233
- assert.equal(meta.resizableImages, true)
234
- assert.deepEqual(meta.fileAttachmentsAcceptedFileTypes, ['image/*'])
235
- assert.equal(meta.fileAttachmentsMaxSize, 2_000_000)
236
- assert.equal(meta.fileAttachmentsDirectory, 'articles')
237
- assert.equal(meta.fileAttachmentsVisibility, 'private')
238
- })
239
- })
240
-
241
- describe('RichTextField merge tags + mentions', () => {
242
- it('mergeTags + mentions default to empty arrays', () => {
243
- const meta = RichTextField.make('body').toMeta()
244
- assert.deepEqual(meta.mergeTags, [])
245
- assert.deepEqual(meta.mentions, [])
246
- })
247
-
248
- it('mergeTags([...]) round-trips through meta', () => {
249
- const meta = RichTextField.make('body')
250
- .mergeTags(['firstName', 'company'])
251
- .toMeta()
252
- assert.deepEqual(meta.mergeTags, ['firstName', 'company'])
253
- })
254
-
255
- it('mentions([...]) serializes each provider via toMeta()', () => {
256
- const meta = RichTextField.make('body')
257
- .mentions([
258
- MentionProvider.make('@').items([
259
- { id: 'sleman', label: 'Sleman' },
260
- { id: 'alex', label: 'Alex', group: 'Team' },
261
- ]),
262
- MentionProvider.make('#').items([
263
- { id: 'general', label: 'general' },
264
- ]),
265
- ])
266
- .toMeta()
267
- assert.equal(meta.mentions.length, 2)
268
- assert.equal(meta.mentions[0]!.trigger, '@')
269
- assert.equal(meta.mentions[0]!.items.length, 2)
270
- assert.equal(meta.mentions[0]!.items[0]!.id, 'sleman')
271
- assert.equal(meta.mentions[0]!.items[1]!.group, 'Team')
272
- assert.equal(meta.mentions[1]!.trigger, '#')
273
- })
274
- })
275
-
276
- describe('MentionProvider', () => {
277
- it('rejects non-single-character triggers', () => {
278
- assert.throws(() => MentionProvider.make(''), /single character/)
279
- assert.throws(() => MentionProvider.make('@@'), /single character/)
280
- })
281
-
282
- it('items() replaces the static list', () => {
283
- const p = MentionProvider.make('@').items([{ id: 'a', label: 'A' }])
284
- assert.equal(p.getTrigger(), '@')
285
- assert.equal(p.getItems().length, 1)
286
- assert.equal(p.getItems()[0]!.id, 'a')
287
- })
288
-
289
- it('toMeta() copies the items array (snapshot, not reference)', () => {
290
- const items = [{ id: 'a', label: 'A' }]
291
- const meta = MentionProvider.make('@').items(items).toMeta()
292
- items.push({ id: 'b', label: 'B' })
293
- assert.equal(meta.items.length, 1)
294
- })
295
-
296
- it('static providers report isAsync=false and emit no async flag', () => {
297
- const p = MentionProvider.make('@').items([{ id: 'a', label: 'A' }])
298
- assert.equal(p.isAsync(), false)
299
- const meta = p.toMeta()
300
- assert.equal('async' in meta, false)
301
- })
302
-
303
- it('itemsUsing(fn) flips isAsync=true and empties the inlined items', () => {
304
- const p = MentionProvider.make('@').itemsUsing(async () => [{ id: 'a', label: 'A' }])
305
- assert.equal(p.isAsync(), true)
306
- const meta = p.toMeta()
307
- assert.equal(meta.async, true)
308
- assert.deepEqual(meta.items, [])
309
- })
310
-
311
- it('runResolver runs the static list when no async fn is set', async () => {
312
- const p = MentionProvider.make('@').items([
313
- { id: 'sleman', label: 'Sleman' },
314
- { id: 'alex', label: 'Alex' },
315
- ])
316
- const items = await p.runResolver('al', { user: null })
317
- // Returns the full list — filtering is the menu's job.
318
- assert.equal(items.length, 2)
319
- })
320
-
321
- it('runResolver awaits an async resolver and forwards query + ctx', async () => {
322
- let seenQuery: string | undefined
323
- let seenUser: unknown
324
- const p = MentionProvider.make('@').itemsUsing(async (query, ctx) => {
325
- seenQuery = query
326
- seenUser = ctx.user
327
- return [{ id: query, label: `Hit: ${query}` }]
328
- })
329
- const items = await p.runResolver('alex', { user: { id: 1 } })
330
- assert.equal(seenQuery, 'alex')
331
- assert.deepEqual(seenUser, { id: 1 })
332
- assert.equal(items.length, 1)
333
- assert.equal(items[0]!.id, 'alex')
334
- assert.equal(items[0]!.label, 'Hit: alex')
335
- })
336
-
337
- it('runResolver coerces non-array returns to []', async () => {
338
- const p = MentionProvider.make('@').itemsUsing((async () => null) as never)
339
- const items = await p.runResolver('q', {})
340
- assert.deepEqual(items, [])
341
- })
342
-
343
- it('items() after itemsUsing() warns and switches to static (last call wins)', () => {
344
- const orig = console.warn
345
- const warnings: string[] = []
346
- console.warn = (...args: unknown[]) => warnings.push(String(args[0]))
347
- try {
348
- const p = MentionProvider.make('@')
349
- .itemsUsing(async () => [{ id: 'x', label: 'X' }])
350
- .items([{ id: 'a', label: 'A' }])
351
- assert.equal(p.isAsync(), false)
352
- assert.equal(warnings.length, 1)
353
- assert.match(warnings[0]!, /MentionProvider.*items\(\) called after.*itemsUsing/)
354
- } finally {
355
- console.warn = orig
356
- }
357
- })
358
-
359
- it('itemsUsing() after items() warns and clears the static list', () => {
360
- const orig = console.warn
361
- const warnings: string[] = []
362
- console.warn = (...args: unknown[]) => warnings.push(String(args[0]))
363
- try {
364
- const p = MentionProvider.make('@')
365
- .items([{ id: 'a', label: 'A' }])
366
- .itemsUsing(async () => [{ id: 'x', label: 'X' }])
367
- assert.equal(p.isAsync(), true)
368
- assert.equal(warnings.length, 1)
369
- assert.match(warnings[0]!, /MentionProvider.*itemsUsing\(\) called after.*items\(\)/)
370
- } finally {
371
- console.warn = orig
372
- }
373
- })
374
- })
375
-
376
- describe('RichTextField mention resolution', () => {
377
- it('hasAsyncMentions() is false when every provider is static', () => {
378
- const f = RichTextField.make('body').mentions([
379
- MentionProvider.make('@').items([{ id: 'a', label: 'A' }]),
380
- ])
381
- assert.equal(f.hasAsyncMentions(), false)
382
- })
383
-
384
- it('hasAsyncMentions() is true when at least one provider is async', () => {
385
- const f = RichTextField.make('body').mentions([
386
- MentionProvider.make('@').items([{ id: 'a', label: 'A' }]),
387
- MentionProvider.make('#').itemsUsing(async () => []),
388
- ])
389
- assert.equal(f.hasAsyncMentions(), true)
390
- })
391
-
392
- it('resolveMention dispatches by trigger char', async () => {
393
- const f = RichTextField.make('body').mentions([
394
- MentionProvider.make('@').itemsUsing(async (q) => [{ id: q, label: `User:${q}` }]),
395
- MentionProvider.make('#').itemsUsing(async (q) => [{ id: q, label: `Channel:${q}` }]),
396
- ])
397
- const userHits = await f.resolveMention('@', 'sleman', {})
398
- const chanHits = await f.resolveMention('#', 'general', {})
399
- assert.equal(userHits?.[0]?.label, 'User:sleman')
400
- assert.equal(chanHits?.[0]?.label, 'Channel:general')
401
- })
402
-
403
- it('resolveMention returns null for unknown triggers', async () => {
404
- const f = RichTextField.make('body').mentions([
405
- MentionProvider.make('@').itemsUsing(async () => []),
406
- ])
407
- const items = await f.resolveMention('!', 'q', {})
408
- assert.equal(items, null)
409
- })
410
-
411
- it('mentionsUrl is omitted from meta until withMentionsUrl stamps it', () => {
412
- const f = RichTextField.make('body').mentions([
413
- MentionProvider.make('@').itemsUsing(async () => []),
414
- ])
415
- assert.equal('mentionsUrl' in f.toMeta(), false)
416
- f.withMentionsUrl('/admin/articles/_form/article-form/mentions')
417
- assert.equal(f.toMeta().mentionsUrl, '/admin/articles/_form/article-form/mentions')
418
- })
419
- })
420
-
421
- describe('RichTextField storage', () => {
422
- it('defaults to json', () => {
423
- const meta = RichTextField.make('body').toMeta()
424
- assert.equal(meta.storage, 'json')
425
- })
426
-
427
- it('storage("html") opts into HTML serialization', () => {
428
- const meta = RichTextField.make('body').storage('html').toMeta()
429
- assert.equal(meta.storage, 'html')
430
- })
431
- })
432
-
433
- describe('Block.toMeta', () => {
434
- it('uses block name as label fallback', () => {
435
- const meta = Block.make('hero').toMeta()
436
- assert.equal(meta.name, 'hero')
437
- assert.equal(meta.label, 'hero')
438
- assert.equal(meta.icon, undefined)
439
- assert.deepEqual(meta.schema, [])
440
- })
441
-
442
- it('preserves icon and label when set', () => {
443
- const meta = Block.make('callout').label('Callout block').icon('💡').toMeta()
444
- assert.equal(meta.label, 'Callout block')
445
- assert.equal(meta.icon, '💡')
446
- })
447
- })