From efd4e9c8a74abea8c77fe8b2f6a4d094bd12837b Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Wed, 12 Aug 2026 23:44:22 +0200 Subject: [PATCH] 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. --- docs/architecture/ribbon.md | 2 +- .../components/ribbon/tabs/TableTabs.svelte | 69 ++++++++++++++----- src/lib/i18n/locales/de.ts | 4 ++ src/lib/i18n/locales/en.ts | 4 ++ src/styles/global.css | 5 +- 5 files changed, 65 insertions(+), 19 deletions(-) diff --git a/docs/architecture/ribbon.md b/docs/architecture/ribbon.md index 69efed9..cb1c311 100644 --- a/docs/architecture/ribbon.md +++ b/docs/architecture/ribbon.md @@ -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 diff --git a/src/lib/components/ribbon/tabs/TableTabs.svelte b/src/lib/components/ribbon/tabs/TableTabs.svelte index d3ae8a3..b90f137 100644 --- a/src/lib/components/ribbon/tabs/TableTabs.svelte +++ b/src/lib/components/ribbon/tabs/TableTabs.svelte @@ -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 @@ {:else} -
- run((c) => c.addRowBefore())} /> - run((c) => c.addRowAfter())} /> +
+ toggleMenu('tableDelete')} + /> + {#if isMenuOpen('tableDelete')} + + {/if}
-
- run((c) => c.addColumnBefore())} /> - run((c) => c.addColumnAfter())} /> -
- - -
- - -
- run((c) => c.deleteRow())} /> - run((c) => c.deleteColumn())} /> - run((c) => c.deleteTable())} /> +
+
+ run((c) => c.addRowBefore())} /> + run((c) => c.addRowAfter())} /> + run((c) => c.addColumnBefore())} /> + run((c) => c.addColumnAfter())} />
@@ -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 { diff --git a/src/lib/i18n/locales/de.ts b/src/lib/i18n/locales/de.ts index 2061d31..c0fcb46 100644 --- a/src/lib/i18n/locales/de.ts +++ b/src/lib/i18n/locales/de.ts @@ -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', diff --git a/src/lib/i18n/locales/en.ts b/src/lib/i18n/locales/en.ts index 15acb9f..a0b8ce1 100644 --- a/src/lib/i18n/locales/en.ts +++ b/src/lib/i18n/locales/en.ts @@ -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', diff --git a/src/styles/global.css b/src/styles/global.css index 78c599b..aa1e39c 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -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