diff --git a/workspace/_assets_/nav-data.js b/workspace/_assets_/nav-data.js index 96f53d3..7d82f7c 100644 --- a/workspace/_assets_/nav-data.js +++ b/workspace/_assets_/nav-data.js @@ -10,9 +10,10 @@ var NAV_DATA = { title: 'Guides', path: 'guides/index.html', children: [ - { title: 'Writing TypeScript Code', path: 'guides/writing-typescript-code/index.html' }, - { title: 'Writing Editor Panels', path: 'guides/writing-editor-panels/index.html' }, - { title: 'Locales', path: 'guides/locales/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 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', diff --git a/workspace/guides/index.html b/workspace/guides/index.html index 8d1931e..31267df 100644 --- a/workspace/guides/index.html +++ b/workspace/guides/index.html @@ -29,6 +29,30 @@

+
+

Writing Editor Panels

+

+ How to create a new panel for the editor: component files, toolbar + requirement, connectedCallback initialisation, duplicate + support, registration in the tab container menu, and responsive layout rules. +

+

+ Read the guide +

+
+ +
+

Writing Backend Routes

+

+ 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 server/index.ts. +

+

+ Read the guide +

+
+

Locales

diff --git a/workspace/guides/writing-backend-routes/index.html b/workspace/guides/writing-backend-routes/index.html new file mode 100644 index 0000000..f5a662a --- /dev/null +++ b/workspace/guides/writing-backend-routes/index.html @@ -0,0 +1,190 @@ + + + + + + Writing Backend Routes — Roject + + + + +

+ +
+

Writing Backend Routes

+

How to add a new API endpoint to the Express server: file layout, auth, request handling, file storage, and mounting.

+
+ +
+

Summary

+ +
+

+ Every API feature lives in its own router file under server/routes/. + The router is created with Express's Router(), handlers are attached + to it, and it is exported and mounted in server/index.ts at an + /api/<name> path. The server runs via ts-node + — there is no separate compilation step for server-side TypeScript. + See server/routes/layout.ts (simple JSON CRUD) and + server/routes/rojos.ts (streaming) as reference implementations. +

+
+
+ +
+

Steps

+ +
+

1 — Create the route file

+

Create server/routes/<name>.ts. All route files follow the same skeleton:

+
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;
+

+ requireAuth is applied with router.use so it covers + every handler in the file. If only some routes need auth, apply it per-handler + as a middleware argument instead. +

+
+ +
+

2 — Access the session

+

+ The session is typed via a declare module in + server/middleware/auth.ts. The available fields are + req.session.userId and req.session.username, both + string | undefined. After requireAuth they are + guaranteed to be set — use the non-null assertion (!) freely: +

+
const userId = req.session.userId!;
+const username = req.session.username!;
+
+ +
+

3 — Read request data

+

+ Both body parsers are applied globally in server/index.ts + — no per-route setup is needed: +

+
    +
  • JSON body (Content-Type: application/json) — available as req.body, cast to your type: req.body as { field: string }
  • +
  • Plain text body (Content-Type: text/plain) — available as req.body (a string). Used for raw file content saves.
  • +
  • Query paramsreq.query.paramName as string
  • +
  • Route paramsreq.params.paramName
  • +
+
+ +
+

4 — Store data as JSON files

+

+ There is no database. Persistent data goes in subdirectories of + storage/. Reference the path relative to the compiled output + location using __dirname: +

+
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' );
+}
+

+ IDs should be UUIDs from crypto.randomUUID(), never user-supplied + strings used directly as filenames. If you must derive a filename from user + input, sanitise it: s.replace( /[^a-zA-Z0-9_-]/g, '_' ). +

+
+ +
+

5 — Return responses

+

Standard patterns used across the codebase:

+
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 ) } );
+

+ Always return after sending a response inside a conditional block + to prevent Express from throwing "headers already sent": +

+
if ( !id ) { res.status( 400 ).json( { error: 'Missing id' } ); return; }
+
+ +
+

6 — Streaming responses

+

+ For SSE / streaming (e.g. AI output), set the headers explicitly, flush them, + write newline-delimited JSON chunks, then end the response: +

+
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();
+

+ Wrap the whole block in try/catch and check + !res.headersSent before sending an error response, since headers + may already be flushed by the time an exception occurs. +

+
+ +
+

7 — Mount in server/index.ts

+

+ Open server/index.ts and add two lines — an import and a + app.use call — following the existing pattern: +

+
import myRouter from './routes/my-feature';
+
+app.use( '/api/my-feature', myRouter );
+

+ Restart the server after this change. No build step is needed — + ts-node compiles on the fly. +

+
+ +
+ +
+ Roject — writing backend routes +
+ +
+ + + + + diff --git a/workspace/guides/writing-editor-panels/index.html b/workspace/guides/writing-editor-panels/index.html new file mode 100644 index 0000000..927753e --- /dev/null +++ b/workspace/guides/writing-editor-panels/index.html @@ -0,0 +1,258 @@ + + + + + + Writing Editor Panels — Roject + + + + +
+ +
+

Writing Editor Panels

+

How to create a new panel that lives inside a tab container in the editor.

+
+ +
+

Summary

+ +
+

+ Every editor panel is a custom element registered with + customElements.define. It lives inside a + <tab-container>, 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 connectedCallback, and + must be registered in TabContainer.openMenu so users can add it. + See html-editor-panel and rojo-chat-panel as + reference implementations. +

+
+
+ +
+

Steps

+ +
+

1 — Create the component files

+

+ Create two files following the naming convention: +

+
src/components/<name>/<name>.ts       ← TypeScript source
+public/components/<name>/<name>.css   ← CSS (maintained directly here, not compiled)
+

+ The TypeScript compiles to public/components/<name>/<name>.js + via tsconfig.client.json (npm run build). +

+
+ +
+

2 — Write the custom element

+

+ Extend HTMLElement and guard connectedCallback + with an _initialized flag so it only runs once. Set up the + panel's full HTML structure inside connectedCallback: +

+
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 );
+

+ All state that should be independent per instance (IDs, history, etc.) + must be initialised inside connectedCallback, not at class + level — because Duplicate creates a fresh element via + document.createElement, which triggers + connectedCallback again on the new instance. +

+
+ +
+

3 — Include a toolbar

+

+ 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 (flex: 1), action buttons on the right. + Button style should match the other panels (see + html-editor-panel.css for the canonical colours and sizing). +

+
/* 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;
+}
+
+ +
+

4 — Implement addContextMenuEntries

+

+ Import and implement the EditorPanel interface from + tab-container.ts. 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. +

+
import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/context-menu.js';
+
+// inside the class:
+addContextMenuEntries( dir: ContextMenuDirectory ): void
+{
+  dir.add( new ContextMenuReadOnlyEntry( dir, 'My Panel' ) );
+}
+
+ +
+

5 — Register in TabContainer.openMenu

+

+ Open src/components/tab-container/tab-container.ts and add + an entry to the panelTypes array inside openMenu: +

+
{ label: 'My Panel', panelType: 'my-panel', tag: 'my-panel' },
+

+ label — the text shown in the Add submenu.
+ panelType — internal identifier (used for dirty-state tracking and queries).
+ tag — the custom element tag passed to document.createElement. +

+

+ Duplicate works automatically once the panel is registered — + it calls the same factory, creating a new independent instance. +

+
+ +
+

6 — Wire into editor.html

+

+ Add the CSS link and module script to public/editor.html: +

+
<link rel="stylesheet" href="/components/my-panel/my-panel.css">
+<script type="module" src="/components/my-panel/my-panel.js"></script>
+ +

Using a UMD vendor library (e.g. CodeMirror, markdown-it)

+

+ 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: +

+
    +
  1. Download the minified build and place it in public/vendor/.
  2. +
  3. Add a plain <script> tag in editor.html + before the module script. Order matters — the global must + exist before the module runs: +
    <script src="/vendor/some-lib.min.js"></script>
    +<script type="module" src="/components/my-panel/my-panel.js"></script>
    +
  4. +
  5. In the TypeScript source file, declare the global at the top so the + compiler accepts it without a type package: +
    declare const SomeLib: any;
    +
  6. +
  7. 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. +
  8. +
+

+ Existing examples: markdown-it (used in rojo-chat-panel), + CodeMirror 5 and its language mode files (used in code-panel). +

+
+ +
+

7 — Update the tab label with panel:label-change

+

+ When a panel loads a file it should update its own tab title to reflect the + open filename. Dispatch a bubbling CustomEvent named + panel:label-change with a detail.label string — + the TabContainer listens for it and updates the tab automatically: +

+
_updateTabLabel( path: string ): void
+{
+  const name = path ? path.slice( path.lastIndexOf( '/' ) + 1 ) : '';
+  this.dispatchEvent( new CustomEvent( 'panel:label-change',
+  {
+    bubbles: true,
+    detail: { label: '📄 ' + name },
+  } ) );
+}
+

+ Call this from your _loadDocument (or equivalent) method, after + setting this.currentPath. The event must bubble so it reaches + the ancestor <tab-container>. +

+

+ The initial tab label (shown before any file is opened) is set in + TabContainer.openMenu via the label field of the + panelTypes entry — that is the only place the label is set + without this event. +

+
+ +
+ +
+

Responsive Layout

+ +
+ Panels must work on desktop, tablet, and mobile in both orientations +

+ The editor shell handles the outer panel arrangement and switching between + landscape and portrait modes. Inside the panel, use height: 100% + 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 + flex: 1 for the scrollable area and flex-shrink: 0 + for the toolbar and any fixed-height input areas. +

+
+ +
+ Input areas at the bottom should not overlap the content +

+ If the panel has an input area (like a chat box), place it as the last + child and give it flex-shrink: 0. The scrollable content + area above it takes flex: 1. 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. +

+
+ +
+ +
+ Roject — writing editor panels +
+ +
+ + + + + diff --git a/workspace/index.html b/workspace/index.html index f1a49b4..f84eb02 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -59,6 +59,16 @@

+
+

Reference

+

+ Technical reference for core systems: the Editor singleton, events, and the client/server compilation split. +
+ + Read more about the reference +

+
+
To get the full picture, follow each page (as human or agent). diff --git a/workspace/outline/index.html b/workspace/outline/index.html index 83f93e9..55b264e 100644 --- a/workspace/outline/index.html +++ b/workspace/outline/index.html @@ -11,7 +11,7 @@
- Rojects
@@ -259,6 +259,21 @@
+
+

Frontend — Editor Singleton & Client/Server Split

+

+ The Editor singleton (src/editor/Editor.ts) is the + central hub of the frontend editor — it owns all open document state, the + FileEditorRegistry, and the five events that panels and the tab + container subscribe to (onDocumentOpened, + onDocumentDirty, onDocumentSaved, + onFilesChanged, onFileTypeUnknown). For the full + event reference and the TypeScript compilation split between client and server, + see the + Editor Singleton reference. +

+
+

Frontend

diff --git a/workspace/reference/editor-singleton/index.html b/workspace/reference/editor-singleton/index.html new file mode 100644 index 0000000..6b200ee --- /dev/null +++ b/workspace/reference/editor-singleton/index.html @@ -0,0 +1,285 @@ + + + + + + Editor Singleton & Client/Server Split — Roject + + + + +

+ +
+

Editor Singleton & Client/Server Split

+

+ The Editor 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. +

+
+ +
+

The Editor Singleton

+ +
+

+ Editor lives at src/editor/Editor.ts and compiles + to public/editor/Editor.js. It is a singleton accessed everywhere + via Editor.get(). It owns all open document state, the + FileEditorRegistry, 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. +

+

+ Never import Editor in server-side code. It is a + browser-only module. The split between client and server code is described in + the section below. +

+
+ +
+

Accessing the singleton

+
import { Editor } from '../../editor/Editor.js';
+
+const editor = Editor.get();
+

+ The .js extension is required in all client-side imports because + the TypeScript output is consumed directly by the browser as ES modules + (no bundler). +

+
+ +
+

Properties

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyTypeDescription
projectIdstringUUID of the open project. Set by EditorShell on init from the URL query string.
projectNamestringDisplay name of the open project. Set by EditorShell on init.
openDocsMap<string, { content, dirty }>In-memory cache of all documents fetched this session. Keyed by file path.
activeDocstring | nullFile path of the most recently opened document.
fileEditorRegistryFileEditorRegistrySuffix-to-editor-tag resolver. Loaded lazily on first openDocument call.
+
+ +
+

Methods

+ +

Editor.get(): Editor

+

Returns the singleton instance, creating it on first call.

+ +

openDocument( filePath: string ): Promise<void>

+

+ The main entry point for opening a file. Loads the registry (once), resolves + the editorTag for the file's suffix. If no tag is found, dispatches + onFileTypeUnknown and returns early. Otherwise fetches the file + content (cached after first fetch), then dispatches onDocumentOpened. +

+ +

markDirty( filePath: string, content: string ): void

+

+ Called by a panel whenever the user edits content. Updates the in-memory cache + and dispatches onDocumentDirty. The tab container uses this to + show the dirty dot. +

+ +

save( filePath: string ): Promise<void>

+

+ PUTs the cached content to /api/files/${projectId}/${filePath} + with Content-Type: text/plain. Clears the dirty flag and + dispatches onDocumentSaved. +

+
+
+ +
+

Events

+ +
+

+ All events use EventSlot from the shared library — + not DOM events and not a pub/sub bus. Add a listener with + Editor.get().onSomeEvent.addListener( e => ... ). + Listeners are called synchronously when the event is dispatched. +

+
+ +
+

onDocumentOpenedEventSlot<DocumentOpenedEvent>

+
interface DocumentOpenedEvent {
+  path: string;      // file path relative to project root
+  content: string;   // raw file content
+  editorTag: string; // custom element tag resolved by FileEditorRegistry
+}
+

+ Fired after the file is fetched and the editor tag is resolved. Every editor + panel listens to this. Panels must check editorTag and + return early if it does not match their own tag — all panels receive + every event. +

+
Editor.get().onDocumentOpened.addListener( ( e ) =>
+{
+  if ( 'my-panel' !== e.editorTag ) return;
+  this._loadDocument( e.path, e.content );
+} );
+
+ +
+

onDocumentDirtyEventSlot<DocumentPathEvent>

+
interface DocumentPathEvent { path: string; }
+

+ Fired by markDirty. The TabContainer listens and + sets tab.dirty = true for the tab whose panel's + currentPath matches. Panels do not need to listen to this + themselves — they call markDirty and update their own Save button. +

+
+ +
+

onDocumentSavedEventSlot<DocumentPathEvent>

+

+ Fired by save after a successful PUT. The TabContainer + listens and clears the dirty dot for the matching tab. +

+
+ +
+

onFilesChangedEventSlot<void>

+

+ Fired with no payload when the file tree changes (file or folder created, + renamed, or deleted). FileTreePanel listens and re-fetches the + tree. Dispatch it after any operation that modifies the filesystem: +

+
Editor.get().onFilesChanged.dispatch();
+
+ +
+

onFileTypeUnknownEventSlot<DocumentPathEvent>

+

+ Fired when openDocument is called for a file whose suffix has no + entry in FileEditorRegistry. FileTreePanel listens + and shows a 3-second error banner. No file content is fetched when this fires. +

+
+ +
+ +
+

Client / Server Split

+ +
+

Two separate TypeScript pipelines

+

+ Client and server TypeScript are compiled independently and must never import + from each other's side. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SideSourceOutputHow it runs
Clientsrc/public/npm run buildtsc --build tsconfig.client.json
Serverserver/none (in-process)npm startts-node with tsconfig.ts-node.json
Library (browser)src/library-ts/browser/public/library-ts/browser/Compiled via TypeScript project references as part of npm run build
+
+ +
+

CSS is not compiled

+

+ Component CSS is written directly in + public/components/<name>/<name>.css and is never + processed by TypeScript. Do not put CSS files in src/. + Edit the file in public/ directly; the browser picks it up + on next reload with no build step. +

+
+ +
+

After changing code

+
    +
  • Client TypeScript changed — run npm run build, then reload the browser.
  • +
  • Server TypeScript changed — restart the server (npm start). No build step.
  • +
  • CSS changed — reload the browser. No build step.
  • +
+
+ +
+

Module imports on the client

+

+ Client TypeScript uses module: ESNext and + moduleResolution: bundler. There is no bundler — the browser + receives individual .js files as ES modules. + All imports in client code must include the .js + extension, even though the source files end in .ts: +

+
import { Editor } from '../../editor/Editor.js';
+import { EventSlot } from '../library-ts/browser/events/EventSlot.js';
+

+ TypeScript resolves these correctly during compilation because + moduleResolution: bundler allows importing .js + paths that correspond to .ts source files. +

+
+ +
+ +
+ Roject — editor singleton & client/server split +
+ +
+ + + + + diff --git a/workspace/reference/index.html b/workspace/reference/index.html new file mode 100644 index 0000000..c3b21bc --- /dev/null +++ b/workspace/reference/index.html @@ -0,0 +1,44 @@ + + + + + + Reference — Roject + + + + +
+ +
+

Reference

+

Technical reference for the core systems in Roject. Read the relevant page before touching the system it describes.

+
+ +
+ +
+

Editor Singleton & Client/Server Split

+

+ The Editor 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. +

+

+ Read the reference +

+
+ +
+ +
+ Roject — reference +
+ +
+ + + + +