@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,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
- })