docs: Writing Backend Routes guide, Editor Singleton reference, UMD + panel:label-change in editor panels guide

This commit is contained in:
Rokojori 2026-07-11 14:24:17 +02:00
parent 7b460a207d
commit c44f14c733
8 changed files with 838 additions and 4 deletions

View File

@ -12,6 +12,7 @@ var NAV_DATA = {
children: [
{ title: 'Writing TypeScript Code', path: 'guides/writing-typescript-code/index.html' },
{ title: 'Writing Editor Panels', path: 'guides/writing-editor-panels/index.html' },
{ title: 'Writing Backend Routes', path: 'guides/writing-backend-routes/index.html' },
{ title: 'Locales', path: 'guides/locales/index.html' },
]
},
@ -23,6 +24,13 @@ var NAV_DATA = {
{ title: 'Update Outline', path: 'actions/update-outline/index.html' },
]
},
{
title: 'Reference',
path: 'reference/index.html',
children: [
{ title: 'Editor Singleton & Client/Server Split', path: 'reference/editor-singleton/index.html' },
]
},
{
title: 'History',
path: 'history/index.html',

View File

@ -29,6 +29,30 @@
</p>
</div>
<div class="card">
<h3>Writing Editor Panels</h3>
<p>
How to create a new panel for the editor: component files, toolbar
requirement, <code>connectedCallback</code> initialisation, duplicate
support, registration in the tab container menu, and responsive layout rules.
</p>
<p style="margin-top:0.75rem">
<a href="writing-editor-panels/index.html">Read the guide</a>
</p>
</div>
<div class="card">
<h3>Writing Backend Routes</h3>
<p>
How to add a new API endpoint to the Express server: router file layout,
auth middleware, reading request data, JSON file storage, streaming responses,
and mounting in <code>server/index.ts</code>.
</p>
<p style="margin-top:0.75rem">
<a href="writing-backend-routes/index.html">Read the guide</a>
</p>
</div>
<div class="card">
<h3>Locales</h3>
<p>

View File

@ -0,0 +1,190 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Writing Backend Routes — Roject</title>
<link rel="stylesheet" href="../../_assets_/styles.css">
<link rel="stylesheet" href="../../_assets_/nav.css">
</head>
<body>
<div class="page">
<header>
<h1>Writing Backend Routes</h1>
<p class="subtitle">How to add a new API endpoint to the Express server: file layout, auth, request handling, file storage, and mounting.</p>
</header>
<section>
<h2>Summary</h2>
<div class="card">
<p>
Every API feature lives in its own router file under <code>server/routes/</code>.
The router is created with Express's <code>Router()</code>, handlers are attached
to it, and it is exported and mounted in <code>server/index.ts</code> at an
<code>/api/&lt;name&gt;</code> path. The server runs via <code>ts-node</code>
— there is no separate compilation step for server-side TypeScript.
See <code>server/routes/layout.ts</code> (simple JSON CRUD) and
<code>server/routes/rojos.ts</code> (streaming) as reference implementations.
</p>
</div>
</section>
<section>
<h2>Steps</h2>
<div class="card">
<h3>1 — Create the route file</h3>
<p>Create <code>server/routes/&lt;name&gt;.ts</code>. All route files follow the same skeleton:</p>
<pre><code>import { Router } from 'express';
import { requireAuth } from '../middleware/auth';
const router = Router();
router.use( requireAuth );
router.get( '/', ( req, res ) =>
{
res.json( { ok: true } );
} );
export default router;</code></pre>
<p style="margin-top:0.75rem">
<code>requireAuth</code> is applied with <code>router.use</code> so it covers
every handler in the file. If only some routes need auth, apply it per-handler
as a middleware argument instead.
</p>
</div>
<div class="card">
<h3>2 — Access the session</h3>
<p>
The session is typed via a <code>declare module</code> in
<code>server/middleware/auth.ts</code>. The available fields are
<code>req.session.userId</code> and <code>req.session.username</code>, both
<code>string | undefined</code>. After <code>requireAuth</code> they are
guaranteed to be set — use the non-null assertion (<code>!</code>) freely:
</p>
<pre><code>const userId = req.session.userId!;
const username = req.session.username!;</code></pre>
</div>
<div class="card">
<h3>3 — Read request data</h3>
<p>
Both body parsers are applied globally in <code>server/index.ts</code>
— no per-route setup is needed:
</p>
<ul style="margin-top:0.5rem">
<li><strong>JSON body</strong> (<code>Content-Type: application/json</code>) — available as <code>req.body</code>, cast to your type: <code>req.body as &#123; field: string &#125;</code></li>
<li><strong>Plain text body</strong> (<code>Content-Type: text/plain</code>) — available as <code>req.body</code> (a string). Used for raw file content saves.</li>
<li><strong>Query params</strong><code>req.query.paramName as string</code></li>
<li><strong>Route params</strong><code>req.params.paramName</code></li>
</ul>
</div>
<div class="card">
<h3>4 — Store data as JSON files</h3>
<p>
There is no database. Persistent data goes in subdirectories of
<code>storage/</code>. Reference the path relative to the compiled output
location using <code>__dirname</code>:
</p>
<pre><code>import fs from 'fs';
import path from 'path';
const DATA_DIR = path.join( __dirname, '..', '..', 'storage', 'my-feature' );
function ensureDir(): void
{
if ( !fs.existsSync( DATA_DIR ) ) fs.mkdirSync( DATA_DIR, { recursive: true } );
}
function readData( id: string ): any
{
ensureDir();
const fp = path.join( DATA_DIR, id + '.json' );
if ( !fs.existsSync( fp ) ) return null;
return JSON.parse( fs.readFileSync( fp, 'utf8' ) );
}
function writeData( id: string, data: any ): void
{
ensureDir();
fs.writeFileSync( path.join( DATA_DIR, id + '.json' ), JSON.stringify( data ), 'utf8' );
}</code></pre>
<p style="margin-top:0.75rem">
IDs should be UUIDs from <code>crypto.randomUUID()</code>, never user-supplied
strings used directly as filenames. If you must derive a filename from user
input, sanitise it: <code>s.replace( /[^a-zA-Z0-9_-]/g, '_' )</code>.
</p>
</div>
<div class="card">
<h3>5 — Return responses</h3>
<p>Standard patterns used across the codebase:</p>
<pre><code>res.json( { ok: true } ); // success, no payload
res.json( data ); // success with payload
res.status( 400 ).json( { error: 'Bad input' } ); // client error
res.status( 401 ).json( { error: 'Not authenticated' } ); // handled by requireAuth
res.status( 404 ).json( { error: 'Not found' } );
res.status( 500 ).json( { error: String( err ) } );</code></pre>
<p style="margin-top:0.75rem">
Always <code>return</code> after sending a response inside a conditional block
to prevent Express from throwing "headers already sent":
</p>
<pre><code>if ( !id ) { res.status( 400 ).json( { error: 'Missing id' } ); return; }</code></pre>
</div>
<div class="card">
<h3>6 — Streaming responses</h3>
<p>
For SSE / streaming (e.g. AI output), set the headers explicitly, flush them,
write newline-delimited JSON chunks, then end the response:
</p>
<pre><code>res.setHeader( 'Content-Type', 'text/event-stream' );
res.setHeader( 'Cache-Control', 'no-cache' );
res.setHeader( 'Connection', 'keep-alive' );
res.flushHeaders();
for await ( const chunk of someStream )
{
res.write( JSON.stringify( { type: 'DATA', value: chunk } ) + '\n' );
}
res.write( JSON.stringify( { type: 'DONE' } ) + '\n' );
res.end();</code></pre>
<p style="margin-top:0.75rem">
Wrap the whole block in <code>try/catch</code> and check
<code>!res.headersSent</code> before sending an error response, since headers
may already be flushed by the time an exception occurs.
</p>
</div>
<div class="card">
<h3>7 — Mount in server/index.ts</h3>
<p>
Open <code>server/index.ts</code> and add two lines — an import and a
<code>app.use</code> call — following the existing pattern:
</p>
<pre><code>import myRouter from './routes/my-feature';
app.use( '/api/my-feature', myRouter );</code></pre>
<p style="margin-top:0.75rem">
Restart the server after this change. No build step is needed —
<code>ts-node</code> compiles on the fly.
</p>
</div>
</section>
<footer>
Roject &mdash; writing backend routes
</footer>
</div>
<script>var NAV_ROOT = '../../';</script>
<script src="../../_assets_/nav-data.js"></script>
<script src="../../_assets_/nav.js"></script>
</body>
</html>

View File

@ -0,0 +1,258 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Writing Editor Panels — Roject</title>
<link rel="stylesheet" href="../../_assets_/styles.css">
<link rel="stylesheet" href="../../_assets_/nav.css">
</head>
<body>
<div class="page">
<header>
<h1>Writing Editor Panels</h1>
<p class="subtitle">How to create a new panel that lives inside a tab container in the editor.</p>
</header>
<section>
<h2>Summary</h2>
<div class="card">
<p>
Every editor panel is a custom element registered with
<code>customElements.define</code>. It lives inside a
<code>&lt;tab-container&gt;</code>, gets its own tab, and can be opened,
closed, dragged to another container, and duplicated. A panel must have a
toolbar, must be self-initialising on <code>connectedCallback</code>, and
must be registered in <code>TabContainer.openMenu</code> so users can add it.
See <code>html-editor-panel</code> and <code>rojo-chat-panel</code> as
reference implementations.
</p>
</div>
</section>
<section>
<h2>Steps</h2>
<div class="card">
<h3>1 — Create the component files</h3>
<p>
Create two files following the naming convention:
</p>
<pre><code>src/components/&lt;name&gt;/&lt;name&gt;.ts ← TypeScript source
public/components/&lt;name&gt;/&lt;name&gt;.css ← CSS (maintained directly here, not compiled)</code></pre>
<p style="margin-top:0.75rem">
The TypeScript compiles to <code>public/components/&lt;name&gt;/&lt;name&gt;.js</code>
via <code>tsconfig.client.json</code> (<code>npm run build</code>).
</p>
</div>
<div class="card">
<h3>2 — Write the custom element</h3>
<p>
Extend <code>HTMLElement</code> and guard <code>connectedCallback</code>
with an <code>_initialized</code> flag so it only runs once. Set up the
panel's full HTML structure inside <code>connectedCallback</code>:
</p>
<pre><code>class MyPanel extends HTMLElement
{
_initialized = false;
connectedCallback(): void
{
if ( this._initialized ) return;
this._initialized = true;
this.className = 'my-panel';
this.innerHTML = `
&lt;div class="mp-toolbar"&gt;&lt;/div&gt;
&lt;div class="mp-content"&gt;&lt;/div&gt;
`;
}
}
customElements.define( 'my-panel', MyPanel );</code></pre>
<p style="margin-top:0.75rem">
All state that should be independent per instance (IDs, history, etc.)
must be initialised inside <code>connectedCallback</code>, not at class
level — because <strong>Duplicate</strong> creates a fresh element via
<code>document.createElement</code>, which triggers
<code>connectedCallback</code> again on the new instance.
</p>
</div>
<div class="card">
<h3>3 — Include a toolbar</h3>
<p>
Every panel must have a toolbar as its first child. The toolbar is a flex
row, fixed height, that does not shrink. Typical layout: icon or label on
the left, spacer (<code>flex: 1</code>), action buttons on the right.
Button style should match the other panels (see
<code>html-editor-panel.css</code> for the canonical colours and sizing).
</p>
<pre><code>/* in public/components/my-panel/my-panel.css */
my-panel {
display: flex;
flex-direction: column;
height: 100%;
background: #0f1117;
}
.mp-toolbar {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 8px;
background: #13151f;
border-bottom: 1px solid #2a2d3a;
flex-shrink: 0;
}
.mp-content {
flex: 1;
overflow: auto;
}</code></pre>
</div>
<div class="card">
<h3>4 — Implement <code>addContextMenuEntries</code></h3>
<p>
Import and implement the <code>EditorPanel</code> interface from
<code>tab-container.ts</code>. This allows the panel to append its own
entries to the tab container's context menu when it is the active tab.
At minimum, add a read-only label identifying the panel type or its
current state.
</p>
<pre><code>import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/context-menu.js';
// inside the class:
addContextMenuEntries( dir: ContextMenuDirectory ): void
{
dir.add( new ContextMenuReadOnlyEntry( dir, 'My Panel' ) );
}</code></pre>
</div>
<div class="card">
<h3>5 — Register in TabContainer.openMenu</h3>
<p>
Open <code>src/components/tab-container/tab-container.ts</code> and add
an entry to the <code>panelTypes</code> array inside <code>openMenu</code>:
</p>
<pre><code>{ label: 'My Panel', panelType: 'my-panel', tag: 'my-panel' },</code></pre>
<p style="margin-top:0.75rem">
<code>label</code> — the text shown in the Add submenu.<br>
<code>panelType</code> — internal identifier (used for dirty-state tracking and queries).<br>
<code>tag</code> — the custom element tag passed to <code>document.createElement</code>.
</p>
<p style="margin-top:0.75rem">
<strong>Duplicate</strong> works automatically once the panel is registered —
it calls the same factory, creating a new independent instance.
</p>
</div>
<div class="card">
<h3>6 — Wire into editor.html</h3>
<p>
Add the CSS link and module script to <code>public/editor.html</code>:
</p>
<pre><code>&lt;link rel="stylesheet" href="/components/my-panel/my-panel.css"&gt;
&lt;script type="module" src="/components/my-panel/my-panel.js"&gt;&lt;/script&gt;</code></pre>
<p style="margin-top:1rem"><strong>Using a UMD vendor library (e.g. CodeMirror, markdown-it)</strong></p>
<p>
When a panel depends on a third-party library that ships as a UMD bundle
(a single JS file that sets a global variable), follow this pattern:
</p>
<ol style="margin-top:0.5rem;line-height:1.9">
<li>Download the minified build and place it in <code>public/vendor/</code>.</li>
<li>Add a plain <code>&lt;script&gt;</code> tag in <code>editor.html</code>
<strong>before</strong> the module script. Order matters — the global must
exist before the module runs:
<pre style="margin-top:0.5rem"><code>&lt;script src="/vendor/some-lib.min.js"&gt;&lt;/script&gt;
&lt;script type="module" src="/components/my-panel/my-panel.js"&gt;&lt;/script&gt;</code></pre>
</li>
<li>In the TypeScript source file, declare the global at the top so the
compiler accepts it without a type package:
<pre style="margin-top:0.5rem"><code>declare const SomeLib: any;</code></pre>
</li>
<li>Use the global directly in your code. TypeScript will not complain, and
the browser will find it at runtime because the plain script ran first.
</li>
</ol>
<p style="margin-top:0.75rem">
Existing examples: <code>markdown-it</code> (used in <code>rojo-chat-panel</code>),
CodeMirror 5 and its language mode files (used in <code>code-panel</code>).
</p>
</div>
<div class="card">
<h3>7 — Update the tab label with <code>panel:label-change</code></h3>
<p>
When a panel loads a file it should update its own tab title to reflect the
open filename. Dispatch a bubbling <code>CustomEvent</code> named
<code>panel:label-change</code> with a <code>detail.label</code> string —
the <code>TabContainer</code> listens for it and updates the tab automatically:
</p>
<pre><code>_updateTabLabel( path: string ): void
&#123;
const name = path ? path.slice( path.lastIndexOf( '/' ) + 1 ) : '';
this.dispatchEvent( new CustomEvent( 'panel:label-change',
&#123;
bubbles: true,
detail: &#123; label: '📄 ' + name &#125;,
&#125; ) );
&#125;</code></pre>
<p style="margin-top:0.75rem">
Call this from your <code>_loadDocument</code> (or equivalent) method, after
setting <code>this.currentPath</code>. The event must bubble so it reaches
the ancestor <code>&lt;tab-container&gt;</code>.
</p>
<p style="margin-top:0.75rem">
The initial tab label (shown before any file is opened) is set in
<code>TabContainer.openMenu</code> via the <code>label</code> field of the
<code>panelTypes</code> entry — that is the only place the label is set
without this event.
</p>
</div>
</section>
<section>
<h2>Responsive Layout</h2>
<div class="decision">
<strong>Panels must work on desktop, tablet, and mobile in both orientations</strong>
<p>
The editor shell handles the outer panel arrangement and switching between
landscape and portrait modes. Inside the panel, use <code>height: 100%</code>
on the root element and a column flex layout so the panel always fills the
available space. Avoid fixed pixel heights for content zones — use
<code>flex: 1</code> for the scrollable area and <code>flex-shrink: 0</code>
for the toolbar and any fixed-height input areas.
</p>
</div>
<div class="decision">
<strong>Input areas at the bottom should not overlap the content</strong>
<p>
If the panel has an input area (like a chat box), place it as the last
child and give it <code>flex-shrink: 0</code>. The scrollable content
area above it takes <code>flex: 1</code>. On narrow screens the input
area will naturally occupy more vertical proportion — keep it compact
(avoid large padding or decorative margins) so content remains visible.
</p>
</div>
</section>
<footer>
Roject &mdash; writing editor panels
</footer>
</div>
<script>var NAV_ROOT = '../../';</script>
<script src="../../_assets_/nav-data.js"></script>
<script src="../../_assets_/nav.js"></script>
</body>
</html>

View File

@ -59,6 +59,16 @@
</p>
</div>
<div class="card">
<h3>Reference</h3>
<p>
Technical reference for core systems: the Editor singleton, events, and the client/server compilation split.
<br>
<a href="./reference/index.html">Read more about the reference</a>
</p>
</div>
<div>
To get the full picture, follow each page (as human or agent).

View File

@ -11,7 +11,7 @@
<div class="page">
<div style="width:100%;height:260px;overflow:hidden;background:#000;margin-bottom:2rem;border-radius:8px;">
<img src="../../src/rojos/rojects.svg" alt="Rojects"
<img src="../../src/rojos/roject.svg" alt="Rojects"
style="width:100%;height:100%;object-fit:cover;object-position:center;">
</div>
@ -259,6 +259,21 @@
</div>
</div>
<div class="card">
<h3>Frontend — Editor Singleton &amp; Client/Server Split</h3>
<p>
The <code>Editor</code> singleton (<code>src/editor/Editor.ts</code>) is the
central hub of the frontend editor — it owns all open document state, the
<code>FileEditorRegistry</code>, and the five events that panels and the tab
container subscribe to (<code>onDocumentOpened</code>,
<code>onDocumentDirty</code>, <code>onDocumentSaved</code>,
<code>onFilesChanged</code>, <code>onFileTypeUnknown</code>). For the full
event reference and the TypeScript compilation split between client and server,
see the
<a href="../reference/editor-singleton/index.html">Editor Singleton reference</a>.
</p>
</div>
<div class="card">
<h3>Frontend</h3>
<p>

View File

@ -0,0 +1,285 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Editor Singleton &amp; Client/Server Split — Roject</title>
<link rel="stylesheet" href="../../_assets_/styles.css">
<link rel="stylesheet" href="../../_assets_/nav.css">
</head>
<body>
<div class="page">
<header>
<h1>Editor Singleton &amp; Client/Server Split</h1>
<p class="subtitle">
The <code>Editor</code> class is the central hub of the frontend editor.
This page documents its events, methods, and properties, and explains the
TypeScript compilation split between client and server code.
</p>
</header>
<section>
<h2>The Editor Singleton</h2>
<div class="card">
<p>
<code>Editor</code> lives at <code>src/editor/Editor.ts</code> and compiles
to <code>public/editor/Editor.js</code>. It is a singleton accessed everywhere
via <code>Editor.get()</code>. It owns all open document state, the
<code>FileEditorRegistry</code>, and the events that panels and the tab
container subscribe to. It is the only place where files are fetched from
the server and where saves are sent back.
</p>
<p style="margin-top:0.75rem">
<strong>Never import <code>Editor</code> in server-side code.</strong> It is a
browser-only module. The split between client and server code is described in
the section below.
</p>
</div>
<div class="card">
<h3>Accessing the singleton</h3>
<pre><code>import { Editor } from '../../editor/Editor.js';
const editor = Editor.get();</code></pre>
<p style="margin-top:0.75rem">
The <code>.js</code> extension is required in all client-side imports because
the TypeScript output is consumed directly by the browser as ES modules
(no bundler).
</p>
</div>
<div class="card">
<h3>Properties</h3>
<table style="width:100%;border-collapse:collapse;font-size:0.85rem">
<thead>
<tr style="border-bottom:1px solid #2a2d3a;text-align:left">
<th style="padding:6px 8px">Property</th>
<th style="padding:6px 8px">Type</th>
<th style="padding:6px 8px">Description</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #1e2030">
<td style="padding:6px 8px"><code>projectId</code></td>
<td style="padding:6px 8px"><code>string</code></td>
<td style="padding:6px 8px">UUID of the open project. Set by <code>EditorShell</code> on init from the URL query string.</td>
</tr>
<tr style="border-bottom:1px solid #1e2030">
<td style="padding:6px 8px"><code>projectName</code></td>
<td style="padding:6px 8px"><code>string</code></td>
<td style="padding:6px 8px">Display name of the open project. Set by <code>EditorShell</code> on init.</td>
</tr>
<tr style="border-bottom:1px solid #1e2030">
<td style="padding:6px 8px"><code>openDocs</code></td>
<td style="padding:6px 8px"><code>Map&lt;string, &#123; content, dirty &#125;&gt;</code></td>
<td style="padding:6px 8px">In-memory cache of all documents fetched this session. Keyed by file path.</td>
</tr>
<tr style="border-bottom:1px solid #1e2030">
<td style="padding:6px 8px"><code>activeDoc</code></td>
<td style="padding:6px 8px"><code>string | null</code></td>
<td style="padding:6px 8px">File path of the most recently opened document.</td>
</tr>
<tr>
<td style="padding:6px 8px"><code>fileEditorRegistry</code></td>
<td style="padding:6px 8px"><code>FileEditorRegistry</code></td>
<td style="padding:6px 8px">Suffix-to-editor-tag resolver. Loaded lazily on first <code>openDocument</code> call.</td>
</tr>
</tbody>
</table>
</div>
<div class="card">
<h3>Methods</h3>
<p><strong><code>Editor.get(): Editor</code></strong></p>
<p>Returns the singleton instance, creating it on first call.</p>
<p style="margin-top:1rem"><strong><code>openDocument( filePath: string ): Promise&lt;void&gt;</code></strong></p>
<p>
The main entry point for opening a file. Loads the registry (once), resolves
the <code>editorTag</code> for the file's suffix. If no tag is found, dispatches
<code>onFileTypeUnknown</code> and returns early. Otherwise fetches the file
content (cached after first fetch), then dispatches <code>onDocumentOpened</code>.
</p>
<p style="margin-top:1rem"><strong><code>markDirty( filePath: string, content: string ): void</code></strong></p>
<p>
Called by a panel whenever the user edits content. Updates the in-memory cache
and dispatches <code>onDocumentDirty</code>. The tab container uses this to
show the dirty dot.
</p>
<p style="margin-top:1rem"><strong><code>save( filePath: string ): Promise&lt;void&gt;</code></strong></p>
<p>
PUTs the cached content to <code>/api/files/${projectId}/${filePath}</code>
with <code>Content-Type: text/plain</code>. Clears the dirty flag and
dispatches <code>onDocumentSaved</code>.
</p>
</div>
</section>
<section>
<h2>Events</h2>
<div class="card">
<p>
All events use <code>EventSlot</code> from the shared library —
not DOM events and not a pub/sub bus. Add a listener with
<code>Editor.get().onSomeEvent.addListener( e =&gt; ... )</code>.
Listeners are called synchronously when the event is dispatched.
</p>
</div>
<div class="card">
<h3><code>onDocumentOpened</code><code>EventSlot&lt;DocumentOpenedEvent&gt;</code></h3>
<pre><code>interface DocumentOpenedEvent &#123;
path: string; // file path relative to project root
content: string; // raw file content
editorTag: string; // custom element tag resolved by FileEditorRegistry
&#125;</code></pre>
<p style="margin-top:0.75rem">
Fired after the file is fetched and the editor tag is resolved. Every editor
panel listens to this. <strong>Panels must check <code>editorTag</code> and
return early if it does not match their own tag</strong> — all panels receive
every event.
</p>
<pre><code>Editor.get().onDocumentOpened.addListener( ( e ) =&gt;
&#123;
if ( 'my-panel' !== e.editorTag ) return;
this._loadDocument( e.path, e.content );
&#125; );</code></pre>
</div>
<div class="card">
<h3><code>onDocumentDirty</code><code>EventSlot&lt;DocumentPathEvent&gt;</code></h3>
<pre><code>interface DocumentPathEvent &#123; path: string; &#125;</code></pre>
<p style="margin-top:0.75rem">
Fired by <code>markDirty</code>. The <code>TabContainer</code> listens and
sets <code>tab.dirty = true</code> for the tab whose panel's
<code>currentPath</code> matches. Panels do not need to listen to this
themselves — they call <code>markDirty</code> and update their own Save button.
</p>
</div>
<div class="card">
<h3><code>onDocumentSaved</code><code>EventSlot&lt;DocumentPathEvent&gt;</code></h3>
<p style="margin-top:0.25rem">
Fired by <code>save</code> after a successful PUT. The <code>TabContainer</code>
listens and clears the dirty dot for the matching tab.
</p>
</div>
<div class="card">
<h3><code>onFilesChanged</code><code>EventSlot&lt;void&gt;</code></h3>
<p>
Fired with no payload when the file tree changes (file or folder created,
renamed, or deleted). <code>FileTreePanel</code> listens and re-fetches the
tree. Dispatch it after any operation that modifies the filesystem:
</p>
<pre><code>Editor.get().onFilesChanged.dispatch();</code></pre>
</div>
<div class="card">
<h3><code>onFileTypeUnknown</code><code>EventSlot&lt;DocumentPathEvent&gt;</code></h3>
<p>
Fired when <code>openDocument</code> is called for a file whose suffix has no
entry in <code>FileEditorRegistry</code>. <code>FileTreePanel</code> listens
and shows a 3-second error banner. No file content is fetched when this fires.
</p>
</div>
</section>
<section>
<h2>Client / Server Split</h2>
<div class="card">
<h3>Two separate TypeScript pipelines</h3>
<p>
Client and server TypeScript are compiled independently and must never import
from each other's side.
</p>
<table style="width:100%;border-collapse:collapse;font-size:0.85rem;margin-top:0.75rem">
<thead>
<tr style="border-bottom:1px solid #2a2d3a;text-align:left">
<th style="padding:6px 8px">Side</th>
<th style="padding:6px 8px">Source</th>
<th style="padding:6px 8px">Output</th>
<th style="padding:6px 8px">How it runs</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #1e2030">
<td style="padding:6px 8px">Client</td>
<td style="padding:6px 8px"><code>src/</code></td>
<td style="padding:6px 8px"><code>public/</code></td>
<td style="padding:6px 8px"><code>npm run build</code><code>tsc --build tsconfig.client.json</code></td>
</tr>
<tr style="border-bottom:1px solid #1e2030">
<td style="padding:6px 8px">Server</td>
<td style="padding:6px 8px"><code>server/</code></td>
<td style="padding:6px 8px">none (in-process)</td>
<td style="padding:6px 8px"><code>npm start</code><code>ts-node</code> with <code>tsconfig.ts-node.json</code></td>
</tr>
<tr>
<td style="padding:6px 8px">Library (browser)</td>
<td style="padding:6px 8px"><code>src/library-ts/browser/</code></td>
<td style="padding:6px 8px"><code>public/library-ts/browser/</code></td>
<td style="padding:6px 8px">Compiled via TypeScript project references as part of <code>npm run build</code></td>
</tr>
</tbody>
</table>
</div>
<div class="card">
<h3>CSS is not compiled</h3>
<p>
Component CSS is written directly in
<code>public/components/&lt;name&gt;/&lt;name&gt;.css</code> and is never
processed by TypeScript. Do not put CSS files in <code>src/</code>.
Edit the file in <code>public/</code> directly; the browser picks it up
on next reload with no build step.
</p>
</div>
<div class="card">
<h3>After changing code</h3>
<ul style="line-height:1.9">
<li><strong>Client TypeScript changed</strong> — run <code>npm run build</code>, then reload the browser.</li>
<li><strong>Server TypeScript changed</strong> — restart the server (<code>npm start</code>). No build step.</li>
<li><strong>CSS changed</strong> — reload the browser. No build step.</li>
</ul>
</div>
<div class="card">
<h3>Module imports on the client</h3>
<p>
Client TypeScript uses <code>module: ESNext</code> and
<code>moduleResolution: bundler</code>. There is no bundler — the browser
receives individual <code>.js</code> files as ES modules.
<strong>All imports in client code must include the <code>.js</code>
extension</strong>, even though the source files end in <code>.ts</code>:
</p>
<pre><code>import { Editor } from '../../editor/Editor.js';
import { EventSlot } from '../library-ts/browser/events/EventSlot.js';</code></pre>
<p style="margin-top:0.75rem">
TypeScript resolves these correctly during compilation because
<code>moduleResolution: bundler</code> allows importing <code>.js</code>
paths that correspond to <code>.ts</code> source files.
</p>
</div>
</section>
<footer>
Roject &mdash; editor singleton &amp; client/server split
</footer>
</div>
<script>var NAV_ROOT = '../../';</script>
<script src="../../_assets_/nav-data.js"></script>
<script src="../../_assets_/nav.js"></script>
</body>
</html>

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reference — Roject</title>
<link rel="stylesheet" href="../_assets_/styles.css">
<link rel="stylesheet" href="../_assets_/nav.css">
</head>
<body>
<div class="page">
<header>
<h1>Reference</h1>
<p class="subtitle">Technical reference for the core systems in Roject. Read the relevant page before touching the system it describes.</p>
</header>
<section>
<div class="card">
<h3>Editor Singleton &amp; Client/Server Split</h3>
<p>
The <code>Editor</code> singleton is the central hub of the frontend — it owns
all open document state, the file editor registry, and the events that panels
and containers listen to. This page also documents the TypeScript compilation
split between client and server code.
</p>
<p style="margin-top:0.75rem">
<a href="editor-singleton/index.html">Read the reference</a>
</p>
</div>
</section>
<footer>
Roject &mdash; reference
</footer>
</div>
<script>var NAV_ROOT = '../';</script>
<script src="../_assets_/nav-data.js"></script>
<script src="../_assets_/nav.js"></script>
</body>
</html>