feat: the table's four row/column inserts are big buttons

Rows & Columns follows Word: four big buttons carrying a 28px glyph and a
two-line label, with the three deletions folded into one Entfernen dropdown
beside them so the group keeps its width.

Two lines cost the band 12px (--w-ribbon-h 96 -> 108). The labels size to
min-content, not max-width, which a wrapped box ignores; and the inserts drop
the reserved caret slot, without which the group's foot label sits 8px below
every other group's.
This commit is contained in:
Steffen Becker committed 2026-08-12 23:44:22 +02:00
1 parent 2f3e48ecf9
commit efd4e9c8a7
5 files changed
+65 -19

No files matched your search

+1 -1
View File
@@ -46,7 +46,7 @@ so the split is invisible there.
Ribbon.svelte
├─ .ribbon-tabs File pill · quick access (save, undo, redo) · tabs · contextual tabs
│ · spacer · document name · appearance · UI language
└─ .ribbon-body the active tab's groups, a fixed --w-ribbon-h (96px) band
└─ .ribbon-body the active tab's groups, a fixed --w-ribbon-h (108px) band
```
The band's height never changes: it scrolls horizontally with a hidden scrollbar instead of
+53 -16
View File
@@ -3,6 +3,8 @@
import { CellSelection } from '@tiptap/pm/tables';
import RibbonGroup from '../RibbonGroup.svelte';
import RibbonButton from '../RibbonButton.svelte';
import Icon from '../Icon.svelte';
import type { IconName } from '../icons';
import ColorPicker from '../../ColorPicker.svelte';
import TableBorderPicker from '../../TableBorderPicker.svelte';
import TableStylePicker from '../../TableStylePicker.svelte';
@@ -76,6 +78,14 @@
editor.chain().focus().setCellAttribute('cellPadding', next).run();
}
// Word folds the three deletions into one big button's menu, so the four inserts
// beside it are the group's whole width.
const DELETES: { icon: IconName; label: () => string; cmd: (c: ChainedCommands) => ChainedCommands }[] = [
{ icon: 'deleteRow', label: () => t().table.deleteRow, cmd: (c) => c.deleteRow() },
{ icon: 'deleteCol', label: () => t().table.deleteColumn, cmd: (c) => c.deleteColumn() },
{ icon: 'deleteTable', label: () => t().table.deleteTable, cmd: (c) => c.deleteTable() },
];
const VALIGNS: { key: CellVerticalAlign | null; icon: 'alignTop' | 'alignMiddle' | 'alignBottom'; label: () => string }[] = [
{ key: null, icon: 'alignTop', label: () => t().table.cellAlignTop },
{ key: 'middle', icon: 'alignMiddle', label: () => t().table.cellAlignMiddle },
@@ -124,23 +134,32 @@
</RibbonGroup>
{:else}
<RibbonGroup label={t().ribbon.groups.rowsColumns}>
<div class="rb-col">
<RibbonButton variant="small" icon="rowAbove" label={t().table.insertRowAbove} onclick={() => run((c) => c.addRowBefore())} />
<RibbonButton variant="small" icon="rowBelow" label={t().table.insertRowBelow} onclick={() => run((c) => c.addRowAfter())} />
<div class="rb-menu-wrap" use:clickOutside={'tableDelete'}>
<RibbonButton
variant="big"
icon="deleteRow"
label={t().common.remove}
title={t().common.remove}
caret
active={isMenuOpen('tableDelete')}
onclick={() => toggleMenu('tableDelete')}
/>
{#if isMenuOpen('tableDelete')}
<div class="ribbon-menu" use:anchored role="menu">
{#each DELETES as d}
<button role="menuitem" onclick={() => { closeMenu(); run(d.cmd); }}>
<Icon name={d.icon} size={16} />{d.label()}
</button>
{/each}
</div>
{/if}
</div>
<div class="rb-col">
<RibbonButton variant="small" icon="colLeft" label={t().table.insertColumnLeft} onclick={() => run((c) => c.addColumnBefore())} />
<RibbonButton variant="small" icon="colRight" label={t().table.insertColumnRight} onclick={() => run((c) => c.addColumnAfter())} />
</div>
</RibbonGroup>
<div class="ribbon-sep"></div>
<RibbonGroup label={t().common.remove}>
<div class="rb-col">
<RibbonButton variant="small" icon="deleteRow" label={t().table.deleteRow} onclick={() => run((c) => c.deleteRow())} />
<RibbonButton variant="small" icon="deleteCol" label={t().table.deleteColumn} onclick={() => run((c) => c.deleteColumn())} />
<RibbonButton variant="small" icon="deleteTable" label={t().table.deleteTable} onclick={() => run((c) => c.deleteTable())} />
<div class="rb-mini-sep"></div>
<div class="inserts">
<RibbonButton variant="big" icon="rowAbove" label={t().ribbon.insertAbove} onclick={() => run((c) => c.addRowBefore())} />
<RibbonButton variant="big" icon="rowBelow" label={t().ribbon.insertBelow} onclick={() => run((c) => c.addRowAfter())} />
<RibbonButton variant="big" icon="colLeft" label={t().ribbon.insertLeft} onclick={() => run((c) => c.addColumnBefore())} />
<RibbonButton variant="big" icon="colRight" label={t().ribbon.insertRight} onclick={() => run((c) => c.addColumnAfter())} />
</div>
</RibbonGroup>
@@ -227,6 +246,24 @@
.rb-menu-wrap { position: relative; }
/* Wrapped onto two lines, as in Word — on one they are half the group's width.
`min-content` is what keeps the button off the label's unwrapped width, and
the caret slot goes because the two lines already reach the group's foot. */
.inserts {
display: flex;
align-items: stretch;
gap: 2px;
}
.inserts :global(.rb-label) {
width: min-content;
white-space: normal;
text-align: center;
line-height: 1.15;
}
.inserts :global(.rb-stack-caret) { display: none; }
/* The dialog positions itself in viewport coordinates, so the trigger only has
to hand it the button's rect. */
.split-trigger {
+4
View File
@@ -537,6 +537,10 @@ const de: Messages = {
margins: 'Seitenränder',
size: 'Größe',
breaks: 'Umbrüche',
insertAbove: 'Darüber einfügen',
insertBelow: 'Darunter einfügen',
insertLeft: 'Links einfügen',
insertRight: 'Rechts einfügen',
sectionBreak: 'Abschnittsumbruch',
sectionBreakHint: 'Beginnt eine eigene Kopf- und Fußzeile',
indentLeft: 'Einzug links',
+4
View File
@@ -541,6 +541,10 @@ const en = {
margins: 'Margins',
size: 'Size',
breaks: 'Breaks',
insertAbove: 'Insert above',
insertBelow: 'Insert below',
insertLeft: 'Insert left',
insertRight: 'Insert right',
sectionBreak: 'Section break',
sectionBreakHint: 'Starts its own header and footer',
indentLeft: 'Indent left',
+3 -2
View File
@@ -280,8 +280,9 @@
--w-accent-soft: #e8f1fb;
--w-menu-shadow: 0 8px 24px rgb(0 0 0 / 18%);
--w-font: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
/* Body height: three control rows plus the group-label row. A tuning knob. */
--w-ribbon-h: 96px;
/* Body height: three control rows plus the group-label row, and a big button's
label may take two of them. A tuning knob. */
--w-ribbon-h: 108px;
}
/* Word's dark mode: the Fluent dark neutrals, and the accent lightened because