docs: Writing Backend Routes guide, Editor Singleton reference, UMD + panel:label-change in editor panels guide
This commit is contained in:
parent
7b460a207d
commit
c44f14c733
|
|
@ -12,6 +12,7 @@ var NAV_DATA = {
|
||||||
children: [
|
children: [
|
||||||
{ title: 'Writing TypeScript Code', path: 'guides/writing-typescript-code/index.html' },
|
{ title: 'Writing TypeScript Code', path: 'guides/writing-typescript-code/index.html' },
|
||||||
{ title: 'Writing Editor Panels', path: 'guides/writing-editor-panels/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' },
|
{ title: 'Locales', path: 'guides/locales/index.html' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -23,6 +24,13 @@ var NAV_DATA = {
|
||||||
{ title: 'Update Outline', path: 'actions/update-outline/index.html' },
|
{ 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',
|
title: 'History',
|
||||||
path: 'history/index.html',
|
path: 'history/index.html',
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,30 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<div class="card">
|
||||||
<h3>Locales</h3>
|
<h3>Locales</h3>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
||||||
|
|
@ -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/<name></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/<name>.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 { field: string }</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 — 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>
|
||||||
|
|
@ -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><tab-container></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/<name>/<name>.ts ← TypeScript source
|
||||||
|
public/components/<name>/<name>.css ← CSS (maintained directly here, not compiled)</code></pre>
|
||||||
|
<p style="margin-top:0.75rem">
|
||||||
|
The TypeScript compiles to <code>public/components/<name>/<name>.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 = `
|
||||||
|
<div class="mp-toolbar">…</div>
|
||||||
|
<div class="mp-content">…</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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><link rel="stylesheet" href="/components/my-panel/my-panel.css">
|
||||||
|
<script type="module" src="/components/my-panel/my-panel.js"></script></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><script></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><script src="/vendor/some-lib.min.js"></script>
|
||||||
|
<script type="module" src="/components/my-panel/my-panel.js"></script></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
|
||||||
|
{
|
||||||
|
const name = path ? path.slice( path.lastIndexOf( '/' ) + 1 ) : '';
|
||||||
|
this.dispatchEvent( new CustomEvent( 'panel:label-change',
|
||||||
|
{
|
||||||
|
bubbles: true,
|
||||||
|
detail: { label: '📄 ' + name },
|
||||||
|
} ) );
|
||||||
|
}</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><tab-container></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 — 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>
|
||||||
|
|
@ -59,6 +59,16 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
To get the full picture, follow each page (as human or agent).
|
To get the full picture, follow each page (as human or agent).
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
<div class="page">
|
<div class="page">
|
||||||
|
|
||||||
<div style="width:100%;height:260px;overflow:hidden;background:#000;margin-bottom:2rem;border-radius:8px;">
|
<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;">
|
style="width:100%;height:100%;object-fit:cover;object-position:center;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -259,6 +259,21 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Frontend — Editor Singleton & 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">
|
<div class="card">
|
||||||
<h3>Frontend</h3>
|
<h3>Frontend</h3>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
||||||
|
|
@ -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 & 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 & 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<string, { content, dirty }></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<void></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<void></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 => ... )</code>.
|
||||||
|
Listeners are called synchronously when the event is dispatched.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3><code>onDocumentOpened</code> — <code>EventSlot<DocumentOpenedEvent></code></h3>
|
||||||
|
<pre><code>interface DocumentOpenedEvent {
|
||||||
|
path: string; // file path relative to project root
|
||||||
|
content: string; // raw file content
|
||||||
|
editorTag: string; // custom element tag resolved by FileEditorRegistry
|
||||||
|
}</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 ) =>
|
||||||
|
{
|
||||||
|
if ( 'my-panel' !== e.editorTag ) return;
|
||||||
|
this._loadDocument( e.path, e.content );
|
||||||
|
} );</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3><code>onDocumentDirty</code> — <code>EventSlot<DocumentPathEvent></code></h3>
|
||||||
|
<pre><code>interface DocumentPathEvent { path: string; }</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<DocumentPathEvent></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<void></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<DocumentPathEvent></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/<name>/<name>.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 — editor singleton & 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>
|
||||||
|
|
@ -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 & 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 — reference
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script>var NAV_ROOT = '../';</script>
|
||||||
|
<script src="../_assets_/nav-data.js"></script>
|
||||||
|
<script src="../_assets_/nav.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue