feat: the ribbon's contextual tabs for a table, a picture and a shape

They appear while the caret is in the object and go with it, handing the strip
back to Home. Table Design carries the style gallery, shading, borders and the
style options; Table Layout the rows, columns, merge, split, alignment — and
cell margins, which round-tripped with nothing to set them.

The floating toolbars stay: a mini-toolbar beside the object and a contextual
tab above the document are both Word's, and both reuse the same pickers.
This commit is contained in:
Steffen Becker committed 2026-08-12 01:54:49 +02:00
1 parent 0f53571b1f
commit ad3f0507fd
6 files changed
+542 -3

No files matched your search

+63 -3
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import type { Editor } from '@tiptap/core';
import { NodeSelection } from '@tiptap/pm/state';
import Icon from './Icon.svelte';
import RibbonMenu from './RibbonMenu.svelte';
import HistoryButton from '../HistoryButton.svelte';
@@ -9,6 +10,8 @@
import ReferencesTab from './tabs/ReferencesTab.svelte';
import ReviewTab from './tabs/ReviewTab.svelte';
import ViewTab from './tabs/ViewTab.svelte';
import TableTabs from './tabs/TableTabs.svelte';
import FrameTabs from './tabs/FrameTabs.svelte';
import UiLanguagePicker from '../UiLanguagePicker.svelte';
import { clickOutside, isMenuOpen, toggleMenu, closeMenu } from './menu.svelte';
import { t } from '../../i18n/i18n.svelte';
@@ -85,11 +88,39 @@
} = $props();
const TABS = ['home', 'insert', 'layout', 'references', 'review', 'view'] as const;
type Tab = (typeof TABS)[number];
const CONTEXTUAL = ['tableDesign', 'tableLayout', 'pictureFormat', 'shapeFormat'] as const;
type Tab = (typeof TABS)[number] | (typeof CONTEXTUAL)[number];
// Word opens on Home every time, so the active tab is not persisted.
let tab = $state<Tab>('home');
// Word's contextual tabs: they appear while the caret is in the object and
// vanish with it, so a tab that disappears hands the strip back to Home.
let inTable = $derived(tick >= 0 && !!editor?.isActive('table'));
let selectedNode = $derived.by<string | null>(() => {
if (tick < 0 || !editor) return null;
const sel = editor.state.selection;
return sel instanceof NodeSelection ? sel.node.type.name : null;
});
let frameAttrs = $derived.by<Record<string, unknown> | null>(() => {
if (tick < 0 || !editor) return null;
const sel = editor.state.selection;
return sel instanceof NodeSelection ? sel.node.attrs : null;
});
let shown = $derived([
...TABS,
...(inTable ? (['tableDesign', 'tableLayout'] as const) : []),
...(selectedNode === 'image' ? (['pictureFormat'] as const) : []),
...(selectedNode === 'textBox' ? (['shapeFormat'] as const) : []),
] as Tab[]);
$effect(() => {
if (!shown.includes(tab)) tab = 'home';
});
let docNameSizerWidth = $state(0);
function run(fn?: () => void) {
@@ -159,8 +190,13 @@
<HistoryButton {editor} {tick} direction="redo" />
<span class="qa-sep"></span>
{#each TABS as id}
<button class="ribbon-tab" class:active={tab === id} onclick={() => (tab = id)}>
{#each shown as id}
<button
class="ribbon-tab"
class:active={tab === id}
class:contextual={!(TABS as readonly string[]).includes(id)}
onclick={() => (tab = id)}
>
{t().ribbon.tabs[id]}
</button>
{/each}
@@ -229,6 +265,18 @@
<ReviewTab {editor} {tick} {documentLanguage} {onLanguage} />
{:else if tab === 'view'}
<ViewTab bind:showRuler bind:showFormattingMarks {zoom} {onZoom} {onDebugDump} />
{:else if tab === 'tableDesign' || tab === 'tableLayout'}
<TableTabs {editor} {tick} which={tab === 'tableDesign' ? 'design' : 'layout'} />
{:else if tab === 'pictureFormat' || tab === 'shapeFormat'}
<FrameTabs
{editor}
which={tab === 'pictureFormat' ? 'picture' : 'shape'}
wrap={(frameAttrs?.wrap ?? 'inline') as never}
shapeKind={frameAttrs?.shapeKind as never}
fillColor={frameAttrs?.fillColor as string | null}
strokeColor={frameAttrs?.strokeColor as string | null}
strokeWidthPt={frameAttrs?.strokeWidthPt as number}
/>
{/if}
</div>
</div>
@@ -279,6 +327,18 @@
}
.ribbon-tab:hover { color: var(--w-accent); }
/* Word tints a contextual tab so it reads as belonging to the selected object
rather than to the document. */
.ribbon-tab.contextual { color: var(--w-accent-dark); }
.ribbon-tab.contextual::before {
content: '';
position: absolute;
inset: 2px 4px auto;
height: 2px;
border-radius: 1px;
background: var(--w-accent-soft);
}
.ribbon-tab.active { color: var(--w-accent); }
/* Word's underline is inset from the tab's edges, not full width. */
+22
View File
@@ -55,6 +55,28 @@ export const STROKED = {
highlighter: ['M5.5 9.5 10 5l3 3-4.5 4.5H5.5z', 'M9.25 3.25 12.75 6.75', 'M2.5 13.5h4'],
shading: ['M4 8.5 8.5 4l4 4-4.5 4.5a1.4 1.4 0 0 1-2 0L4 10.5a1.4 1.4 0 0 1 0-2z', 'M6.5 2 8.5 4', 'M2.5 13.5h11'],
rowAbove: ['M1.5 9h13v5.5h-13z', 'M1.5 11.75h13', 'M8 1.5v5M5.5 4h5'],
rowBelow: ['M1.5 1.5h13V7h-13z', 'M1.5 4.25h13', 'M8 9.5v5M5.5 12h5'],
colLeft: ['M9 1.5h5.5v13H9z', 'M11.75 1.5v13', 'M1.5 8h5M4 5.5v5'],
colRight: ['M1.5 1.5H7v13H1.5z', 'M4.25 1.5v13', 'M9.5 8h5M12 5.5v5'],
deleteRow: ['M1.5 5.5h13v5h-13z', 'M4 3 12 13M12 3 4 13'],
deleteCol: ['M5.5 1.5h5v13h-5z', 'M3 4 13 12M13 4 3 12'],
deleteTable: ['M2 2.5h12v11H2z', 'M2 6h12M2 10h12M6 2.5v11M10 2.5v11', 'M3 3 13 13'],
merge: ['M2 3h12v10H2z', 'M2 8h4M10 8h4', 'M6.5 6 8.5 8l-2 2M9.5 6 7.5 8l2 2'],
split: ['M2 3h12v10H2z', 'M8 3v10', 'M5 6.5 3 8l2 1.5M11 6.5 13 8l-2 1.5'],
headerRow: ['M2 3h12v10H2z', 'M2 6.25h12', 'M3.4 4.6h9.2'],
firstColumn: ['M2 3h12v10H2z', 'M5.5 3v10', 'M3.75 5v6'],
alignTop: ['M2 3h12', 'M8 5.5v7M5.5 8 8 5.5 10.5 8'],
alignMiddle: ['M2 8h12', 'M8 2.5v3M8 10.5v3'],
alignBottom: ['M2 13h12', 'M8 3.5v7M5.5 8 8 10.5 10.5 8'],
cellMargins: ['M2 2.5h12v11H2z', 'M4.5 5h7v6h-7z'],
wrapInline: ['M2 4h12M2 8h12M2 12h12'],
wrapLeft: ['M2 3.5h6v9H2z', 'M9.5 4.5h4.5M9.5 7h4.5M9.5 9.5h4.5M9.5 12h4.5'],
wrapRight: ['M8 3.5h6v9H8z', 'M2 4.5h4.5M2 7h4.5M2 9.5h4.5M2 12h4.5'],
wrapTopBottom: ['M4 6.5h8v3H4z', 'M2 3h12M2 13h12'],
shapeRect: ['M2 4h12v8H2z'],
shapeRound: ['M4 4h8a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z'],
shapeEllipse: ['M8 12.5c3.6 0 6.5-2 6.5-4.5S11.6 3.5 8 3.5 1.5 5.5 1.5 8s2.9 4.5 6.5 4.5z'],
toc: ['M2 3h6M2 6.5h5M2 10h6M2 13.5h4', 'M11 3h3M10 6.5h4M11 10h3M10 13.5h4'],
tocLevels: ['M2.5 3.5h11M4.5 7h9M6.5 10.5h7M8.5 14h5'],
wordCount: ['M2.5 2.5h11v11h-11z', 'M5 6h6M5 9h4'],
@@ -0,0 +1,146 @@
<script lang="ts">
import type { Editor } from '@tiptap/core';
import RibbonGroup from '../RibbonGroup.svelte';
import RibbonButton from '../RibbonButton.svelte';
import ColorPicker from '../../ColorPicker.svelte';
import type { WrapMode } from '../../../editor/extensions/image';
import type { ShapeKind } from '../../../editor/extensions/textBox';
import { t } from '../../../i18n/i18n.svelte';
// Word's Picture Format and Shape Format: the same wrap modes, plus a shape's
// own fill, outline and kind.
let { editor, which, wrap, shapeKind, fillColor, strokeColor, strokeWidthPt }: {
editor: Editor | null;
which: 'picture' | 'shape';
wrap: WrapMode;
shapeKind?: ShapeKind;
fillColor?: string | null;
strokeColor?: string | null;
strokeWidthPt?: number;
} = $props();
const WRAPS: { key: WrapMode; icon: 'wrapInline' | 'wrapLeft' | 'wrapRight' | 'wrapTopBottom'; label: () => string }[] = [
{ key: 'inline', icon: 'wrapInline', label: () => t().image.wrapInline },
{ key: 'left', icon: 'wrapLeft', label: () => t().image.wrapLeft },
{ key: 'right', icon: 'wrapRight', label: () => t().image.wrapRight },
{ key: 'topBottom', icon: 'wrapTopBottom', label: () => t().image.wrapTopBottom },
];
const SHAPES: { key: ShapeKind; label: () => string }[] = [
{ key: 'textbox', label: () => t().textBox.shapeRect },
{ key: 'roundRect', label: () => t().textBox.shapeRoundRect },
{ key: 'ellipse', label: () => t().textBox.shapeEllipse },
];
function setWrap(w: WrapMode) {
if (which === 'picture') editor?.chain().focus().setImageWrap(w).run();
else editor?.chain().focus().setTextBoxAttrs({ wrap: w }).run();
}
</script>
<RibbonGroup label={t().ribbon.groups.arrange}>
{#each WRAPS as w}
<RibbonButton variant="big" icon={w.icon} label={w.label()} title={w.label()} active={wrap === w.key} onclick={() => setWrap(w.key)} />
{/each}
</RibbonGroup>
{#if which === 'shape'}
<div class="ribbon-sep"></div>
<RibbonGroup label={t().ribbon.groups.shapeStyles}>
{#each SHAPES as s}
<RibbonButton variant="big" icon={s.key === 'ellipse' ? 'shapeEllipse' : s.key === 'roundRect' ? 'shapeRound' : 'shapeRect'} label={s.label()} title={s.label()} active={shapeKind === s.key} onclick={() => editor?.chain().focus().setTextBoxAttrs({ shapeKind: s.key }).run()} />
{/each}
<div class="rb-captioned">
<ColorPicker
{editor}
currentColor={fillColor ?? null}
defaultColor="#4472C4"
title={t().textBox.fillColor}
chevronTitle={t().textBox.fillColor}
clearLabel={t().textBox.noFill}
onApply={(c) => editor?.chain().focus().setTextBoxAttrs({ fillColor: c }).run()}
onClear={() => editor?.chain().focus().setTextBoxAttrs({ fillColor: null }).run()}
icon={fillIcon}
/>
<span class="rb-caption">{t().textBox.fillColor}</span>
</div>
<div class="rb-captioned">
<ColorPicker
{editor}
currentColor={strokeColor ?? null}
defaultColor="#2F5496"
title={t().textBox.borderColor}
chevronTitle={t().textBox.borderColor}
clearLabel={t().textBox.noBorder}
onApply={(c) => editor?.chain().focus().setTextBoxAttrs({ strokeColor: c }).run()}
onClear={() => editor?.chain().focus().setTextBoxAttrs({ strokeColor: null }).run()}
icon={strokeIcon}
/>
<span class="rb-caption">{t().textBox.borderColor}</span>
</div>
<label class="field">
<span>{t().textBox.borderWidth}</span>
<input
type="text"
inputmode="decimal"
value={strokeWidthPt ?? 1}
onchange={(e) => {
const v = parseFloat((e.currentTarget as HTMLInputElement).value.replace(',', '.'));
if (!isNaN(v)) editor?.chain().focus().setTextBoxAttrs({ strokeWidthPt: Math.max(0, v) }).run();
}}
/>
</label>
</RibbonGroup>
{/if}
{#snippet fillIcon()}<span class="swatch" style="background: {fillColor ?? 'transparent'}"></span>{/snippet}
{#snippet strokeIcon()}<span class="swatch outline" style="border-color: {strokeColor ?? 'currentColor'}"></span>{/snippet}
<style>
.swatch {
width: 14px;
height: 14px;
border: 1px solid var(--w-border-strong);
border-radius: 2px;
}
.swatch.outline { background: transparent; border-width: 2px; }
.rb-captioned {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
padding: 4px 7px 6px;
}
.rb-caption, .field {
font-family: var(--w-font);
font-size: 12px;
color: var(--w-text);
white-space: nowrap;
}
.field {
display: flex;
align-items: center;
gap: 6px;
color: var(--w-text-dim);
}
.field input {
width: 52px;
height: 24px;
border: 1px solid var(--w-border-strong);
border-radius: 3px;
background: var(--w-surface);
padding: 0 5px;
color: var(--w-text);
font: inherit;
text-align: right;
}
.field input:focus { outline: none; border-color: var(--w-accent); }
</style>
@@ -0,0 +1,295 @@
<script lang="ts">
import type { Editor, ChainedCommands } from '@tiptap/core';
import { CellSelection } from '@tiptap/pm/tables';
import RibbonGroup from '../RibbonGroup.svelte';
import RibbonButton from '../RibbonButton.svelte';
import ColorPicker from '../../ColorPicker.svelte';
import TableBorderPicker from '../../TableBorderPicker.svelte';
import TableStylePicker from '../../TableStylePicker.svelte';
import TableSplitDialog from '../../TableSplitDialog.svelte';
import { anchored, clickOutside, isMenuOpen, toggleMenu, closeMenu } from '../menu.svelte';
import { isHeaderStyled } from '../../../editor/extensions/tableHeaderRow';
import { DEFAULT_CELL_PADDING, parseCellPadding, type CellPadding } from '../../../editor/extensions/tableCellPadding';
import type { CellVerticalAlign } from '../../../editor/extensions/tableCellAlign';
import { t } from '../../../i18n/i18n.svelte';
// Word splits the table's contextual tabs in two: Design paints it, Layout
// changes its shape.
let { editor, tick, which }: {
editor: Editor | null;
tick: number;
which: 'design' | 'layout';
} = $props();
let splitOpen = $state(false);
let splitAt = $state({ top: 0, left: 0 });
function openSplit(e: MouseEvent) {
const r = (e.currentTarget as HTMLElement).getBoundingClientRect();
splitAt = { top: r.bottom + 4, left: r.left };
splitOpen = true;
}
function run(cmd: (chain: ChainedCommands) => ChainedCommands) {
if (!editor) return;
cmd(editor.chain().focus()).run();
}
const canMerge = $derived(tick >= 0 && !!editor && editor.can().mergeCells());
const isHeaderRow = $derived(tick >= 0 && !!editor && isHeaderStyled(editor.state, 'row'));
const isHeaderCol = $derived(tick >= 0 && !!editor && isHeaderStyled(editor.state, 'column'));
// One attribute over the selected cells: the shared value, '' if they disagree.
function cellAttr(name: string): string | null {
if (!editor) return null;
const sel = editor.state.selection;
const values = new Set<string | null>();
if (sel instanceof CellSelection) {
sel.forEachCell((cell) => values.add((cell.attrs[name] as string) ?? null));
} else {
const from = sel.$from;
for (let d = from.depth; d > 0; d--) {
const role = from.node(d).type.spec.tableRole;
if (role === 'cell' || role === 'header_cell') {
values.add((from.node(d).attrs[name] as string) ?? null);
break;
}
}
}
if (values.size === 0) return null;
return values.size > 1 ? '' : ([...values][0] ?? null);
}
const cellColor = $derived(tick >= 0 ? cellAttr('backgroundColor') : null);
const vAlign = $derived(tick >= 0 ? cellAttr('verticalAlign') : null);
// The cell's margins in cm, [top, right, bottom, left]; null outside a table.
const padding = $derived.by<CellPadding | null>(() => {
if (tick < 0 || !editor || !editor.isActive('table')) return null;
// A cell that never set its margins reports none, and shows the format default.
return parseCellPadding(cellAttr('cellPadding')) ?? DEFAULT_CELL_PADDING;
});
function setPaddingEdge(i: number, cm: number) {
if (!editor || !padding) return;
const next = [...padding] as CellPadding;
next[i] = cm;
editor.chain().focus().setCellAttribute('cellPadding', next).run();
}
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 },
{ key: 'bottom', icon: 'alignBottom', label: () => t().table.cellAlignBottom },
];
</script>
{#if which === 'design'}
<RibbonGroup label={t().styles.tableStyles}>
<div class="rb-captioned">
<TableStylePicker {editor} {tick} />
<span class="rb-caption">{t().table.tableStyle}</span>
</div>
</RibbonGroup>
<div class="ribbon-sep"></div>
<RibbonGroup label={t().ribbon.groups.tableDecor}>
<div class="rb-captioned">
<ColorPicker
{editor}
currentColor={cellColor}
defaultColor="#D9D9D9"
title={t().table.cellShading}
chevronTitle={t().table.chooseCellColor}
clearLabel={t().table.noFill}
onApply={(c) => editor?.chain().focus().setCellAttribute('backgroundColor', c).run()}
onClear={() => editor?.chain().focus().setCellAttribute('backgroundColor', null).run()}
icon={shadeIcon}
/>
<span class="rb-caption">{t().table.cellShading}</span>
</div>
<div class="rb-captioned">
<TableBorderPicker {editor} {tick} />
<span class="rb-caption">{t().borders.title}</span>
</div>
</RibbonGroup>
<div class="ribbon-sep"></div>
<RibbonGroup label={t().ribbon.groups.tableOptions}>
<div class="rb-col">
<RibbonButton variant="small" icon="headerRow" label={t().styles.regions.headerRow} active={isHeaderRow} onclick={() => run((c) => c.toggleHeaderRowStyle())} />
<RibbonButton variant="small" icon="firstColumn" label={t().styles.regions.firstColumn} active={isHeaderCol} onclick={() => run((c) => c.toggleHeaderColumnStyle())} />
</div>
</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>
<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>
</RibbonGroup>
<div class="ribbon-sep"></div>
<RibbonGroup label={t().ribbon.groups.merge}>
<RibbonButton variant="big" icon="merge" label={t().table.mergeCells} disabled={!canMerge} onclick={() => run((c) => c.mergeCells())} />
<span class="split-anchor">
<button class="split-trigger" onclick={openSplit} title={t().table.splitCells}>
<RibbonButton variant="big" icon="split" label={t().table.splitCellsAria} />
</button>
<TableSplitDialog
open={splitOpen}
top={splitAt.top}
left={splitAt.left}
onApply={(cols, rows) => { splitOpen = false; editor?.chain().focus().splitCellInto(cols, rows).run(); }}
onClose={() => (splitOpen = false)}
/>
</span>
</RibbonGroup>
<div class="ribbon-sep"></div>
<RibbonGroup label={t().align.section}>
{#each VALIGNS as v}
<RibbonButton
icon={v.icon}
title={v.label()}
active={vAlign === v.key}
onclick={() => editor?.chain().focus().setCellAttribute('verticalAlign', v.key).run()}
/>
{/each}
</RibbonGroup>
<div class="ribbon-sep"></div>
<RibbonGroup label={t().ribbon.groups.cellSize}>
<div class="rb-menu-wrap" use:clickOutside={'cellMargins'}>
<RibbonButton
variant="big"
icon="cellMargins"
label={t().ribbon.cellMargins}
title={t().ribbon.cellMargins}
disabled={!padding}
caret
caretActive={isMenuOpen('cellMargins')}
onclick={() => toggleMenu('cellMargins')}
onCaret={() => toggleMenu('cellMargins')}
/>
{#if isMenuOpen('cellMargins') && padding}
<div class="ribbon-menu margin-menu" use:anchored role="menu">
<div class="rb-menu-label">{t().ribbon.cellMargins}</div>
<div class="margin-grid">
{#each (['top', 'right', 'bottom', 'left'] as const) as edge, i}
<label class="margin-field">
<span>{t().toolbarExpanded.margins[edge]}</span>
<input
type="text"
inputmode="decimal"
value={padding[i]}
onchange={(e) => {
const v = parseFloat((e.currentTarget as HTMLInputElement).value.replace(',', '.'));
if (!isNaN(v)) setPaddingEdge(i, Math.max(0, v));
}}
/>
</label>
{/each}
</div>
</div>
{/if}
</div>
</RibbonGroup>
{/if}
{#snippet shadeIcon()}<span class="shade-glyph"></span>{/snippet}
<style>
.rb-col {
display: flex;
flex-direction: column;
justify-content: center;
gap: 2px;
height: 100%;
}
.rb-menu-wrap { position: relative; }
/* The dialog positions itself in viewport coordinates, so the trigger only has
to hand it the button's rect. */
.split-trigger {
display: contents;
border: none;
background: none;
padding: 0;
cursor: pointer;
}
.margin-menu { min-width: 210px; }
.margin-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
padding: 2px 7px 4px;
}
.margin-field {
display: flex;
align-items: center;
gap: 6px;
font-family: var(--w-font);
font-size: 12px;
color: var(--w-text-dim);
}
.margin-field input {
width: 52px;
height: 24px;
border: 1px solid var(--w-border-strong);
border-radius: 3px;
background: var(--w-surface);
padding: 0 5px;
color: var(--w-text);
font: inherit;
text-align: right;
}
.margin-field input:focus { outline: none; border-color: var(--w-accent); }
.shade-glyph {
width: 14px;
height: 14px;
border: 1px solid currentColor;
border-radius: 2px;
background: repeating-linear-gradient(45deg, currentColor 0 1px, transparent 1px 3px);
}
.rb-captioned {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
padding: 4px 7px 6px;
}
.rb-caption {
font-family: var(--w-font);
font-size: 12px;
color: var(--w-text);
white-space: nowrap;
}
</style>
+8
View File
@@ -476,6 +476,13 @@ const de: Messages = {
language: 'Sprache',
show: 'Anzeigen',
debug: 'Debug',
tableDecor: 'Schattierung & Rahmen',
tableOptions: 'Tabellenformatoptionen',
rowsColumns: 'Zeilen & Spalten',
merge: 'Zusammenführen',
cellSize: 'Zellengröße',
arrange: 'Anordnen',
shapeStyles: 'Formenarten',
editing: 'Bearbeiten',
},
case: {
@@ -524,6 +531,7 @@ const de: Messages = {
tocOptions: 'Ebenen',
tocMaxLevel: 'Tiefste angezeigte Ebene',
debugDump: 'Dump',
cellMargins: 'Zellenränder',
chrome: {
title: 'Symbolleisten-Stil',
ribbon: 'Menüband',
+8
View File
@@ -480,6 +480,13 @@ const en = {
language: 'Language',
show: 'Show',
debug: 'Debug',
tableDecor: 'Shading & Borders',
tableOptions: 'Table Style Options',
rowsColumns: 'Rows & Columns',
merge: 'Merge',
cellSize: 'Cell Size',
arrange: 'Arrange',
shapeStyles: 'Shape Styles',
editing: 'Editing',
},
case: {
@@ -528,6 +535,7 @@ const en = {
tocOptions: 'Levels',
tocMaxLevel: 'Deepest level shown',
debugDump: 'Dump',
cellMargins: 'Cell margins',
chrome: {
title: 'Toolbar style',
ribbon: 'Ribbon',