feat(editor): the dark scheme everywhere — the surface, documentation, mode buttons, program windows

The surface turns dark with light labels and cables and a faint light outline
round Java's icons; documentation rectangles become a tint of their colour and
texts are drawn towards light; the mode buttons use Java's pictures freed from
their grey ground (tools/dark-pictures/make.py); native controls follow
color-scheme; the desktop's program windows paint through Java-look tokens
whose light values are Java's own. The exported image and the report keep
Java's colours.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 22:27:03 +00:00
co-authored by Claude Opus 5.5
parent 8c4e5049f6
commit ae10ee6c31
17 changed files with 536 additions and 127 deletions
+208
View File
@@ -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<void> };
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);
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+6 -3
View File
@@ -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}
/>
</g>
);
@@ -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,
}}
>
@@ -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()) {
@@ -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',
+109 -109
View File
@@ -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 {
+44
View File
@@ -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;
+26 -5
View File
@@ -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 (
<>
<img className="mode-picture" data-scheme="light" src={light} alt="" width={50} height={50} />
<img className="mode-picture" data-scheme="dark" src={dark} alt="" width={50} height={50} />
</>
);
}
const modeButton: CSSProperties = {
width: 52,
height: 52,
@@ -46,6 +66,7 @@ export function ModeBar({
<button
type="button"
name="documentationMode"
className="mode-button"
style={modeButton}
aria-pressed={mode === 'documentation'}
aria-label={t('mode.documentation')}
@@ -53,13 +74,13 @@ export function ModeBar({
disabled={!canRun && mode === 'simulation'}
onClick={() => onMode('documentation')}
>
<img src={mode === 'documentation' ? documentationOn : documentation} alt="" width={50} height={50} />
<ModePicture light={mode === 'documentation' ? documentationOn : documentation} dark={mode === 'documentation' ? documentationOnDark : documentationDark} />
</button>
<button type="button" name="designMode" style={modeButton} aria-pressed={mode === 'design'} aria-label={t('mode.design')} title={t('mode.design')} disabled={!canRun && mode === 'simulation'} onClick={() => onMode('design')}>
<img src={mode === 'design' ? designOn : design} alt="" width={50} height={50} />
<button type="button" name="designMode" className="mode-button" style={modeButton} aria-pressed={mode === 'design'} aria-label={t('mode.design')} title={t('mode.design')} disabled={!canRun && mode === 'simulation'} onClick={() => onMode('design')}>
<ModePicture light={mode === 'design' ? designOn : design} dark={mode === 'design' ? designOnDark : designDark} />
</button>
<button type="button" name="simulationMode" style={modeButton} aria-pressed={mode === 'simulation'} aria-label={t('mode.simulation')} title={t('mode.simulation')} disabled={!canRun} onClick={() => onMode('simulation')}>
<img src={mode === 'simulation' ? simulationOn : simulation} alt="" width={50} height={50} />
<button type="button" name="simulationMode" className="mode-button" style={modeButton} aria-pressed={mode === 'simulation'} aria-label={t('mode.simulation')} title={t('mode.simulation')} disabled={!canRun} onClick={() => onMode('simulation')}>
<ModePicture light={mode === 'simulation' ? simulationOn : simulation} dark={mode === 'simulation' ? simulationOnDark : simulationDark} />
</button>
{mode === 'simulation' ? (
<label style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '0 8px', minHeight: 52, borderRadius: 8, background: 'var(--surface-raised)', border: '1px solid var(--border-default)' }}>
+48 -6
View File
@@ -45,6 +45,28 @@
--capture-on-layer: #000000;
--capture-chosen: #000000;
--capture-on-chosen: #ffffff;
/* A documentation text's border in the documentation mode (textPanelBorderNormalColor). */
--doc-text-border: rgb(179 179 179);
/* Java's Swing look in the desktop's program windows (Metal): a panel, a field, its border and text, a button, a
* table's head, a chosen row, the focus, a refused entry; the taskbar (GUIDesktopPanel). */
--java-panel: #eeeeee;
--java-field: #ffffff;
--java-text: #000000;
--java-border: #a0a0a0;
--java-border-strong: #808080;
--java-grid: #c0c0c0;
--java-line: #b0b0b0;
--java-rule: #dddddd;
--java-disabled: #a0a0a0;
--java-button: #f8f8f8;
--java-head: #e4e4e4;
--java-tab: #e0e0e0;
--java-selected: #b8cfe5;
--java-focus: rgb(99 130 191);
--java-invalid: #ff0000;
--java-invalid-text: #ff1414;
--java-taskbar: rgb(180 180 180);
color-scheme: light;
}
:root[data-theme='dark'] {
@@ -70,12 +92,13 @@
--danger: #f2695a;
--danger-soft: #3a1a17;
--danger-on-soft: #ffa79b;
/* Java's icons are drawn for a light ground: the surface stays light, a shade darker. */
--canvas-bg: #e9ecf1;
--canvas-cable: rgb(64 64 64);
--canvas-cable-wireless: rgb(150 150 150);
--canvas-cable-active: rgb(0 200 50);
--canvas-label: #000000;
/* The surface dark too, a step above the sheets, so that Java's black switches and routers keep their edge
* (each icon gets a faint light outline, sheet.css); labels and cables light. */
--canvas-bg: #262b33;
--canvas-cable: rgb(150 158 170);
--canvas-cable-wireless: rgb(105 113 125);
--canvas-cable-active: rgb(40 220 90);
--canvas-label: #e6e9ef;
--connection-plugged: rgb(0 190 0);
--connection-free: rgb(210 40 40);
--connection-marked: rgb(235 215 0);
@@ -86,6 +109,25 @@
--capture-on-layer: #e6e9ef;
--capture-chosen: #e6e9ef;
--capture-on-chosen: #14171c;
--doc-text-border: #4a5361;
--java-panel: #20252c;
--java-field: #15181d;
--java-text: #e6e9ef;
--java-border: #4a5361;
--java-border-strong: #5c6573;
--java-grid: #3a414c;
--java-line: #3a414c;
--java-rule: #2e343d;
--java-disabled: #6b7380;
--java-button: #2b3038;
--java-head: #2a2f37;
--java-tab: #262b33;
--java-selected: #2c4a66;
--java-focus: #6f9ad6;
--java-invalid: #f2695a;
--java-invalid-text: #ff8a7d;
--java-taskbar: #2a2f37;
color-scheme: dark;
}
html,
@@ -1,13 +1,14 @@
// How the editor paints a documentation item, against the pixels Java's
// JDocuElement paints (packages/doc/test/documentation.java.json, DocuRun):
// a rectangle in the documentation mode and in the others, its border, the
// text's border; the focused text's frame; where an edge is taken hold of.
// text's border; the focused text's frame; where an edge is taken hold of —
// and the editor's own dark paint, which Java has not.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { edgesAt, fontFamily, holdFrame, rectangleBorder, rectangleFill, TEXT_BORDER, FOCUS_BORDER, FOCUS_BAND, textColor } from '../src/canvas/documentationPaint.js';
import { darkRectangleBorder, darkRectangleFill, darkTextColor, edgesAt, fontFamily, holdFrame, rectangleBorder, rectangleFill, TEXT_BORDER, FOCUS_BORDER, FOCUS_BAND, textColor } from '../src/canvas/documentationPaint.js';
type Rgba = [number, number, number, number];
const java = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../../doc/test/documentation.java.json'), 'utf8')) as {
@@ -40,6 +41,14 @@ describe("Java's paint of the documentation", () => {
expect(textColor({ red: 0, green: 0, blue: 1, alpha: 0.5 })).toBe('rgba(0, 0, 255, 0.5)');
});
it('paints a rectangle in the dark scheme as a tint of its colour, stronger while documenting, and draws a text towards light', () => {
const grey = { red: 0.8, green: 0.8, blue: 0.8, alpha: 1 };
expect(darkRectangleFill(grey, true)).toBe('rgba(204, 204, 204, 0.28)');
expect(darkRectangleFill(grey, false)).toBe('rgba(204, 204, 204, 0.2)');
expect(darkRectangleBorder({ red: 1, green: 0, blue: 0, alpha: 1 })).toBe('rgba(255, 0, 0, 0.7)');
expect(darkTextColor({ red: 0, green: 0, blue: 0, alpha: 1 })).toBe('color-mix(in oklab, rgba(0, 0, 0, 1) 15%, var(--canvas-label))');
});
it("grows a focused text's frame as Java does, and finds the edges a drag takes", () => {
const [x, y, width, height] = java.dropped.textUnfocused.bounds as [number, number, number, number];
const [fx, fy, fw, fh] = java.dropped.text.bounds as [number, number, number, number];