Replace Inter/Baskerville with self-hosted Manrope/Alegreya/JetBrains Mono variable fonts. Migrate all colors from hex to OKLCH tokens (30+ CSS custom properties) with full dark/light mode support. Replace Unicode emoji with lucide-react SVG icons throughout. Convert all page layouts to inline styles (Tailwind CSS 4 flex/grid classes unreliable in this project). Code-split routes via React.lazy (760KB → 15KB initial shell + 10 lazy chunks). Add global styles: scrollbar theming, selection color, input/button bases, :focus-visible ring, prefers-reduced-motion. Setup wizard gets 4-phase indicator with numbered circles, PathInput and StatusDot components. Toast container gets aria-live="polite". Tab close buttons changed to proper <button> elements with aria-labels. All 8 pages (Dashboard, Editor, Build, Server, Toolset, Repos, Settings, Setup) rewritten with consistent card/section/button patterns.
23 KiB
Layonara Forge — Agent Handoff Document
What Is This
Layonara Forge is a purpose-built NWN (Neverwinter Nights) Development IDE that runs as a local web application in Docker. It lets contributors fork, clone, edit, build, and run a complete Layonara NWNX NWN server with zero native tooling — only Docker required. The project name was chosen during brainstorming: "Forge" evokes crafting/building in a fantasy context.
Project Location
- Forge codebase:
/home/jmg/dev/layonara/layonara-forge/ - Design spec:
/home/jmg/dev/docs/superpowers/specs/2026-04-20-layonara-forge-design.md - Implementation plans:
/home/jmg/dev/docs/superpowers/plans/2026-04-20-forge-plan-{1..6}-*.md - Gitea integration plan: See Cursor plan file
gitea_+_forge_integration_8e0df077.plan.md - Design context:
.impeccable.mdat project root (personality: arcane, precise, deep)
Architecture
Host Machine
├── Browser → localhost:3000 (Forge UI)
├── NWN Toolset → writes GFFs to modules/temp0/
├── ~/layonara-workspace/ (repos, server data, config)
└── Docker Socket
Docker Network
├── layonara-forge (Node.js + React, serves UI, manages everything via Docker socket)
├── layonara-builder (ephemeral: nasher, nwn_script_comp, layonara_nwn, cmake, gcc)
├── layonara-nwserver (ghcr.io/plenarius/unified — NWN:EE + NWNX)
└── layonara-mariadb (mariadb:10.11 — game database)
The Forge container manages sibling containers via the Docker socket (Portainer pattern). Contributors clone one repo, set two paths in .env, run docker compose up, and open a browser.
Tech Stack
- Backend: Node.js 20 + Express 5 + TypeScript (ES modules)
- Frontend: React 19 + Vite 6 + Tailwind CSS 4 (utility classes unreliable — inline styles used for layout)
- Icons: lucide-react (SVG icons throughout)
- Fonts: Self-hosted variable fonts via @fontsource-variable (Manrope, Alegreya, JetBrains Mono)
- Code editor: Monaco Editor with NWScript Monarch tokenizer
- NWScript LSP: Forked
layonara/nwscript-ee-language-serverconnected via WebSocket JSON-RPC - Terminal: xterm.js with child_process.spawn shell sessions
- Docker API: dockerode
- Git: simple-git (named import:
import { simpleGit } from "simple-git") - Git provider: Gitea at
https://gitea.layonara.com(NOT GitHub — see Gitea section) - Real-time: WebSocket via ws library
What's Built (55 commits + UI overhaul session)
All 6 implementation plans are complete. The codebase compiles and both Docker images build successfully. A complete UI/UX overhaul was performed (see "UI/UX Overhaul" section below).
Backend Services (packages/backend/src/services/)
| Service | File | Purpose |
|---|---|---|
| WebSocket | ws.service.ts |
Event broadcasting to all connected clients |
| Workspace | workspace.service.ts |
Directory structure management, forge.json config |
| Docker | docker.service.ts |
Container CRUD, image pulls, ephemeral container runs |
| Build | build.service.ts |
Module compile/pack, hot-reload, hak builds, NWNX builds |
| Server | server.service.ts |
NWN server stack lifecycle (start/stop/restart MariaDB + nwserver) |
| Toolset | toolset.service.ts |
temp0/ file watcher, GFF→JSON conversion, change management |
| Editor | editor.service.ts |
File CRUD, directory trees, workspace search |
| Git | git.service.ts |
Clone, pull, commit, push, diff, upstream polling |
| Git Provider | git-provider.service.ts |
Gitea API (fork, PR, token validation) |
| Terminal | terminal.service.ts |
Shell session management via child_process |
| LSP | lsp.service.ts |
NWScript language server process management |
Backend Routes (packages/backend/src/routes/)
| Route | Prefix | Endpoints |
|---|---|---|
| workspace | /api/workspace |
GET /config, PUT /config, POST /init |
| docker | /api/docker |
containers, pull, start/stop/restart, logs |
| build | /api/build |
module/compile, module/pack, deploy, compile-single, haks, nwnx |
| server | /api/server |
status, start, stop, restart, generate-config, seed-db, sql |
| toolset | /api/toolset |
status, start, stop, changes, apply, apply-all, discard |
| editor | /api/editor |
tree, file CRUD, search, resref, tlk, 2da, gff-schema |
| github | /api/github |
validate-pat (actually Gitea token), fork, forks, pr, prs |
| repos | /api/repos |
clone, list (/), status (/:repo/status), pull, commit, push, diff |
| terminal | /api/terminal |
sessions CRUD |
Frontend Pages (packages/frontend/src/pages/)
| Page | Route | Purpose |
|---|---|---|
| Dashboard | / |
Server status, repo summary, quick actions (3-column cards) |
| Editor | /editor |
Monaco editor with file explorer, tabs, GFF visual editors |
| Build | /build |
Module/hak/NWNX build sections with streaming output |
| Server | /server |
Controls, log viewer with filter, SQL console |
| Toolset | /toolset |
temp0/ watcher status, change table, diff viewer |
| Repos | /repos |
Git status cards, commit dialog, PR creation |
| Settings | /settings |
PAT, theme, editable paths, Docker images, shortcuts, reset |
| Setup | /setup |
4-phase onboarding wizard with 10 steps |
Special Features
- NWScript syntax highlighting: Monarch tokenizer with keyword/type/comment/string/preprocessor rules
- SQL highlighting in NWScript strings: Detects
NWNX_SQL_PrepareQuery()calls, highlights SQL keywords in teal - Resref auto-lookup: Backend indexes all GFF JSON files, hover on resref strings shows the item/creature/area
- TLK preview: Hover on integer literals shows the TLK string (handles 16777216 custom offset)
- 2DA intellisense: Parses 2da files, provides completion for
Get2DAStringcalls - Visual GFF editors: Form-based editors for .uti, .utc, .are, .dlg, .utp, .utm JSON files
- Conventional commit enforcement: Type dropdown (feat/fix/refactor/etc), rejects malformed messages
- Dark/light theme: OKLCH CSS custom properties toggled via
lightclass on root element
UI/UX Overhaul (April 21, 2026 session)
A complete design overhaul was performed using the Impeccable design skill system. The design context is documented in .impeccable.md.
Design System
Personality: Arcane, precise, deep — a craftsman's workbench.
Fonts (all self-hosted via @fontsource-variable, no Google Fonts):
- Body/UI: Manrope Variable — warm geometric sans
- Headings: Alegreya Variable — calligraphic serif with manuscript roots
- Code/mono: JetBrains Mono Variable
Color palette (full OKLCH, 30+ tokens in globals.css):
- Surfaces tinted toward amber (hue 65) — "warm darks, not cold ones"
- 3-level depth:
--forge-bg→--forge-surface→--forge-surface-raised - Accent: evolved gold
oklch(58% 0.155 65)with hover and subtle variants - Semantic colors: success (forest green, hue 150), danger (brick red, hue 25), warning (golden, hue 80), info (steel blue, hue 230)
- Each semantic color has base, bg, and border variants for both dark and light modes
- Log panels: dedicated
--forge-log-bg/--forge-log-texttokens
Icons: lucide-react SVG icons throughout (Code2, Wrench, Hammer, Play, GitBranch, Settings, Sun/Moon, Terminal, etc.)
Type scale (fixed rem for IDE density):
--text-xs(11px) through--text-2xl(28px), ~1.25 ratio
What Changed
Foundation:
- Replaced Inter font with Manrope Variable, Baskerville with Alegreya Variable
- Removed Google Fonts
<link>— all fonts bundled as npm deps - Full OKLCH palette replacing all hex values (~60 hard-coded colors replaced)
- All Tailwind semantic color classes (
green-400,red-500/20, etc.) replaced with forge tokens - Global CSS: scrollbar theming, selection color, input/button base styles,
:focus-visiblering,prefers-reduced-motion
IDE Shell (IDELayout.tsx):
- Lucide SVG icons replacing Unicode emoji in nav rail
- Removed 3px left border stripe (impeccable anti-pattern ban)
- Sidebar only shows on
/editorroute (was showing on all pages) - All layout uses inline styles (Tailwind flex classes were not reliably applying)
- Terminal toggle bar with Terminal/Chevron icons
All 8 pages rewritten with consistent patterns:
- Card containers:
--forge-surfacebg,--forge-border,0.75remradius - Section headers: uppercase,
--text-xs, icon + label - Buttons: accent primary, outline secondary, danger for destructive
- Status badges: semantic colors with dots
- All inline styles (Tailwind utility classes unreliable for layout in this project)
Setup wizard:
- 4-phase indicator (Environment → Authentication → Repositories → Finalize) with numbered circles + connecting lines, matching James's work app wizard pattern
- Steps reordered: Workspace + NWN Home before Gitea Token
- PathInput component with folder icon for path fields
- StatusDot component replacing emoji (✅❌⏳) with styled HTML elements
- Navigation: ghost "← Back" left, accent "Next →" right, border-top separator
Performance:
- Routes code-split via
React.lazy()— 10 chunks instead of 1 (760KB → initial 15KB app shell) - Page chunks: Editor 98KB, Setup 16KB, Repos 13KB, others 5-8KB each
Accessibility:
:focus-visibleoutline on all interactive elementsaria-label="Main navigation"on nav- Tab close button changed from
<span>to<button aria-label="Close tab"> - Toast container has
aria-live="polite"+role="status" window.confirm()guards on destructive actions (Discard All, Reset Setup)- SetupGuard shows "Loading Forge…" instead of blank screen
Important: Tailwind CSS 4 Quirk
Tailwind CSS 4 utility classes for layout (flex, flex-1, items-center, etc.) do NOT reliably apply in this project. All critical layout uses inline styles instead. This is a conscious decision, not laziness. The Tailwind @import "tailwindcss" is still loaded and works for some utilities (rounded, overflow-hidden, etc.) but do not rely on Tailwind classes for flex/grid layout. Use inline style={{}} props.
Docker Images
layonara-forge (563MB)
- Base:
node:20-slim - Multi-stage build: builder stage compiles TS + Vite, production stage has only runtime deps
- Serves React frontend as static files from Express
- The Dockerfile is at repo root:
Dockerfile
layonara-builder (577MB)
- Base:
ubuntu:24.04 - All tools installed from pre-built GitHub Release binaries (no Nim compilation)
- The Dockerfile is at
builder/Dockerfile - Tools: nwn_gff, nwn_script_comp, nasher, layonara_nwn, cmake, gcc, git
Critical: The builder Dockerfile downloads pre-built binaries from:
layonara/neverwinter.nimreleases (nwn_gff, nwn_script_comp, etc.)squattingmonk/nasher.nimreleases (nasher)plenarius/layonara_nwnreleases (layonara_nwn)
If any of these release URLs break, the builder image won't build.
Gitea Infrastructure
GitHub is no longer the primary git provider for contributors. Gitea is self-hosted on xandrial.
Setup
- Gitea URL:
https://gitea.layonara.com - Host: xandrial (159.69.30.129, Hetzner CPX41)
- Managed by: Coolify on leanthar, service UUID
xo2yy8rml79lkzmf92cgeory - Database: PostgreSQL 16 sidecar in same Coolify service
- Auth: Authentik OIDC SSO (same login as Nextcloud and email)
- SSH: Port 2222 for git-over-SSH
- Admin account:
orth
Repos on Gitea
| Repo | Branch | Push Mirror → GitHub |
|---|---|---|
layonara/nwn-module |
ee |
Yes, sync on commit |
layonara/nwn-haks |
64bit |
Yes, sync on commit |
NOT on Gitea
plenarius/unified(NWNX) stays on GitHub — read-only, no contributions through Forge
Branch Protection
eeon nwn-module: onlyorthcan push directly64biton nwn-haks: onlyorthcan push directly- Contributors must fork within the layonara org and PR
Push Mirrors
Each Gitea repo has a push mirror configured to sync to the corresponding GitHub repo. This triggers existing GitHub CI/CD and Discord bot webhooks. Commit attribution is preserved (it's in the git objects).
How the Forge Connects
GIT_PROVIDER_URLenv var (default:https://gitea.layonara.com)- Backend uses
git-provider.service.tswhich calls Gitea API at$GIT_PROVIDER_URL/api/v1 - Clone URLs:
https://<token>@gitea.layonara.com/<user>/<repo>.git - The
unifiedrepo clones from GitHub directly (no token needed, public repo)
Forked Dependencies
layonara/neverwinter.nim (fork of niv/neverwinter.nim)
- Purpose: Adds
-n(no entry point) and-E(all errors) flags tonwn_script_comp - Cherry-picked: PRs #152 and #153 from cgtudor's branches
- Release workflow:
.github/workflows/release.yml— builds on tag push, creates GitHub Release with pre-built linux tarball - Current release:
v2.1.2-layonara - Upstream status: Waiting for niv to merge the PRs. When merged, this fork can be retired.
- Remote setup:
origin= layonara fork,upstream= niv/neverwinter.nim (push disabled:no_push)
layonara/nwscript-ee-language-server (fork of PhilippeChab/nwscript-ee-language-server)
- Purpose: Migrates diagnostics from
nwnsctonwn_script_comp - Merged: PR #77 (cgtudor's tdn-hack branch) with conflict resolution
- Status: Waiting for upstream compiler PRs to merge, then PhilippeChab can merge PR #77, then this fork can be retired.
- Included in Forge: As a git submodule at
lsp/nwscript-language-server/ - Remote setup:
origin= layonara fork,upstream= PhilippeChab (push disabled:no_push)
plenarius/layonara_nwn
- Not a fork: James's own repo
- Release workflow added:
.github/workflows/release.yml— builds on tag push - Current release:
v0.1.1
Known Issues & Incomplete Work
TypeScript
- All backend TS errors are resolved (Express 5
*pathreturnsstring[], simple-git uses named import) - Frontend uses
"moduleResolution": "bundler"and"noImplicitAny": falsein tsconfig to avoid strict-mode issues from subagent-generated code - Frontend build script is
"build": "vite build"(skipstsc -bwhich fights with bundler resolution)
Not Yet Tested End-to-End
- Full setup wizard flow against live Gitea (the UI renders but hasn't been walked through with real forking/cloning)
- Module build → server start → connect with NWN client
- Toolset temp0/ watcher with real GFF files
- Hot-reload pipeline with a real nasher build
- Push mirrors actually triggering GitHub CI
- LSP with real
.nssfiles (the bridge architecture is built but URI mapping may need tuning)
Remaining UI Work
- GFF visual editors (ItemEditor, CreatureEditor, AreaEditor, DialogEditor) still use Tailwind classes — may need inline style conversion
- CommitDialog component styling could be improved
- FileExplorer sidebar styling uses older patterns (Tailwind classes for layout)
- EditorTabs uses older patterns
- SearchPanel uses older patterns
- ErrorBoundary component not styled
- Light mode needs visual verification — all tokens have light variants but the overall look hasn't been tested
- The vendor bundle is still 598KB (Monaco dominates) — could be split further with
manualChunks
Database
db/schema.sqlcontains the full schema from James's local dev DB (nwn_dev) + seed data for cnr_misc and pwdata- DM row insertion happens during setup wizard (contributor provides CD key)
- Architecture is ready for richer seed data (production dump) but not implemented yet
Infrastructure
- Gitea on xandrial needs monitoring/backup strategy (no backup service configured yet)
- The Gitea PostgreSQL database should be backed up regularly
Key Conventions
- NEVER close GitHub issues without explicit permission — state changes fire Discord webhooks to the player community
- NEVER push to
nwnxee/unified— James's fork isplenarius/unified - No pushing layonara-forge to any remote until James says so — everything is local
- Conventional commits:
feat:,fix:,refactor:,docs:,chore: - NWN resref limit: 16 characters max for all filenames
- Express 5: Uses
*pathfor catch-all/wildcard routes, NOT bare* - simple-git: Use named import
import { simpleGit } from "simple-git", NOT default import - Inline styles for layout: Do NOT use Tailwind classes for flex/grid layout — they don't reliably apply. Use inline
style={{}}props. - CSS variables for all colors: Never use hex values. Use
var(--forge-*)tokens fromglobals.css. - lucide-react for icons: Never use Unicode emoji for UI icons. Import from
lucide-react.
Environment Variables
Forge Container
| Var | Default | Purpose |
|---|---|---|
WORKSPACE_PATH |
/workspace |
Where repos, server data, config live |
NWN_HOME_PATH |
/nwn-home |
NWN documents directory (for Toolset temp0/) |
GIT_PROVIDER_URL |
https://gitea.layonara.com |
Gitea instance URL |
PORT |
3000 |
HTTP server port |
Coolify
| Var | Location | Purpose |
|---|---|---|
COOLIFY_API_TOKEN |
~/.env.coolify |
API access to Coolify on leanthar |
COOLIFY_URL |
~/.env.coolify |
https://leanthar.layonara.com |
Gitea
| Item | Value |
|---|---|
| API token | eb79a92cea7dad657a0c81ddd2290a1be95057e2 (orth's token, name: forge-setup) |
| Gitea service UUID in Coolify | xo2yy8rml79lkzmf92cgeory |
File Structure
layonara-forge/
├── Dockerfile # Forge image (multi-stage Node.js build)
├── docker-compose.yml # Forge container with socket + workspace mounts
├── .env.example # WORKSPACE_PATH, NWN_HOME_PATH, GIT_PROVIDER_URL
├── .impeccable.md # Design context (personality, palette, principles)
├── .agents/skills/ # Impeccable design skills (18 commands)
├── builder/
│ └── Dockerfile # Builder image (pre-built binaries, no Nim)
├── db/
│ └── schema.sql # MariaDB schema + seed data
├── lsp/
│ └── nwscript-language-server/ # Git submodule (forked LSP)
├── packages/
│ ├── backend/
│ │ └── src/
│ │ ├── index.ts # Express + WS + upgrade handlers
│ │ ├── config/ # repos.ts, env-template.ts
│ │ ├── gff/ # GFF schema definitions (6 types)
│ │ ├── nwscript/ # resref-index, tlk-index, twoda-index
│ │ ├── routes/ # All API routes
│ │ └── services/ # All backend services
│ └── frontend/
│ └── src/
│ ├── App.tsx # Router with SetupGuard + React.lazy routes
│ ├── components/ # editor/, gff/, terminal/, Toast, etc.
│ ├── hooks/ # useWebSocket, useEditorState, useTheme, useLspClient
│ ├── layouts/ # IDELayout (inline styles), SetupLayout
│ ├── lib/ # lspClient.ts (JSON-RPC bridge)
│ ├── pages/ # All page components (inline styles, lucide icons)
│ ├── services/ # api.ts
│ └── styles/ # globals.css (OKLCH tokens, font imports, global styles)
Running Locally
Native (dev mode, fastest iteration)
cd /home/jmg/dev/layonara/layonara-forge
WORKSPACE_PATH=/tmp/forge-test NWN_HOME_PATH=/home/jmg/dev/nwn/local-server/home GIT_PROVIDER_URL=https://gitea.layonara.com npm run dev
# Frontend: http://localhost:5173 (proxies to backend)
# Backend: http://localhost:3000
Important: If you run without WORKSPACE_PATH, it defaults to /workspace which doesn't exist natively. The File Explorer will show "Repository not cloned" and repos will show as uncloned.
Docker (production mode)
cd /home/jmg/dev/layonara/layonara-forge
cp .env.example .env # edit paths
docker compose up -d
# Open http://localhost:3000
Building Docker images
docker build -t layonara-builder builder/ # ~45 seconds
docker build -t layonara-forge . # ~2 minutes
Current State (as of UI overhaul session end)
- All code is local — nothing has been pushed to any remote for the layonara-forge repo itself
- Frontend build passes clean (no TS errors, no lint issues)
- All 8 pages styled with consistent design system (cards, icons, tokens)
- Setup wizard has 4-phase indicator, path inputs, status dots
- Routes are code-split (10 chunks)
- 28 frontend files modified in the UI overhaul
- The
layonara/neverwinter.nimandlayonara/nwscript-ee-language-serverforks are on GitHub with changes pushed plenarius/layonara_nwnhas a release workflow added and v0.1.1 release published
Priority for Next Session
- Integration test the setup wizard against live Gitea (fork, clone, build cycle) — needs
gitinstalled or Docker environment - Test module build → server start → NWN client connection
- Test Toolset temp0/ sync with real GFF files
- Polish remaining components (GFF editors, CommitDialog, FileExplorer, EditorTabs, SearchPanel — still use Tailwind classes)
- Test light mode visually
- Set up Gitea backup on xandrial