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.
This commit is contained in:
Steffen Becker committed 2026-08-17 20:05:15 +02:00
1 parent 501bfe501f
commit 10b650c64c
12 files changed
+48 -31

No files matched your search

+9 -9
View File
@@ -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
+1 -1
View File
@@ -1038,7 +1038,7 @@
}
</script>
<!-- The ribbon docks in flow, so only the classic island needs the editor's top
<!-- The ribbon docks in flow, so only the modern island needs the editor's top
padding to clear an overlay. -->
<main style="--toolbar-overlay-h: {chromeMode === 'ribbon' ? 0 : toolbarRegionH}px">
{#snippet toolbarScrollbar()}
+2 -2
View File
@@ -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).
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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);
+4 -4
View File
@@ -312,8 +312,8 @@
<button class:selected={chromeMode === 'ribbon'} onclick={() => { chromeMode = 'ribbon'; closeMenu(); }}>
{t().ribbon.chrome.ribbon}<span class="menu-sub">{t().ribbon.chrome.ribbonHint}</span>
</button>
<button class:selected={chromeMode === 'classic'} onclick={() => { chromeMode = 'classic'; closeMenu(); }}>
{t().ribbon.chrome.classic}<span class="menu-sub">{t().ribbon.chrome.classicHint}</span>
<button class:selected={chromeMode === 'modern'} onclick={() => { chromeMode = 'modern'; closeMenu(); }}>
{t().ribbon.chrome.modern}<span class="menu-sub">{t().ribbon.chrome.modernHint}</span>
</button>
<hr />
<div class="rb-menu-label">{t().appearance.title}</div>
@@ -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) {
+2 -4
View File
@@ -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: {
+2 -4
View File
@@ -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: {
+1 -1
View File
@@ -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.
+4 -2
View File
@@ -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 {
+2 -2
View File
@@ -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,
+19
View File
@@ -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');
});
});