Files
mdg-filius/packages/editor/src/main.tsx
T
julian.kiedaischandClaude Opus 5.5 dfd5ec03ce feat(editor): F4.6a simulation mode and a computer's desktop
Java's mode buttons start and stop the room, now in a worker of the
editor's page (LocalSimHost), with Java's speed slider. While the
simulation runs the design is locked as in Java: no palette, no sheet,
nothing moves, no undo. Cables that carry a frame light up in Java's
green, and a tap on a computer opens its desktop: Java's title, the
software installation first, then the installed programs in the order
of Java's list for the language, a taskbar with the network icon and
the network as the simulation runs it. The room sends that over a new
topic, node:<id>. Desktops stay over a return to the design and show
again at the next start. The programs' windows follow from F4.6b.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:41:41 +00:00

114 lines
4.6 KiB
TypeScript

// The editor's page. Embedded in the dashboard (F6) it gets its document and
// language over the bridge; on its own — for development and the browser
// tests — it opens one of FILIUS' examples, an empty project or a file
// (`?example=<name>` opens that example at once, `?example=new` an empty
// project, `?lang=en` and `?theme=dark` set the rest).
import { StrictMode, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import * as Y from 'yjs';
import { desktopView, deviceSheet, emptyProject, topologyView, writeProject } from '@mdg-filius/doc';
import { openProjectArchive } from '@mdg-filius/fls';
import { startBridge } from './bridge/dashboardBridge.js';
import { Editor } from './Editor.js';
import { LanguageProvider, translate, type Language } from './i18n/i18n.js';
import './theme/tokens.css';
const EXAMPLES = import.meta.glob('../../../corpus/java-examples/*.fls', { query: '?url', import: 'default', eager: true }) as Record<string, string>;
/** `?example=new`: an empty project. */
const NEW_PROJECT = 'new';
const exampleUrl = (name: string) => Object.entries(EXAMPLES).find(([path]) => path.endsWith(`/${name}`))?.[1];
function docOf(bytes: Uint8Array): Y.Doc {
const doc = new Y.Doc();
writeProject(doc, openProjectArchive(bytes, () => crypto.randomUUID()));
return doc;
}
function App() {
const params = new URLSearchParams(window.location.search);
const [language, setLanguage] = useState<Language>(params.get('lang') === 'en' ? 'en' : 'de');
const [doc, setDoc] = useState<Y.Doc | null>(null);
const [example, setExample] = useState(params.get('example') ?? '');
useEffect(() => {
if (params.get('theme') === 'dark') document.documentElement.dataset.theme = 'dark';
const parent = params.get('parent');
if (parent === null) return undefined;
return startBridge(window, parent, {
onLocale: (locale) => setLanguage(locale.startsWith('en') ? 'en' : 'de'),
onTheme: (theme, tokens) => {
document.documentElement.dataset.theme = theme;
for (const [name, value] of Object.entries(tokens)) document.documentElement.style.setProperty(name, value);
},
});
// The page's parameters are read once.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (example === NEW_PROJECT) {
const doc = new Y.Doc();
writeProject(doc, emptyProject());
setDoc(doc);
return;
}
const url = exampleUrl(example);
if (url === undefined) return;
void fetch(url)
.then((response) => response.arrayBuffer())
.then((buffer) => setDoc(docOf(new Uint8Array(buffer))));
}, [example]);
// The development page lets its browser tests read the document's drawing (never the editor embedded in the dashboard).
useEffect(() => {
(window as unknown as { filiusDev?: unknown }).filiusDev = doc === null ? undefined : { view: () => topologyView(doc), sheet: (id: string) => deviceSheet(doc, id), desktop: (id: string) => desktopView(doc, id) };
}, [doc]);
const t = (key: string) => translate(language, key);
return (
<LanguageProvider language={language}>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<header style={{ display: 'flex', gap: 12, alignItems: 'center', padding: '8px 12px', borderBottom: '1px solid var(--border-default)', background: 'var(--surface-card)' }}>
<strong>{t('dev.title')}</strong>
<label>
{t('dev.example')}{' '}
<select value={example} onChange={(event) => setExample(event.target.value)}>
<option value="" />
<option value={NEW_PROJECT}>{t('dev.newProject')}</option>
{Object.keys(EXAMPLES)
.map((path) => path.slice(path.lastIndexOf('/') + 1))
.sort()
.map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
</label>
<label>
{t('dev.openFile')}{' '}
<input
type="file"
accept=".fls"
onChange={(event) => {
const file = event.target.files?.[0];
if (file !== undefined) void file.arrayBuffer().then((buffer) => setDoc(docOf(new Uint8Array(buffer))));
}}
/>
</label>
</header>
<main style={{ flex: 1, minHeight: 0 }}>
{doc === null ? null : <Editor key={doc.guid} doc={doc} />}
</main>
</div>
</LanguageProvider>
);
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);