feat(editor): the simulation opens sheets, not windows in the window
A tap on a device in the simulation now only chooses it; its three-dot button (or a long press) opens what Java's click opened: the desktop, and newly a switch's SAT table and a home router's NAT table, beside each card's data exchange. The desktop, the tables and the data exchange are no windows in the window any more but the simulation's sheet (SimulationSheet), after the dashboard's AppSheet: below the toolbar to the bottom, a head with icon, title, who else has it open, see-through and close. One at a time — it covers the surface. It stays over a return to the design with the program open on a desktop; a sheet whose device was removed closes. See-through fades the body (0.55) and keeps the head; the choice is kept in the browser. Java opens these as movable windows, several at once, with a click; named in CHANGES.md. Tests open through the menu (showFromMenu, showDesktop in editorPage.ts). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+11
-44
@@ -1,5 +1,5 @@
|
||||
// A computer's desktop in the simulation, after Java's GUIDesktopWindow and
|
||||
// GUIDesktopPanel: a window titled with the computer's label and address
|
||||
// GUIDesktopPanel: a sheet titled with the computer's label and address
|
||||
// (the label alone where it is the address), Java's background, the
|
||||
// software installation's icon first and then the installed programs in the
|
||||
// order of Java's list, a grey taskbar with "home screen" in the middle and,
|
||||
@@ -8,8 +8,8 @@
|
||||
// has a "change" button under read-only fields, which writes back what they
|
||||
// show; it is left out.
|
||||
//
|
||||
// On a wide screen the window lies over the surface and moves by its title
|
||||
// bar, as Java's windows do; on a phone it fills the surface. A program's
|
||||
// Java's window is the simulation's sheet here (SimulationSheet), not a
|
||||
// window in the window. A program's
|
||||
// icon opens its window over the desktop (ProgramFrame), the home button
|
||||
// leaves it; so far the command line (F4.6b), the web browser and the web
|
||||
// server (F4.6d), the e-mail program and server (F4.6e), the text editor
|
||||
@@ -19,7 +19,7 @@
|
||||
// text editor's from its open file, the image viewer's from its picture, as
|
||||
// Java's do.
|
||||
|
||||
import { useCallback, useEffect, useState, type PointerEvent as ReactPointerEvent } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type * as Y from 'yjs';
|
||||
import { desktopView } from '@mdg-filius/doc';
|
||||
import { browserTitle } from '@mdg-filius/protocol';
|
||||
@@ -46,31 +46,22 @@ import { GnutellaPanel } from './GnutellaPanel.js';
|
||||
import { ImageViewerPanel, useImageViewer } from './ImageViewerPanel.js';
|
||||
import { WebServerPanel } from './WebServerPanel.js';
|
||||
import './desktop.css';
|
||||
import { WindowWatchers } from '../remote/PresenceMarks.js';
|
||||
import type { PresenceUser } from '../remote/presence.js';
|
||||
import { SimulationSheet, type SheetFrame } from '../simulation/SimulationSheet.js';
|
||||
|
||||
export interface DesktopWindowProps {
|
||||
/** Who else in the project has this window open (plan §7.4). */
|
||||
readonly watchers?: readonly PresenceUser[] | undefined;
|
||||
export interface DesktopSheetProps {
|
||||
/** The sheet's place, its see-through and close, who else has it open. */
|
||||
readonly frame: SheetFrame;
|
||||
readonly doc: Y.Doc;
|
||||
readonly id: string;
|
||||
/** The room: the computer's network as it runs, its programs' windows. */
|
||||
readonly simulation: Simulation;
|
||||
/** Whether its cable carried a frame at the last tick. */
|
||||
readonly active: boolean;
|
||||
/** The window's top left corner on the surface. */
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
/** Its place in the stack of desktops. */
|
||||
readonly z: number;
|
||||
/** The window open on it — a program's or the installation's —, kept by the editor over a return to the design (Java's desktop keeps it). */
|
||||
readonly program: DesktopWindowName | null;
|
||||
readonly onProgram: (program: DesktopWindowName | null) => void;
|
||||
/** The person's origin, for what the installation changes in the document. */
|
||||
readonly origin: object;
|
||||
readonly onMove: (x: number, y: number) => void;
|
||||
readonly onFront: () => void;
|
||||
readonly onClose: () => void;
|
||||
}
|
||||
|
||||
/** What can be open on a desktop: a program's window or the software installation. */
|
||||
@@ -91,7 +82,7 @@ function useDesktopView(doc: Y.Doc, id: string) {
|
||||
return view;
|
||||
}
|
||||
|
||||
export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: open, onProgram: setOpen, origin, onMove, onFront, onClose, watchers }: DesktopWindowProps) {
|
||||
export function DesktopSheet({ frame, doc, id, simulation, active, program: open, onProgram: setOpen, origin }: DesktopSheetProps) {
|
||||
const t = useTranslate();
|
||||
const language = useLanguage();
|
||||
const view = useDesktopView(doc, id);
|
||||
@@ -102,7 +93,6 @@ export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: o
|
||||
const settings = simulation.nodes.get(id);
|
||||
const [info, setInfo] = useState(false);
|
||||
const home = useCallback(() => setOpen(null), [setOpen]);
|
||||
const [drag, setDrag] = useState<{ readonly pointer: number; readonly dx: number; readonly dy: number; moved: boolean } | null>(null);
|
||||
if (view === undefined) return null;
|
||||
const programs = desktopPrograms(view.programs, language);
|
||||
// A program removed while its window was open: the desktop shows.
|
||||
@@ -110,21 +100,6 @@ export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: o
|
||||
const address = settings?.ipAddress ?? '';
|
||||
const title = view.addressAsName ? view.label : `${view.label} - ${address}`;
|
||||
|
||||
// The title bar moves the window; the pointer is captured once it moves (pointer-capture trap).
|
||||
const onPointerDown = (event: ReactPointerEvent<HTMLElement>) => {
|
||||
if ((event.target as Element).closest('button') !== null) return;
|
||||
setDrag({ pointer: event.pointerId, dx: event.clientX - x, dy: event.clientY - y, moved: false });
|
||||
};
|
||||
const onPointerMove = (event: ReactPointerEvent<HTMLElement>) => {
|
||||
if (drag === null || drag.pointer !== event.pointerId) return;
|
||||
if (!drag.moved) {
|
||||
drag.moved = true;
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
}
|
||||
onMove(Math.max(0, event.clientX - drag.dx), Math.max(0, event.clientY - drag.dy));
|
||||
};
|
||||
const onPointerUp = () => setDrag(null);
|
||||
|
||||
const rows: readonly [string, string][] = [
|
||||
[t('network.ipAddress'), address],
|
||||
[t('network.subnetMask'), settings?.subnetMask ?? ''],
|
||||
@@ -134,15 +109,7 @@ export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: o
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="desktop-window" role="dialog" aria-label={title} data-desktop={id} style={{ left: x, top: y, zIndex: 5 + z }} onPointerDownCapture={onFront}>
|
||||
<header className="desktop-titlebar" onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={onPointerUp} onPointerCancel={onPointerUp}>
|
||||
<img src={ICON_URLS[view.kind === 'notebook' ? 'laptop' : 'server']} alt="" />
|
||||
<h2>{title}</h2>
|
||||
<WindowWatchers watchers={watchers} />
|
||||
<button type="button" className="desktop-close" aria-label={t('sheet.close')} title={t('sheet.close')} onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
<SimulationSheet frame={frame} title={title} icon={ICON_URLS[view.kind === 'notebook' ? 'laptop' : 'server']} attributes={{ 'data-desktop': id }}>
|
||||
<div className="desktop-screen" style={{ backgroundImage: `url(${background})` }}>
|
||||
<ul className="desktop-icons">
|
||||
<li>
|
||||
@@ -230,6 +197,6 @@ export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: o
|
||||
<img src={active ? networkOn : networkOff} alt="" />
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
</SimulationSheet>
|
||||
);
|
||||
}
|
||||
@@ -1,69 +1,92 @@
|
||||
/* A computer's desktop after Java's GUIDesktopPanel: 640 × 460 pixels below
|
||||
* the title bar, a grey taskbar at the bottom (180, 180, 180; 44 pixels high
|
||||
* for a finger, Java's has 30), icons of 120 × 96 with white names on Java's
|
||||
* background. On a phone it fills the surface. Java's colours stay in both
|
||||
* themes: the screen is the simulated computer's, not the editor's. */
|
||||
.desktop-window {
|
||||
/* The simulation's sheet (SimulationSheet), after the dashboard's AppSheet:
|
||||
* it comes out of the toolbar's lower edge and covers the surface to the
|
||||
* bottom — a head (icon, title, who else has it open, see-through, ×) and the
|
||||
* one body. Not a window in the window: it neither moves nor stacks. A
|
||||
* computer's desktop inside keeps Java's colours in both themes (the screen is
|
||||
* the simulated computer's, not the editor's) and a grey taskbar (180, 180,
|
||||
* 180; 44 pixels high for a finger, Java's has 30). */
|
||||
.simulation-sheet {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 640px;
|
||||
max-width: 100%;
|
||||
height: calc(460px + 14px + 36px);
|
||||
max-height: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
border-top: 1px solid var(--border-default);
|
||||
background: var(--surface-card);
|
||||
box-shadow: 0 8px 24px color-mix(in srgb, var(--shadow-color) 25%, transparent);
|
||||
color: var(--text-primary);
|
||||
box-shadow: 0 -4px 16px color-mix(in srgb, var(--shadow-color) 18%, transparent);
|
||||
animation: simulation-sheet-in 140ms ease-out;
|
||||
}
|
||||
|
||||
/* Below the mode buttons, which stay in reach. */
|
||||
@media (max-width: 639px) {
|
||||
.desktop-window {
|
||||
inset: 72px 0 0 0 !important;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border-radius: 0;
|
||||
/* „Durchsichtig“: the network shows through the body, the sheet still takes
|
||||
* the pointer; the head stays as it is, so that its title and buttons read. */
|
||||
.simulation-sheet[data-see-through='true'] {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.simulation-sheet[data-see-through='true'] .simulation-sheet__body {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
@keyframes simulation-sheet-in {
|
||||
from {
|
||||
transform: translateY(-1rem);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-titlebar {
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.simulation-sheet {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.simulation-sheet__body {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.simulation-sheet__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 36px;
|
||||
padding: 0 0 0 8px;
|
||||
background: var(--surface-raised);
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
cursor: move;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
flex: none;
|
||||
min-width: 0;
|
||||
min-height: 44px;
|
||||
padding: 0 4px 0 12px;
|
||||
border-bottom: 1px solid var(--border-subtle, var(--border-default));
|
||||
background: var(--surface-card);
|
||||
}
|
||||
|
||||
.desktop-titlebar img {
|
||||
.simulation-sheet__head img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.desktop-titlebar h2 {
|
||||
.simulation-sheet__head h2 {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.desktop-close {
|
||||
.simulation-sheet__see-through,
|
||||
.simulation-sheet__close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: none;
|
||||
min-width: 44px;
|
||||
min-height: 36px;
|
||||
min-height: 44px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font: inherit;
|
||||
@@ -71,6 +94,20 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.simulation-sheet__see-through[aria-pressed='true'] {
|
||||
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.simulation-sheet__body {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: opacity 140ms ease-out;
|
||||
}
|
||||
|
||||
.desktop-screen {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
@@ -1252,13 +1289,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* A switch's or home router's viewer (SatViewer, NatViewer): a table and its
|
||||
* reset, in a window as small as Java's dialog allows the NAT table's six columns. */
|
||||
.viewer-window {
|
||||
width: 600px;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
/* A switch's or home router's table (SatViewer, NatViewer) and its reset. */
|
||||
.viewer-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -1342,15 +1373,9 @@
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* The data exchange window (AggregatedExchangeDialog): Java's is three
|
||||
* quarters of the screen wide and four fifths high; the tabs, a bar for what
|
||||
/* The data exchange (AggregatedExchangeDialog): the tabs, a bar for what
|
||||
* Java's right mouse button offers, the table (a row per frame in its layer's
|
||||
* colour, Java's fonts by column) and the chosen frame's details below. */
|
||||
.capture-window {
|
||||
width: 960px;
|
||||
height: 640px;
|
||||
}
|
||||
|
||||
.capture-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user