Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/lld/06-frontend.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ Since T20 the page script is split into feature files under `src/js/src/page/`,

What the parts do, together:

- **Layout.** Split panes (split.js), Maximize (`ToggleFunction`, Esc restores), Stacked or Side by side (`ToggleRotateEditor`), Hide/Show editor (`ToggleEditor` + `syncEditorLayoutUI`), and the phone layout (`applyMobileLayout`).
- **Layout.** Split panes (split.js), Maximize (`ToggleFunction`, Esc restores; an icon button, the first one in the app bar, before Files), Genie pinned beside the IDE (below), Stacked or Side by side (`ToggleRotateEditor`), Hide/Show editor (`ToggleEditor` + `syncEditorLayoutUI`), and the phone layout (`applyMobileLayout`).
- **Genie pinned beside the IDE** (`chat-widget/src/index.ts` "pinned beside the IDE", `02-layout.js`, `#genie-dock` in `index.html`). A pin button in the panel's header (between Peer chat and ×) docks the panel as a column at the right of the workspace row, after the editor/terminal split, like a side bar. The panel element is moved into `#genie-dock` (class `is-docked`, which drops the floating position, size and shadow); a grip on the dock's left edge resizes it (drag, arrow keys, double click for the default 380px; 300px to 680px and at most 60% of the row; `--genie-dock-w`, remembered in `localStorage` `genie-dock-width`). The pin itself is `localStorage` `genie-pinned`. While pinned, the widget fires `genie-pin` ({pinned}) on `window` and `02-layout.js` stacks the editor over the terminal (`SetEditorDirection`; a hidden editor stays hidden); unpinning puts the layout back unless the user chose one meanwhile. × hides a pinned panel and keeps the pin; Ask Genie and Ctrl+K bring it back docked. The dock is inside `#ide-shell`, so Maximize keeps it. Below 1100px or on a phone the pin button is hidden and the panel floats as before (the choice is kept, and the panel docks again when the window is wide enough).
- **Editor.** Ace setup, theme persistence, language mode from `data-editor`, `updateEditorContent`, download and upload. `defineOpenreplAceTheme()` registers the default "OpenREPL Dark" Ace theme (`ace/theme/openrepl_dark`, the mockup's colours); every other Ace theme stays in `#select-theme`. The terminal uses the same 14px size as the editor (set in `js/src/xterm.ts`).
- **Language switch.** `actionOnchange` fetches `/demo?q=` and renders the demo animation, usage table and links (LLD 04).
- **Run and debug.** `CompileandRun`, `RunandDebug` dispatch `optionrun` and `optiondebug`. `ToggleReconnect` reconnects.
Expand Down
43 changes: 40 additions & 3 deletions src/js/src/page/02-layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ window.SaveSelectedNodeToFile = SaveSelectedNodeToFile;
window.ToggleEditor = ToggleEditor;
window.syncEditorLayoutUI = syncEditorLayoutUI;
window.ToggleRotateEditor = ToggleRotateEditor;
window.SetEditorDirection = SetEditorDirection;
window.ToggleReconnect = ToggleReconnect;

function ToggleFunction() {
Expand All @@ -21,9 +22,12 @@ function ToggleFunction() {
shell.classList.toggle("is-maximized", on);
document.body.classList.toggle("ide-maximized", on);
var btn = get("#togglescreen-button");
if (btn) btn.setAttribute("aria-pressed", on ? "true" : "false");
var label = get("#togglescreen-label");
if (label) label.textContent = on ? "Restore" : "Maximize";
if (btn) {
// an icon button: the pressed state swaps the icon (CSS), the words are for the tooltip and screen readers
btn.setAttribute("aria-pressed", on ? "true" : "false");
btn.setAttribute("aria-label", on ? "Restore the workspace" : "Maximize the workspace");
btn.title = on ? "Restore the workspace (Esc)" : "Maximize the workspace (Esc to restore)";
}
window.dispatchEvent(new Event('resize'));
}

Expand Down Expand Up @@ -230,6 +234,39 @@ function ToggleRotateEditor() {
ToggleEditor();
}

// Puts the editor and the terminal in a direction ('horizontal' side by side,
// 'vertical' stacked) without toggling what is not asked for: a hidden editor
// stays hidden (ToggleRotateEditor would show it).
function SetEditorDirection(dir) {
if (direction === null || direction === dir) return false;
if (einst === null) {
direction = dir;
syncEditorLayoutUI();
} else {
ToggleRotateEditor();
}
return true;
}

// Genie pinned beside the IDE (chat-widget/src/index.ts fires "genie-pin" when
// the pin turns on or off). The IDE is stacked while it is pinned, so that the
// panel has room, and goes back to what it was when Genie is unpinned, unless
// the user chose a layout meanwhile.
var pinLayout = { before: null, set: null };
window.addEventListener("genie-pin", function (e) {
var pinned = !!(e.detail && e.detail.pinned);
if (pinned) {
if (pinLayout.set === null && direction !== null) {
pinLayout.before = direction;
if (SetEditorDirection("vertical")) pinLayout.set = "vertical";
else pinLayout.set = direction; // it was stacked already: nothing to give back
}
} else if (pinLayout.set !== null) {
if (direction === pinLayout.set && pinLayout.before && pinLayout.before !== direction) SetEditorDirection(pinLayout.before);
pinLayout = { before: null, set: null };
}
});

function ToggleReconnect() {
// The execution node is away and the countdown is running (11-terminal-state.js).
if (window.awayWaitMs && window.awayWaitMs() > 0) return;
Expand Down
99 changes: 99 additions & 0 deletions src/js/test/dock-layout.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// The page's side of pinning Genie beside the IDE (src/page/02-layout.js):
// pinned, the editor goes over the terminal to make room; unpinned, the layout
// goes back to what it was, unless the user chose one in between. The widget
// (chat-widget/src/index.ts) only fires "genie-pin"; this runs the page script
// with a small pretend page and the real layout functions.
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import vm from "node:vm";
import path from "node:path";
import { fileURLToPath } from "node:url";

const here = path.dirname(fileURLToPath(import.meta.url));
const source = fs.readFileSync(path.join(here, "..", "src", "page", "02-layout.js"), "utf8").replace(/^export \{\};.*$/m, "");

function page(startDirection = "horizontal", editorShown = true) {
const listeners = {};
const classList = { toggle() {}, add() {}, remove() {}, contains: () => false };
const el = { classList, textContent: "", hidden: false, style: {}, setAttribute() {}, removeAttribute() {} };
const window = {
addEventListener: (type, fn) => (listeners[type] = fn),
dispatchEvent: () => true,
fire: (pinned) => listeners["genie-pin"]({ detail: { pinned } }),
};
const ctx = {
window,
document: { body: { classList }, documentElement: { style: { setProperty() {} } }, getElementById: () => el },
get: () => el,
ismob: () => false,
Split: () => ({ destroy() {} }),
getComputedStyle: () => ({ getPropertyValue: () => "0" }),
setTimeout: () => 0,
Event: class {},
console,
$: () => ({}),
};
// the script keeps its state in page globals: window.direction, window.einst
ctx.direction = startDirection;
ctx.einst = editorShown ? { destroy() {} } : null;
vm.createContext(ctx);
vm.runInContext(source, ctx);
// the script's own top-level `window.einst = null` is the page's first state: put ours back
ctx.direction = startDirection;
ctx.einst = editorShown ? { destroy() {} } : null;
return { ctx, fire: window.fire };
}

test("pinned, the IDE is stacked; unpinned, it goes back", () => {
const p = page("horizontal");
p.fire(true);
assert.equal(p.ctx.direction, "vertical");
p.fire(false);
assert.equal(p.ctx.direction, "horizontal");
});

test("a layout the user chose while pinned is kept when it is unpinned", () => {
const p = page("horizontal");
p.fire(true);
p.ctx.ToggleRotateEditor(); // the user goes back to side by side
assert.equal(p.ctx.direction, "horizontal");
p.fire(false);
assert.equal(p.ctx.direction, "horizontal");
});

test("an IDE that was stacked already stays stacked after the pin", () => {
const p = page("vertical");
p.fire(true);
assert.equal(p.ctx.direction, "vertical");
p.fire(false);
assert.equal(p.ctx.direction, "vertical");
});

test("a hidden editor stays hidden: pinning only turns the direction", () => {
const p = page("horizontal", false);
p.fire(true);
assert.equal(p.ctx.direction, "vertical");
assert.equal(p.ctx.einst, null, "the editor was shown by the pin");
p.fire(false);
assert.equal(p.ctx.direction, "horizontal");
assert.equal(p.ctx.einst, null);
});

test("a pin before the page has chosen a layout does nothing, and later ones work", () => {
const p = page(null);
p.fire(true);
assert.equal(p.ctx.direction, null);
p.fire(false);
p.ctx.direction = "horizontal";
p.fire(true);
assert.equal(p.ctx.direction, "vertical");
});

test("a second pin event does not forget what the layout was", () => {
const p = page("horizontal");
p.fire(true);
p.fire(true);
p.fire(false);
assert.equal(p.ctx.direction, "horizontal");
});
2 changes: 1 addition & 1 deletion src/resources/about.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ <h2 class="section__title">What you can do</h2>
<li>Share a live link so others can watch and type along, or make a code link to a copy of your code.</li>
<li>Fork a terminal to run a server and a client side by side.</li>
<li>Keep files in your workspace, and sign in to keep them between visits.</li>
<li>Ask Genie, the AI helper, about your code and your terminal output, review its suggestions as a diff, let it carry out a task in Agent mode when you are signed in (it can also work in your terminal, open terminal tabs and manage your files), or right-click selected code to explain, fix, comment or test it. Genie keeps working when you close its panel, and the Ask Genie button tells you when it is done or needs you.</li>
<li>Ask Genie, the AI helper, about your code and your terminal output, review its suggestions as a diff, let it carry out a task in Agent mode when you are signed in (it can also work in your terminal, open terminal tabs and manage your files), or right-click selected code to explain, fix, comment or test it. Genie keeps working when you close its panel, and the Ask Genie button tells you when it is done or needs you. Pin it to dock it beside the editor.</li>
<li>Practice with generated interview questions, with a coach for hints, a solution review and complexity.</li>
</ul>
<hr />
Expand Down
Loading
Loading