fix: one shape and one baseline for the ribbon's buttons
Ten of the fifteen caret buttons ran the same toggleMenu from both halves, so they were split for nothing. They are single buttons now, with the caret under the label rather than beside it — 13px narrower each, and Word's own Paste button. The tint takes the whole button, label included: a row mixes buttons whose label is inside the button with pickers whose caption is a sibling, so half of them lit up under the words and half did not. `captionClicks` gives the caption the click to match, and the picker's trigger takes the icon box's 34px so glyphs and words land on one line.
This commit is contained in:
1 parent
a78e21b524
commit
4b84e2efac
10 files changed
+113
-41
No files matched your search
@@ -34,17 +34,26 @@
|
||||
} = $props();
|
||||
|
||||
const ICON_SIZE = { big: 28, small: 14, icon: 20 } as const;
|
||||
|
||||
// Nothing to split when the whole button opens the menu, so the caret joins the
|
||||
// column under the label instead of sitting beside the icon.
|
||||
let stackCaret = $derived(caret && !onCaret && variant === 'big');
|
||||
</script>
|
||||
|
||||
{#snippet face(withLabel = true)}
|
||||
<span class="rb-face">
|
||||
{#if content}{@render content()}
|
||||
{:else if icon}<Icon name={icon} size={ICON_SIZE[variant]} />{/if}
|
||||
{#if caret && !onCaret}
|
||||
{#if caret && !onCaret && !stackCaret}
|
||||
<Icon name="chevronDown" size={10} />
|
||||
{/if}
|
||||
</span>
|
||||
{#if withLabel && label && variant !== 'icon'}<span class="rb-label">{label}</span>{/if}
|
||||
<!-- Held open on every big button, carrying a caret or nothing: the group centres
|
||||
its controls, so one taller button lifts its own icon and label off the row. -->
|
||||
{#if variant === 'big' && !onCaret}
|
||||
<span class="rb-stack-caret">{#if caret}<Icon name="chevronDown" size={10} />{/if}</span>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if caret && onCaret}
|
||||
@@ -67,7 +76,15 @@
|
||||
</button>
|
||||
</span>
|
||||
{:else}
|
||||
<button class="rb rb-{variant}" class:active {disabled} {title} {onclick}>
|
||||
<button
|
||||
class="rb rb-{variant}"
|
||||
class:active
|
||||
{disabled}
|
||||
{title}
|
||||
aria-haspopup={caret ? 'menu' : undefined}
|
||||
aria-expanded={caret ? active : undefined}
|
||||
{onclick}
|
||||
>
|
||||
{@render face()}
|
||||
</button>
|
||||
{/if}
|
||||
@@ -98,8 +115,8 @@
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* Icon over label. The hover tint paints the icon box, not the whole button —
|
||||
Word's signature, and what keeps a labelled column from looking like a slab. */
|
||||
/* Icon over label, and the tint takes both. Word paints only the icon box, but
|
||||
then the words beside an identical-looking neighbour light up and these do not. */
|
||||
.rb-big {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
@@ -115,9 +132,9 @@
|
||||
margin: 0 -4px;
|
||||
}
|
||||
|
||||
.rb-big:hover:not(:disabled) .rb-face { background: var(--w-hover); }
|
||||
.rb-big:active:not(:disabled) .rb-face { background: var(--w-pressed); }
|
||||
.rb-big.active .rb-face { background: var(--w-active); }
|
||||
.rb-big:hover:not(:disabled) { background: var(--w-hover); }
|
||||
.rb-big:active:not(:disabled) { background: var(--w-pressed); }
|
||||
.rb-big.active { background: var(--w-active); }
|
||||
|
||||
/* A row in a stacked mini-column (Cut / Copy under a big Paste). */
|
||||
.rb-small {
|
||||
@@ -145,6 +162,14 @@
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Rides closer to the label than the 4px column gap: the two read as one line. */
|
||||
.rb-stack-caret {
|
||||
display: flex;
|
||||
height: 10px;
|
||||
margin-top: -3px;
|
||||
color: var(--w-text-dim);
|
||||
}
|
||||
|
||||
/* Split button: the two halves read as one control, so the hover outline sits on
|
||||
the wrapper and each half only tints its own surface. */
|
||||
.rb-split {
|
||||
@@ -169,15 +194,15 @@
|
||||
}
|
||||
|
||||
/* A big split button stacks, as in Word: the icon runs the command, the label
|
||||
and its caret open the menu. Welded to the right the caret would float at
|
||||
icon height, beside the label rather than with it. */
|
||||
and the caret under it open the menu. Icon, label and caret each get their
|
||||
own line — abreast, the caret widens the button by its own width. */
|
||||
.rb-split-col { flex-direction: column; }
|
||||
.rb-split-col .rb-big { padding: 4px 7px 0; }
|
||||
.rb-split-col .rb-big { padding: 3px 7px 0; }
|
||||
|
||||
.rb-split-col .rb-caret {
|
||||
flex-direction: column;
|
||||
width: auto;
|
||||
gap: 3px;
|
||||
padding: 4px 7px 6px;
|
||||
padding: 0 7px 2px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
color: var(--w-text);
|
||||
}
|
||||
|
||||
@@ -111,3 +111,16 @@ export function clickOutside(node: HTMLElement, id?: string) {
|
||||
window.addEventListener('mousedown', handler);
|
||||
return { destroy() { window.removeEventListener('mousedown', handler); } };
|
||||
}
|
||||
|
||||
// A reused picker keeps its trigger inside its own component, so the caption the
|
||||
// ribbon puts beside it is not part of that button: hovering the words lit nothing
|
||||
// and clicking them did nothing. Forwards a click that missed every button to the
|
||||
// first one, so glyph and caption behave as the single control they look like.
|
||||
export function captionClicks(node: HTMLElement) {
|
||||
function handler(e: MouseEvent) {
|
||||
if ((e.target as HTMLElement).closest('button, select, input')) return;
|
||||
node.querySelector('button')?.click();
|
||||
}
|
||||
node.addEventListener('click', handler);
|
||||
return { destroy() { node.removeEventListener('click', handler); } };
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { captionClicks } from '../menu.svelte';
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import RibbonGroup from '../RibbonGroup.svelte';
|
||||
import RibbonButton from '../RibbonButton.svelte';
|
||||
@@ -70,7 +71,7 @@
|
||||
{#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">
|
||||
<div class="rb-captioned" use:captionClicks>
|
||||
<ColorPicker
|
||||
{editor}
|
||||
currentColor={fillColor ?? null}
|
||||
@@ -84,7 +85,7 @@
|
||||
/>
|
||||
<span class="rb-caption">{t().textBox.fillColor}</span>
|
||||
</div>
|
||||
<div class="rb-captioned">
|
||||
<div class="rb-captioned" use:captionClicks>
|
||||
<ColorPicker
|
||||
{editor}
|
||||
currentColor={strokeColor ?? null}
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
<RibbonButton title={t().toolbarExpanded.growFont} content={growIcon} onclick={() => editor && stepFontSize(editor, 1)} />
|
||||
<RibbonButton title={t().toolbarExpanded.shrinkFont} content={shrinkIcon} onclick={() => editor && stepFontSize(editor, -1)} />
|
||||
<div class="rb-menu-wrap" use:clickOutside={'case'}>
|
||||
<RibbonButton icon="changeCase" title={t().ribbon.case.title} caretActive={isMenuOpen('case')} caret onCaret={() => toggleMenu('case')} onclick={() => toggleMenu('case')} />
|
||||
<RibbonButton icon="changeCase" title={t().ribbon.case.title} active={isMenuOpen('case')} caret onclick={() => toggleMenu('case')} />
|
||||
{#if isMenuOpen('case')}
|
||||
<div class="ribbon-menu" use:anchored role="menu">
|
||||
{#each CASES as c}
|
||||
@@ -311,7 +311,7 @@
|
||||
/>
|
||||
{/each}
|
||||
<div class="rb-menu-wrap" use:clickOutside={'lineHeight'}>
|
||||
<RibbonButton icon="lineSpacing" title={t().toolbarExpanded.lineSpacing} caret caretActive={isMenuOpen('lineHeight')} onclick={() => toggleMenu('lineHeight')} onCaret={() => toggleMenu('lineHeight')} />
|
||||
<RibbonButton icon="lineSpacing" title={t().toolbarExpanded.lineSpacing} caret active={isMenuOpen('lineHeight')} onclick={() => toggleMenu('lineHeight')} />
|
||||
{#if isMenuOpen('lineHeight')}
|
||||
<div class="ribbon-menu" use:anchored role="menu">
|
||||
{#each LINE_HEIGHTS as h}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import BookmarkDialog from '../../BookmarkDialog.svelte';
|
||||
import CrossRefDialog from '../../CrossRefDialog.svelte';
|
||||
import FormulaDialog from '../../FormulaDialog.svelte';
|
||||
import { anchored, clickOutside, isMenuOpen, toggleMenu, closeMenu } from '../menu.svelte';
|
||||
import { captionClicks, anchored, clickOutside, isMenuOpen, toggleMenu, closeMenu } from '../menu.svelte';
|
||||
import { OPEN_LINK_DIALOG_EVENT } from '../../../editor/extensions/link';
|
||||
import { OPEN_BOOKMARK_DIALOG_EVENT, bookmarkNames, findBookmark } from '../../../editor/extensions/bookmark';
|
||||
import { OPEN_CROSS_REF_DIALOG_EVENT } from '../../../editor/extensions/crossReference';
|
||||
@@ -198,7 +198,7 @@
|
||||
<div class="ribbon-sep"></div>
|
||||
|
||||
<RibbonGroup label={t().ribbon.groups.tables}>
|
||||
<div class="rb-captioned">
|
||||
<div class="rb-captioned" use:captionClicks>
|
||||
<TablePicker
|
||||
{editor}
|
||||
bind:open={tableOpen}
|
||||
@@ -242,7 +242,7 @@
|
||||
<RibbonButton variant="big" icon="header" label={t().ribbon.header} title={t().toolbarExpanded.editHeader} disabled={!editor} onclick={() => onEditZone?.('header')} />
|
||||
<RibbonButton variant="big" icon="footer" label={t().ribbon.footer} title={t().toolbarExpanded.editFooter} disabled={!editor} onclick={() => onEditZone?.('footer')} />
|
||||
<div class="rb-menu-wrap" use:clickOutside={'pageField'}>
|
||||
<RibbonButton variant="big" icon="pageNumber" label={t().ribbon.pageNumber} title={t().ribbon.pageNumber} caret caretActive={isMenuOpen('pageField')} onclick={() => toggleMenu('pageField')} onCaret={() => toggleMenu('pageField')} />
|
||||
<RibbonButton variant="big" icon="pageNumber" label={t().ribbon.pageNumber} title={t().ribbon.pageNumber} caret active={isMenuOpen('pageField')} onclick={() => toggleMenu('pageField')} />
|
||||
{#if isMenuOpen('pageField')}
|
||||
<div class="ribbon-menu" use:anchored role="menu">
|
||||
<button onclick={() => insertPageField('pageNumber')}>{t().hf.pageNumber}</button>
|
||||
@@ -255,7 +255,7 @@
|
||||
<div class="ribbon-sep"></div>
|
||||
|
||||
<RibbonGroup label={t().ribbon.groups.text}>
|
||||
<div class="rb-captioned">
|
||||
<div class="rb-captioned" use:captionClicks>
|
||||
<DateTimePicker
|
||||
bind:open={dateOpen}
|
||||
{editor}
|
||||
@@ -268,7 +268,7 @@
|
||||
<div class="ribbon-sep"></div>
|
||||
|
||||
<RibbonGroup label={t().ribbon.groups.symbols}>
|
||||
<div class="rb-captioned">
|
||||
<div class="rb-captioned" use:captionClicks>
|
||||
<SpecialCharPicker
|
||||
bind:open={charOpen}
|
||||
{editor}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
|
||||
<RibbonGroup label={t().ribbon.groups.pageSetup}>
|
||||
<div class="rb-menu-wrap" use:clickOutside={'margins'}>
|
||||
<RibbonButton variant="big" icon="margins" label={t().ribbon.margins} title={t().toolbarExpanded.pageMargins} caret caretActive={isMenuOpen('margins')} onclick={() => toggleMenu('margins')} onCaret={() => toggleMenu('margins')} />
|
||||
<RibbonButton variant="big" icon="margins" label={t().ribbon.margins} title={t().toolbarExpanded.pageMargins} caret active={isMenuOpen('margins')} onclick={() => toggleMenu('margins')} />
|
||||
{#if isMenuOpen('margins')}
|
||||
<div class="ribbon-menu margin-menu" use:anchored role="menu">
|
||||
{#each MARGIN_PRESETS as p}
|
||||
@@ -106,7 +106,7 @@
|
||||
</div>
|
||||
|
||||
<div class="rb-menu-wrap" use:clickOutside={'orientation'}>
|
||||
<RibbonButton variant="big" icon="orientation" label={t().toolbarExpanded.orientation} title={t().toolbarExpanded.orientation} caret caretActive={isMenuOpen('orientation')} onclick={() => toggleMenu('orientation')} onCaret={() => toggleMenu('orientation')} />
|
||||
<RibbonButton variant="big" icon="orientation" label={t().toolbarExpanded.orientation} title={t().toolbarExpanded.orientation} caret active={isMenuOpen('orientation')} onclick={() => toggleMenu('orientation')} />
|
||||
{#if isMenuOpen('orientation')}
|
||||
<div class="ribbon-menu" use:anchored role="menu">
|
||||
{#each (['portrait', 'landscape'] as const) as o}
|
||||
@@ -117,7 +117,7 @@
|
||||
</div>
|
||||
|
||||
<div class="rb-menu-wrap" use:clickOutside={'pageFormat'}>
|
||||
<RibbonButton variant="big" icon="pageSize" label={t().ribbon.size} title={t().toolbarExpanded.pageFormat} caret caretActive={isMenuOpen('pageFormat')} onclick={() => toggleMenu('pageFormat')} onCaret={() => toggleMenu('pageFormat')} />
|
||||
<RibbonButton variant="big" icon="pageSize" label={t().ribbon.size} title={t().toolbarExpanded.pageFormat} caret active={isMenuOpen('pageFormat')} onclick={() => toggleMenu('pageFormat')} />
|
||||
{#if isMenuOpen('pageFormat')}
|
||||
<div class="ribbon-menu format-menu" use:anchored role="menu">
|
||||
<div class="menu-scroll">
|
||||
@@ -140,9 +140,8 @@
|
||||
title={hfActive ? t().toolbarExpanded.columnsNotInHf : t().toolbarExpanded.columns}
|
||||
disabled={!editor || !!hfActive}
|
||||
caret
|
||||
caretActive={isMenuOpen('columns')}
|
||||
active={isMenuOpen('columns')}
|
||||
onclick={() => toggleMenu('columns')}
|
||||
onCaret={() => toggleMenu('columns')}
|
||||
/>
|
||||
{#if isMenuOpen('columns')}
|
||||
<div class="ribbon-menu" use:anchored role="menu">
|
||||
@@ -174,9 +173,8 @@
|
||||
title={t().ribbon.breaks}
|
||||
disabled={!editor || !!hfActive}
|
||||
caret
|
||||
caretActive={isMenuOpen('breaks')}
|
||||
active={isMenuOpen('breaks')}
|
||||
onclick={() => toggleMenu('breaks')}
|
||||
onCaret={() => toggleMenu('breaks')}
|
||||
/>
|
||||
{#if isMenuOpen('breaks')}
|
||||
<div class="ribbon-menu breaks-menu" use:anchored role="menu">
|
||||
@@ -271,6 +269,13 @@
|
||||
gap: 3px 12px;
|
||||
}
|
||||
|
||||
.field span { min-width: 74px; }
|
||||
/* The label takes whatever its grid column is wide, so the boxes line up on the
|
||||
column's right edge. A fixed label width only holds until a translation
|
||||
outgrows it, and then one row of the pair sits deeper than the other. */
|
||||
.margin-field > span, .field > span {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.field input:disabled { opacity: 0.5; }
|
||||
</style>
|
||||
@@ -48,9 +48,8 @@
|
||||
title={t().ribbon.tocOptions}
|
||||
disabled={!toc}
|
||||
caret
|
||||
caretActive={isMenuOpen('tocLevels')}
|
||||
active={isMenuOpen('tocLevels')}
|
||||
onclick={() => toggleMenu('tocLevels')}
|
||||
onCaret={() => toggleMenu('tocLevels')}
|
||||
/>
|
||||
{#if isMenuOpen('tocLevels')}
|
||||
<div class="ribbon-menu" use:anchored role="menu">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import RibbonGroup from '../RibbonGroup.svelte';
|
||||
import RibbonButton from '../RibbonButton.svelte';
|
||||
import LanguagePicker from '../../LanguagePicker.svelte';
|
||||
import { anchored, clickOutside, isMenuOpen, toggleMenu } from '../menu.svelte';
|
||||
import { captionClicks, anchored, clickOutside, isMenuOpen, toggleMenu } from '../menu.svelte';
|
||||
import { countText, type TextStats } from '../../../utils/wordCount';
|
||||
import type { DocumentLanguage } from '../../../storage/documentLanguage';
|
||||
import { t } from '../../../i18n/i18n.svelte';
|
||||
@@ -65,7 +65,7 @@
|
||||
<div class="ribbon-sep"></div>
|
||||
|
||||
<RibbonGroup label={t().ribbon.groups.language}>
|
||||
<div class="rb-captioned">
|
||||
<div class="rb-captioned" use:captionClicks>
|
||||
<LanguagePicker value={documentLanguage} onChange={onLanguage} />
|
||||
<span class="rb-caption">{t().spellPicker.label}</span>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
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 { 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';
|
||||
import type { CellVerticalAlign } from '../../../editor/extensions/tableCellAlign';
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
{#if which === 'design'}
|
||||
<RibbonGroup label={t().styles.tableStyles}>
|
||||
<div class="rb-captioned">
|
||||
<div class="rb-captioned" use:captionClicks>
|
||||
<TableStylePicker {editor} {tick} />
|
||||
<span class="rb-caption">{t().table.tableStyle}</span>
|
||||
</div>
|
||||
@@ -94,7 +94,7 @@
|
||||
<div class="ribbon-sep"></div>
|
||||
|
||||
<RibbonGroup label={t().ribbon.groups.tableDecor}>
|
||||
<div class="rb-captioned">
|
||||
<div class="rb-captioned" use:captionClicks>
|
||||
<ColorPicker
|
||||
{editor}
|
||||
currentColor={cellColor}
|
||||
@@ -108,7 +108,7 @@
|
||||
/>
|
||||
<span class="rb-caption">{t().table.cellShading}</span>
|
||||
</div>
|
||||
<div class="rb-captioned">
|
||||
<div class="rb-captioned" use:captionClicks>
|
||||
<TableBorderPicker {editor} {tick} />
|
||||
<span class="rb-caption">{t().borders.title}</span>
|
||||
</div>
|
||||
@@ -186,9 +186,8 @@
|
||||
title={t().ribbon.cellMargins}
|
||||
disabled={!padding}
|
||||
caret
|
||||
caretActive={isMenuOpen('cellMargins')}
|
||||
active={isMenuOpen('cellMargins')}
|
||||
onclick={() => toggleMenu('cellMargins')}
|
||||
onCaret={() => toggleMenu('cellMargins')}
|
||||
/>
|
||||
{#if isMenuOpen('cellMargins') && padding}
|
||||
<div class="ribbon-menu margin-menu" use:anchored role="menu">
|
||||
|
||||
+32
-2
@@ -452,7 +452,38 @@
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 4px 7px 6px;
|
||||
--toolbar-btn-size: 34px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
--toolbar-btn-size: 28px;
|
||||
}
|
||||
|
||||
/* Mirrors .rb-big's box, down to the slot it holds open for a caret, so a picker
|
||||
and a labelled button in the same row put their glyph and their words on one
|
||||
line. Their triggers carry their own frame and padding, which is dropped. */
|
||||
/* A picker root is a block, so its inline-flex trigger would sit on a text
|
||||
baseline and carry the descender gap with it. */
|
||||
.rb-captioned > * { display: flex; }
|
||||
|
||||
.rb-captioned :is(.table-trigger, .char-trigger, .dt-trigger, .tsp-trigger, .bp-trigger, .lang-picker) {
|
||||
height: 34px;
|
||||
min-height: 34px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rb-captioned::after {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 10px;
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
/* The trigger lives in the picker's own component, so the tint goes on the wrapper
|
||||
instead — glyph and caption light up together, as they do on a ribbon button.
|
||||
`captionClicks` gives the caption the click to match. */
|
||||
.rb-captioned:hover { background: var(--w-hover); }
|
||||
.rb-captioned:hover :is(.table-trigger, .char-trigger, .dt-trigger, .tsp-trigger, .bp-trigger, .color-split) {
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* The first svg is the glyph; a second one is the trigger's own caret and keeps
|
||||
@@ -464,7 +495,6 @@
|
||||
/* The spell-check picker is chromeless for the status bar; in a group it needs
|
||||
the combo frame and the same box as the labelled buttons beside it. */
|
||||
.rb-captioned .lang-picker {
|
||||
height: 34px;
|
||||
padding: 0 6px;
|
||||
border: 1px solid var(--w-border-strong);
|
||||
border-radius: 3px;
|
||||
|
||||
Reference in new issue
Block a user