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>
114 lines
4.6 KiB
TypeScript
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>,
|
|
);
|