@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,412 +0,0 @@
1
- import { describe, it } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { readBlockFieldValue, coerceBlockValues, clampPanelWidth } from './BlockSidePanel.js'
4
- import { BlockNodeExtension } from '../extensions/BlockNodeExtension.js'
5
-
6
- describe('readBlockFieldValue', () => {
7
- it('passes strings through for text fields', () => {
8
- const target = { type: 'text', value: 'hello world' }
9
- const result = readBlockFieldValue(target, { fieldType: 'text' })
10
- assert.equal(result, 'hello world')
11
- })
12
-
13
- it('passes strings through for textarea fields', () => {
14
- const target = { type: 'textarea', value: 'multi\nline' }
15
- const result = readBlockFieldValue(target, { fieldType: 'textarea' })
16
- assert.equal(result, 'multi\nline')
17
- })
18
-
19
- it('treats toggle as a boolean from `checked`', () => {
20
- assert.equal(readBlockFieldValue({ value: 'on', checked: true }, { fieldType: 'toggle' }), true)
21
- assert.equal(readBlockFieldValue({ value: 'off', checked: false }, { fieldType: 'toggle' }), false)
22
- })
23
-
24
- it('treats checkbox as a boolean from `checked`', () => {
25
- assert.equal(readBlockFieldValue({ value: 'on', checked: true }, { fieldType: 'checkbox' }), true)
26
- assert.equal(readBlockFieldValue({ value: 'off', checked: false }, { fieldType: 'checkbox' }), false)
27
- })
28
-
29
- it('coerces number fields to a Number, null when empty', () => {
30
- assert.equal(readBlockFieldValue({ value: '42', type: 'number' }, { fieldType: 'number' }), 42)
31
- assert.equal(readBlockFieldValue({ value: '3.14', type: 'number' }, { fieldType: 'number' }), 3.14)
32
- assert.equal(readBlockFieldValue({ value: '', type: 'number' }, { fieldType: 'number' }), null)
33
- })
34
-
35
- it('keeps slider values as Number too', () => {
36
- assert.equal(readBlockFieldValue({ value: '7' }, { fieldType: 'slider' }), 7)
37
- })
38
-
39
- it('falls back to raw string when number parse fails (NaN guard)', () => {
40
- // Browsers normally clamp invalid numeric input to '', but defensively
41
- // we must not silently emit NaN — round-trips into `JSON.stringify`
42
- // become `null`, and the next reload would lose the value.
43
- assert.equal(readBlockFieldValue({ value: 'abc' }, { fieldType: 'number' }), 'abc')
44
- })
45
-
46
- it('treats unknown fieldTypes as plain string', () => {
47
- assert.equal(readBlockFieldValue({ value: 'whatever' }, { fieldType: 'futuristic' }), 'whatever')
48
- assert.equal(readBlockFieldValue({ value: 'no type' }, {}), 'no type')
49
- })
50
- })
51
-
52
- describe('coerceBlockValues — flat fields', () => {
53
- it('passes plain strings through for text / textarea / select', () => {
54
- const result = coerceBlockValues(
55
- { title: 'hello', body: 'multi\nline', size: 'lg' },
56
- [
57
- { name: 'title', fieldType: 'text' },
58
- { name: 'body', fieldType: 'textarea' },
59
- { name: 'size', fieldType: 'select' },
60
- ],
61
- )
62
- assert.deepEqual(result, { title: 'hello', body: 'multi\nline', size: 'lg' })
63
- })
64
-
65
- it('coerces toggle / checkbox `true` / `false` strings to booleans', () => {
66
- const result = coerceBlockValues(
67
- { active: 'true', subscribe: 'false' },
68
- [
69
- { name: 'active', fieldType: 'toggle' },
70
- { name: 'subscribe', fieldType: 'checkbox' },
71
- ],
72
- )
73
- assert.equal(result['active'], true)
74
- assert.equal(result['subscribe'], false)
75
- })
76
-
77
- it('coerces number / slider, null on empty, raw string on NaN', () => {
78
- const result = coerceBlockValues(
79
- { count: '42', dial: '7', empty: '', bad: 'abc' },
80
- [
81
- { name: 'count', fieldType: 'number' },
82
- { name: 'dial', fieldType: 'slider' },
83
- { name: 'empty', fieldType: 'number' },
84
- { name: 'bad', fieldType: 'number' },
85
- ],
86
- )
87
- assert.equal(result['count'], 42)
88
- assert.equal(result['dial'], 7)
89
- assert.equal(result['empty'], null)
90
- assert.equal(result['bad'], 'abc')
91
- })
92
- })
93
-
94
- describe('coerceBlockValues — deferred V1 field types', () => {
95
- it('parses tagsInput JSON-encoded hidden input back to string[]', () => {
96
- const result = coerceBlockValues(
97
- { tags: '["alpha","beta"]', empty: '' },
98
- [
99
- { name: 'tags', fieldType: 'tagsInput' },
100
- { name: 'empty', fieldType: 'tagsInput' },
101
- ],
102
- )
103
- assert.deepEqual(result['tags'], ['alpha', 'beta'])
104
- assert.deepEqual(result['empty'], [])
105
- })
106
-
107
- it('returns [] for tagsInput when JSON parse fails (defensive)', () => {
108
- const result = coerceBlockValues(
109
- { tags: 'not json' },
110
- [{ name: 'tags', fieldType: 'tagsInput' }],
111
- )
112
- assert.deepEqual(result['tags'], [])
113
- })
114
-
115
- it('parses keyValue JSON-encoded hidden input back to object', () => {
116
- const result = coerceBlockValues(
117
- { meta: '{"author":"sue","year":2026}' },
118
- [{ name: 'meta', fieldType: 'keyValue' }],
119
- )
120
- assert.deepEqual(result['meta'], { author: 'sue', year: 2026 })
121
- })
122
-
123
- it('returns {} for keyValue when JSON parse fails or value is array', () => {
124
- const r1 = coerceBlockValues({ m: 'bad' }, [{ name: 'm', fieldType: 'keyValue' }])
125
- assert.deepEqual(r1['m'], {})
126
- const r2 = coerceBlockValues({ m: '["a","b"]' }, [{ name: 'm', fieldType: 'keyValue' }])
127
- assert.deepEqual(r2['m'], {})
128
- })
129
-
130
- it('passes single-file fileUpload URL through as a string', () => {
131
- const result = coerceBlockValues(
132
- { hero: '/uploads/cover.png' },
133
- [{ name: 'hero', fieldType: 'fileUpload' }],
134
- )
135
- assert.equal(result['hero'], '/uploads/cover.png')
136
- })
137
-
138
- it('parses multi-file fileUpload JSON-encoded array', () => {
139
- const result = coerceBlockValues(
140
- { gallery: '["/a.png","/b.png"]' },
141
- [{ name: 'gallery', fieldType: 'fileUpload', multiple: true }],
142
- )
143
- assert.deepEqual(result['gallery'], ['/a.png', '/b.png'])
144
- })
145
-
146
- it('passes markdown textarea through as a plain string', () => {
147
- const result = coerceBlockValues(
148
- { body: '# Heading\n\nSome **bold** text.' },
149
- [{ name: 'body', fieldType: 'markdown' }],
150
- )
151
- assert.equal(result['body'], '# Heading\n\nSome **bold** text.')
152
- })
153
-
154
- it('parses checkboxList JSON-encoded hidden input back to string[]', () => {
155
- const result = coerceBlockValues(
156
- { roles: '["admin","editor"]' },
157
- [{ name: 'roles', fieldType: 'checkboxList' }],
158
- )
159
- assert.deepEqual(result['roles'], ['admin', 'editor'])
160
- })
161
- })
162
-
163
- describe('coerceBlockValues — Repeater rows', () => {
164
- it('coerces each row against the field template recursively', () => {
165
- const result = coerceBlockValues(
166
- {
167
- items: [
168
- { title: 'first', qty: '3', enabled: 'true', tags: '["x","y"]' },
169
- { title: 'second', qty: '', enabled: 'false', tags: '[]' },
170
- ],
171
- },
172
- [
173
- {
174
- name: 'items',
175
- fieldType:'repeater',
176
- template: [
177
- { name: 'title', fieldType: 'text' },
178
- { name: 'qty', fieldType: 'number' },
179
- { name: 'enabled', fieldType: 'toggle' },
180
- { name: 'tags', fieldType: 'tagsInput' },
181
- ],
182
- },
183
- ],
184
- )
185
- assert.deepEqual(result['items'], [
186
- { title: 'first', qty: 3, enabled: true, tags: ['x', 'y'] },
187
- { title: 'second', qty: null, enabled: false, tags: [] },
188
- ])
189
- })
190
-
191
- it('returns [] for a repeater whose value is missing or non-array', () => {
192
- const result = coerceBlockValues(
193
- { items: undefined as unknown },
194
- [{ name: 'items', fieldType: 'repeater', template: [] }],
195
- )
196
- assert.deepEqual(result['items'], [])
197
- })
198
-
199
- it('handles a Repeater nested inside another Repeater', () => {
200
- const result = coerceBlockValues(
201
- {
202
- sections: [
203
- {
204
- heading: 'A',
205
- rows: [
206
- { label: 'r1', n: '1' },
207
- { label: 'r2', n: '2' },
208
- ],
209
- },
210
- ],
211
- },
212
- [
213
- {
214
- name: 'sections',
215
- fieldType: 'repeater',
216
- template: [
217
- { name: 'heading', fieldType: 'text' },
218
- {
219
- name: 'rows',
220
- fieldType: 'repeater',
221
- template: [
222
- { name: 'label', fieldType: 'text' },
223
- { name: 'n', fieldType: 'number' },
224
- ],
225
- },
226
- ],
227
- },
228
- ],
229
- )
230
- assert.deepEqual(result['sections'], [
231
- {
232
- heading: 'A',
233
- rows: [
234
- { label: 'r1', n: 1 },
235
- { label: 'r2', n: 2 },
236
- ],
237
- },
238
- ])
239
- })
240
- })
241
-
242
- describe('coerceBlockValues — Builder rows', () => {
243
- it('coerces each row.data against the matching block template', () => {
244
- const result = coerceBlockValues(
245
- {
246
- content: [
247
- { type: 'paragraph', data: { text: 'hello', live: 'true' } },
248
- { type: 'image', data: { src: '/a.png', width: '320' } },
249
- ],
250
- },
251
- [
252
- {
253
- name: 'content',
254
- fieldType: 'builder',
255
- blocks: [
256
- {
257
- name: 'paragraph',
258
- template: [
259
- { name: 'text', fieldType: 'textarea' },
260
- { name: 'live', fieldType: 'toggle' },
261
- ],
262
- },
263
- {
264
- name: 'image',
265
- template: [
266
- { name: 'src', fieldType: 'text' },
267
- { name: 'width', fieldType: 'number' },
268
- ],
269
- },
270
- ],
271
- },
272
- ],
273
- )
274
- assert.deepEqual(result['content'], [
275
- { type: 'paragraph', data: { text: 'hello', live: true } },
276
- { type: 'image', data: { src: '/a.png', width: 320 } },
277
- ])
278
- })
279
-
280
- it('passes unknown Builder block types through verbatim (no schema match)', () => {
281
- const result = coerceBlockValues(
282
- {
283
- content: [
284
- { type: 'unknown', data: { foo: 'bar' } },
285
- ],
286
- },
287
- [{
288
- name: 'content',
289
- fieldType: 'builder',
290
- blocks: [{ name: 'paragraph', template: [] }],
291
- }],
292
- )
293
- // Unknown type: data is preserved as-is, no coercion attempted.
294
- assert.deepEqual(result['content'], [
295
- { type: 'unknown', data: { foo: 'bar' } },
296
- ])
297
- })
298
- })
299
-
300
- describe('BlockNodeExtension options', () => {
301
- it('defaults onEdit to undefined when not configured', () => {
302
- const ext = BlockNodeExtension
303
- // `addOptions` returns the defaults; not exercising the full Tiptap
304
- // configure pipeline here because that requires a live editor.
305
- const defaults = ext.config.addOptions?.call({ name: 'pilotiqBlock', parent: undefined })
306
- assert.deepEqual(defaults, { blocks: [] })
307
- assert.equal((defaults as { onEdit?: unknown } | undefined)?.onEdit, undefined)
308
- })
309
-
310
- it('configure({ onEdit }) round-trips through Tiptap options', () => {
311
- const seen: number[] = []
312
- const onEdit = (p: number): void => { seen.push(p) }
313
- const configured = BlockNodeExtension.configure({ blocks: [], onEdit })
314
- // Tiptap stores configured options on `.options` after configure().
315
- const opts = configured.options as { blocks: unknown[]; onEdit?: (p: number) => void }
316
- assert.equal(typeof opts.onEdit, 'function')
317
- opts.onEdit?.(5)
318
- opts.onEdit?.(11)
319
- assert.deepEqual(seen, [5, 11])
320
- })
321
-
322
- it('configure without onEdit leaves it undefined', () => {
323
- const configured = BlockNodeExtension.configure({ blocks: [] })
324
- const opts = configured.options as { blocks: unknown[]; onEdit?: (p: number) => void }
325
- assert.equal(opts.onEdit, undefined)
326
- })
327
-
328
- it('addKeyboardShortcuts exposes Mod-e', () => {
329
- // The shortcut handler closes over `this.editor` and `this.options` —
330
- // we don't need the live editor to verify the binding key is wired.
331
- const ctx = {
332
- editor: { state: { selection: {} } },
333
- options: { blocks: [] },
334
- name: 'pilotiqBlock',
335
- } as unknown as ThisParameterType<NonNullable<typeof BlockNodeExtension.config.addKeyboardShortcuts>>
336
- const shortcuts = BlockNodeExtension.config.addKeyboardShortcuts?.call(ctx)
337
- assert.ok(shortcuts && typeof shortcuts === 'object')
338
- assert.equal(typeof (shortcuts as { 'Mod-e'?: unknown })['Mod-e'], 'function')
339
- })
340
-
341
- it('Mod-e returns false when onEdit is unset (no host listening)', () => {
342
- const ctx = {
343
- editor: { state: { selection: { node: { type: { name: 'pilotiqBlock' } }, from: 7 } } },
344
- options: { blocks: [] },
345
- name: 'pilotiqBlock',
346
- } as unknown as ThisParameterType<NonNullable<typeof BlockNodeExtension.config.addKeyboardShortcuts>>
347
- const shortcuts = BlockNodeExtension.config.addKeyboardShortcuts?.call(ctx) as Record<string, () => boolean>
348
- assert.equal(shortcuts['Mod-e']?.(), false)
349
- })
350
-
351
- it('Mod-e returns false when the selection is not a block NodeSelection', () => {
352
- const calls: number[] = []
353
- const ctx = {
354
- editor: { state: { selection: { from: 3, to: 3 } } },
355
- options: { blocks: [], onEdit: (p: number) => calls.push(p) },
356
- name: 'pilotiqBlock',
357
- } as unknown as ThisParameterType<NonNullable<typeof BlockNodeExtension.config.addKeyboardShortcuts>>
358
- const shortcuts = BlockNodeExtension.config.addKeyboardShortcuts?.call(ctx) as Record<string, () => boolean>
359
- assert.equal(shortcuts['Mod-e']?.(), false)
360
- assert.deepEqual(calls, [])
361
- })
362
-
363
- it('Mod-e calls onEdit(pos) and returns true when a block is NodeSelected', () => {
364
- const calls: number[] = []
365
- const ctx = {
366
- editor: { state: { selection: { node: { type: { name: 'pilotiqBlock' } }, from: 12 } } },
367
- options: { blocks: [], onEdit: (p: number) => calls.push(p) },
368
- name: 'pilotiqBlock',
369
- } as unknown as ThisParameterType<NonNullable<typeof BlockNodeExtension.config.addKeyboardShortcuts>>
370
- const shortcuts = BlockNodeExtension.config.addKeyboardShortcuts?.call(ctx) as Record<string, () => boolean>
371
- assert.equal(shortcuts['Mod-e']?.(), true)
372
- assert.deepEqual(calls, [12])
373
- })
374
- })
375
-
376
- describe('clampPanelWidth', () => {
377
- it('falls back to the default for non-finite values', () => {
378
- assert.equal(clampPanelWidth(NaN), 320)
379
- assert.equal(clampPanelWidth(Infinity), 320)
380
- assert.equal(clampPanelWidth(-Infinity), 320)
381
- assert.equal(clampPanelWidth(undefined), 320)
382
- assert.equal(clampPanelWidth(null), 320)
383
- assert.equal(clampPanelWidth(''), 320)
384
- assert.equal(clampPanelWidth('garbage'), 320)
385
- })
386
-
387
- it('clamps below the minimum up to 240', () => {
388
- assert.equal(clampPanelWidth(0), 240)
389
- assert.equal(clampPanelWidth(-50), 240)
390
- assert.equal(clampPanelWidth(239), 240)
391
- })
392
-
393
- it('clamps above the maximum down to 600', () => {
394
- assert.equal(clampPanelWidth(601), 600)
395
- assert.equal(clampPanelWidth(2000), 600)
396
- assert.equal(clampPanelWidth(99_999), 600)
397
- })
398
-
399
- it('passes finite values inside the range through unchanged', () => {
400
- assert.equal(clampPanelWidth(240), 240)
401
- assert.equal(clampPanelWidth(320), 320)
402
- assert.equal(clampPanelWidth(450), 450)
403
- assert.equal(clampPanelWidth(600), 600)
404
- })
405
-
406
- it('parses numeric strings (localStorage round-trip)', () => {
407
- assert.equal(clampPanelWidth('320'), 320)
408
- assert.equal(clampPanelWidth('450'), 450)
409
- assert.equal(clampPanelWidth('900'), 600)
410
- assert.equal(clampPanelWidth('100'), 240)
411
- })
412
- })