feat(editor): a modem offers a code or joins one, and says under it how it stands (K4, K5)

The modem's sheet chooses how it finds its partner: the other modem on its
port in this project, as FILIUS' file pairs them; another project's, by
offering this modem's code (shown large, with a copy button) or by joining
a code typed in as one reads it off the board. In the action mode each
modem says under it how it stands — connected, waiting, taken, no second
modem on the port, cable missing, on the server only.

Accepted in three browsers on a real F, each in a copy of FILIUS' modem
example: Ben's notebook pings the computer behind Mia's access point; Rita
joins the same code, reads that it is taken, and gets it once Ben stops.
FILIUS' modem example runs in the page's own room as well.

The .fls export writes an offering modem as Java's server and a joining
one as its client; the code has no place there and is named as lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 18:34:54 +00:00
co-authored by Claude Opus 5.5
parent 7b943d3d96
commit 87c09c6c9d
12 changed files with 355 additions and 34 deletions
+23 -1
View File
@@ -107,9 +107,22 @@ export interface CanvasProps {
readonly renderDocText?: (item: DocumentationView, frame: ScreenFrame) => React.ReactNode;
/** The cables that carry a frame now, drawn as Java draws them then (JCablePanel.farbeBlinken). */
readonly activeLinks?: ReadonlySet<string>;
/** A line under a device's label, by node (how a modem stands while the simulation runs). */
readonly notes?: ReadonlyMap<string, DeviceNote>;
}
/** A line under a device's label: its text, and whether it is good news, a wait or trouble. */
export interface DeviceNote {
readonly text: string;
readonly tone: 'good' | 'waiting' | 'trouble';
}
const NOTE_COLORS: Readonly<Record<DeviceNote['tone'], string>> = { good: 'var(--accent)', waiting: 'var(--text-secondary)', trouble: 'var(--danger)' };
/** How far the note sits below the label's baseline. */
const NOTE_LINE = 14;
const NO_LINKS: ReadonlySet<string> = new Set();
const NO_NOTES: ReadonlyMap<string, DeviceNote> = new Map();
const NO_CHOICES: ReadonlyMap<string, readonly string[]> = new Map();
const NO_COLORS: readonly string[] = [];
@@ -195,6 +208,7 @@ function Device({
selected,
pending,
chosenBy,
note,
}: {
readonly node: NodeView;
readonly box: DeviceBox;
@@ -203,6 +217,7 @@ function Device({
readonly pending: boolean;
/** The colours of the others who have chosen it, each a frame further out. */
readonly chosenBy: readonly string[];
readonly note: DeviceNote | undefined;
}) {
return (
<g
@@ -211,7 +226,7 @@ function Device({
data-box={`${box.x} ${box.y} ${box.width} ${box.height}`}
data-selected={selected ? 'true' : undefined}
role="img"
aria-label={`${kindName}: ${node.label}`}
aria-label={note === undefined ? `${kindName}: ${node.label}` : `${kindName}: ${node.label} – ${note.text}`}
style={{ cursor: 'pointer' }}
>
{/* The whole button takes the pointer, as Java's label does. */}
@@ -249,6 +264,11 @@ function Device({
<text x={box.label.x} y={box.label.y} textAnchor="middle" style={{ font: JAVA_LABEL_FONT, fill: 'var(--canvas-label)' }}>
{node.label}
</text>
{note === undefined ? null : (
<text data-note={note.tone} x={box.label.x} y={box.label.y + NOTE_LINE} textAnchor="middle" style={{ font: JAVA_LABEL_FONT, fill: NOTE_COLORS[note.tone] }}>
{note.text}
</text>
)}
</g>
);
}
@@ -288,6 +308,7 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
locked = false,
chosenBy = NO_CHOICES,
activeLinks = NO_LINKS,
notes = NO_NOTES,
onDocFrame,
renderDocText,
},
@@ -741,6 +762,7 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
selected={selection?.kind === 'device' && selection.id === node.id}
chosenBy={chosenBy.get(node.id) ?? NO_COLORS}
pending={cableFrom?.id === node.id}
note={notes.get(node.id)}
/>
))}
</g>