diff --git a/docs/architecture/ribbon.md b/docs/architecture/ribbon.md index be83264..69efed9 100644 --- a/docs/architecture/ribbon.md +++ b/docs/architecture/ribbon.md @@ -53,6 +53,15 @@ The band's height never changes: it scrolls horizontally with a hidden scrollbar collapsing groups into popovers the way Word does when the window narrows. That is the single biggest simplification here, and the reason two things below are necessary. +A chevron in the tab strip drops the band entirely, leaving the strip (`odf-editor-ribbon-collapsed`). +Clicking any tab brings it back — Word instead floats it over the document until the next click +lands in the text, which is the part not built. + +Word puts that chevron in the band's bottom-right corner, and this one did too. Both ways of +reserving that corner failed: as a layer it covered whichever group's dialog launcher reached the +edge, and since the band scrolls, which one that is varies; in the flow it cost the band a column +at every height. A flex row cannot reserve width in part of its height, so it moved to the strip. + - **A menu must leave the flow.** `.ribbon-body` clips its own overflow, so the `anchored` action (`ribbon/menu.svelte.ts`) switches a panel to `position: fixed` and pins it under its wrapper, clamped inside the window. Panels drop below the **whole band**, not below their own button — diff --git a/src/lib/components/ribbon/Ribbon.svelte b/src/lib/components/ribbon/Ribbon.svelte index 50a8133..78e306e 100644 --- a/src/lib/components/ribbon/Ribbon.svelte +++ b/src/lib/components/ribbon/Ribbon.svelte @@ -19,7 +19,7 @@ import { t } from '../../i18n/i18n.svelte'; import { withShortcut } from '../../i18n/shortcut'; import { shortcutHint } from '../../editor/shortcuts'; - import type { ChromeMode, ThemeMode } from '../../storage/theme'; + import { loadRibbonCollapsed, saveRibbonCollapsed, type ChromeMode, type ThemeMode } from '../../storage/theme'; import type { StyleFamily } from '../../styles/styleSheet'; import { DEFAULT_MARGINS, type PageMargins } from '../../storage/pageMargins'; import type { Orientation } from '../../storage/pageOrientation'; @@ -130,6 +130,9 @@ let paragraphDialogOpen = $state(false); let tabsDialogOpen = $state(false); + let collapsed = $state(loadRibbonCollapsed()); + $effect(() => saveRibbonCollapsed(collapsed)); + function run(fn?: () => void) { closeMenu(); fn?.(); @@ -202,7 +205,7 @@ class="ribbon-tab" class:active={tab === id} class:contextual={!(TABS as readonly string[]).includes(id)} - onclick={() => (tab = id)} + onclick={() => { tab = id; collapsed = false; }} > {t().ribbon.tabs[id]} @@ -225,6 +228,20 @@ .odt + + +
+ {#if !collapsed}
{#if tab === 'home'} (paragraphDialogOpen = true)} /> @@ -287,6 +305,7 @@ /> {/if}
+ {/if} (tabsDialogOpen = true)} /> @@ -319,6 +338,10 @@ user-select: none; } + /* Points down to bring the band back, up to send it away. */ + .rb-collapse :global(svg) { transition: transform 0.12s ease; } + .rb-collapse-up :global(svg) { transform: rotate(180deg); } + /* Those pickers frame their trigger for the classic toolbar's tinted island. A ribbon row is borderless icon buttons, so the frame comes off and the hover tint takes over — same 30px box as .rb-icon, so a row reads as one strip. */ diff --git a/src/lib/i18n/locales/de.ts b/src/lib/i18n/locales/de.ts index 64a51ea..2061d31 100644 --- a/src/lib/i18n/locales/de.ts +++ b/src/lib/i18n/locales/de.ts @@ -474,6 +474,8 @@ const de: Messages = { shapeFormat: 'Formformat', }, saveAs: 'Speichern unter…', + collapse: 'Menüband minimieren', + expand: 'Menüband erweitern', export: 'Exportieren', find: 'Suchen', replace: 'Ersetzen', diff --git a/src/lib/i18n/locales/en.ts b/src/lib/i18n/locales/en.ts index 3418b39..15acb9f 100644 --- a/src/lib/i18n/locales/en.ts +++ b/src/lib/i18n/locales/en.ts @@ -478,6 +478,8 @@ const en = { shapeFormat: 'Shape Format', }, saveAs: 'Save as…', + collapse: 'Collapse the ribbon', + expand: 'Expand the ribbon', export: 'Export', find: 'Find', replace: 'Replace', diff --git a/src/lib/storage/CLAUDE.md b/src/lib/storage/CLAUDE.md index 1827aa1..747c030 100644 --- a/src/lib/storage/CLAUDE.md +++ b/src/lib/storage/CLAUDE.md @@ -20,6 +20,7 @@ Both margins and orientation are passed into the ODT export so the exported docu - **Theme:** `odf-editor-theme` — `'light' | 'dark' | 'allBlack' | 'auto'`. - **Toolbar expanded:** `odf-editor-toolbar-expanded` — boolean string. - **Chrome:** `odf-editor-chrome` — `'classic' | 'ribbon'`; absent = classic. +- **Ribbon collapsed:** `odf-editor-ribbon-collapsed` — boolean string; absent = expanded. - **Formatting marks:** `odf-editor-formatting-marks` — boolean string. - **Ruler:** `odf-editor-ruler` — boolean string; absent = on. - **Zoom:** `odf-editor-zoom` — integer percent. diff --git a/src/lib/storage/theme.ts b/src/lib/storage/theme.ts index 878dd3a..d4a46e3 100644 --- a/src/lib/storage/theme.ts +++ b/src/lib/storage/theme.ts @@ -7,6 +7,7 @@ export type ChromeMode = 'classic' | 'ribbon'; const THEME_KEY = 'odf-editor-theme'; const TOOLBAR_KEY = 'odf-editor-toolbar-expanded'; const CHROME_KEY = 'odf-editor-chrome'; +const RIBBON_COLLAPSED_KEY = 'odf-editor-ribbon-collapsed'; const FORMATTING_MARKS_KEY = 'odf-editor-formatting-marks'; const RULER_KEY = 'odf-editor-ruler'; @@ -36,6 +37,14 @@ export function saveChromeMode(mode: ChromeMode): void { localStorage.setItem(CHROME_KEY, mode); } +export function loadRibbonCollapsed(): boolean { + return localStorage.getItem(RIBBON_COLLAPSED_KEY) === 'true'; +} + +export function saveRibbonCollapsed(collapsed: boolean): void { + localStorage.setItem(RIBBON_COLLAPSED_KEY, String(collapsed)); +} + export function loadFormattingMarks(): boolean { return localStorage.getItem(FORMATTING_MARKS_KEY) === 'true'; }