diff --git a/CHANGES.md b/CHANGES.md index f23d069..4923e80 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -278,6 +278,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `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 | | `src/simulation/PropertiesSheet.tsx` (neu), `src/simulation/DropHold.tsx` (vorher `DropControl.tsx`), `src/simulation/useSimulation.ts` (`restart`), `src/Editor.tsx`, `src/sheet/sheet.css`, `src/desktop/desktop.css`, `src/i18n/*.json` (`properties.*`; `drop.list` entfällt), `browser/viewers.spec.ts`, `browser/shared.spec.ts`, `browser/fromF.spec.ts`, `tools/live/acceptance.ts` | Java `SimulationControl` | geändert (10.10.2026, auf Wunsch des Nutzers): im Aktionsmodus ein eigener Inspector „Eigenschaften“ (Knopf über der Fläche, Blatt neben ihr wie die Geräte-Einstellungen, auf dem Handy über dem ganzen Editor) statt Hinweis und Knöpfen in der Werkzeugleiste: wer die Simulation gestartet hat, „Simulation neu starten“ (für alle, nach einer Rückfrage; die Tabellen des Datenaustauschs verlieren ihre Auswahl), „Pakete verwerfen“ und darunter die verworfenen Pakete samt Anzahl. Wer nur zusieht, sieht Starter und Liste, ohne Neustart und Verwerfen | | `src/canvas/handles.ts` (neu), `src/canvas/Canvas.tsx` (Griffe, Durchtippen, Platz von „⋯“), `src/sheet/sheet.css` (`.doc-handle`), `src/i18n/*.json` (`docu.resize`, `docu.move`), `test/handles.test.ts`, `browser/touch.spec.ts`, `browser/documentation.spec.ts` | Java `JDocuElement` (Rand von 7 zum Greifen) | erweitert (10.10.2026, auf Wunsch des Nutzers): im Dokumentationsmodus zeigt das fokussierte Element Griffe für den Finger — Trefferfläche 44 px, Punkt in Javas Fokusblau auf dem Fokusband: an den Ecken, ab drei Zielen Länge auch an den Kantenmitten; auf einem Element unter zwei Zielen nur rechts unten, außen an der Ecke, damit seine Mitte verschiebt und das Menü öffnet. Ein Text hat über seiner linken oberen Ecke einen Verschiebe-Griff (seine Mitte ist das Eingabefeld). „⋯“ steht über der rechten oberen Ecke; Griff und „⋯“ weichen am Rand der Fläche aus. Ein Tippen auf einen Griff gilt dem, was darunter liegt; langes Drücken öffnet das Menü. Gemessen: Javas Rand war 10–14 Bildschirm-Pixel breit und unsichtbar. WebKit gibt das Ende eines Tippens dem Griff statt der Fläche, die die Berührung gefangen hat; der Griff reicht seine Ereignisse deshalb weiter. Javas Rand für die Maus bleibt. Abweichend von Java: dort keine Griffe | +| `src/theme/tokens.css` (Fläche, `--doc-text-border`, `--java-*`, `color-scheme`), `src/desktop/desktop.css` (109 feste Farben → `--java-*`), `src/canvas/documentationPaint.ts` (`darkRectangleFill`, `darkRectangleBorder`, `darkTextColor`), `src/canvas/Canvas.tsx`, `src/design/DocumentationText.tsx`, `src/simulation/ModeBar.tsx`, `src/sheet/sheet.css`, `src/assets/modes/*_dunkel.png` (neu, aus Javas Bildern freigestellt: `tools/dark-pictures/make.py`), `test/documentationPaint.test.ts`, `browser/theme.spec.ts` (neu) | Java `JCablePanel`, `JDocuElement`, `GUIMainMenu` (Modusbilder), Swing-Metal in den Programmfenstern | geändert (10.10.2026, auf Wunsch des Nutzers): das dunkle Schema überall — die Fläche dunkel (#262b33, vorher bewusst hell #e9ecf1) mit heller Beschriftung, helleren Kabeln und einem feinen hellen Rand um Javas Symbole (sonst versinken die schwarzen Switches); Gliederungsflächen als Tönung ihrer Farbe, Texte zur hellen Schrift hin gemischt; die Modusknöpfe mit freigestellten Bildern, der gedrückte Knopf in der Akzentfarbe; `color-scheme: dark` für Kästchen, Regler und Auswahllisten; die Programmfenster des Desktops über Java-Look-Token, hell mit Javas Werten unverändert. Hell bleiben Javas Desktop-Hintergrund und die Webseite im simulierten Browser (Inhalt). Exportiertes Bild und Bericht malen weiter Javas Farben | ## packages/server diff --git a/packages/editor/browser/theme.spec.ts b/packages/editor/browser/theme.spec.ts new file mode 100644 index 0000000..d0f4968 --- /dev/null +++ b/packages/editor/browser/theme.spec.ts @@ -0,0 +1,208 @@ +// The editor's two schemes in Chromium: in the light one Java's colours as +// they were (a white surface, black labels, Java's grey program windows and +// mode buttons); in the dark one, as the dashboard asks for it, everything +// dark — the surface with light labels and cables, the documentation as a +// tint with light words, the mode buttons freed from Java's grey ground, the +// browser's own controls, and the program windows of the desktop — each pair +// of text and ground readable (contrast ratio as WCAG counts it). + +import { expect, test, type Locator, type Page } from "@playwright/test"; +import { drawing, openEditor, serveEditor, showDesktop } from "./editorPage.js"; + +let server: { readonly base: string; readonly close: () => Promise }; + +test.beforeAll(async () => { + server = await serveEditor(); +}); + +test.afterAll(async () => { + await server.close(); +}); + +/** A computed colour as red, green, blue (0–255) and alpha, whatever its notation (a mix comes as oklab): painted and read back. */ +async function rgba( + page: Page, + color: string, +): Promise<[number, number, number, number]> { + return page.evaluate((value) => { + const canvas = document.createElement("canvas"); + canvas.width = 1; + canvas.height = 1; + const context = canvas.getContext("2d")!; + context.fillStyle = value; + context.fillRect(0, 0, 1, 1); + const [red, green, blue, alpha] = context.getImageData(0, 0, 1, 1).data; + return [red!, green!, blue!, alpha! / 255] as [ + number, + number, + number, + number, + ]; + }, color); +} + +const style = (locator: Locator, property: string) => + locator.evaluate( + (element, name) => getComputedStyle(element).getPropertyValue(name), + property, + ); + +/** WCAG's relative luminance of an opaque colour. */ +function luminance([red, green, blue]: readonly number[]): number { + const linear = (value: number) => + value / 255 <= 0.03928 + ? value / 255 / 12.92 + : ((value / 255 + 0.055) / 1.055) ** 2.4; + return ( + 0.2126 * linear(red!) + 0.7152 * linear(green!) + 0.0722 * linear(blue!) + ); +} + +function contrast(one: readonly number[], other: readonly number[]): number { + const [light, dark] = [luminance(one), luminance(other)].sort( + (a, b) => b - a, + ) as [number, number]; + return (light + 0.05) / (dark + 0.05); +} + +/** What the scheme paints, read from the page. */ +async function paint(page: Page) { + const surface = page.locator("svg.filius-canvas"); + const rectangle = (await drawing(page)).docs.find( + (item) => item.kind === "rectangle", + )!; + const text = (await drawing(page)).docs.find((item) => item.kind === "text")!; + return { + scheme: await page.evaluate( + () => getComputedStyle(document.documentElement).colorScheme, + ), + surface: await rgba(page, await style(surface, "background-color")), + label: await rgba( + page, + await style(page.locator("g.device text").first(), "fill"), + ), + cable: await rgba( + page, + await style(page.locator("path[data-link]").first(), "stroke"), + ), + rectangle: await rgba( + page, + await style( + page.locator(`g[data-doc="${rectangle.id}"] .doc-rectangle`), + "fill", + ), + ), + words: await rgba( + page, + await style(page.locator(`g[data-doc="${text.id}"] .doc-text`), "color"), + ), + iconFilter: await style(page.locator("g.device image").first(), "filter"), + pressedMode: await rgba( + page, + await style( + page.locator('button[name="designMode"]'), + "background-color", + ), + ), + modePicture: await page + .locator('button[name="designMode"] img') + .evaluateAll((all) => + all + .filter((each) => getComputedStyle(each).display !== "none") + .map((each) => each.getAttribute("data-scheme")), + ), + }; +} + +/** The software installation's window on the web server's desktop: its panel, a list and its words. */ +async function installation(page: Page) { + await page + .getByRole("button", { name: "Wechselt in den Aktionsmodus" }) + .click(); + await expect(page.locator("svg.filius-canvas")).toHaveAttribute( + "data-mode", + "simulation", + ); + const web = (await drawing(page)).nodes.find( + (node) => node.name === "Webserver - 192.168.0.10", + )!.id; + await showDesktop(page, web); + await page + .locator(`section[data-desktop="${web}"] [data-program="installation"]`) + .click(); + const window = page.locator(".installation"); + const list = window.locator(".installation-list").first(); + return { + panel: await rgba(page, await style(window, "background-color")), + field: await rgba(page, await style(list, "background-color")), + words: await rgba(page, await style(list.locator("li").first(), "color")), + taskbar: await rgba( + page, + await style(page.locator(".desktop-taskbar"), "background-color"), + ), + }; +} + +test("keeps Java's colours in the light scheme", async ({ page }) => { + await openEditor(page, server.base, "nat_with_mapped_port.fls"); + const light = await paint(page); + expect(light.scheme).toBe("light"); + expect(light.surface).toEqual([255, 255, 255, 1]); + expect(light.label).toEqual([0, 0, 0, 1]); + expect(light.cable).toEqual([64, 64, 64, 1]); + // Java's grey in the design: opaque, a fifth of the way to white; its words black. + expect(light.rectangle).toEqual([215, 215, 215, 1]); + expect(light.words).toEqual([0, 0, 0, 1]); + expect(light.modePicture).toEqual(["light"]); + expect(light.iconFilter).toBe("none"); + const window = await installation(page); + expect(window).toEqual({ + panel: [238, 238, 238, 1], + field: [255, 255, 255, 1], + words: [0, 0, 0, 1], + taskbar: [180, 180, 180, 1], + }); +}); + +test("goes dark everywhere when the dashboard asks, every text readable on its ground", async ({ + page, +}) => { + await openEditor( + page, + server.base, + "nat_with_mapped_port.fls", + "&theme=dark", + ); + const dark = await paint(page); + expect(dark.scheme).toBe("dark"); + expect(luminance(dark.surface)).toBeLessThan(0.05); + expect(contrast(dark.label, dark.surface)).toBeGreaterThanOrEqual(7); + expect(contrast(dark.cable, dark.surface)).toBeGreaterThanOrEqual(3); + // A rectangle a tint of the surface, its words light on it. + expect(dark.rectangle[3]).toBeLessThanOrEqual(0.3); + const tinted = dark.surface + .slice(0, 3) + .map( + (ground, index) => + ground + (dark.rectangle[index]! - ground) * dark.rectangle[3], + ); + expect(contrast(dark.words, tinted)).toBeGreaterThanOrEqual(4.5); + expect(dark.modePicture).toEqual(["dark"]); + // The mode that is on shown by its button, in the accent's soft colour; each icon with a faint light outline. + expect(dark.pressedMode).toEqual( + await rgba( + page, + await page.evaluate(() => + getComputedStyle(document.documentElement).getPropertyValue( + "--accent-soft", + ), + ), + ), + ); + expect(dark.iconFilter).toContain("drop-shadow"); + const window = await installation(page); + expect(luminance(window.panel)).toBeLessThan(0.05); + expect(luminance(window.field)).toBeLessThan(0.05); + expect(luminance(window.taskbar)).toBeLessThan(0.05); + expect(contrast(window.words, window.field)).toBeGreaterThanOrEqual(7); +}); diff --git a/packages/editor/src/assets/modes/aktionsmodus_aktiv_dunkel.png b/packages/editor/src/assets/modes/aktionsmodus_aktiv_dunkel.png new file mode 100644 index 0000000..d982509 Binary files /dev/null and b/packages/editor/src/assets/modes/aktionsmodus_aktiv_dunkel.png differ diff --git a/packages/editor/src/assets/modes/aktionsmodus_dunkel.png b/packages/editor/src/assets/modes/aktionsmodus_dunkel.png new file mode 100644 index 0000000..5c500bd Binary files /dev/null and b/packages/editor/src/assets/modes/aktionsmodus_dunkel.png differ diff --git a/packages/editor/src/assets/modes/dokumodus_aktiv_dunkel.png b/packages/editor/src/assets/modes/dokumodus_aktiv_dunkel.png new file mode 100644 index 0000000..e0fc59e Binary files /dev/null and b/packages/editor/src/assets/modes/dokumodus_aktiv_dunkel.png differ diff --git a/packages/editor/src/assets/modes/dokumodus_dunkel.png b/packages/editor/src/assets/modes/dokumodus_dunkel.png new file mode 100644 index 0000000..3930c62 Binary files /dev/null and b/packages/editor/src/assets/modes/dokumodus_dunkel.png differ diff --git a/packages/editor/src/assets/modes/entwurfsmodus_aktiv_dunkel.png b/packages/editor/src/assets/modes/entwurfsmodus_aktiv_dunkel.png new file mode 100644 index 0000000..08861ba Binary files /dev/null and b/packages/editor/src/assets/modes/entwurfsmodus_aktiv_dunkel.png differ diff --git a/packages/editor/src/assets/modes/entwurfsmodus_dunkel.png b/packages/editor/src/assets/modes/entwurfsmodus_dunkel.png new file mode 100644 index 0000000..ecf5765 Binary files /dev/null and b/packages/editor/src/assets/modes/entwurfsmodus_dunkel.png differ diff --git a/packages/editor/src/canvas/Canvas.tsx b/packages/editor/src/canvas/Canvas.tsx index 42264ce..c81507f 100644 --- a/packages/editor/src/canvas/Canvas.tsx +++ b/packages/editor/src/canvas/Canvas.tsx @@ -36,7 +36,7 @@ import { bounds, cablePath, deviceBox, ICONS, iconOf, JAVA_LABEL_FONT, type Devi import { fitted, middle, shifted, spread, toScreen, toSurface, zoomedAt, zoomIn, zoomOut, type Point, type Size, type Viewport } from './viewport.js'; import { ZoomBar } from './ZoomBar.js'; import { gripPlace, handlePlace, HANDLES, menuPlace, NO_EDGE, shownHandles, TARGET, type Edges } from './handles.js'; -import { EDGE_BAND, edgesAt, FOCUS_BAND, FOCUS_BORDER, fontFamily, holdFrame, rectangleBorder, rectangleFill, TEXT_BORDER, textColor, type Frame } from './documentationPaint.js'; +import { darkRectangleBorder, darkRectangleFill, darkTextColor, EDGE_BAND, edgesAt, FOCUS_BAND, FOCUS_BORDER, fontFamily, holdFrame, rectangleBorder, rectangleFill, textColor, type Frame } from './documentationPaint.js'; /** How far a pointer moves before a press becomes a drag. */ export const DRAG_THRESHOLD = 4; @@ -152,6 +152,8 @@ function Documentation({ item, documenting, focused }: { readonly item: Document fill={rectangleFill(item.color, documenting)} stroke={focused ? 'none' : rectangleBorder(item.color)} strokeWidth={1} + // The dark scheme's paint (sheet.css takes it): Java's is for a white surface. + style={{ '--doc-fill-dark': darkRectangleFill(item.color, documenting), '--doc-stroke-dark': focused ? 'none' : darkRectangleBorder(item.color) } as CSSProperties} /> ); @@ -166,14 +168,15 @@ function Documentation({ item, documenting, focused }: { readonly item: Document style={{ boxSizing: 'border-box', padding: 1, - color: textColor(item.color), + // The words' colour, in the dark scheme drawn towards light (sheet.css). + ...({ '--doc-ink': textColor(item.color), '--doc-ink-dark': darkTextColor(item.color) } as CSSProperties), font: `${item.italic ? 'italic ' : ''}${item.bold ? 'bold ' : ''}${item.size}px ${fontFamily(item.font)}`, whiteSpace: 'pre-wrap', overflowWrap: 'break-word', overflow: 'hidden', width: '100%', height: '100%', - border: documenting && !focused ? `1px solid ${TEXT_BORDER}` : undefined, + border: documenting && !focused ? '1px solid var(--doc-text-border)' : undefined, visibility: focused ? 'hidden' : undefined, }} > diff --git a/packages/editor/src/canvas/documentationPaint.ts b/packages/editor/src/canvas/documentationPaint.ts index da3966b..2b6194e 100644 --- a/packages/editor/src/canvas/documentationPaint.ts +++ b/packages/editor/src/canvas/documentationPaint.ts @@ -40,6 +40,24 @@ export function textColor(color: Color): string { return `rgba(${byte(color.red)}, ${byte(color.green)}, ${byte(color.blue)}, ${color.alpha})`; } +/** + * The editor's dark scheme (not Java's — it has none; the exported image and + * the report keep Java's paint): a rectangle's colour as a tint of the dark + * surface, stronger while documenting, its border the colour itself, faded. + */ +export function darkRectangleFill(color: Color, documenting: boolean): string { + return `rgba(${byte(color.red)}, ${byte(color.green)}, ${byte(color.blue)}, ${documenting ? 0.28 : 0.2})`; +} + +export function darkRectangleBorder(color: Color): string { + return `rgba(${byte(color.red)}, ${byte(color.green)}, ${byte(color.blue)}, 0.7)`; +} + +/** A text's colour on the dark surface: drawn towards the light text colour, so that Java's black reads as light and a red as a light red. */ +export function darkTextColor(color: Color): string { + return `color-mix(in oklab, ${textColor(color)} 15%, var(--canvas-label))`; +} + /** Java's logical font names as the browser has them; any other a font of that name, else sans-serif. */ export function fontFamily(name: string): string { switch (name.toLowerCase()) { diff --git a/packages/editor/src/design/DocumentationText.tsx b/packages/editor/src/design/DocumentationText.tsx index c036f09..769a5cb 100644 --- a/packages/editor/src/design/DocumentationText.tsx +++ b/packages/editor/src/design/DocumentationText.tsx @@ -8,7 +8,7 @@ import { useEffect, useRef } from 'react'; import type * as Y from 'yjs'; import { replaceText, type DocumentationView } from '@mdg-filius/doc'; import type { ScreenFrame } from '../canvas/Canvas.js'; -import { fontFamily, textColor } from '../canvas/documentationPaint.js'; +import { darkTextColor, fontFamily, textColor } from '../canvas/documentationPaint.js'; import { followText } from '../desktop/textBinding.js'; import { useTranslate } from '../i18n/i18n.js'; @@ -37,6 +37,7 @@ export function DocumentationText({ item, frame, text, origin, onDone, onRemove ref={area} aria-label={t('docu.text')} data-doc-text={item.id} + className="doc-text-field" spellCheck={false} onInput={(event) => { const next = event.currentTarget.value; @@ -67,7 +68,8 @@ export function DocumentationText({ item, frame, text, origin, onDone, onRemove resize: 'none', overflow: 'hidden', background: 'transparent', - color: textColor(item.color), + // The words' colour as the drawn text has it, in the dark scheme too (sheet.css). + ...({ '--doc-ink': textColor(item.color), '--doc-ink-dark': darkTextColor(item.color) } as React.CSSProperties), font: `${item.italic ? 'italic ' : ''}${item.bold ? 'bold ' : ''}${item.size}px ${fontFamily(item.font)}`, lineHeight: 'normal', whiteSpace: 'pre-wrap', diff --git a/packages/editor/src/desktop/desktop.css b/packages/editor/src/desktop/desktop.css index 14c91e7..13dc578 100644 --- a/packages/editor/src/desktop/desktop.css +++ b/packages/editor/src/desktop/desktop.css @@ -167,9 +167,9 @@ .desktop-network { margin: 0; padding: 8px 12px; - border: 2px inset #c0c0c0; - background: #eeeeee; - color: #000000; + border: 2px inset var(--java-grid); + background: var(--java-panel); + color: var(--java-text); font: 13px Dialog, sans-serif; } @@ -187,8 +187,8 @@ .desktop-network dd { padding: 2px 6px; - background: #ffffff; - border: 1px solid #b0b0b0; + background: var(--java-field); + border: 1px solid var(--java-line); font-variant-numeric: tabular-nums; user-select: text; } @@ -199,14 +199,14 @@ align-items: stretch; height: 44px; flex: none; - background: rgb(180 180 180); + background: var(--java-taskbar); } .desktop-home, .desktop-network-icon { border: none; background: transparent; - color: #000000; + color: var(--java-text); font: 13px Dialog, sans-serif; cursor: pointer; } @@ -228,7 +228,7 @@ inset: 0; display: flex; flex-direction: column; - background: #ffffff; + background: var(--java-field); } .program-titlebar { @@ -343,8 +343,8 @@ align-items: center; gap: 10px; padding: 10px; - background: #eeeeee; - color: #000000; + background: var(--java-panel); + color: var(--java-text); font: 13px Dialog, sans-serif; } @@ -371,8 +371,8 @@ margin: 0; padding: 0; list-style: none; - background: #ffffff; - border: 1px solid #a0a0a0; + background: var(--java-field); + border: 1px solid var(--java-border); } .installation-list li { @@ -385,11 +385,11 @@ } .installation-list li[aria-selected='true'] { - background: rgb(184 207 229); + background: var(--java-selected); } .installation-list li:focus-visible { - outline: 2px solid rgb(99 130 191); + outline: 2px solid var(--java-focus); outline-offset: -2px; } @@ -404,10 +404,10 @@ .installation-apply { min-width: 44px; min-height: 44px; - border: 1px solid #a0a0a0; + border: 1px solid var(--java-border); border-radius: 6px; - background: #f8f8f8; - color: #000000; + background: var(--java-button); + color: var(--java-text); font: 13px Dialog, sans-serif; cursor: pointer; } @@ -425,7 +425,7 @@ min-height: 0; display: flex; flex-direction: column; - background: #eeeeee; + background: var(--java-panel); font: 13px Dialog, sans-serif; } @@ -441,9 +441,9 @@ min-width: 0; min-height: 36px; padding: 0 6px; - border: 1px solid #a0a0a0; - background: #ffffff; - color: #000000; + border: 1px solid var(--java-border); + background: var(--java-field); + color: var(--java-text); font: 16px Dialog, sans-serif; } @@ -452,10 +452,10 @@ min-width: 44px; min-height: 36px; padding: 0 14px; - border: 1px solid #a0a0a0; + border: 1px solid var(--java-border); border-radius: 6px; - background: #f8f8f8; - color: #000000; + background: var(--java-button); + color: var(--java-text); font: 13px Dialog, sans-serif; cursor: pointer; } @@ -466,7 +466,7 @@ margin: 0 5px 5px; display: flex; background: #ffffff; - border: 1px solid #a0a0a0; + border: 1px solid var(--java-border); } .browser-page iframe { @@ -496,8 +496,8 @@ flex-direction: column; gap: 10px; padding: 5px 10px 10px; - background: #eeeeee; - color: #000000; + background: var(--java-panel); + color: var(--java-text); font: 13px Dialog, sans-serif; } @@ -533,20 +533,20 @@ width: 100%; border-collapse: collapse; table-layout: fixed; - background: #ffffff; + background: var(--java-field); } .web-server-hosts th { padding: 4px; - border: 1px solid #a0a0a0; - background: #e4e4e4; + border: 1px solid var(--java-border); + background: var(--java-head); font-weight: normal; text-align: left; } .web-server-hosts td { padding: 0; - border: 1px solid #c0c0c0; + border: 1px solid var(--java-grid); } .web-server-hosts td input { @@ -556,7 +556,7 @@ padding: 0 5px; border: none; background: transparent; - color: #000000; + color: var(--java-text); font: 16px Dialog, sans-serif; } @@ -566,8 +566,8 @@ margin: 0; overflow: auto; padding: 4px; - border: 1px solid #a0a0a0; - background: #ffffff; + border: 1px solid var(--java-border); + background: var(--java-field); font: 12px ui-monospace, Menlo, Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; @@ -588,8 +588,8 @@ flex-direction: column; gap: 5px; padding: 5px; - background: #eeeeee; - color: #000000; + background: var(--java-panel); + color: var(--java-text); font: 13px Dialog, sans-serif; } @@ -612,10 +612,10 @@ min-width: 44px; min-height: 40px; padding: 0 12px; - border: 1px solid #a0a0a0; + border: 1px solid var(--java-border); border-radius: 6px; - background: #f8f8f8; - color: #000000; + background: var(--java-button); + color: var(--java-text); font: 13px Dialog, sans-serif; cursor: pointer; } @@ -642,23 +642,23 @@ min-width: 0; min-height: 32px; padding: 0 6px; - border: 1px solid #a0a0a0; - background: #ffffff; - color: #000000; + border: 1px solid var(--java-border); + background: var(--java-field); + color: var(--java-text); font: 16px Dialog, sans-serif; } .mail-client [aria-invalid='true'], .mail-server [aria-invalid='true'] { - border-color: #ff0000; - color: #ff0000; + border-color: var(--java-invalid); + color: var(--java-invalid); } .mail-tabs { display: flex; gap: 2px; flex: none; - border-bottom: 1px solid #a0a0a0; + border-bottom: 1px solid var(--java-border); } .mail-tabs button { @@ -667,17 +667,17 @@ gap: 6px; min-height: 40px; padding: 0 12px; - border: 1px solid #a0a0a0; + border: 1px solid var(--java-border); border-bottom: none; border-radius: 6px 6px 0 0; - background: #e0e0e0; - color: #000000; + background: var(--java-tab); + color: var(--java-text); font: 13px Dialog, sans-serif; cursor: pointer; } .mail-tabs button[aria-selected='true'] { - background: #ffffff; + background: var(--java-field); } .mail-list, @@ -685,8 +685,8 @@ flex: 1; min-height: 80px; overflow: auto; - background: #ffffff; - border: 1px solid #a0a0a0; + background: var(--java-field); + border: 1px solid var(--java-border); } .mail-accounts { @@ -711,8 +711,8 @@ .mail-list th, .mail-accounts th { padding: 4px 6px; - border-bottom: 1px solid #a0a0a0; - background: #e4e4e4; + border-bottom: 1px solid var(--java-border); + background: var(--java-head); font-weight: normal; text-align: left; } @@ -729,7 +729,7 @@ .mail-list tr[aria-selected='true'] td, .mail-accounts tr[aria-selected='true'] td { - background: rgb(184 207 229); + background: var(--java-selected); } .mail-preview { @@ -737,8 +737,8 @@ min-height: 80px; overflow: auto; padding: 4px; - border: 1px solid #a0a0a0; - background: #ffffff; + border: 1px solid var(--java-border); + background: var(--java-field); } .mail-preview pre, @@ -763,8 +763,8 @@ min-height: 80px; overflow: auto; padding: 4px; - border: 1px solid #a0a0a0; - background: #ffffff; + border: 1px solid var(--java-border); + background: var(--java-field); } .mail-form { @@ -805,7 +805,7 @@ gap: 6px; overflow: auto; padding: 0 8px 8px; - background: #eeeeee; + background: var(--java-panel); } .mail-dialog h4 { @@ -821,7 +821,7 @@ flex: 1; min-height: 120px; padding: 4px; - border: 1px solid #a0a0a0; + border: 1px solid var(--java-border); font: 16px ui-monospace, Menlo, Consolas, monospace; } @@ -833,9 +833,9 @@ transform: translateX(-50%); width: min(90%, 380px); padding: 12px; - border: 1px solid #808080; + border: 1px solid var(--java-border-strong); border-radius: 6px; - background: #ffffff; + background: var(--java-field); box-shadow: 0 4px 16px rgb(0 0 0 / 30%); } @@ -858,7 +858,7 @@ flex-direction: column; gap: 5px; padding: 5px; - background: #eeeeee; + background: var(--java-panel); } .text-editor-bar { @@ -874,17 +874,17 @@ min-width: 44px; min-height: 40px; padding: 0 12px; - border: 1px solid #a0a0a0; + border: 1px solid var(--java-border); border-radius: 6px; - background: #f8f8f8; - color: #000000; + background: var(--java-button); + color: var(--java-text); font: 13px Dialog, sans-serif; cursor: pointer; } .text-editor-bar button:disabled, .file-dialog-folder button:disabled { - color: #a0a0a0; + color: var(--java-disabled); cursor: default; } @@ -893,9 +893,9 @@ min-height: 0; resize: none; padding: 4px; - border: 1px solid #a0a0a0; - background: #ffffff; - color: #000000; + border: 1px solid var(--java-border); + background: var(--java-field); + color: var(--java-text); font: 16px 'Courier New', Courier, monospace; } @@ -919,8 +919,8 @@ margin: 0; padding: 0; overflow: auto; - border: 1px solid #a0a0a0; - background: #ffffff; + border: 1px solid var(--java-border); + background: var(--java-field); list-style: none; } @@ -933,14 +933,14 @@ padding: 0 6px; border: 0; background: transparent; - color: #000000; + color: var(--java-text); font: 13px Dialog, sans-serif; text-align: left; cursor: pointer; } .file-dialog-entries button[aria-pressed='true'] { - background: #b8cfe5; + background: var(--java-selected); } /* The file explorer (GUIApplicationFileExplorerWindow): the menu's actions @@ -954,7 +954,7 @@ flex-direction: column; gap: 5px; padding: 5px; - background: #eeeeee; + background: var(--java-panel); } .file-explorer-bar { @@ -968,16 +968,16 @@ min-width: 44px; min-height: 40px; padding: 0 10px; - border: 1px solid #a0a0a0; + border: 1px solid var(--java-border); border-radius: 6px; - background: #f8f8f8; - color: #000000; + background: var(--java-button); + color: var(--java-text); font: 13px Dialog, sans-serif; cursor: pointer; } .file-explorer-bar button:disabled { - color: #a0a0a0; + color: var(--java-disabled); cursor: default; } @@ -995,8 +995,8 @@ margin: 0; padding: 0; overflow: auto; - border: 1px solid #a0a0a0; - background: #ffffff; + border: 1px solid var(--java-border); + background: var(--java-field); list-style: none; } @@ -1010,7 +1010,7 @@ padding: 0 6px; border: 0; background: transparent; - color: #000000; + color: var(--java-text); font: 13px Dialog, sans-serif; text-align: left; cursor: pointer; @@ -1019,7 +1019,7 @@ .file-explorer-tree button[aria-current='true'], .file-explorer-tree button[aria-pressed='true'], .file-explorer-entries button[aria-pressed='true'] { - background: #b8cfe5; + background: var(--java-selected); } @media (max-width: 639px) { @@ -1048,7 +1048,7 @@ flex-direction: column; gap: 5px; padding: 5px; - background: #eeeeee; + background: var(--java-panel); } .image-viewer-picture { @@ -1071,9 +1071,9 @@ .simple-client textarea { min-height: 32px; padding: 0 6px; - border: 1px solid #a0a0a0; - background: #ffffff; - color: #000000; + border: 1px solid var(--java-border); + background: var(--java-field); + color: var(--java-text); font: 16px Dialog, sans-serif; } @@ -1083,13 +1083,13 @@ .echo-server [aria-invalid='true'], .simple-client [aria-invalid='true'] { - border-color: #ff0000; - color: #ff0000; + border-color: var(--java-invalid); + color: var(--java-invalid); } .echo-server input:read-only, .simple-client input:read-only { - background: #eeeeee; + background: var(--java-panel); } .simple-client-field, @@ -1134,15 +1134,15 @@ .dns-server input:not([type='checkbox']) { min-height: 32px; padding: 0 6px; - border: 1px solid #a0a0a0; - background: #ffffff; - color: #000000; + border: 1px solid var(--java-border); + background: var(--java-field); + color: var(--java-text); font: 16px Dialog, sans-serif; } .dns-server [aria-invalid='true'] { - border-color: #ff0000; - color: #ff0000; + border-color: var(--java-invalid); + color: var(--java-invalid); } .dns-server-notice { @@ -1152,19 +1152,19 @@ .dns-server-table { width: 100%; border-collapse: collapse; - background: #ffffff; + background: var(--java-field); } .dns-server-table th { padding: 4px 6px; - border-bottom: 1px solid #a0a0a0; + border-bottom: 1px solid var(--java-border); text-align: left; font-weight: normal; } .dns-server-table td { padding: 2px; - border-bottom: 1px solid #dddddd; + border-bottom: 1px solid var(--java-rule); } .dns-server-table td input { @@ -1175,7 +1175,7 @@ .dns-server-table tr[aria-selected='true'] td, .dns-server-table tr[aria-selected='true'] input { - background: #b8cfe5; + background: var(--java-selected); } /* A computer's firewall (GUIApplicationFirewallWindow): two tabs, the boxes, @@ -1215,9 +1215,9 @@ .personal-firewall select { min-height: 32px; padding: 0 6px; - border: 1px solid #a0a0a0; - background: #ffffff; - color: #000000; + border: 1px solid var(--java-border); + background: var(--java-field); + color: var(--java-text); font: 16px Dialog, sans-serif; } @@ -1226,8 +1226,8 @@ } .personal-firewall [aria-invalid='true'] { - border-color: #ff0000; - color: #ff0000; + border-color: var(--java-invalid); + color: var(--java-invalid); } .personal-firewall-table td { @@ -1269,15 +1269,15 @@ .gnutella input { min-height: 32px; padding: 0 6px; - border: 1px solid #a0a0a0; - background: #ffffff; - color: #000000; + border: 1px solid var(--java-border); + background: var(--java-field); + color: var(--java-text); font: 16px Dialog, sans-serif; } .gnutella [aria-invalid='true'] { - border-color: #ff0000; - color: #ff1414; + border-color: var(--java-invalid); + color: var(--java-invalid-text); } .gnutella-table td { diff --git a/packages/editor/src/sheet/sheet.css b/packages/editor/src/sheet/sheet.css index 266fc55..93c86c9 100644 --- a/packages/editor/src/sheet/sheet.css +++ b/packages/editor/src/sheet/sheet.css @@ -50,6 +50,30 @@ overflow-y: auto; } +/* Documentation on the surface: a text's words in their colour; in the dark + * scheme a rectangle as a tint and the words drawn towards light + * (documentationPaint.ts) — Java paints for a white surface. Java's icons get + * a faint light outline there, or its black switches and routers would sink + * into the dark. */ +.doc-text, +.doc-text-field { + color: var(--doc-ink); +} + +:root[data-theme='dark'] .doc-text, +:root[data-theme='dark'] .doc-text-field { + color: var(--doc-ink-dark); +} + +:root[data-theme='dark'] .doc-rectangle { + fill: var(--doc-fill-dark); + stroke: var(--doc-stroke-dark); +} + +:root[data-theme='dark'] .filius-canvas .devices image { + filter: drop-shadow(0 0 1px rgb(255 255 255 / 55%)); +} + /* A focused documentation item's handles (Canvas): a finger's target of * 44 px around a dot in Java's focus blue, over the field a text is typed * into; a text's grip a round button above its corner. */ @@ -117,6 +141,26 @@ pointer-events: auto; } +/* The mode buttons' pictures (ModeBar): Java's in the light scheme; in the + * dark one freed from their grey ground, the button marking the mode that is on. */ +.mode-picture { + display: block; +} + +.mode-picture[data-scheme='dark'], +:root[data-theme='dark'] .mode-picture[data-scheme='light'] { + display: none; +} + +:root[data-theme='dark'] .mode-picture[data-scheme='dark'] { + display: block; +} + +:root[data-theme='dark'] .mode-button[aria-pressed='true'] { + border-color: var(--accent) !important; + background: var(--accent-soft) !important; +} + /* A word beside the mode buttons: that this person only reads. */ .editor-note { display: flex; diff --git a/packages/editor/src/simulation/ModeBar.tsx b/packages/editor/src/simulation/ModeBar.tsx index fcdf006..d296239 100644 --- a/packages/editor/src/simulation/ModeBar.tsx +++ b/packages/editor/src/simulation/ModeBar.tsx @@ -11,8 +11,28 @@ import design from '../assets/modes/entwurfsmodus.png'; import designOn from '../assets/modes/entwurfsmodus_aktiv.png'; import documentation from '../assets/modes/dokumodus.png'; import documentationOn from '../assets/modes/dokumodus_aktiv.png'; +import simulationDark from '../assets/modes/aktionsmodus_dunkel.png'; +import simulationOnDark from '../assets/modes/aktionsmodus_aktiv_dunkel.png'; +import designDark from '../assets/modes/entwurfsmodus_dunkel.png'; +import designOnDark from '../assets/modes/entwurfsmodus_aktiv_dunkel.png'; +import documentationDark from '../assets/modes/dokumodus_dunkel.png'; +import documentationOnDark from '../assets/modes/dokumodus_aktiv_dunkel.png'; import { useTranslate } from '../i18n/i18n.js'; +/** + * A mode's picture: Java's (a grey button's gradient, light blue when on), + * and for the dark scheme the same freed from its ground, the button showing + * whether it is on (sheet.css: .mode-picture). + */ +function ModePicture({ light, dark }: { readonly light: string; readonly dark: string }) { + return ( + <> + + + + ); +} + const modeButton: CSSProperties = { width: 52, height: 52, @@ -46,6 +66,7 @@ export function ModeBar({ - - {mode === 'simulation' ? (