feat: insert caption is in the picture and table tabs too

References keeps it, as in both products, but that is a tab away from the
object being captioned. The contextual tabs mount their own CaptionDialog —
it reads the caret for category and position, so the copies take no props.
This commit is contained in:
Steffen Becker committed 2026-08-16 22:22:29 +02:00
1 parent 53317bf88b
commit 6c731b9233
5 files changed
+46

No files matched your search

+4
View File
@@ -84,6 +84,10 @@ at every height. A flex row cannot reserve width in part of its height, so it mo
- **A reused picker anchors its panel itself**, and no prop says when it opened. `pinPanels`,
one action on the band, watches for a mounted absolutely-positioned node and pins that; the
ColorPicker places its own and only needs the band-aware top.
- **Insert caption sits in three tabs**: References, where both products keep it, plus the
picture/shape and Table Layout contextual tabs, which are open anyway when you caption
something. Each mounts its own `CaptionDialog`; it reads the caret for its category, so the
copies need no props of their own.
- **Each wrapper names the menu it owns.** One shared open-menu id means every `clickOutside`
sees every mousedown; without the id each wrapper would close a sibling's open menu, and the
click on one of its rows would never land.
@@ -5,6 +5,7 @@
import RibbonButton from '../RibbonButton.svelte';
import ColorPicker from '../../ColorPicker.svelte';
import ShapePicker from '../../ShapePicker.svelte';
import CaptionDialog from '../../CaptionDialog.svelte';
import type { WrapMode } from '../../../editor/extensions/image';
import type { ShapeKind } from '../../../editor/extensions/textBox';
import { t } from '../../../i18n/i18n.svelte';
@@ -29,6 +30,8 @@
{ key: 'topBottom', icon: 'wrapTopBottom', label: () => t().image.wrapTopBottom },
];
let captionOpen = $state(false);
function setWrap(w: WrapMode) {
if (which === 'picture') editor?.chain().focus().setImageWrap(w).run();
else editor?.chain().focus().setTextBoxAttrs({ wrap: w }).run();
@@ -41,6 +44,21 @@
{/each}
</RibbonGroup>
<div class="ribbon-sep"></div>
<!-- Also in the References tab, where both products keep it — but that is a tab away
from the object it captions, and this one only shows while that object is selected. -->
<RibbonGroup label={t().ribbon.groups.captions}>
<RibbonButton
variant="big"
icon="caption"
label={t().ribbon.insertCaption}
title={t().caption.title}
disabled={!editor}
onclick={() => (captionOpen = true)}
/>
</RibbonGroup>
{#if which === 'picture'}
<div class="ribbon-sep"></div>
@@ -113,6 +131,8 @@
</RibbonGroup>
{/if}
<CaptionDialog bind:open={captionOpen} {editor} />
{#snippet fillIcon()}<span class="swatch" style="background: {fillColor ?? 'transparent'}"></span>{/snippet}
{#snippet strokeIcon()}<span class="swatch outline" style="border-color: {strokeColor ?? 'currentColor'}"></span>{/snippet}
@@ -11,6 +11,7 @@
import TableSplitDialog from '../../TableSplitDialog.svelte';
import TableSortDialog from '../../TableSortDialog.svelte';
import TableFormulaDialog from '../../TableFormulaDialog.svelte';
import CaptionDialog from '../../CaptionDialog.svelte';
import { captionClicks, 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';
@@ -28,6 +29,8 @@
which: 'design' | 'layout';
} = $props();
let captionOpen = $state(false);
function run(cmd: (chain: ChainedCommands) => ChainedCommands) {
if (!editor) return;
cmd(editor.chain().focus()).run();
@@ -302,8 +305,25 @@
onclick={() => setNumberRecognition(!numberRecognition())}
/>
</RibbonGroup>
<div class="ribbon-sep"></div>
<!-- Also in the References tab, where both products keep it — but that is a tab away
from the table it captions, and this one only shows while the caret is in one. -->
<RibbonGroup label={t().ribbon.groups.captions}>
<RibbonButton
variant="big"
icon="caption"
label={t().ribbon.insertCaption}
title={t().caption.title}
disabled={!inTable}
onclick={() => (captionOpen = true)}
/>
</RibbonGroup>
{/if}
<CaptionDialog bind:open={captionOpen} {editor} />
{#snippet shadeIcon()}<span class="shade-glyph"></span>{/snippet}
<style>
+1
View File
@@ -773,6 +773,7 @@ const de: Messages = {
arrange: 'Anordnen',
shapeStyles: 'Formenarten',
accessibility: 'Barrierefreiheit',
captions: 'Beschriftungen',
editing: 'Bearbeiten',
},
case: {
+1
View File
@@ -779,6 +779,7 @@ const en = {
arrange: 'Arrange',
shapeStyles: 'Shape Styles',
accessibility: 'Accessibility',
captions: 'Captions',
editing: 'Editing',
},
case: {