@ceedcv-maya/shared-editor-react 0.10.0 → 0.11.0

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.
@@ -2,10 +2,13 @@
2
2
  * EditorToolbar button groups extracted into focused subcomponents.
3
3
  * Each group handles a logically distinct set of formatting or block operations.
4
4
  */
5
+ import { Fragment } from 'react';
5
6
  import type { Editor } from '@tiptap/react';
6
7
  import type { ToolbarLabels } from './EditorToolbar';
7
8
  import { ColorPicker } from './ColorPicker';
8
9
  import { Btn } from './EditorToolbarButton';
10
+ import { EditorIcon } from './EditorIcons';
11
+ import { tableMenuActions } from '../lib/tableMenuActions';
9
12
 
10
13
  export interface ToolbarGroupProps {
11
14
  editor: Editor;
@@ -35,31 +38,31 @@ export function FormattingButtons({ editor, labels: L }: ToolbarGroupProps) {
35
38
  onClick={() => editor.chain().focus().toggleBold().run()}
36
39
  title={L.bold}
37
40
  >
38
- <strong>B</strong>
41
+ <EditorIcon name="bold" />
39
42
  </Btn>
40
43
  <Btn
41
44
  active={editor.isActive('italic')}
42
45
  onClick={() => editor.chain().focus().toggleItalic().run()}
43
46
  title={L.italic}
44
47
  >
45
- <em>I</em>
48
+ <EditorIcon name="italic" />
46
49
  </Btn>
47
50
  <Btn
48
51
  active={editor.isActive('underline')}
49
52
  onClick={() => editor.chain().focus().toggleUnderline().run()}
50
53
  title={L.underline}
51
54
  >
52
- <u>U</u>
55
+ <EditorIcon name="underline" />
53
56
  </Btn>
54
57
  <Btn
55
58
  active={editor.isActive('code')}
56
59
  onClick={() => editor.chain().focus().toggleCode().run()}
57
60
  title={L.code}
58
61
  >
59
- {'</>'}
62
+ <EditorIcon name="code" />
60
63
  </Btn>
61
64
  <Btn active={editor.isActive('link')} onClick={setLink} title={L.link}>
62
- 🔗
65
+ <EditorIcon name="link" />
63
66
  </Btn>
64
67
  </>
65
68
  );
@@ -76,13 +79,13 @@ export function AdvancedFormattingButtons({ editor, labels: L }: ToolbarGroupPro
76
79
  onClick={() => editor.chain().focus().toggleStrike().run()}
77
80
  title={L.strike}
78
81
  >
79
- <s>S</s>
82
+ <EditorIcon name="strike" />
80
83
  </Btn>
81
84
 
82
85
  <ColorPicker
83
86
  title={L.textColor}
84
87
  value={(editor.getAttributes('textStyle').color as string | undefined) ?? null}
85
- glyph={<span style={{ fontWeight: 700 }}>A</span>}
88
+ glyph={<EditorIcon name="textColor" />}
86
89
  clearLabel={L.colorDefault}
87
90
  onSelect={(c) => {
88
91
  if (c === null) editor.chain().focus().unsetColor().run();
@@ -92,7 +95,7 @@ export function AdvancedFormattingButtons({ editor, labels: L }: ToolbarGroupPro
92
95
  <ColorPicker
93
96
  title={L.backgroundColor}
94
97
  value={(editor.getAttributes('highlight').color as string | undefined) ?? null}
95
- glyph={<span style={{ fontWeight: 700 }}>▮</span>}
98
+ glyph={<EditorIcon name="highlight" />}
96
99
  clearLabel={L.colorDefault}
97
100
  onSelect={(c) => {
98
101
  if (c === null) editor.chain().focus().unsetHighlight().run();
@@ -114,28 +117,28 @@ export function AlignmentButtons({ editor, labels: L }: ToolbarGroupProps) {
114
117
  onClick={() => editor.chain().focus().setTextAlign('left').run()}
115
118
  title={L.alignLeft}
116
119
  >
117
-
120
+ <EditorIcon name="alignLeft" />
118
121
  </Btn>
119
122
  <Btn
120
123
  active={editor.isActive({ textAlign: 'center' })}
121
124
  onClick={() => editor.chain().focus().setTextAlign('center').run()}
122
125
  title={L.alignCenter}
123
126
  >
124
-
127
+ <EditorIcon name="alignCenter" />
125
128
  </Btn>
126
129
  <Btn
127
130
  active={editor.isActive({ textAlign: 'right' })}
128
131
  onClick={() => editor.chain().focus().setTextAlign('right').run()}
129
132
  title={L.alignRight}
130
133
  >
131
-
134
+ <EditorIcon name="alignRight" />
132
135
  </Btn>
133
136
  <Btn
134
137
  active={editor.isActive({ textAlign: 'justify' })}
135
138
  onClick={() => editor.chain().focus().setTextAlign('justify').run()}
136
139
  title={L.alignJustify}
137
140
  >
138
-
141
+ <EditorIcon name="alignJustify" />
139
142
  </Btn>
140
143
  </>
141
144
  );
@@ -159,7 +162,7 @@ export function IndentButtons({ editor, labels: L }: ToolbarGroupProps) {
159
162
  }}
160
163
  title={L.indent}
161
164
  >
162
-
165
+ <EditorIcon name="indent" />
163
166
  </Btn>
164
167
  <Btn
165
168
  onClick={() => {
@@ -173,7 +176,7 @@ export function IndentButtons({ editor, labels: L }: ToolbarGroupProps) {
173
176
  }}
174
177
  title={L.outdent}
175
178
  >
176
-
179
+ <EditorIcon name="outdent" />
177
180
  </Btn>
178
181
  </>
179
182
  );
@@ -221,41 +224,41 @@ export function ListAndBlockButtons({ editor, labels: L }: ToolbarGroupProps) {
221
224
  onClick={() => editor.chain().focus().toggleBulletList().run()}
222
225
  title={L.bulletList}
223
226
  >
224
-
227
+ <EditorIcon name="bulletList" />
225
228
  </Btn>
226
229
  <Btn
227
230
  active={editor.isActive('orderedList')}
228
231
  onClick={() => editor.chain().focus().toggleOrderedList().run()}
229
232
  title={L.orderedList}
230
233
  >
231
- 1.
234
+ <EditorIcon name="orderedList" />
232
235
  </Btn>
233
236
  <Btn
234
237
  active={editor.isActive('taskList')}
235
238
  onClick={() => editor.chain().focus().toggleTaskList().run()}
236
239
  title={L.taskList}
237
240
  >
238
-
241
+ <EditorIcon name="taskList" />
239
242
  </Btn>
240
243
  <Btn
241
244
  active={editor.isActive('blockquote')}
242
245
  onClick={() => editor.chain().focus().toggleBlockquote().run()}
243
246
  title={L.blockquote}
244
247
  >
245
-
248
+ <EditorIcon name="blockquote" />
246
249
  </Btn>
247
250
  <Btn
248
251
  active={editor.isActive('codeBlock')}
249
252
  onClick={() => editor.chain().focus().toggleCodeBlock().run()}
250
253
  title={L.codeBlock}
251
254
  >
252
- {'{}'}
255
+ <EditorIcon name="codeBlock" />
253
256
  </Btn>
254
257
  <Btn
255
258
  onClick={() => editor.chain().focus().setHorizontalRule().run()}
256
259
  title={L.horizontalRule}
257
260
  >
258
-
261
+ <EditorIcon name="horizontalRule" />
259
262
  </Btn>
260
263
  </>
261
264
  );
@@ -287,20 +290,41 @@ export function TableAndMediaButtons({
287
290
  }
288
291
  title={L.table}
289
292
  >
290
-
293
+ <EditorIcon name="table" />
291
294
  </Btn>
292
295
  <Btn onClick={setIframe} title={L.iframe}>
293
- 🖽
296
+ <EditorIcon name="iframe" />
294
297
  </Btn>
295
298
  {onImage && (
296
299
  <Btn onClick={onImage} title={L.uploadImage}>
297
- 🖼
300
+ <EditorIcon name="image" />
298
301
  </Btn>
299
302
  )}
300
303
  </>
301
304
  );
302
305
  }
303
306
 
307
+ /**
308
+ * Contextual table-editing buttons: add/delete rows & columns, toggle header,
309
+ * delete table. Rendered in the main toolbar only while the selection is inside
310
+ * a table (the caller gates on `editor.isActive('table')`), so it appears and
311
+ * hides in place instead of floating over the content.
312
+ */
313
+ export function TableButtons({ editor, labels: L }: ToolbarGroupProps) {
314
+ return (
315
+ <>
316
+ {tableMenuActions(editor, L).map((a) => (
317
+ <Fragment key={a.key}>
318
+ {a.separatorBefore && <span className="maya-editor-toolbar__sep" aria-hidden />}
319
+ <Btn onClick={a.run} title={a.title} disabled={a.disabled}>
320
+ <EditorIcon name={a.icon} />
321
+ </Btn>
322
+ </Fragment>
323
+ ))}
324
+ </>
325
+ );
326
+ }
327
+
304
328
  /**
305
329
  * Import/export and document buttons: Word import/export, comments, find.
306
330
  */
@@ -323,12 +347,12 @@ export function DocumentButtons({
323
347
  <>
324
348
  {onImportDocx && (
325
349
  <Btn onClick={onImportDocx} title={L.importDocx}>
326
- <span style={{ fontFamily: 'ui-monospace, monospace', fontSize: 11 }}>↥W</span>
350
+ <EditorIcon name="importDocx" />
327
351
  </Btn>
328
352
  )}
329
353
  {onExportDocx && (
330
354
  <Btn onClick={onExportDocx} title={L.exportDocx}>
331
- <span style={{ fontFamily: 'ui-monospace, monospace', fontSize: 11 }}>↧W</span>
355
+ <EditorIcon name="exportDocx" />
332
356
  </Btn>
333
357
  )}
334
358
  {onAddComment && (
@@ -337,12 +361,12 @@ export function DocumentButtons({
337
361
  disabled={editor.state.selection.empty}
338
362
  title={L.addComment}
339
363
  >
340
- 💬
364
+ <EditorIcon name="comment" />
341
365
  </Btn>
342
366
  )}
343
367
  {onToggleFind && (
344
368
  <Btn onClick={onToggleFind} title={L.find}>
345
- 🔍
369
+ <EditorIcon name="find" />
346
370
  </Btn>
347
371
  )}
348
372
  </>
@@ -385,7 +409,7 @@ export function ViewModeButtons({
385
409
  title={L.insertHtml}
386
410
  active={viewMode === 'html'}
387
411
  >
388
- <span style={{ fontFamily: 'ui-monospace, monospace', fontSize: 11 }}>{'<>'}</span>
412
+ <EditorIcon name="htmlSource" />
389
413
  </Btn>
390
414
  )}
391
415
  <span className="maya-editor-toolbar__sep" aria-hidden />
@@ -394,7 +418,7 @@ export function ViewModeButtons({
394
418
  onClick={onToggleFullscreen}
395
419
  title={isFullscreen ? L.exitFullscreen : L.fullscreen}
396
420
  >
397
- {isFullscreen ? '🗗' : '🗖'}
421
+ <EditorIcon name={isFullscreen ? 'exitFullscreen' : 'fullscreen'} />
398
422
  </Btn>
399
423
  )}
400
424
  </>
@@ -326,6 +326,18 @@ export function MayaEditor({
326
326
  onFullscreenChange(isFullscreen);
327
327
  }, [isFullscreen, onFullscreenChange]);
328
328
 
329
+ // Mirror fullscreen to a global `editor-fullscreen` class on <html> so the
330
+ // host AppLayout can hide its fixed sidebar and drop the content margin while
331
+ // the editor covers the viewport. Self-contained: works even when the host
332
+ // doesn't handle `onFullscreenChange` (e.g. the inline editors in the
333
+ // continuous document view) — otherwise the fixed editor sits *under* the
334
+ // still-visible sidebar and the left half is hidden.
335
+ useEffect(() => {
336
+ const root = document.documentElement;
337
+ root.classList.toggle('editor-fullscreen', isFullscreen);
338
+ return () => root.classList.remove('editor-fullscreen');
339
+ }, [isFullscreen]);
340
+
329
341
  if (!editor) return null;
330
342
 
331
343
  const editorReady = viewReady && isEditorReady(editor);
package/src/index.ts CHANGED
@@ -2,8 +2,10 @@ export { MayaEditor } from './components/MayaEditor';
2
2
  export type { MayaEditorProps } from './components/MayaEditor';
3
3
  export { EditorContentHtml } from './components/EditorContentHtml';
4
4
  export { EditorContentJson } from './components/EditorContentJson';
5
- export { EditorToolbar } from './components/EditorToolbar';
5
+ export { EditorToolbar, DEFAULT_LABELS } from './components/EditorToolbar';
6
6
  export type { ToolbarLabels } from './components/EditorToolbar';
7
+ export { buildToolbarLabels } from './lib/buildToolbarLabels';
8
+ export type { TranslateFn } from './lib/buildToolbarLabels';
7
9
 
8
10
  export { IframeBlock } from './extensions/IframeBlock';
9
11
  export { AlertBlock } from './extensions/AlertBlock';
@@ -52,6 +54,3 @@ export type {
52
54
  TiptapDoc,
53
55
  AnchoredComment,
54
56
  } from './types';
55
-
56
- export { default as esTranslations } from './i18n/es.json';
57
- export { default as enTranslations } from './i18n/en.json';
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildToolbarLabels } from './buildToolbarLabels';
3
+ import { DEFAULT_LABELS } from '../components/EditorToolbar';
4
+
5
+ describe('buildToolbarLabels', () => {
6
+ it('maps every label key through t("editor.<key>")', () => {
7
+ const dict: Record<string, string> = {
8
+ 'editor.bold': 'Negrita',
9
+ 'editor.italic': 'Cursiva',
10
+ 'editor.groupFont': 'Fuente',
11
+ 'editor.tableAddColumnAfter': 'Insertar columna a la derecha',
12
+ };
13
+ const t = (k: string) => dict[k] ?? k;
14
+
15
+ const labels = buildToolbarLabels(t);
16
+ expect(labels.bold).toBe('Negrita');
17
+ expect(labels.italic).toBe('Cursiva');
18
+ expect(labels.groupFont).toBe('Fuente');
19
+ expect(labels.tableAddColumnAfter).toBe('Insertar columna a la derecha');
20
+ });
21
+
22
+ it('falls back to the English default when a key is missing (t returns the key)', () => {
23
+ const t = (k: string) => k; // i18next miss behaviour: returns the key
24
+ const labels = buildToolbarLabels(t);
25
+ expect(labels.bold).toBe(DEFAULT_LABELS.bold);
26
+ expect(labels.groupParagraph).toBe(DEFAULT_LABELS.groupParagraph);
27
+ // never leaks a raw "editor.*" key
28
+ expect(labels.bold).not.toContain('editor.');
29
+ });
30
+
31
+ it('honours a custom prefix', () => {
32
+ const t = (k: string) => (k === 'tt.bold' ? 'B!' : k);
33
+ const labels = buildToolbarLabels(t, 'tt');
34
+ expect(labels.bold).toBe('B!');
35
+ });
36
+
37
+ it('returns a fully-populated object (all default keys present)', () => {
38
+ const labels = buildToolbarLabels((k) => k);
39
+ for (const key of Object.keys(DEFAULT_LABELS)) {
40
+ expect(labels[key as keyof typeof labels]).toBeTruthy();
41
+ }
42
+ });
43
+ });
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Build a fully-populated {@link ToolbarLabels} from a translation function.
3
+ *
4
+ * The editor itself is i18n-agnostic (it just takes a `toolbarLabels` prop), so
5
+ * each consumer wires its own i18n. This helper keeps that wiring to one line:
6
+ * pass the app's `t` (bound to the namespace that holds the shared `editor`
7
+ * canon — usually `common`) and it maps every label key to `t('editor.<key>')`.
8
+ *
9
+ * const { t } = useTranslation('common');
10
+ * <MayaEditor toolbarLabels={buildToolbarLabels(t)} … />
11
+ *
12
+ * Any key missing from the active locale falls back to the English default
13
+ * (i18next returns the key unchanged for a miss, which we detect and replace),
14
+ * so a partially-translated locale never shows raw `editor.*` keys.
15
+ */
16
+ import { DEFAULT_LABELS } from '../components/EditorToolbar';
17
+ import type { ToolbarLabels } from '../components/EditorToolbar';
18
+
19
+ /** Minimal shape of an i18next `t` — only the (key) → string call is used. */
20
+ export type TranslateFn = (key: string) => string;
21
+
22
+ export function buildToolbarLabels(
23
+ t: TranslateFn,
24
+ prefix = 'editor',
25
+ ): ToolbarLabels {
26
+ const out: ToolbarLabels = { ...DEFAULT_LABELS };
27
+
28
+ (Object.keys(DEFAULT_LABELS) as (keyof ToolbarLabels)[]).forEach((key) => {
29
+ const fullKey = `${prefix}.${key}`;
30
+ const value = t(fullKey);
31
+ // i18next returns the key itself on a miss → keep the English default.
32
+ if (value && value !== fullKey) {
33
+ out[key] = value;
34
+ }
35
+ });
36
+
37
+ return out;
38
+ }
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { Editor } from '@tiptap/core';
3
+ import { buildMayaEditorExtensions } from './editorExtensions';
4
+ import { tableMenuActions } from './tableMenuActions';
5
+
6
+ function makeEditor(): Editor {
7
+ return new Editor({ extensions: buildMayaEditorExtensions('full') });
8
+ }
9
+
10
+ /** Count tableRow / cell nodes in the current doc. */
11
+ function tableShape(editor: Editor): { rows: number; cols: number } {
12
+ let rows = 0;
13
+ let cols = 0;
14
+ editor.state.doc.descendants((node) => {
15
+ if (node.type.name === 'tableRow') {
16
+ rows += 1;
17
+ if (rows === 1) cols = node.childCount; // cells in first row
18
+ }
19
+ return true;
20
+ });
21
+ return { rows, cols };
22
+ }
23
+
24
+ let current: Editor | null = null;
25
+ afterEach(() => {
26
+ current?.destroy();
27
+ current = null;
28
+ });
29
+
30
+ describe('tableMenuActions', () => {
31
+ it('returns all actions disabled when the selection is not in a table', () => {
32
+ const editor = (current = makeEditor());
33
+ editor.commands.setContent('<p>plain paragraph</p>');
34
+ const actions = tableMenuActions(editor);
35
+ expect(actions.length).toBe(8);
36
+ expect(actions.every((a) => a.disabled)).toBe(true);
37
+ });
38
+
39
+ it('enables actions and adds a column when the cursor is inside a table', () => {
40
+ const editor = (current = makeEditor());
41
+ editor.chain().focus().insertTable({ rows: 2, cols: 2, withHeaderRow: true }).run();
42
+ const before = tableShape(editor);
43
+
44
+ const addCol = tableMenuActions(editor).find((a) => a.key === 'addColumnAfter')!;
45
+ expect(addCol.disabled).toBe(false);
46
+ addCol.run();
47
+
48
+ expect(tableShape(editor).cols).toBe(before.cols + 1);
49
+ });
50
+
51
+ it('adds a row', () => {
52
+ const editor = (current = makeEditor());
53
+ editor.chain().focus().insertTable({ rows: 2, cols: 2, withHeaderRow: true }).run();
54
+ const before = tableShape(editor);
55
+
56
+ tableMenuActions(editor).find((a) => a.key === 'addRowAfter')!.run();
57
+
58
+ expect(tableShape(editor).rows).toBe(before.rows + 1);
59
+ });
60
+
61
+ it('deletes a column', () => {
62
+ const editor = (current = makeEditor());
63
+ editor.chain().focus().insertTable({ rows: 2, cols: 3, withHeaderRow: true }).run();
64
+ const before = tableShape(editor);
65
+
66
+ tableMenuActions(editor).find((a) => a.key === 'deleteColumn')!.run();
67
+
68
+ expect(tableShape(editor).cols).toBe(before.cols - 1);
69
+ });
70
+
71
+ it('deletes the whole table', () => {
72
+ const editor = (current = makeEditor());
73
+ editor.chain().focus().insertTable({ rows: 2, cols: 2, withHeaderRow: true }).run();
74
+ expect(editor.isActive('table')).toBe(true);
75
+
76
+ tableMenuActions(editor).find((a) => a.key === 'deleteTable')!.run();
77
+
78
+ expect(editor.isActive('table')).toBe(false);
79
+ });
80
+
81
+ it('uses provided labels for tooltips', () => {
82
+ const editor = (current = makeEditor());
83
+ editor.chain().focus().insertTable({ rows: 2, cols: 2, withHeaderRow: true }).run();
84
+ const actions = tableMenuActions(editor, { tableAddColumnAfter: 'Insertar columna a la derecha' });
85
+ const addCol = actions.find((a) => a.key === 'addColumnAfter')!;
86
+ expect(addCol.title).toBe('Insertar columna a la derecha');
87
+ });
88
+ });
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Table editing actions exposed by the contextual table toolbar.
3
+ *
4
+ * Pure mapping from an editor instance + labels to a flat, ordered list of
5
+ * buttons. Kept separate from the React component so the command wiring and
6
+ * `can()` gating can be unit-tested against a headless editor.
7
+ */
8
+ import type { Editor } from '@tiptap/core';
9
+ import type { ToolbarLabels } from '../components/EditorToolbar';
10
+ import type { EditorIconName } from '../components/EditorIcons';
11
+
12
+ export interface TableMenuAction {
13
+ /** Stable key for React lists. */
14
+ key: string;
15
+ /** Tooltip / aria-label. */
16
+ title: string;
17
+ /** Name of the SVG icon shown on the button. */
18
+ icon: EditorIconName;
19
+ /** Runs the command. */
20
+ run: () => void;
21
+ /** True when the command can't apply to the current selection. */
22
+ disabled: boolean;
23
+ /** When true, render a visual separator before this action. */
24
+ separatorBefore?: boolean;
25
+ }
26
+
27
+ /**
28
+ * Build the ordered list of table actions for the current editor state.
29
+ * Recomputed on every render so `disabled` reflects the live selection.
30
+ */
31
+ export function tableMenuActions(
32
+ editor: Editor,
33
+ labels?: Partial<ToolbarLabels>,
34
+ ): TableMenuAction[] {
35
+ const L = labels ?? {};
36
+ const chain = () => editor.chain().focus();
37
+
38
+ return [
39
+ {
40
+ key: 'addColumnBefore',
41
+ title: L.tableAddColumnBefore ?? 'Insert column left',
42
+ icon: 'columnAddBefore',
43
+ run: () => chain().addColumnBefore().run(),
44
+ disabled: !editor.can().addColumnBefore(),
45
+ },
46
+ {
47
+ key: 'addColumnAfter',
48
+ title: L.tableAddColumnAfter ?? 'Insert column right',
49
+ icon: 'columnAddAfter',
50
+ run: () => chain().addColumnAfter().run(),
51
+ disabled: !editor.can().addColumnAfter(),
52
+ },
53
+ {
54
+ key: 'addRowBefore',
55
+ title: L.tableAddRowBefore ?? 'Insert row above',
56
+ icon: 'rowAddBefore',
57
+ run: () => chain().addRowBefore().run(),
58
+ disabled: !editor.can().addRowBefore(),
59
+ separatorBefore: true,
60
+ },
61
+ {
62
+ key: 'addRowAfter',
63
+ title: L.tableAddRowAfter ?? 'Insert row below',
64
+ icon: 'rowAddAfter',
65
+ run: () => chain().addRowAfter().run(),
66
+ disabled: !editor.can().addRowAfter(),
67
+ },
68
+ {
69
+ key: 'deleteColumn',
70
+ title: L.tableDeleteColumn ?? 'Delete column',
71
+ icon: 'columnDelete',
72
+ run: () => chain().deleteColumn().run(),
73
+ disabled: !editor.can().deleteColumn(),
74
+ separatorBefore: true,
75
+ },
76
+ {
77
+ key: 'deleteRow',
78
+ title: L.tableDeleteRow ?? 'Delete row',
79
+ icon: 'rowDelete',
80
+ run: () => chain().deleteRow().run(),
81
+ disabled: !editor.can().deleteRow(),
82
+ },
83
+ {
84
+ key: 'toggleHeaderRow',
85
+ title: L.tableToggleHeaderRow ?? 'Toggle header row',
86
+ icon: 'headerRow',
87
+ run: () => chain().toggleHeaderRow().run(),
88
+ disabled: !editor.can().toggleHeaderRow(),
89
+ separatorBefore: true,
90
+ },
91
+ {
92
+ key: 'deleteTable',
93
+ title: L.tableDelete ?? 'Delete table',
94
+ icon: 'tableDelete',
95
+ run: () => chain().deleteTable().run(),
96
+ disabled: !editor.can().deleteTable(),
97
+ },
98
+ ];
99
+ }