feat(editor): F4.3 the editor's frame and surface; a switch's cloud read and written

packages/editor (React 18, Vite) draws a document as Java FILIUS does: Java's
default hardware icons, the device button of JSidebarButton, cables as
JCablePanel's quadratic curve between the buttons' middles, wireless cables
dashed, documentation items below. The surface moves with mouse and finger
(the pointer captured only once it moved) and zooms with Ctrl and the
wheel, a trackpad pinch, two fingers and the zoom bar; the first view shows
all. German and English catalogs with Java's device names, the dashboard's
colour tokens, the bridge with its origin check. packages/doc gets the
drawing's light view of a document.

Java's Switch.cloud (the Internet in the examples) was lost on reading; it
is now read, kept in the model and written, with FlsOracle and the corpus,
written and exported checks renewed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 09:05:25 +00:00
co-authored by Claude Opus 5.5
parent 0fd42f10e2
commit 63273916b5
49 changed files with 2442 additions and 8 deletions
+53
View File
@@ -0,0 +1,53 @@
// The bridge to the dashboard: only the expected origin, only this version,
// and the editor says when it listens.
import { describe, expect, it } from 'vitest';
import { BRIDGE_VERSION, startBridge } from '../src/bridge/dashboardBridge.js';
function fakeWindow() {
const posted: [unknown, string][] = [];
const listeners = new Set<(event: { origin: string; data: unknown }) => void>();
return {
posted,
window: {
parent: { postMessage: (message: unknown, origin: string) => posted.push([message, origin]) },
addEventListener: (_: 'message', listener: (event: { origin: string; data: unknown }) => void) => listeners.add(listener),
removeEventListener: (_: 'message', listener: (event: { origin: string; data: unknown }) => void) => listeners.delete(listener),
},
send: (origin: string, data: unknown) => {
for (const listener of listeners) listener({ origin, data });
},
listening: () => listeners.size,
};
}
describe('the bridge to the dashboard', () => {
it('says it is ready, to the dashboard only', () => {
const fake = fakeWindow();
startBridge(fake.window, 'https://dashboard.example', { onLocale: () => {}, onTheme: () => {} });
expect(fake.posted).toEqual([[{ v: BRIDGE_VERSION, type: 'ready' }, 'https://dashboard.example']]);
});
it('takes language and colours from the dashboard, and nothing from elsewhere', () => {
const fake = fakeWindow();
const got: unknown[] = [];
const stop = startBridge(fake.window, 'https://dashboard.example', {
onLocale: (locale) => got.push(['locale', locale]),
onTheme: (theme, tokens) => got.push(['theme', theme, tokens]),
});
fake.send('https://evil.example', { v: 1, type: 'setLocale', locale: 'en' });
fake.send('https://dashboard.example', { v: 2, type: 'setLocale', locale: 'en' });
fake.send('https://dashboard.example', { v: 1, type: 'setLocale', locale: 7 });
fake.send('https://dashboard.example', { v: 1, type: 'setTheme', theme: 'pink' });
fake.send('https://dashboard.example', null);
expect(got).toEqual([]);
fake.send('https://dashboard.example', { v: 1, type: 'setLocale', locale: 'en-GB' });
fake.send('https://dashboard.example', { v: 1, type: 'setTheme', theme: 'dark', tokens: { '--accent': '#123456', color: 'red', '--bad': 3 } });
expect(got).toEqual([
['locale', 'en-GB'],
['theme', 'dark', { '--accent': '#123456' }],
]);
stop();
expect(fake.listening()).toBe(0);
});
});
+58
View File
@@ -0,0 +1,58 @@
// The catalogs: the same keys in both languages, placeholders filled, and —
// where Java FILIUS names a thing — Java's word in its bundle (M11).
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { CATALOGS, translate } from '../src/i18n/i18n.js';
const bundles = join(dirname(fileURLToPath(import.meta.url)), '../../../tools/java-oracle/.upstream/src/main/resources/filius/messages');
/** A Java .properties file: ISO-8859-1, \uXXXX escapes, `key = value`. */
function properties(file: string): Map<string, string> {
const text = new TextDecoder('latin1').decode(readFileSync(join(bundles, file)));
const entries = new Map<string, string>();
for (const line of text.split(/\r?\n/)) {
const match = /^([\w.]+)\s*=\s*(.*)$/.exec(line);
if (match !== null) entries.set(match[1]!, match[2]!.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) => String.fromCharCode(Number.parseInt(hex, 16))).trim());
}
return entries;
}
/** The editor's keys whose words are Java's, and the key of each in Java's bundle. */
const FROM_JAVA: Readonly<Record<string, string>> = {
'device.pc': 'hw_rechner_msg1',
'device.notebook': 'hw_notebook_msg1',
'device.networkSwitch': 'hw_switch_msg1',
'device.router': 'hw_vermittlungsrechner_msg1',
'device.gateway': 'hw_gateway_msg1',
'device.cable': 'hw_kabel_msg1',
};
describe('the catalogs', () => {
it('have the same keys in German and English, each with a text', () => {
expect(Object.keys(CATALOGS.en).sort()).toEqual(Object.keys(CATALOGS.de).sort());
for (const catalog of Object.values(CATALOGS)) for (const text of Object.values(catalog)) expect(text.trim()).not.toBe('');
});
it('fill placeholders, and show a key they lack', () => {
expect(translate('de', 'zoom.level', { percent: 75 })).toBe('75 %');
expect(translate('en', 'zoom.level')).toBe('{percent} %');
expect(translate('de', 'no.such.key')).toBe('no.such.key');
});
it('name every kind of device', () => {
for (const kind of ['pc', 'notebook', 'networkSwitch', 'router', 'gateway', 'remoteLink', 'unsupported']) expect(CATALOGS.de[`device.${kind}`]).toBeDefined();
// Java's Modem.TYPE, the same in every language.
expect([CATALOGS.de['device.remoteLink'], CATALOGS.en['device.remoteLink']]).toEqual(['Modem', 'Modem']);
});
it.skipIf(!existsSync(bundles))("use Java's words where Java has them (M11)", () => {
const [de, en] = [properties('MessagesBundle_de_DE.properties'), properties('MessagesBundle_en_GB.properties')];
for (const [key, javaKey] of Object.entries(FROM_JAVA)) {
expect(CATALOGS.de[key], key).toBe(de.get(javaKey));
expect(CATALOGS.en[key], key).toBe(en.get(javaKey));
}
});
});
+58
View File
@@ -0,0 +1,58 @@
// Java's drawing of a device and a cable (JSidebarButton, JCablePanel).
import { describe, expect, it } from 'vitest';
import { bounds, cablePath, deviceBox, iconOf, ICONS, JAVA_LABEL_FONT_HEIGHT } from '../src/canvas/layout.js';
describe("Java's drawing", () => {
it('picks the icon of each kind, the cloud for a switch drawn as one', () => {
expect((['pc', 'notebook', 'networkSwitch', 'router', 'gateway', 'remoteLink', 'unsupported'] as const).map((kind) => iconOf(kind, false))).toEqual([
'server',
'laptop',
'switch',
'router',
'gateway',
'modem',
'cloud',
]);
expect(iconOf('networkSwitch', true)).toBe('cloud');
expect(iconOf('router', true)).toBe('router');
});
it('makes a button as wide as its icon or its label, plus 5 and twice the border, and as high as both plus 5', () => {
const short = deviceBox({ kind: 'pc', cloud: false, x: 100, y: 200 }, 30);
expect(short).toMatchObject({ x: 100, y: 200, width: 64 + 21, height: JAVA_LABEL_FONT_HEIGHT + 94 + 5, icon: 'server' });
// The icon at the top, in the middle; the label's baseline under it, in the middle.
expect(short.iconRect).toEqual({ x: 100 + (85 - 64) / 2, y: 200, width: 64, height: 94 });
expect(short.label).toEqual({ x: 100 + 85 / 2, y: 200 + 94 + 14 });
const long = deviceBox({ kind: 'router', cloud: false, x: 0, y: 0 }, 124.6);
expect(long.width).toBe(125 + 21);
expect(long.iconRect.x).toBe((146 - 77) / 2);
});
it('meets cables in the middle of the button, in whole pixels as Java does', () => {
const box = deviceBox({ kind: 'notebook', cloud: false, x: 11, y: 7 }, 10);
// 84 + 21 = 105 wide, 17 + 80 + 5 = 102 high.
expect(box.center).toEqual({ x: Math.trunc(11 + 52.5), y: Math.trunc(7 + 51) });
});
it("draws a cable as JCablePanel's curve", () => {
// Rising to the right (y falls): the control point a quarter across the panel.
// Panel from (98, 48); control (98 + trunc((2 + 302) / 4), 48 + trunc((52 + 2) / 4)) = (174, 61).
expect(cablePath({ x: 100, y: 100 }, { x: 400, y: 50 })).toBe('M 100 100 Q 174 61 400 50');
// Falling to the right: three quarters across.
expect(cablePath({ x: 100, y: 50 }, { x: 400, y: 100 })).toBe(`M 100 50 Q ${98 + 3 * Math.trunc(304 / 4)} 61 400 100`);
// The same either way round.
expect(cablePath({ x: 400, y: 100 }, { x: 100, y: 50 })).toBe(`M 400 100 Q ${98 + 3 * Math.trunc(304 / 4)} 61 100 50`);
// Straight down: not falling (x does not change); the panel from (8, 8), control (8 + trunc(4 / 4), 8 + trunc(84 / 4)).
expect(cablePath({ x: 10, y: 10 }, { x: 10, y: 90 })).toBe('M 10 10 Q 9 29 10 90');
});
it('knows every icon it draws', () => {
expect(Object.keys(ICONS).sort()).toEqual(['cloud', 'gateway', 'laptop', 'modem', 'router', 'server', 'switch']);
});
it('spans the rectangles around everything', () => {
expect(bounds([])).toEqual({ x: 0, y: 0, width: 0, height: 0 });
expect(bounds([{ x: 10, y: 20, width: 5, height: 5 }, { x: -5, y: 30, width: 10, height: 10 }])).toEqual({ x: -5, y: 20, width: 20, height: 20 });
});
});
+15
View File
@@ -0,0 +1,15 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"noEmit": true,
"composite": false,
"declaration": false,
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"types": ["node", "vite/client"],
"resolveJsonModule": true
},
"include": [".", "../browser", "../src"]
}
+59
View File
@@ -0,0 +1,59 @@
// The view onto the surface: zoom steps and limits, zooming around a point,
// fitting the content in.
import { describe, expect, it } from 'vitest';
import { clampZoom, fitted, MAX_ZOOM, middle, MIN_ZOOM, shifted, spread, toScreen, toSurface, zoomedAt, zoomIn, zoomOut, ZOOM_STEPS } from '../src/canvas/viewport.js';
describe('the view onto the surface', () => {
it('keeps the zoom between a quarter and twice', () => {
expect([clampZoom(0.1), clampZoom(1.3), clampZoom(9)]).toEqual([MIN_ZOOM, 1.3, MAX_ZOOM]);
});
it('steps along the ladder from any value, and back to where it was', () => {
expect(ZOOM_STEPS.map(zoomIn)).toEqual([0.5, 0.75, 1, 1.25, 1.5, 2, 2]);
expect(ZOOM_STEPS.map(zoomOut)).toEqual([0.25, 0.25, 0.5, 0.75, 1, 1.25, 1.5]);
expect([zoomIn(0.8), zoomOut(0.8), zoomIn(1.0005)]).toEqual([1, 0.75, 1.25]);
for (const step of ZOOM_STEPS.slice(1, -1)) expect(zoomOut(zoomIn(step))).toBe(step);
});
it('turns screen and surface pixels into each other', () => {
const view = { scale: 0.5, x: 100, y: -20 };
expect(toScreen(view, { x: 40, y: 80 })).toEqual({ x: 120, y: 20 });
expect(toSurface(view, toScreen(view, { x: 40, y: 80 }))).toEqual({ x: 40, y: 80 });
});
it('zooms so that the point zoomed at stays put, within the limits', () => {
const view = { scale: 1, x: 30, y: 40 };
const point = { x: 200, y: 150 };
const zoomed = zoomedAt(view, 1.5, point);
expect(zoomed.scale).toBe(1.5);
expect(toScreen(zoomed, toSurface(view, point))).toEqual(point);
expect(zoomedAt(view, 10, point).scale).toBe(MAX_ZOOM);
expect(toScreen(zoomedAt(view, 10, point), toSurface(view, point))).toEqual(point);
});
it('moves by what the pointer moved', () => {
expect(shifted({ scale: 2, x: 1, y: 2 }, { x: 10, y: -5 })).toEqual({ scale: 2, x: 11, y: -3 });
});
it('fits the content in the middle with a margin, never above 100 %', () => {
const content = { x: 100, y: 50, width: 800, height: 400 };
const view = fitted(content, { width: 448, height: 448 });
expect(view.scale).toBe(0.5);
// Left and right margin alike, the content in the middle vertically.
expect(toScreen(view, { x: 100, y: 50 })).toEqual({ x: 24, y: 124 });
expect(toScreen(view, { x: 900, y: 450 })).toEqual({ x: 424, y: 324 });
expect(fitted({ x: 0, y: 0, width: 100, height: 100 }, { width: 1000, height: 1000 }).scale).toBe(1);
expect(fitted({ x: 0, y: 0, width: 100_000, height: 10 }, { width: 500, height: 500 }).scale).toBe(MIN_ZOOM);
});
it('shows an empty surface at 100 % with its origin at the margin', () => {
expect(fitted({ x: 0, y: 0, width: 0, height: 0 }, { width: 500, height: 500 })).toEqual({ scale: 1, x: 24, y: 24 });
expect(fitted({ x: 10, y: 10, width: 50, height: 50 }, { width: 0, height: 0 })).toEqual({ scale: 1, x: 14, y: 14 });
});
it('measures two fingers', () => {
expect(spread({ x: 0, y: 0 }, { x: 3, y: 4 })).toBe(5);
expect(middle({ x: 0, y: 10 }, { x: 4, y: 20 })).toEqual({ x: 2, y: 15 });
});
});