From 317c0d96029e04534587a10710db54fdff6fba10 Mon Sep 17 00:00:00 2001 From: Julian Kiedaisch Date: Sat, 10 Oct 2026 19:59:47 +0000 Subject: [PATCH] feat(editor): the left bar folds away, and the device sheet takes the whole editor on a phone A button at the top of the palette and the documentation's bar folds it to a slim column, as the dashboard's own bar does; one state for both, kept in the browser. Up to 639 px the device sheet covers the whole editor, over the bar and the mode buttons, instead of sharing half the height below the surface. Co-Authored-By: Claude Opus 5.5 --- CHANGES.md | 2 + packages/editor/browser/design.spec.ts | 40 +++++++++ packages/editor/browser/documentation.spec.ts | 3 +- packages/editor/browser/sheet.spec.ts | 38 ++++++-- packages/editor/src/Editor.tsx | 8 +- .../src/design/DocumentationSidebar.tsx | 24 ++--- packages/editor/src/design/Palette.tsx | 12 ++- packages/editor/src/design/SideBar.tsx | 87 +++++++++++++++++++ packages/editor/src/i18n/de.json | 2 + packages/editor/src/i18n/en.json | 2 + packages/editor/src/sheet/SheetTabs.tsx | 2 +- packages/editor/src/sheet/sheet.css | 72 ++++++++++++--- 12 files changed, 247 insertions(+), 45 deletions(-) create mode 100644 packages/editor/src/design/SideBar.tsx diff --git a/CHANGES.md b/CHANGES.md index 68b94c9..446c1c9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -272,6 +272,8 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `test/remoteSimHost.test.ts`, `browser/shared.spec.ts`, `browser/serverF.ts` | — | neu (F5.3b), erweitert (F5.5: ein großes Bild, von Mia importiert, bei Ben im Bildbetrachter, in F's Speicher unter seinem SHA-256): `RemoteSimHost` auf nachgebauten Sockets mit gestellter Uhr; drei Personen in drei Browsern an einem echten F (aus dem Bündel gestartet): verschieben, nur lesen, eine Simulation für alle mit Namen, ein Ping in einer Befehlszeile bei der anderen, Eintippen als Leser abgewiesen, eingefroren und aufgetaut, Zugang beendet; Absagen ohne Sitzung und ohne Projekt | | `src/canvas/Canvas.tsx` (`hasMenu`), `src/Editor.tsx` (Aktionsmodus), `src/simulation/SimulationSheet.tsx`, `src/desktop/DesktopSheet.tsx` (vorher `DesktopWindow.tsx`), `src/simulation/ViewerSheet.tsx` (vorher `ViewerWindow.tsx`), `src/simulation/CaptureSheet.tsx` (vorher `CaptureWindow.tsx`), `src/desktop/desktop.css`, `src/i18n/*.json` (`menu.showSat`, `menu.showNat`, `sheet.seeThrough`), `browser/*.spec.ts` (`showFromMenu`, `showDesktop`), `tools/live/acceptance.ts` | Java `GUIEvents.kontextMenueAktionsmodus`, `GUIKnotenItem` (Klick im Aktionsmodus), `GUIDesktopWindow`, `SatViewer`, `NatViewer`, `AggregatedExchangeDialog` | geändert (F6.5, auf Wunsch des Nutzers): im Aktionsmodus wählt ein Tipp das Gerät nur aus, sein Dreipunkt-Knopf erscheint wie im Entwurf (nur wo das Menü etwas anbietet); geöffnet wird über das Menü — Desktop, „SAT Tabelle anzeigen“ am Switch, „NAT-Tabelle anzeigen“ am Heimrouter, Datenaustausch je Karte. Desktop, Tabellen und Datenaustausch sind keine Fenster im Fenster mehr, sondern das Blatt des Aktionsmodus nach dem Muster des Dashboard-Blatts (`AppSheet`): unter der Werkzeugleiste bis zum unteren Rand, Kopf mit Symbol, Titel, Anwesenden, „Durchsichtig“ und ×; eins zur Zeit (es deckt die Fläche, ein anderes öffnet man nach dem Schließen); ein Blatt, dessen Gerät entfernt wurde, schließt; es übersteht die Rückkehr in den Entwurf, ein Desktop behält sein offenes Programm. „Durchsichtig“ (Deckkraft 0,55, im Browser gemerkt) lässt das Netz durchscheinen. Das Geräteblatt bleibt im Aktionsmodus zu, die anderen sehen keinen Rahmen um ein dort gewähltes Gerät. Abweichend von Java: Java öffnet Desktop und Tabellen mit einem Klick aufs Gerät, als verschiebbare Fenster, mehrere zugleich, und sein Menü nur mit Rechtsklick | | `src/sheet/SheetTabs.tsx` (`variant="segmented"`), `src/sheet/sheet.css` (`.sheet-segments`), `src/sheet/ServicePanels.tsx` (DHCP-Server), `browser/sheet.spec.ts` | Java `JDHCPKonfiguration` (Reiter `jdhcpkonfiguration_msg10/11`) | geändert (10.10.2026, auf Wunsch des Nutzers): die beiden Seiten des DHCP-Servers („Grundeinstellungen“, „Statische Adresszuweisung“) als Segment-Control nach dem Muster des Dashboards — ein Rahmen, gleiche Anteile, das gewählte in der Akzentfarbe. Javas Wörter bleiben; gemessen: „Grundeinstellungen“ ist bei 14 px 138 px breit, eine Hälfte des 340-px-Blatts hält 139 (halbfett 157 px). Darum: Wörter brechen in ihrem Segment an Leerzeichen um, das gewählte ist nicht fett (die Breite spränge), und wo das längste Wort eines Segments samt Innenabstand nicht mehr in seinen Anteil passt (Handy, andere Systemschrift), stehen die Segmente untereinander, als ein Control — entschieden durch Messen der Wörter in der Schrift der Segmente (Canvas), nicht durch eine feste Breite. Abweichend von Java: dort Reiter (JTabbedPane) | +| `src/design/SideBar.tsx` (neu), `src/design/Palette.tsx`, `src/design/DocumentationSidebar.tsx`, `src/Editor.tsx`, `src/sheet/sheet.css` (`.side-bar`), `src/i18n/*.json` (`design.foldBar`, `design.unfoldBar`), `browser/design.spec.ts`, `browser/documentation.spec.ts` | Java `GUIDesignSidebar`, `GUIDocumentationSidebar`; Dashboard `AppShell` („Leiste einklappen“) | erweitert (10.10.2026, auf Wunsch des Nutzers): die Leiste links (Palette, Dokumentationsleiste) klappt mit einem Knopf oben ein und aus — eingeklappt eine schmale Spalte (57 px statt 137) mit nur dem Knopf, nach dem Muster der Dashboard-Leiste; der Knopf steht in beiden Zuständen an derselben Stelle. Ein Zustand für beide Leisten, in diesem Browser gemerkt (`filius.barFolded`). Gemessen: die Fläche gewinnt 80 px (iPad hochkant mit Blatt 291 → 371 px, Handy 253 → 333 px). Abweichend von Java: dort ist die Leiste fest | +| `src/sheet/sheet.css` (`.editor-root`, Handy), `src/Editor.tsx` (Wurzel), `browser/sheet.spec.ts` | — | geändert (10.10.2026, auf Wunsch des Nutzers): auf dem Handy (bis 639 px) nimmt das Geräte-Blatt den ganzen Editor ein, über der Leiste und den Modusknöpfen; eine Meldung bleibt darüber, Schließen gibt die Fläche zurück. Vorher stand es unter der Fläche (höchstens halb so hoch), neben einer Leiste von 137 px blieben ihm 253 von 390 px. Folge: die DHCP-Segmente stehen auf einem 390-px-Handy jetzt nebeneinander, untereinander erst unter etwa 335 px | ## packages/server diff --git a/packages/editor/browser/design.spec.ts b/packages/editor/browser/design.spec.ts index 9e82df8..d4ae62b 100644 --- a/packages/editor/browser/design.spec.ts +++ b/packages/editor/browser/design.spec.ts @@ -245,3 +245,43 @@ test('by touch: a long press opens the menu, a finger drags a device', async ({ await expect(page.getByRole('menu')).toBeHidden(); await context.close(); }); + +test('folds the palette to a slim column with its button in the same place, kept for the next visit and for the documentation', async ({ page }) => { + await openEditor(page, server.base, 'new'); + const palette = page.getByRole('navigation', { name: 'Komponenten' }); + const fold = palette.getByRole('button', { name: 'Leiste einklappen' }); + const before = { fold: (await fold.boundingBox())!, bar: (await palette.boundingBox())!, surface: (await page.locator('svg.filius-canvas').boundingBox())! }; + // The button stands at the top, over the cable. + expect(before.fold.y + before.fold.height).toBeLessThanOrEqual((await paletteCentre(page, 'cable')).y); + await expect(fold).toHaveAttribute('aria-expanded', 'true'); + await fold.click(); + + const unfold = palette.getByRole('button', { name: 'Leiste ausklappen' }); + await expect(unfold).toHaveAttribute('aria-expanded', 'false'); + await expect(palette.locator('[data-palette]')).toHaveCount(0); + const folded = { fold: (await unfold.boundingBox())!, bar: (await palette.boundingBox())!, surface: (await page.locator('svg.filius-canvas').boundingBox())! }; + expect([folded.fold.x, folded.fold.y, folded.fold.width, folded.fold.height]).toEqual([before.fold.x, before.fold.y, 44, 44]); + expect(folded.bar.width).toBe(57); + expect(folded.surface.x).toBe(before.surface.x - (before.bar.width - 57)); + expect(folded.surface.width).toBe(before.surface.width + (before.bar.width - 57)); + + // Folded on the next visit, and the documentation's bar with it; unfolded there, the palette is back. + await openEditor(page, server.base, 'new'); + await expect(palette.getByRole('button', { name: 'Leiste ausklappen' })).toBeVisible(); + await expect(palette.locator('[data-palette]')).toHaveCount(0); + await page.getByRole('button', { name: 'Wechselt in den Dokumentationsmodus' }).click(); + const sidebar = page.getByRole('navigation', { name: 'Dokumentation' }); + await expect(sidebar.locator('[data-docu]')).toHaveCount(0); + await sidebar.getByRole('button', { name: 'Leiste ausklappen' }).click(); + await expect(sidebar.locator('[data-docu]')).toHaveCount(2); + await page.getByRole('button', { name: 'Wechselt in den Entwurfsmodus' }).click(); + await expect(palette.locator('[data-palette]')).toHaveCount(7); + expect((await palette.boundingBox())!.width).toBe(before.bar.width); +}); + +test('names the fold button in English when asked', async ({ page }) => { + await openEditor(page, server.base, 'new', '&lang=en'); + const palette = page.getByRole('navigation', { name: 'Components' }); + await palette.getByRole('button', { name: 'Collapse the bar' }).click(); + await expect(palette.getByRole('button', { name: 'Expand the bar' })).toHaveAttribute('title', 'Expand the bar'); +}); diff --git a/packages/editor/browser/documentation.spec.ts b/packages/editor/browser/documentation.spec.ts index 3276adf..681e623 100644 --- a/packages/editor/browser/documentation.spec.ts +++ b/packages/editor/browser/documentation.spec.ts @@ -54,7 +54,8 @@ test('makes, moves, resizes, colours and types documentation as Java does, and p await expect(canvas).toHaveAttribute('data-mode', 'documentation'); await expect(page.getByRole('navigation', { name: 'Komponenten' })).toHaveCount(0); const sidebar = page.getByRole('navigation', { name: 'Dokumentation' }); - await expect(sidebar.getByRole('button')).toHaveText(['Text', 'Gliederungsfläche', 'Exportieren', 'Report']); + await expect(sidebar.getByRole('button', { name: 'Leiste einklappen' })).toBeVisible(); + await expect(sidebar.locator('.side-bar__items').getByRole('button')).toHaveText(['Text', 'Gliederungsfläche', 'Exportieren', 'Report']); // A rectangle dragged onto the surface: 100 × 50, its middle where it was dropped, grey, with the focus. const surface = (await canvas.boundingBox())!; diff --git a/packages/editor/browser/sheet.spec.ts b/packages/editor/browser/sheet.spec.ts index 594e356..c3f7afd 100644 --- a/packages/editor/browser/sheet.spec.ts +++ b/packages/editor/browser/sheet.spec.ts @@ -1,7 +1,7 @@ // The device sheet in Chromium (Playwright): it opens with the device that // is chosen, shows Java's fields, takes a field when it is left, marks and // keeps a text its rule refuses, moves a notebook into a wireless network — -// beside the surface on a wide screen, below it on a phone. +// beside the surface on a wide screen, over the whole editor on a phone. import { expect, test, type Page } from '@playwright/test'; import type { HomeRouterSheet, HostSheet, RouterSheet } from '@mdg-filius/doc'; @@ -561,10 +561,14 @@ test("stacks the DHCP server's segments exactly where a segment's longest word, test.describe('on a phone', () => { test.use({ viewport: { width: 390, height: 844 }, hasTouch: true }); - test("stacks the DHCP server's segments where a word no longer fits its share — still one control", async ({ page }) => { + test("sets the DHCP server's segments side by side in the sheet that covers the phone, and stacks them on a small one — still one control", async ({ page }) => { await openEditor(page, server.base, 'new'); - const { control, boxes } = await dhcpSegments(page); + const { control } = await dhcpSegments(page); + await expect(control).not.toHaveAttribute('data-stacked', 'true'); + // 320 px: the share of „Grundeinstellungen" falls below the word. + await page.setViewportSize({ width: 320, height: 700 }); await expect(control).toHaveAttribute('data-stacked', 'true'); + const boxes = await Promise.all([0, 1].map(async (index) => (await control.getByRole('tab').nth(index).boundingBox())!)); await expect(control).toHaveAttribute('aria-orientation', 'vertical'); const [basic, assignments] = boxes as [{ x: number; y: number; width: number; height: number }, { x: number; y: number; width: number; height: number }]; expect(Math.round(basic.x)).toBe(Math.round(assignments.x)); @@ -574,22 +578,38 @@ test.describe('on a phone', () => { } }); - test('opens below the surface with a tap, the device still in sight', async ({ page }) => { + test('covers the whole editor with a tap, the bar and the mode buttons too, and gives the surface back when closed', async ({ page }) => { await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls'); const id = await deviceNamed(page, 'Notebook 1.10'); const at = await onDevice(page, id); await page.touchscreen.tap(at.x, at.y); await expect(sheet(page)).toBeVisible(); - const surface = (await page.locator('svg.filius-canvas').boundingBox())!; - const aside = (await sheet(page).boundingBox())!; - expect(aside.y).toBeGreaterThanOrEqual(surface.y + surface.height - 1); - expect(aside.height).toBeLessThanOrEqual(844 / 2); - expect(surface.height).toBeGreaterThan(200); + expect(await sheet(page).boundingBox()).toEqual(await page.locator('.editor-root').boundingBox()); + // What lies under it does not take the finger: the palette, the mode buttons. + for (const under of [page.locator('[data-palette="pc"]'), page.getByRole('button', { name: 'Wechselt in den Aktionsmodus' })]) { + const box = (await under.boundingBox())!; + const top = await page.evaluate(([x, y]) => Boolean(document.elementFromPoint(x!, y!)?.closest('.device-sheet')), [box.x + box.width / 2, box.y + box.height / 2]); + expect(top).toBe(true); + } const fieldBox = (await field(page, 'ipAddress').boundingBox())!; expect(fieldBox.height).toBeGreaterThanOrEqual(44); + await sheet(page).getByRole('button', { name: 'Schließen' }).tap(); + await expect(sheet(page)).toHaveCount(0); + await expect(page.locator('svg.filius-canvas')).toBeVisible(); }); }); +test('covers the editor from the phone width down, 639 px, and stands beside the surface from 640', async ({ page }) => { + await page.setViewportSize({ width: 640, height: 800 }); + await openEditor(page, server.base, 'new'); + await page.locator('[data-palette="pc"]').click(); + await expect(sheet(page)).toBeVisible(); + const surface = (await page.locator('svg.filius-canvas').boundingBox())!; + expect((await sheet(page).boundingBox())!.x).toBeGreaterThanOrEqual(surface.x + surface.width - 1); + await page.setViewportSize({ width: 639, height: 800 }); + await expect.poll(async () => (await sheet(page).boundingBox())!.width).toBe(639); +}); + test('speaks English when asked', async ({ page }) => { await openEditor(page, server.base, 'new', '&lang=en'); await page.locator('[data-palette="networkSwitch"]').click(); diff --git a/packages/editor/src/Editor.tsx b/packages/editor/src/Editor.tsx index f01fe32..4785897 100644 --- a/packages/editor/src/Editor.tsx +++ b/packages/editor/src/Editor.tsx @@ -61,6 +61,7 @@ import { networkImage } from './design/exportImage.js'; import { exportNames } from './canvas/networkImage.js'; import { saveFile } from './download.js'; import { Palette, type PaletteKind } from './design/Palette.js'; +import { useBarFolded } from './design/SideBar.js'; import { RouterDialog } from './design/RouterDialog.js'; import { DeviceSheet } from './sheet/DeviceSheet.js'; import { DesktopSheet, type DesktopWindowName } from './desktop/DesktopSheet.js'; @@ -173,6 +174,7 @@ export function Editor({ doc, createSimHost, projectName, readOnly = false, froz const editorMode: EditorMode = simulating ? 'simulation' : documenting ? 'documentation' : 'design'; const [sheet, setSheet] = useState(null); const [seeThrough, setSeeThrough] = useSeeThrough(); + const [barFolded, foldBar] = useBarFolded(); // The sheet begins below the toolbar, which wraps on a narrow screen. const toolbar = useRef(null); const [sheetTop, setSheetTop] = useState(68); @@ -443,9 +445,9 @@ export function Editor({ doc, createSimHost, projectName, readOnly = false, froz }, [manager, selection, menu, routerAt, remove, simulating, readOnly]); return ( -
+
{simulating ? null : documenting ? ( - + ) : readOnly ? null : ( )}
diff --git a/packages/editor/src/design/DocumentationSidebar.tsx b/packages/editor/src/design/DocumentationSidebar.tsx index 4dd5fc1..c96800e 100644 --- a/packages/editor/src/design/DocumentationSidebar.tsx +++ b/packages/editor/src/design/DocumentationSidebar.tsx @@ -4,12 +4,13 @@ // tapped, it lands in the middle of the view (Java's tap makes nothing). // „Exportieren“ saves the surface as an image (F4.8c), „Report“ the report as // a PDF (F4.8d); if that fails, Java's „Fehler beim Speichern aufgetreten!“ -// shows below them. +// shows below them. The bar folds away as the palette does (SideBar). import { useRef, useState, type CSSProperties } from 'react'; import type { DocumentationItemKind } from '@mdg-filius/core'; import { DRAG_THRESHOLD } from '../canvas/Canvas.js'; import { useTranslate } from '../i18n/i18n.js'; +import { SideBar } from './SideBar.js'; import addRectangle from '../assets/documentation/add_small.png'; import addText from '../assets/documentation/add_text_small.png'; import exportIcon from '../assets/documentation/download_small.png'; @@ -24,6 +25,8 @@ export interface DocumentationSidebarProps { readonly onReport: () => Promise; /** Who only reads makes no texts or rectangles; the picture and the report stay. */ readonly readOnly?: boolean; + readonly folded: boolean; + readonly onFold: (folded: boolean) => void; } const ITEMS: readonly { readonly kind: DocumentationItemKind; readonly icon: string; readonly label: string }[] = [ @@ -50,7 +53,7 @@ const item: CSSProperties = { touchAction: 'none', }; -export function DocumentationSidebar({ onItem, onExport, onReport, readOnly = false }: DocumentationSidebarProps) { +export function DocumentationSidebar({ onItem, onExport, onReport, readOnly = false, folded, onFold }: DocumentationSidebarProps) { const t = useTranslate(); const [ghost, setGhost] = useState<{ kind: DocumentationItemKind; x: number; y: number } | null>(null); const press = useRef<{ kind: DocumentationItemKind; x: number; y: number; dragging: boolean } | null>(null); @@ -94,20 +97,7 @@ export function DocumentationSidebar({ onItem, onExport, onReport, readOnly = fa }; return ( - + ); } diff --git a/packages/editor/src/design/Palette.tsx b/packages/editor/src/design/Palette.tsx index 2faabaa..d0b23ba 100644 --- a/packages/editor/src/design/Palette.tsx +++ b/packages/editor/src/design/Palette.tsx @@ -1,7 +1,8 @@ // The palette (Java's design sidebar, GUIDesignSidebar): the cable tool // first, then the devices in Java's order, each with Java's icon and name. // A device is dragged onto the surface, or tapped to land in the middle of -// the view; the cable is a tool that stays on until tapped again. +// the view; the cable is a tool that stays on until tapped again. The bar +// folds away (SideBar). import { useRef, useState, type CSSProperties } from 'react'; import { PALETTE } from '@mdg-filius/doc'; @@ -11,6 +12,7 @@ import { ICON_URLS } from '../canvas/icons.js'; import { iconOf } from '../canvas/layout.js'; import cableIcon from '../assets/hardware/kabel.png'; import { DRAG_THRESHOLD } from '../canvas/Canvas.js'; +import { SideBar } from './SideBar.js'; export type PaletteKind = (typeof PALETTE)[number]; @@ -19,6 +21,8 @@ export interface PaletteProps { readonly onCableTool: (on: boolean) => void; /** A device dropped at a point of the screen, or tapped (no point). */ readonly onDevice: (kind: PaletteKind, at: { readonly clientX: number; readonly clientY: number } | null) => void; + readonly folded: boolean; + readonly onFold: (folded: boolean) => void; } const item: CSSProperties = { @@ -40,7 +44,7 @@ const item: CSSProperties = { touchAction: 'none', }; -export function Palette({ cableTool, onCableTool, onDevice }: PaletteProps) { +export function Palette({ cableTool, onCableTool, onDevice, folded, onFold }: PaletteProps) { const t = useTranslate(); const [ghost, setGhost] = useState<{ kind: PaletteKind; x: number; y: number } | null>(null); const press = useRef<{ kind: PaletteKind; x: number; y: number; dragging: boolean } | null>(null); @@ -74,7 +78,7 @@ export function Palette({ cableTool, onCableTool, onDevice }: PaletteProps) { }; return ( - + ); } diff --git a/packages/editor/src/design/SideBar.tsx b/packages/editor/src/design/SideBar.tsx new file mode 100644 index 0000000..ddf30fc --- /dev/null +++ b/packages/editor/src/design/SideBar.tsx @@ -0,0 +1,87 @@ +// The bar at the left of the surface — the design's palette, the +// documentation's sidebar — that folds away to make room for the surface on a +// small screen (Java has no such button; it has the room). As the dashboard's +// own bar does („Leiste einklappen“, AppShell): folded, it is a slim column +// with the one button that brings it back, and that button stands at the top +// in the same place either way. One state for both bars, kept in this +// browser: whoever folds it on a phone wants it folded next time too. + +import { useCallback, useState, type ReactNode } from "react"; +import { useTranslate } from "../i18n/i18n.js"; + +const FOLDED_KEY = "filius.barFolded"; + +/** Whether the bar is folded, and the change, kept in this browser (where it can be). */ +export function useBarFolded(): [boolean, (folded: boolean) => void] { + const [folded, setFolded] = useState(() => { + try { + return window.localStorage.getItem(FOLDED_KEY) === "true"; + } catch { + return false; + } + }); + const change = useCallback((next: boolean) => { + setFolded(next); + try { + window.localStorage.setItem(FOLDED_KEY, String(next)); + } catch { + // Not kept; it still holds for this page. + } + }, []); + return [folded, change]; +} + +/** A panel with its left column, the arrow saying which way it goes — the dashboard's sign. */ +function BarIcon({ folded }: { readonly folded: boolean }) { + return ( + + ); +} + +export function SideBar({ + label, + folded, + onFold, + children, +}: { + readonly label: string; + readonly folded: boolean; + readonly onFold: (folded: boolean) => void; + readonly children: ReactNode; +}) { + const t = useTranslate(); + const word = t(folded ? "design.unfoldBar" : "design.foldBar"); + return ( + + ); +} diff --git a/packages/editor/src/i18n/de.json b/packages/editor/src/i18n/de.json index 466d523..9abbc06 100644 --- a/packages/editor/src/i18n/de.json +++ b/packages/editor/src/i18n/de.json @@ -51,6 +51,8 @@ "connections.remove": "Schnittstelle entfernen", "design.actions": "Aktionen", "design.palette": "Komponenten", + "design.foldBar": "Leiste einklappen", + "design.unfoldBar": "Leiste ausklappen", "design.redo": "Wiederholen", "design.undo": "Rückgängig", "desktop.home": "Startbildschirm", diff --git a/packages/editor/src/i18n/en.json b/packages/editor/src/i18n/en.json index 4077222..3e3533b 100644 --- a/packages/editor/src/i18n/en.json +++ b/packages/editor/src/i18n/en.json @@ -51,6 +51,8 @@ "connections.remove": "Remove interface", "design.actions": "Actions", "design.palette": "Components", + "design.foldBar": "Collapse the bar", + "design.unfoldBar": "Expand the bar", "design.redo": "Redo", "design.undo": "Undo", "desktop.home": "Home Screen", diff --git a/packages/editor/src/sheet/SheetTabs.tsx b/packages/editor/src/sheet/SheetTabs.tsx index 4f80b2c..a7c297b 100644 --- a/packages/editor/src/sheet/SheetTabs.tsx +++ b/packages/editor/src/sheet/SheetTabs.tsx @@ -8,7 +8,7 @@ // 340 px sheet holds 139. So a segment's words may wrap at their spaces // ("Statische / Adresszuweisung"), the chosen one is not set bold (157 px; the // width would jump on every choice), and where even the longest word of a -// segment no longer fits its share — a phone, another system font, English — +// segment no longer fits its share — a small phone, another system font — // the segments stand under one another, still one control. That is decided by // measuring the words in the segments' own font, not by a breakpoint: a // breakpoint would hold for one font only. diff --git a/packages/editor/src/sheet/sheet.css b/packages/editor/src/sheet/sheet.css index 2fe4f89..3df542e 100644 --- a/packages/editor/src/sheet/sheet.css +++ b/packages/editor/src/sheet/sheet.css @@ -1,6 +1,57 @@ -/* The device sheet beside the surface on a wide screen, below it on a phone - * (the dashboard's phone width, 639 px): the surface shrinks instead of - * being covered, so the device stays in sight. */ +/* The editor: the bar at the left, the surface, the device sheet beside it. + * The sheet on a phone (the dashboard's phone width, 639 px) covers all of + * it, the bar and the mode buttons too — the root holds it. */ +.editor-root { + position: relative; + display: flex; + height: 100%; +} + +/* The bar at the left (SideBar): the fold button at the top, the items below + * it scrolling on their own; folded, only the button's slim column. */ +.side-bar { + display: flex; + flex-direction: column; + flex-shrink: 0; + gap: 4px; + width: 124px; + padding: 6px; + border-right: 1px solid var(--border-default); + background: var(--surface-card); +} + +.side-bar[data-folded="true"] { + width: 44px; +} + +.side-bar__fold { + display: grid; + place-items: center; + flex: none; + width: 44px; + height: 44px; + border: none; + border-radius: 8px; + background: transparent; + color: var(--text-secondary); + cursor: pointer; +} + +.side-bar__fold:hover { + background: var(--accent-soft); + color: var(--text-primary); +} + +.side-bar__items { + display: flex; + flex-direction: column; + gap: 4px; + min-height: 0; + overflow-y: auto; +} + +/* The device sheet beside the surface: the surface shrinks instead of being + * covered, so the device stays in sight. */ .editor-work { flex: 1; min-width: 0; @@ -57,18 +108,17 @@ color: var(--text-primary); } +/* On a phone a third of the width beside the surface or half its height + * below it leaves neither enough to work in: the sheet takes the whole + * editor, over the bar and the mode buttons (50); a message (60) stays over + * it, and closing it gives the surface back. */ @media (max-width: 639px) { - .editor-work { - flex-direction: column; - } - .device-sheet { + position: absolute; + inset: 0; + z-index: 55; width: auto; - max-height: 50%; border-left: none; - border-top: 1px solid var(--border-default); - border-radius: 16px 16px 0 0; - box-shadow: 0 -4px 16px color-mix(in srgb, var(--shadow-color) 15%, transparent); } }