# Lumen β full documentation
> A transparent, no-magic, no-build vanilla-JS OOP UI framework. What you write is what runs.
---
# Lumen
> A transparent, no-magic, no-build vanilla-JS OOP UI framework. **What you write is what runs.**
>
> Un framework de UI en JS vanilla, OOP, transparente, sin magia y sin build. **Lo que escribes es lo que corre.**
**π Live docs & examples Β· DocumentaciΓ³n y ejemplos en vivo:** https://dragones-tech.github.io/lumen/site/
## Install Β· Instalar
No build, native ES modules, zero runtime deps. Full guide: [en](docs/en/install.md) Β· [es](docs/es/install.md).
```html
```
```bash
# 2) or copy src/ into your project (most transparent)
# 3) or npm:
npm i @jehosogo/lumenjs # published on npm
# or from GitHub: npm i github:dragones-tech/lumen
```
## Principles Β· Principios
- **No build.** Native ES modules. The only thing you need is a static file server (ES module imports are blocked on `file://`). Your `.js` reaches the browser untouched.
- **No magic.** No global side effects on import, no hidden proxies, no compiler rewriting your code. Explicit OOP with a lifecycle you can read.
- **No Web Components.** Plain classes (not `extends HTMLElement`), so you keep full control of `animate`/`unmount`. A Custom Element adapter is an opt-in escape hatch.
- **Separate HTML.** Markup lives in native `` elements β real HTML, full editor autocomplete.
- **Style-agnostic.** Ships zero CSS. Bring plain CSS, native `@scope`, Tailwind β whatever you like.
- **Typed without a build.** Plain `.js` + JSDoc types, checked with `tsc --noEmit`.
## Run the examples Β· Correr los ejemplos
```bash
# from the repo root β zero-dep Node static server with live reload
npm run serve # save a file β the browser reloads (set LIVERELOAD=0 to disable)
# then open http://localhost:8000/examples/event-emitter/
```
## Docs site Β· Sitio de docs
A bilingual (EN/ES) documentation site **built with Lumen itself** β sidebar + router +
each page's live example embedded. Un sitio de documentaciΓ³n bilingΓΌe **hecho con Lumen**.
```bash
npm run serve
# open http://localhost:8000/site/
```
## Type-check Β· Verificar tipos
```bash
npm install # dev-only: TypeScript, for checking JSDoc types (never runs in the browser)
npm run check
```
## Modules Β· MΓ³dulos
| Module | Status | Docs |
|---|---|---|
| `event-emitter` | β
done | [en](docs/en/event-emitter.md) Β· [es](docs/es/event-emitter.md) |
| `dom` | β
done | [en](docs/en/dom.md) Β· [es](docs/es/dom.md) |
| `animate` | β
done | [en](docs/en/animate.md) Β· [es](docs/es/animate.md) |
| `view` | β
done | [en](docs/en/view.md) Β· [es](docs/es/view.md) β abstract base class you extend |
| `model` | β
done | [en](docs/en/model.md) Β· [es](docs/es/model.md) |
| `collection` | β
done | [en](docs/en/collection.md) Β· [es](docs/es/collection.md) |
| `collection-view` | β
done | [en](docs/en/collection-view.md) Β· [es](docs/es/collection-view.md) |
| `region` | β
done | [en](docs/en/region.md) Β· [es](docs/es/region.md) |
| `router` | β
done | [en](docs/en/router.md) Β· [es](docs/es/router.md) |
| `http` | β
done | [en](docs/en/http.md) Β· [es](docs/es/http.md) β `fetch` wrapper |
| `i18n` | β
done | [en](docs/en/i18n.md) Β· [es](docs/es/i18n.md) β translations |
| `validate` | β
done | [en](docs/en/validate.md) Β· [es](docs/es/validate.md) β model + UI validation |
| `element` | β
done | [en](docs/en/element.md) Β· [es](docs/es/element.md) β use a View as a Custom Element (`defineElement`) |
| `canvas` | π§ͺ experimental | [en](docs/en/canvas.md) Β· [es](docs/es/canvas.md) β render the same Model/Collection to canvas (`Stage`/`Node2D`/`CanvasLayer`); import from `lumenjs/canvas` |
## Guides Β· GuΓas
- **Styling** (Lumen is style-agnostic; plain CSS, `@scope`, Tailwind): [en](docs/en/styling.md) Β· [es](docs/es/styling.md) β runnable [Tailwind example](examples/tailwind/).
- **Deployment & loading** (HTTP/2, import maps, `modulepreload` β no bundler needed): [en](docs/en/deployment.md) Β· [es](docs/es/deployment.md).
- **For AI agents** (llms.txt, AGENTS.md): [en](docs/en/llms.md) Β· [es](docs/es/llms.md).
- **Credits & lineage** (homage to Backbone & Marionette): [en](docs/en/credits.md) Β· [es](docs/es/credits.md).
## For AI agents Β· Para agentes de IA
Machine-readable docs so an assistant (Claude, Codex, Cursorβ¦) writes idiomatic Lumen:
- [`llms.txt`](https://dragones-tech.github.io/lumen/llms.txt) β index of all docs ([llmstxt.org](https://llmstxt.org) convention).
- [`llms-full.txt`](https://dragones-tech.github.io/lumen/llms-full.txt) β every page in one file; paste it into your assistant.
- [`AGENTS.md`](AGENTS.md) β project conventions for coding agents (and `CLAUDE.md` for Claude Code).
Regenerate the `llms.*` files after editing docs with `npm run llms`.
## Credits Β· CrΓ©ditos
Lumen is a respectful, modernized homage to **[Backbone.js](https://backbonejs.org/)**
(Model, Collection, events-first) and **[Marionette.js](https://marionette.js.org/)**
(View, Region, CollectionView, layouts). The patterns are theirs; the implementation is
rebuilt on the modern platform β no jQuery, no Underscore, no build. No Backbone/Marionette
code is used; these are conceptual inspirations. See [credits](docs/en/credits.md).
---
# Installation
Lumen is no-build, native ES modules, with zero runtime dependencies β so "installing" can
be as light as pointing at a CDN or copying a folder. Pick what fits your setup.
## Option 1 β CDN, no install (quickest)
Add an import map and load from a CDN (jsDelivr serves the GitHub repo directly):
```html
```
Pin a tag or commit for production instead of `@main` (e.g. `@v0.1.0`) so it can't change under you.
## Option 2 β copy `src/` (most transparent)
Lumen is just plain `.js` files. Copy the repo's `src/` into your project (e.g.
`vendor/lumen/`) and import it β relatively or via an import map:
```html
```
You own the code, no package manager, nothing to update behind your back. Very on-brand.
## Option 3 β npm / GitHub
```bash
npm i @jehosogo/lumenjs # published on npm
# or straight from GitHub:
npm i github:dragones-tech/lumen
```
Bundlers and Node resolve `@jehosogo/lumenjs` via the package's `exports`. In the
browser **without** a bundler, still add an import map pointing the specifier at the
installed files (e.g. `node_modules/@jehosogo/lumenjs/src/index.js`), since browsers
don't resolve bare specifiers on their own.
## Serve it
Any static server works (ES module imports are blocked on `file://`). The repo ships a
zero-dep one with live reload:
```bash
npm run serve # β http://localhost:8000
```
## Type-checking (optional)
Types are JSDoc; check them with TypeScript (dev-only, never runs in the browser):
```bash
npm i -D typescript
npm run check # tsc --noEmit
```
> Status: published on npm as `@jehosogo/lumenjs`, source on GitHub (`dragones-tech/lumen`);
> the CDN and `github:` install also work.
---
# EventEmitter
A tiny, typed, leak-free event emitter. It is the foundation of messaging in Lumen.
## Philosophy
- **No global singleton.** You create emitters with `new` and pass them around explicitly. Importing the module has *no side effects* β nothing is registered, nothing touches `window`.
- **Symmetric cleanup.** Every `on()` returns an unsubscribe function. You can also pass an `AbortSignal`, so aborting one controller removes every listener bound to it at once. (This is how `View` will tear listeners down on unmount, without leaks.)
- **Safe by default.** `off()` on an event that was never registered is a no-op, not a crash.
## API
| Method | Returns | Description |
|---|---|---|
| `on(event, handler, { signal? })` | `() => void` | Subscribe. Returns an unsubscribe function. |
| `once(event, handler, { signal? })` | `() => void` | Subscribe for a single emission, then auto-remove. |
| `off(event, handler)` | `void` | Remove a specific handler. No-op if absent. |
| `emit(event, payload)` | `void` | Call every handler synchronously with `payload`. |
| `clear(event?)` | `void` | Remove all handlers for `event`, or all events if omitted. |
| `listenerCount(event)` | `number` | How many handlers are registered for `event`. |
## Typing your events
Pass an events map as the generic to get autocomplete and payload checking:
```js
/** @typedef {{ 'todo:add': { text: string }, 'todo:clear': void }} TodoEvents */
/** @type {EventEmitter} */
const bus = new EventEmitter();
bus.on('todo:add', (p) => console.log(p.text)); // p is { text: string }
bus.emit('todo:add', { text: 'milk' }); // payload checked
```
For events with no data, type the payload as `void` and pass `undefined`.
## Example
```js
import { EventEmitter } from 'lumenjs/event-emitter';
const bus = new EventEmitter();
// Subscribe; keep the unsubscribe handle.
const off = bus.on('ping', (n) => console.log('ping', n));
bus.emit('ping', 1); // logs: ping 1
off();
bus.emit('ping', 2); // nothing β already unsubscribed
// Group cleanup with an AbortController.
const ac = new AbortController();
bus.on('tick', () => console.log('tick'), { signal: ac.signal });
bus.on('tock', () => console.log('tock'), { signal: ac.signal });
ac.abort(); // removes BOTH listeners at once
```
## App-wide messaging (a shared bus)
This is how independent views, models and collections talk to each other β the role
Backbone's Radio played, minus the global singleton. Create an `EventEmitter`, share it by
importing it, and let unrelated views subscribe (each with `this.signal`, so it auto-cleans on
unmount). Models and Collections already emit events you can subscribe to the same way. A
running example β a catalog, a cart badge, and toasts coordinating through a shared bus and a
shared collection β is at [examples/messaging](../../examples/messaging/).
## Design notes
- Handlers are stored in a `Map>`. The `Set` dedupes the same handler reference and makes `off()` O(1).
- `emit()` iterates over a **copy** of the handler set, so a handler may call `on()`/`off()` during emission without disturbing the current pass.
- This replaces the old framework's `eventing` package, fixing two bugs: the global frozen singleton (now instantiable) and `off()` throwing when the event was never registered (now guarded).
---
# dom
DOM helpers β the bridge between your separate HTML (`` elements) and your component classes.
## Philosophy
- **No magic.** `clone` is a convenient deep-clone of a ``, `refs` is one `querySelectorAll` collected into an object, `$`/`$$` are typed wrappers. Importing the module has **no side effects** β nothing touches `window`, nothing observes the document. (The old framework installed a global `window.createElement` and a document-wide `MutationObserver` on import; Lumen does neither.)
- **The pattern:** write markup once in a `` β `clone()` a fresh node β `refs()` the elements you'll update. A component then touches only what changed, instead of re-rendering its whole subtree.
## API
| Function | Returns | Description |
|---|---|---|
| `$(selector, root?)` | `Element \| null` | `querySelector`, typed and scoped (default root `document`). |
| `$$(selector, root?)` | `Element[]` | `querySelectorAll` as a real array, not a live NodeList. |
| `clone(template, root?)` | `HTMLElement` | Deep-clone a ``'s single root element. Accepts a `` or a selector. |
| `refs(root)` | `Record` | Collect every `[data-ref]` descendant into a keyed object. |
## Conventions
- **One root per template.** `clone()` returns the template's first element child. Wrap each component's markup in a single root element.
- **`data-ref` for references.** `