From 4b84e2efacffbdf4d8767dd4cfa27e03721d9340 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Wed, 12 Aug 2026 18:14:43 +0200 Subject: [PATCH] fix: one shape and one baseline for the ribbon's buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/lib/components/ribbon/RibbonButton.svelte | 49 ++++++++++++++----- src/lib/components/ribbon/menu.svelte.ts | 13 +++++ .../components/ribbon/tabs/FrameTabs.svelte | 5 +- src/lib/components/ribbon/tabs/HomeTab.svelte | 4 +- .../components/ribbon/tabs/InsertTab.svelte | 10 ++-- .../components/ribbon/tabs/LayoutTab.svelte | 21 +++++--- .../ribbon/tabs/ReferencesTab.svelte | 3 +- .../components/ribbon/tabs/ReviewTab.svelte | 4 +- .../components/ribbon/tabs/TableTabs.svelte | 11 ++--- src/styles/global.css | 34 ++++++++++++- 10 files changed, 113 insertions(+), 41 deletions(-) diff --git a/src/lib/components/ribbon/RibbonButton.svelte b/src/lib/components/ribbon/RibbonButton.svelte index e93b525..6f35bca 100644 --- a/src/lib/components/ribbon/RibbonButton.svelte +++ b/src/lib/components/ribbon/RibbonButton.svelte @@ -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'); {#snippet face(withLabel = true)} {#if content}{@render content()} {:else if icon}{/if} - {#if caret && !onCaret} + {#if caret && !onCaret && !stackCaret} {/if} {#if withLabel && label && variant !== 'icon'}{label}{/if} + + {#if variant === 'big' && !onCaret} + {#if caret}{/if} + {/if} {/snippet} {#if caret && onCaret} @@ -67,7 +76,15 @@ {:else} - {/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); } diff --git a/src/lib/components/ribbon/menu.svelte.ts b/src/lib/components/ribbon/menu.svelte.ts index d3526a1..4dfbbb0 100644 --- a/src/lib/components/ribbon/menu.svelte.ts +++ b/src/lib/components/ribbon/menu.svelte.ts @@ -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); } }; +} diff --git a/src/lib/components/ribbon/tabs/FrameTabs.svelte b/src/lib/components/ribbon/tabs/FrameTabs.svelte index 31c35cf..2b6f2f9 100644 --- a/src/lib/components/ribbon/tabs/FrameTabs.svelte +++ b/src/lib/components/ribbon/tabs/FrameTabs.svelte @@ -1,4 +1,5 @@