From 10b650c64c9ee33664ab2cf4eb1bc41fba624b4f Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Mon, 17 Aug 2026 20:05:15 +0200 Subject: [PATCH] refactor: the floating island's mode id is 'modern', matching its label The id said 'classic' while the UI called the ribbon Classic and the island Modern (beta). Type, i18n keys, comments and docs now agree; a stored legacy 'classic' still selects the island. --- docs/architecture/ribbon.md | 18 +++++++++--------- src/App.svelte | 2 +- src/lib/components/CLAUDE.md | 4 ++-- src/lib/components/ColorPicker.svelte | 2 +- src/lib/components/NavigatorPane.svelte | 2 +- src/lib/components/ribbon/Ribbon.svelte | 8 ++++---- src/lib/i18n/locales/de.ts | 6 ++---- src/lib/i18n/locales/en.ts | 6 ++---- src/lib/storage/CLAUDE.md | 2 +- src/lib/storage/theme.ts | 6 ++++-- src/styles/global.css | 4 ++-- tests/unit/chrome-mode.test.ts | 19 +++++++++++++++++++ 12 files changed, 48 insertions(+), 31 deletions(-) create mode 100644 tests/unit/chrome-mode.test.ts diff --git a/docs/architecture/ribbon.md b/docs/architecture/ribbon.md index b0f6713..987f5ba 100644 --- a/docs/architecture/ribbon.md +++ b/docs/architecture/ribbon.md @@ -6,16 +6,16 @@ compared. Everything the ribbon drives is the same editor, through the same comm ## The mode switch -`chromeMode: 'classic' | 'ribbon'` lives in `storage/theme.ts` beside the other UI-chrome prefs +`chromeMode: 'modern' | 'ribbon'` lives in `storage/theme.ts` beside the other UI-chrome prefs (`edentext-chrome`, default `'ribbon'`). `App.svelte` forks on it: the ribbon docks as a plain flex child, the island keeps its absolute overlay. Reachable from both sides — the theme dropdown -in classic, the strip's appearance menu in the ribbon. +in the modern chrome, the strip's appearance menu in the ribbon. -`--toolbar-overlay-h` is written **only** in classic mode. `editor.css` reads it as +`--toolbar-overlay-h` is written **only** in modern mode. `editor.css` reads it as `var(--toolbar-overlay-h, 0px)`, so the ribbon's docked layout needs no second rule. -**Classic is the baseline the ribbon is measured against, so it stays untouched.** Where a shared -helper would improve both, the ribbon takes it and classic keeps its copy; where a dialog is +**The modern chrome is the baseline the ribbon is measured against, so it stays untouched.** Where a shared +helper would improve both, the ribbon takes it and the modern chrome keeps its copy; where a dialog is mounted inside `ToolbarExpanded`, the ribbon mounts its own rather than hoisting it. Only one chrome is mounted at a time, so the two sets never coexist. @@ -29,12 +29,12 @@ active underline is 2.5px **inset 13px from each side**, not a full-width border The logo's other colour, `--brand-clay`, marks the **contextual tabs** — the ones that exist only for the selected object — as their 2px bar, over a `--w-contextual` label darkened to stay readable (the clay itself is 3.3:1 on white). Both brand tokens are defined once, from -`favicon.svg`; the classic chrome's island hairline already gradients between the same pair. +`favicon.svg`; the modern chrome's island hairline already gradients between the same pair. `.ribbon` remaps the app's own tokens onto that palette (`--color-surface`, `--color-primary`, `--toolbar-btn-size`, …). Custom properties inherit, so every reused picker — `ColorPicker`, `HistoryButton`, `TablePicker`, `TableStylePicker`, the border pickers — adopts the ribbon's look -with no edit of its own, while the classic toolbar keeps the themed set. The **status bar** takes +with no edit of its own, while the modern toolbar keeps the themed set. The **status bar** takes the same remap while the ribbon is mounted (`.statusbar.w-chrome`, `App.svelte`) — it closes the frame the chrome opens, and the app's dark neutrals are bluer than Word's. @@ -127,7 +127,7 @@ margins, Save As. ## Dialogs `ParagraphDialog` and `TabsDialog` open from Word's ↘ launcher in a group's corner. They are -chrome-agnostic, so classic could gain launchers of its own at no cost. +chrome-agnostic, so the modern chrome could gain launchers of its own at no cost. There is **no Font dialog**: the Home tab already carries change case and the underline and strikethrough line styles, and the two things left over — `letterSpacingPt` and `kerning` — live @@ -135,7 +135,7 @@ on a named style, not on a run. Setting them as direct formatting would mean a n and export work on both formats, which is more than a chrome rebuild. The style manager is where they belong. -## What retiring classic would free +## What retiring the modern chrome would free Recorded so the option stays visible, not as a plan: `Toolbar.svelte` and `ToolbarExpanded.svelte` (~3200 lines), the overlay island and its hand-rolled horizontal diff --git a/src/App.svelte b/src/App.svelte index 29c986a..9102764 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -1038,7 +1038,7 @@ } -
{#snippet toolbarScrollbar()} diff --git a/src/lib/components/CLAUDE.md b/src/lib/components/CLAUDE.md index cae8902..ef6ddd9 100644 --- a/src/lib/components/CLAUDE.md +++ b/src/lib/components/CLAUDE.md @@ -150,7 +150,7 @@ reject in place through `acceptRevision`/`rejectRevision`. Opened from the Revie ## AutoText (`AutoTextDialog.svelte`, `editor/extensions/autoText.ts`) LibreOffice's Tools ▸ AutoText, reachable where Word puts it too (the ribbon's Insert ▸ -Text group, the classic chrome's Tools menu): the library, a click to insert, and "new +Text group, the modern chrome's Tools menu): the library, a click to insert, and "new from selection" — the selected slice serialized to HTML. `hasSelection` is derived off `open` because the dialog stays mounted and a modal freezes the selection it opened on. @@ -187,7 +187,7 @@ the code keep the technical name. LibreOffice's Navigator and Word's Navigation pane: the outline, click to jump, and the four chapter operations on hover. Docked right beside `CommentsPane`, not left where both -reference products put it — the classic chrome's floating "Tools" chip lives there. The +reference products put it — the modern chrome's floating "Tools" chip lives there. The toolbar island overlays the row's top, so the pane takes a `margin-top` of `--toolbar-overlay-h` (`App.svelte` sets it; 0 for the ribbon, which is in flow). diff --git a/src/lib/components/ColorPicker.svelte b/src/lib/components/ColorPicker.svelte index 436bf53..e435d5c 100644 --- a/src/lib/components/ColorPicker.svelte +++ b/src/lib/components/ColorPicker.svelte @@ -94,7 +94,7 @@ const anchor = split?.getBoundingClientRect(); if (!anchor) return; // A ribbon panel drops below the whole band, or it would cover the controls - // under it. The classic toolbar has no band, so this finds nothing there. + // under it. The modern toolbar has no band, so this finds nothing there. const band = split?.closest('.ribbon-body')?.getBoundingClientRect(); pos = { top: Math.max(anchor.bottom, band?.bottom ?? 0) + 3, left: anchor.left }; await tick(); diff --git a/src/lib/components/NavigatorPane.svelte b/src/lib/components/NavigatorPane.svelte index 326e97a..77b51eb 100644 --- a/src/lib/components/NavigatorPane.svelte +++ b/src/lib/components/NavigatorPane.svelte @@ -110,7 +110,7 @@ background: var(--color-surface); font-family: var(--font-sans); font-size: 0.8rem; - /* The classic chrome's toolbar island overlays the top of the row; a margin, not + /* The modern chrome's toolbar island overlays the top of the row; a margin, not padding, or the sticky header would scroll up under it. The ribbon sets the variable to 0, being in flow. */ margin-top: var(--toolbar-overlay-h, 0px); diff --git a/src/lib/components/ribbon/Ribbon.svelte b/src/lib/components/ribbon/Ribbon.svelte index 85869ee..1dd70df 100644 --- a/src/lib/components/ribbon/Ribbon.svelte +++ b/src/lib/components/ribbon/Ribbon.svelte @@ -312,8 +312,8 @@ -
{t().appearance.title}
@@ -373,7 +373,7 @@ /* Word's palette reaches the reused pickers (ColorPicker, HistoryButton, the table pickers …) by remapping the app's own tokens here: custom properties inherit, so those components adopt the ribbon's look with no edits, and the - classic toolbar keeps the themed set. */ + modern toolbar keeps the themed set. */ .ribbon { --color-surface: var(--w-surface); --color-toolbar-bg: var(--w-chrome); @@ -400,7 +400,7 @@ .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 + /* Those pickers frame their trigger for the modern 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. */ .ribbon :global(.color-split) { diff --git a/src/lib/i18n/locales/de.ts b/src/lib/i18n/locales/de.ts index 585545f..f16658d 100644 --- a/src/lib/i18n/locales/de.ts +++ b/src/lib/i18n/locales/de.ts @@ -852,14 +852,12 @@ const de: Messages = { cellMargins: 'Zellenränder', altText: 'Alternativtext', altTextHint: 'Das Bild beschreiben', - // Keyed by the mode id, which names the shape: the ribbon is the look users - // know as classic, the floating island the modern one. chrome: { title: 'Symbolleisten-Stil', ribbon: 'Klassisch', ribbonHint: 'Mit Registerkarten, wie in Word', - classic: 'Modern (beta)', - classicHint: 'Eine schwebende Symbolleiste', + modern: 'Modern (beta)', + modernHint: 'Eine schwebende Symbolleiste', }, }, app: { diff --git a/src/lib/i18n/locales/en.ts b/src/lib/i18n/locales/en.ts index c903fb4..0055718 100644 --- a/src/lib/i18n/locales/en.ts +++ b/src/lib/i18n/locales/en.ts @@ -858,14 +858,12 @@ const en = { cellMargins: 'Cell margins', altText: 'Alt text', altTextHint: 'Describe the image', - // Keyed by the mode id, which names the shape: the ribbon is the look users - // know as classic, the floating island the modern one. chrome: { title: 'Toolbar style', ribbon: 'Classic', ribbonHint: 'Tabbed, like Word', - classic: 'Modern (beta)', - classicHint: 'One floating toolbar', + modern: 'Modern (beta)', + modernHint: 'One floating toolbar', }, }, app: { diff --git a/src/lib/storage/CLAUDE.md b/src/lib/storage/CLAUDE.md index 5976af5..4362d95 100644 --- a/src/lib/storage/CLAUDE.md +++ b/src/lib/storage/CLAUDE.md @@ -31,7 +31,7 @@ All keys share the `edentext-` prefix — a namespace, not a description: on Git - **Recent files:** `edentext-recent-files` — `{id, name, at}` per entry, newest first, eight kept (`recentFiles.ts`). Only the label lives here: the `FileSystemFileHandle` that reopens one is structured-cloneable but not JSON, so it sits in the `edentext-recent` IndexedDB store under the same id, and each write sweeps the handles no entry names. Two handles compare only through `isSameEntry`, which is async — hence the walk when recording a file already in the list. Reopening re-asks for permission (the API drops it on reload); a refusal, a moved file or a browser with no picker leaves the entry unopenable, and it is dropped with a message instead of failing again. - **Theme:** `edentext-theme` — `'light' | 'dark' | 'allBlack' | 'auto'`. - **Toolbar expanded:** `edentext-toolbar-expanded` — boolean string. -- **Chrome:** `edentext-chrome` — `'classic' | 'ribbon'`; absent = ribbon. +- **Chrome:** `edentext-chrome` — `'modern' | 'ribbon'` ('classic' read as the modern island's legacy stored name); absent = ribbon. - **Ribbon collapsed:** `edentext-ribbon-collapsed` — boolean string; absent = expanded. - **Formatting marks:** `edentext-formatting-marks` — boolean string. - **Ruler:** `edentext-ruler` — boolean string; absent = on. diff --git a/src/lib/storage/theme.ts b/src/lib/storage/theme.ts index f619fef..b63f933 100644 --- a/src/lib/storage/theme.ts +++ b/src/lib/storage/theme.ts @@ -2,7 +2,7 @@ export type ThemeMode = 'light' | 'dark' | 'allBlack' | 'auto'; // Which chrome mounts above the document: the floating command island, or the // Word-style ribbon. Both drive the same editor; only one is mounted at a time. -export type ChromeMode = 'classic' | 'ribbon'; +export type ChromeMode = 'modern' | 'ribbon'; const THEME_KEY = 'edentext-theme'; const TOOLBAR_KEY = 'edentext-toolbar-expanded'; @@ -36,7 +36,9 @@ export function saveToolbarExpanded(expanded: boolean): void { } export function loadChromeMode(): ChromeMode { - return localStorage.getItem(CHROME_KEY) === 'classic' ? 'classic' : 'ribbon'; + const stored = localStorage.getItem(CHROME_KEY); + // 'classic' is the floating island's legacy stored name, kept readable. + return stored === 'modern' || stored === 'classic' ? 'modern' : 'ribbon'; } export function saveChromeMode(mode: ChromeMode): void { diff --git a/src/styles/global.css b/src/styles/global.css index 35d79fa..313d014 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -346,7 +346,7 @@ contents from a caller's snippet, which Svelte scopes to that caller. */ /* `anchored` (menu.svelte.ts) switches this to position: fixed and pins it under the trigger — the ribbon band clips its own overflow, so a panel has to leave - the flow to be seen. z-index clears the classic island (200) and the editor's + the flow to be seen. z-index clears the modern island (200) and the editor's own overlays. */ .ribbon-menu { position: absolute; @@ -432,7 +432,7 @@ margin: 0 4px; } -/* The reused pickers style their trigger for the classic toolbar's tinted island: +/* The reused pickers style their trigger for the modern toolbar's tinted island: a frame on hover, the accent as the open state. Beside a row of ribbon buttons that reads as a different control, so both become the ribbon's own tints. */ .ribbon .table-trigger:hover, diff --git a/tests/unit/chrome-mode.test.ts b/tests/unit/chrome-mode.test.ts new file mode 100644 index 0000000..d1933ef --- /dev/null +++ b/tests/unit/chrome-mode.test.ts @@ -0,0 +1,19 @@ +// The chrome default is the ribbon; a stored choice wins, and the floating +// island's legacy stored name 'classic' still selects it. +import { describe, it, expect, beforeEach } from 'vitest'; +import { loadChromeMode, saveChromeMode } from '../../src/lib/storage/theme'; + +describe('chrome mode', () => { + beforeEach(() => localStorage.clear()); + + it('defaults to the ribbon', () => { + expect(loadChromeMode()).toBe('ribbon'); + }); + + it('keeps a stored choice, the legacy name included', () => { + saveChromeMode('modern'); + expect(loadChromeMode()).toBe('modern'); + localStorage.setItem('edentext-chrome', 'classic'); + expect(loadChromeMode()).toBe('modern'); + }); +});