A small, functional, zero-build component library and application framework. Three things make up its core: a simple, optimised DOM reconciler, a curried component model, and a broad UI library built on top of those two. No JSX, no build step, no compiler — every file is plain ES modules served as-is, opened in a browser.
Important: a lot of docs are generated by Claude. They're better than what I would write, but there's a chance for error (1 is still a chance). If you spot any, please open a PR or at least a bug report.
dervoJS ships:
- a full UI component library (forms, layout, tables, charts, markdown, pickers, …)
- a CRUD module that compiles OpenAPI 3.0/3.1 specs into ready-to-render list / show / new / edit / delete views
- a game engine (
createGame) for Twine-style narrative games with NPCs, save/load, optional background music, and built-in debugger - an HbbTV module — remote-key decoding, channel info, DSM-CC stream events, plus a spatial focus manager and
data-focus-*widgets that make a TV app navigable end-to-end with arrows + OK + BACK - three live demos: a component playground (
demo/), a Twine-style RPG (demoGame/), and a TV-remote demo with focus manager + profiler combo (demoHbbtv/)
Components and elements are pure functions of their options. They take a plain object in, return a plain { tag, props, children } vnode out. Composition happens through function composition (pipe, compose, lift, bind from odocosJS); state changes happen through a single setState that merges immutable patches. The reconciler is the one place imperative DOM lives. Everything above it is a pure transformation of state into a tree.
// Pure function of props, returns a vnode. No "instance", no lifecycle, no this.
const Greet = ({ name }) => div({ className: 'greet' })([`Hello, ${name}.`]);Every primitive and every component is curried into stages, so each call returns a function ready to be partially-applied, memoised, or composed:
tag(props)(children) // element: div({ className: 'card' })(['hi'])
Component(opts)(children) // component: Card({ title: 'Stats' })([Badge({})(['ok'])])
Leaf(opts) // leaf: Clock({ time: 42 })Currying lets you bind options once and reuse the half-applied result — const DangerBtn = Button({ variant: 'danger' }); makes DangerBtn(['Delete']) work like a child component. The store, mount, router, HTTP client, validators and CRUD compiler are curried the same way, so partial application is the natural unit of reuse.
Due to the separation of logic, data and structure, it is very easy to first design the look of your web-app using static data in the store or just hardcoded data. After that functionality can be mocked or added. Debugging can also be done by using a websocket to push changes to the state.
src/state.js walks the live DOM and the new vnode tree in parallel:
- same tag → patch only the props that actually changed (tracked via
WeakMap), recurse into children - different tag →
replaceChild - text nodes → compare
nodeValue - keyed children → matched via
Maplookup and re-ordered withinsertBefore(one move per displaced child) - focus + text-selection → snapshotted and restored across patches
- SVG → auto-dispatched to
createElementNSfor the standard SVG tag set data-*→ forwarded tosetAttributeso they reflect to the DOM (the HbbTV focus manager relies on this)- batches updates through
requestAnimationFrame— manysetStatecalls within one frame collapse to a single render
The renderer is a few hundred lines, has no virtual DOM diff cache, and gets out of the way. It's "optimised" by not being clever — it just doesn't touch what it doesn't need to.
There is no package.json, no bundler, no transpiler, no TypeScript build, no plugin pipeline. Every file is a simple ES modules served as-is by any static HTTP server. optimise-imports.js exists as an optional production-time pass that rewrites broad import … from '…/src/index.js' to direct module paths so the browser only fetches what it actually uses. The script was used for the game editor, so small changes are needed to use it for your project.
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
<body>
<script type="module">
import { div, Button, createStore, mount, initStyles } from './src/index.js';
initStyles({ theme: 'light' });
const store = createStore({ count: 0 });
const { setState } = store;
const view = state =>
div({ className: 'stack' })([
`Count: ${state.count}`,
Button({ onClick: () => setState(s => ({ count: s.count + 1 })) })(['+1']),
]);
mount(store)(document.body)(view);
</script>
</body>
</html>Demos plus game editor can be found here:
- demo/index.html — component library playground
- demoGame/index.html — Hero Trainer RPG demo
- demoHbbtv/index.html — HbbTV remote-control demo
- gameEditor/index.html — Graphical game editor
dervoJS/
├── src/
│ ├── index.js # public API — every export
│ ├── elements.js # 90+ curried HTML/SVG tag factories (div, span, button, …)
│ ├── state.js # createStore, mount, reconciler, profiler hooks
│ ├── styles.js # initStyles, theme tokens, toggleTheme, setTokens
│ ├── validate.js # composable form validation rules
│ ├── cache.js # memoComponent / memoLeaf / memoize
│ ├── listeners.js # pub/sub bus, debounce, key/resize/visibility listeners, alarms
│ ├── utils.js # cn(), fire()
│ ├── dervo.css # full stylesheet (~900 lines, light + dark tokens)
│ ├── router.js # createRouter + Link/NavLink/NavBar/NavMenu/Breadcrumbs
│ ├── ws.js # createWS — curried WebSocket factory
│ ├── http.js # createHttp + defaultHttp — curried HTTP client
│ ├── openapi.js # compileResource — OpenAPI 3.0/3.1 compiler
│ ├── game.js # createGame + Scene/Choice/ChoiceList + NPC helpers + bgm
│ ├── hbbtv.js # HbbTV/OIPF wrapper + bootHbbtv + createFocusManager + onKeyCombo
│ │
│ └── components/ # the UI library
│ ├── Button, TextInput, NumberInput, Select, Checkbox, Toggle, Slider,
│ │ Card, Badge, Alert, List, Table, Modal, Tabs, Clock, ProgressBar,
│ │ Dropzone, Img, Media, ColorPicker, DateTimePicker, Layout,
│ │ Typography, Markdown, Highlight, Charts, KeyMap, GlitchImg,
│ │ CanvasBg, ImageBg, MultiStep, VisuallyHidden, SkipLink
│ ├── StateDebugger, FloatingPanel, RenderProfiler, ListenersDebugger
│ ├── CrudResource.js # auto-CRUD from an OpenAPI spec
│ ├── GameWidgets.js # Stats, Resources, Inventory, Shop
│ └── HbbtvWidgets.js # Focusable, FocusList, FocusGrid, FocusScroll, TabBar (UNSTYLED)
│
├── demo/ # component-library playground
│ ├── index.html, app.js, store.js
│ └── panels/ # one file per demo page (buttons, table, crud, charts, …)
│
├── demoGame/ # Twine-style RPG demo
│ ├── index.html, main.js
│ ├── items.js # item catalogue + initial state
│ ├── world.js # NPC definitions + enemies
│ ├── combat.js # turn-based combat scene + helpers
│ ├── character.js # layered SVG character + sidebar
│ └── scenes.js # town, gym, library, tavern, shop, wardrobe, …
│
├── demoHbbtv/ # HbbTV remote-control demo
│ ├── index.html # HbbTV <object> hooks + minimal demo CSS
│ ├── main.js # bus wiring: focus manager, router, profiler combo
│ ├── store.js # createStore + helpers (pushKey, pushPick, …)
│ ├── router.js # direct-jump shortcuts (colour buttons)
│ ├── components/ # Layout, Keypads, KeyBuffer, KeycodeTable, PicksLog, …
│ └── pages/ # Remote (Pad / Keycodes sub-pages), Broadcast, List, Grid
│
├── gameEditor/ # visual editor that produces createGame-compatible projects
│
└── lib/
└── odocosjs/ # functional micro-library (Observable, vnode, Monadic functions
# curried HTTP, localStorage + IndexedDB, …)
Every file is a ES modules. There is no package manager, bundler, or transpiler involved. You can still use a bundler, if you want to, you just don't have to
Every element and component is curried:
// direct odocos vnode
vnode('div')({ className: 'long' })([
vnode('h1')({ className: 'head1' })(['Head']),
vnode('p')({ className: 'paragraph' })(['Paragraph']),
])
// element: tag(props)(children)
div({ className: 'card' })(['Hello'])
// component: Component(opts)(children)
Card({ title: 'Stats' })([Badge({ variant: 'green' })(['OK'])])
// leaf component (no children):
Clock({ time: 42, size: 'lg', label: 'elapsed' })The base comes from odocosJS: vnode(tag)(props)(children) produces a plain { tag, props, children } object. dervoJS wraps every HTML tag as a named export (div, span, button, …) plus SVG-aware tags (svg, rect, circle, path, …) and pre-styled shortcuts (flexRow, flexCol, absDiv, …).
A leaf with no children is Leaf(opts). A conditional child is ...(cond ? [node] : []) or ...(cond ? [node] : empty), not cond ? node : null. The reconciler and createNode both reject null children. By design. This single rule eliminates a whole class of "did this render?" bugs and keeps the renderer's code path linear.
createStore(initial) returns { getState, setState, subscribe }. setState merges a patch (object or prev => patch). mount(store)(root)(view) wires the view function to the store and re-renders on every change via requestAnimationFrame batching. Mount returns { destroy } so dynamic apps can unmount cleanly.
const store = createStore({ count: 0 });
const { setState } = store;
setState({ count: 1 });
setState(prev => ({ count: prev.count + 1 }));
const handle = mount(store)(document.body)(state => div({})([`Count: ${state.count}`]));
// later
handle.destroy();| Component | Description |
|---|---|
Button |
primary / secondary / danger / success / ghost; sm/md/lg |
TextInput |
labelled input with hint + error styling |
NumberInput |
stepper (− / field / +) with min/max/step |
Select |
dropdown with label, placeholder, hint, error; supports <optgroup> |
Checkbox, Toggle, Slider, ProgressBar |
the obvious ones |
MultiStep |
wizard-style stepper with per-step validation and bus events |
| Component | Description |
|---|---|
Card |
bordered surface with optional title / footer |
Badge |
coloured pill (blue, green, red, yellow, gray, purple) |
Alert |
feedback banner (info, success, warning, error) |
List |
maps an array through a render fn, shows empty state |
Table |
sortable, filterable, sticky headers, column filters, global search |
Clock |
stateless HH:MM:SS display (pair with createInterval) |
Modal, Tabs, Dropzone, FloatingPanel (draggable, resizable, position persisted by id).
Img, Video, Audio, VideoStream (live MediaStream via getUserMedia / WebRTC).
ColorPicker (swatch palette + native wheel + hex input), DateTimePicker (month grid + optional time).
Container, Row/Col (12-col responsive grid), Stack, Grid, Divider, Spacer, AspectBox, Float, Clearfix, PageLayout, AppShell, TwoPane, BlogLayout, DragList + useDragListGroup, FloatingPanel.
import { Typography, H1, P, markdownToVnode } from './src/index.js';
Typography({ toc: true, tocPosition: 'right' })([
H1({})(['Getting started']),
P({})(['Install and run.']),
]);
markdownToVnode('# Hello\n\nThis is **bold** and `code`.');Syntax highlighting via tokenizer / highlight / defaultRegistry (ships with JS, TS, Python, Rust, C/C++, Java, Haskell, CSS, Bash, …).
PieChart, BarChart, LineChart, MultiLineChart, SparkLine — pure SVG, no dependencies, hover callbacks.
StateDebugger— live inspector, inline JSON editing, key watchers.RenderProfiler— per-frame timings, DOM op counters, sparkline. Off by default; activates when rendered.ListenersDebugger— inspect all named pub/sub buses.
import { createStore, mount } from './src/index.js';
const store = createStore({ count: 0 });
mount(store)(document.body)(state =>
div({})([`count: ${state.count}`])
);
// Curried for partial application
const mountTo = mount(store);
mountTo(document.getElementById('header'))(HeaderView);
mountTo(document.getElementById('main'))(MainView);import { validate, validateForm, isFormValid,
required, minLength, email, pattern, range } from './src/index.js';
const check = validateForm({
name: validate(required(), minLength(2)),
email: validate(required(), email()),
});
const errors = check({ name: '', email: 'bad' });
isFormValid(errors); // falseBuilt-in rules: required, minLength, maxLength, email, ip, pattern, range. Custom rules: value => errorString | null, value => boolean, or [predicate, msg] tuple.
import { memoComponent, memoLeaf, memoize } from './src/index.js';
const MemoCard = memoComponent(Card);
const MemoBtn = memoLeaf(Button);
const FastCard = memoize(200)(Card);Cache keys are serialised with stableKey(opts), replacing function values so inline arrows don't bust the cache.
import { initStyles, toggleTheme, setTokens, resetTokens, tokens } from './src/index.js';
initStyles({ theme: 'dark', colors: { accent: '#e11d48' }, fonts: { sans: 'Inter' } });
toggleTheme();
setTokens({ accent: '#7c3aed' })();
resetTokens(['accent']);The stylesheet defines ~50 semantic CSS custom properties (--bg, --surface, --text, --accent, --danger, --border, …) with light and dark palettes.
import { createBus, getBus, onWindowResize, onBreakpoint,
onKeydown, createAlarm, onVisibilityChange } from './src/index.js';
const bus = createBus();
bus.on('save', data => console.log(data));
bus.emit('save', { id: 1 });
// Named, reusable buses
const sharedBus = getBus('app');
sharedBus.on('navigate', path => goto(path));getBus(id) returns a singleton named bus -> used by the HbbTV module to fan all remote-key events through a single subscribable channel.
import { createWS } from './src/index.js';
const ws = createWS({ url: 'wss://api.example.com/live', reconnect: true, baseDelay: 500 })({
onOpen: () => setState({ wsStatus: 'open' }),
onMessage: data => setState(s => ({ feed: [data, ...s.feed] })),
});
ws.send({ type: 'subscribe', channel: 'prices' });
ws.destroy();import { createRouter, NavBar, NavLink, Breadcrumbs } from './src/index.js';
const router = createRouter([
{ path: '/', handler: ctx => setState({ page: 'home' }) },
{ path: '/user/:id', handler: ctx => setState({ page: 'user', id: ctx.params.id }) },
{ path: '*', handler: ctx => setState({ page: '404' }) },
], { mode: 'hash', base: '/demo' });Hash or history mode; named params; query parsing; nav helpers.
note, this is kind of overkill for most use cases; there's also a simpler HTTP client in odocosJS that's a better fit for small applications
import { createHttp, defaultHttp } from './src/index.js';
// Default bare fetch, JSON
const http = defaultHttp;
// Auth (wrap fetch)
const authed = (url, init = {}) => fetch(url, {
...init, headers: { ...init.headers, Authorization: `Bearer ${token}` },
});
const http = createHttp(authed);
// Or static headers
const http = createHttp(fetch, { 'X-Api-Key': KEY });Shape contract (any object that matches works):
get : url => opts => Promise<json>
list : url => opts => Promise<[items, totalCount]>
post : url => payload => opts => Promise<json>
put : url => payload => opts => Promise<json>
patch : url => payload => opts => Promise<json>
remove : url => payload => opts => Promise<json>
list reads the x-total-count header (json-server convention).
createCrud compiles an OpenAPI 3.0 or 3.1 spec into ready-to-render CRUD views. Curried so every binding step gives a reusable handle:
import { createCrud, defaultHttp } from './src/index.js';
const withHttp = createCrud(http);
const withApi = withHttp('/api');
const Crud = withApi(openapiSpec);
const Users = Crud('users');
const Projects = Crud('projects');
Users({ state: s.users, setState: patchUsers, view: 'list' });
Users({ state: s.users, setState: patchUsers, view: 'edit', id: 42 });
Projects({ state: s.projects, setState: patchProjects, view: 'new' });The compiler maps OpenAPI keywords onto existing components and validators:
| Schema | Component | Validators |
|---|---|---|
type: string |
TextInput |
minLength · maxLength · pattern |
type: string, format: email |
TextInput type=email |
email() |
type: string, format: date / date-time |
DateTimePicker |
— |
enum: [...] |
Select |
— |
type: integer / number |
NumberInput |
range(min, max) |
type: boolean |
Toggle |
— |
type: array, items |
repeating + / × | recursive |
type: object, properties |
nested Card |
recursive |
nullable (3.0) / type: […, 'null'] (3.1) |
same as base | drops required |
const (3.1) |
Select (single option) |
— |
readOnly: true |
hidden from forms | — |
compileResource(spec)(resource) is exposed separately for debugging or to drive your own renderers.
See the live demo in demo/panels/crud.js.
createGame builds a self-contained Twine-style game with AppShell layout, scene routing, NPC dialogue, save/load, optional background music, and a state debugger — the author writes scenes + initial state, the engine handles the rest.
import { createGame, Scene, NpcChoices, NpcLine, p } from './src/index.js';
const game = createGame({
title: 'Forest Adventure',
start: 'intro',
state: { hp: 100, gold: 0, inventory: [] },
scenes: {
intro: ctx => Scene({
title: 'Awakening',
body: [p({})(['You wake in a damp forest.']), ...NpcLine(ctx)],
choices: [
{ label: 'Go north', to: 'forest' },
{ label: 'Pray', to: 'shrine', if: c => c.state.hp > 0 },
...NpcChoices(ctx),
],
})(ctx),
forest: ctx => Scene({ /* ... */ })(ctx),
},
npcs: {
mara: {
name: 'Mara the Hermit',
locations: ['hermitHut'],
greeting: 'Mara sits by the fire.',
dialogue: ctx => Scene({ /* ... */ })(ctx),
},
},
sidebar: ctx => [characterPortrait(ctx.state), statsPanel(ctx.state)],
music: ctx => MUSIC_BY_SCENE[ctx.scene] || 'audio/theme.mp3',
musicVolume: 0.5,
debug: true,
});
game.mount(document.body);Scene({ title, body, choices })(ctx)— Twine-style descriptor.Choice({ label, to, action, if? })(ctx)/ChoiceList(choices)(ctx)— declarative buttons;ifis a boolean or predicate.withTick(action)— wraps an action so the NPC world ticks afterwards (random walk all NPCs).NpcChoices(ctx)— generates "Talk to X" descriptors for NPCs at the current scene.NpcLine(ctx)— short flavour line for NPCs present.ctx(per scene):{ state, setState, getState, goto, back, restart, save, load, hasSave, clearSave, listSlots, npcs, npcsAt, tickWorld, talkTo, scene, history, debug }.
Pass music: ctx => url | '' and the engine manages a single hidden <audio> element across the game. The resolver is invoked on every state change; whenever the returned URL differs from the last one the engine swaps src + play(). Empty / falsy URLs pause and clear the source. Browsers block autoplay until the first user gesture — the engine catches the rejected play() and retries on the next pointerdown / keydown. musicVolume (0..1, default 0.5) is applied once on element creation. Calling destroy() on the mount handle removes the audio element and unwires the retry listeners.
Backed by odocosJS's localObjectStorage. Each slot is a separate key under dervo-game:<title> (override via saveKey):
ctx.save('autosave'); // serialise current state
ctx.load('autosave'); // restore from slot
ctx.hasSave('autosave'); // boolean
ctx.listSlots(); // [ 'default', 'autosave', ... ]The default top-bar exposes 💾 / 📂 buttons; 🌗 toggles the theme; ☰ collapses the sidebar; ⚙ toggles a floating StateDebugger + RenderProfiler + ListenersDebugger panel (matches the component-playground pattern).
src/components/GameWidgets.js — pre-styled common patterns:
Stats({ values, bonuses?, max? })— labelled stat rows with bars.Resources([{ label, value, max?, suffix? }])— gold / energy / HP rows.Inventory({ ctx, items, slots, returnTo })— slot-based equip scene.Shop({ ctx, items, returnTo })— buy-from-stock grid; auto-disabled when broke.
See the full game in demoGame/.
Thin functional wrapper over the HbbTV / OIPF DOM APIs used by hybrid broadcast-broadband apps on TVs and set-top boxes. Every function gracefully degrades on desktop browsers — develop in Chrome, deploy to an STB unchanged.
Required DOM (once, in your HbbTV index.html):
<object id="appmgr" type="application/oipfApplicationManager"></object>
<object id="oipfcfg" type="application/oipfConfiguration"></object>
<object id="video" type="video/broadcast"></object>import { bootHbbtv, getBus } from './src/index.js';
const bus = bootHbbtv(); // shows app, activates keyset, wires events to bus
bus.on('boot', ({ hbbtv, channel }) => { /* ... */ }); // one-shot at startup
bus.on('key', ({ key, raw }) => { /* ... */ }); // every remote keypress
bus.on('stream', ({ name, text }) => { /* ... */ }); // DSM-CC stream eventsbootHbbtv is the one-call factory; the lower-level pieces are also exported:
| Function | What it does |
|---|---|
initApp({ show? }) |
show / hide the OIPF application |
initKeys(mask) |
request which remote keys the app receives (KEYSET.ALL, KEYSET.RED | KEYSET.NAVIGATION, …) |
KEYSET |
{ RED, GREEN, YELLOW, BLUE, NAVIGATION, VCR, SCROLL, INFO, NUMERIC, ALPHA, ALL } |
decodeKey(event) |
pure: KeyboardEvent → 'red' / 'ok' / 'play' / '5' / … (HbbTV codes + browser keys + desktop fallback) |
onRemoteKey(handler)({ keys?, preventDefault? }) |
curried listener with filter, returns { destroy } |
getChannelInfo() |
{ name, onid, tsid, sid } from the broadcast object |
getVideoBroadcast() |
handle around <object id="video"> (play / pause / stop / seek / setSize) |
onStreamEvent({ targetURL, eventName })(handler) |
curried DSM-CC subscription |
isHbbtvCapable() |
boolean |
onKeyCombo(bus, combo, handler, { window? }) |
fires when a sequence of keys (e.g. '991') is pressed within a window |
createFocusManager({ bus, store, stateKey?, scrollStep?, home? }) — DOM-driven spatial nav.
- State:
state.<stateKey> = { id: <focusId|null> }.id: null→ no focus;id: 'foo'→ arrows / OK / BACK are consumed by the manager. - Discovery: each focusable marks itself with
data-focus="<id>". Optionaldata-focus-scroll="x|y|xy"makes it a scroll container. Optionaldata-focus-row="<row>"constrains LEFT/RIGHT to focusables in the same row (UP/DOWN cross rows freely). - Arrow handling: scroll the focused element if it can scroll further in that direction; otherwise pick the nearest neighbour by
getBoundingClientRect(filtered to the correct side, scored by axis distance + 2 × perpendicular distance). - Activation: pressing OK on the focused element emits
bus.emit('activated', { id }). Decoupled — subscribe and dispatch however you like. - BACK: if a
home: () => focusId | nullcallback is provided, BACK focuses the home id; otherwise it releases focus.
Widgets (src/components/HbbtvWidgets.js)
All widgets are unstyled — they ship only the markup + functional CSS (flex/grid/overflow). Hook the classes they stamp (.focusable, .focusable-active, .focus-list-item, .tab, .tab-active, .focusable-scroll, …) to apply your own look.
| Widget | What it renders |
|---|---|
Focusable({ id, focus, scroll?, row?, maxHeight?, … })(children) |
generic focus wrapper |
FocusList({ items, focus, render?, direction?, gap?, row? }) |
vertical/horizontal stack of focusables |
FocusGrid({ items, cols, focus, render?, gap?, row? }) |
CSS-grid of focusable cells |
FocusScroll({ id, focus, axis?, maxHeight?, maxWidth?, row? })(children) |
single scrollable focusable container |
TabBar({ tabs, current, focus, idPrefix?, gap?, row? }) |
focusable tab strip; idPrefix='subtab-' for sub-tabs |
See the full demo in demoHbbtv/.
Note: those demos were used to find out, where users may struggle to use the library. So a lot of code is written by a junior called Claude.
1. Component playground — demo/
The full library on display. Side nav with ~30 panels — buttons, inputs, tables, charts, markdown, modals, layouts, theme editor, validated forms, the full CRUD demo against a fake in-memory backend, websocket play, router, keymap, glitch effects, accessibility helpers, etc.
Open demo/index.html.
2. Hero Trainer — demoGame/
Twine-style RPG built on createGame:
- Stats (STR / AGI / INT / CHA) trained at the Gym / Library / Track.
- Resources (HP, Energy, Gold) earned at the Tavern.
- Shop with hats / shirts / pants / weapons → equip in Wardrobe → stat bonuses + visual changes.
- Three wandering NPCs (Eldra, Brom, Mara) that move between rooms each tick.
- Locked passages: troll on the Old Bridge (fight or pay 50g), Dark Cave needs the Cave Key.
- Turn-based combat (Attack / Defend / Flee) — five enemies, capstone Dark One.
- Layered SVG character in the sidebar — face changes with HP (smile → flat → wince + sweat drop), clothes/weapons reflect equipment.
Open demoGame/index.html.
3. HbbTV demo — demoHbbtv/
Full TV-remote demo built on the HbbTV module:
- Four pages, all navigated by remote: Remote (visual pad + key buffer / keycodes table), Broadcast (DVB channel info + stream events log), List (20-item scrollable focusable list), Grid (4×4 selectable cards).
- Spatial focus: arrows nav between any focusables; UP/DOWN cross tabs/content; LEFT/RIGHT obeys row isolation (
row='nav' / 'subnav'). - Sub-pages: Remote splits into Pad and Keycodes via a sub-
TabBarwithidPrefix='subtab-'. - Debug combo:
9-9-1within 1 s opens the floatingRenderProfiler. - Scrollable focusables: the picks log and list rows scroll while focused; once at the edge, the next press hops to the spatial neighbour.
Desktop fallback: r/g/y/b for colour buttons, arrows + space for nav/OK, esc for BACK, 0-9 numerics, p/s play_pause/stop.
Open demoHbbtv/index.html.
Game editor — gameEditor/
A visual editor for Twine-style RPGs that round-trips to createGame-compatible JS. Projects are plain JSON; the editor previews live in one tab and exports a runnable game folder from another. Built entirely on dervoJS.
Open gameEditor/index.html.
Targets modern evergreen browsers (Chrome, Firefox, Safari, Edge). Requirements:
- ES modules (
<script type="module">) - CSS custom properties
requestAnimationFrameMap,Set,WeakMap- Optional chaining (
?.), nullish coalescing (??) matchMedia(breakpoint listeners)getUserMedia(only forVideoStream)indexedDB(only when usinglib/odocosjs/src/indexedDbStorage.js)- HbbTV OIPF DOM APIs (only when targeting STBs — desktop browsers no-op gracefully)
No polyfills. No transpilation. IE is not supported.
dervoJS is built on odocosJS, a functional JavaScript micro-library providing:
- Church-encoded types —
Maybe(Just/Nothing),Either(Left/Right),Pair - Combinators —
id,constant,Y,pipe,compose,curry,flip,bind,lift,fromMaybe,orElse,guard - Observable — reactive
getValue/setValue/onChange(used bycreateStore) - vnode —
vnode(tag)(props)(children)produces plain{ tag, props, children }objects - createNode — turns a vnode tree into real DOM
- httpUtils —
get / post / put / patch / remove / getList, all curried (used bycreateHttp) - localObjectStorage —
set / get / remove / getKeys, JSON-aware (used by the game engine's save/load) (wrote that originally 2020 or so when I started with JS, so my first "lib") - indexedDbStorage —
openDb(dbName)(storeName) → { set, get, remove, clear, getKeys, getAll, getItems }; same shape aslocalObjectStoragebut async, structured-cloneable, multi-MB blob friendly. Curried factory, lazy connection, Maybe-wrapped reads. - iterator / list / tree — pure-functional collection helpers
- memo / scheduler / Observable / Task — building blocks for caching, batching, side-effect orchestration
- extra — image compression (
base64ToWebP)
dervoJS uses Observable for its store, createNode for initial DOM construction, and the curried vnode/vsnode factories for its element system. The reconciler, component library, theming, CRUD, game engine, and HbbTV layer are all dervoJS code.
GPLv3
- See ./LICENSE for the license
- See lib/odocosjs/LICENSE for the odocosJS base library.
Using the library as a library inside of your website without making it GPLv3 would technically violate the license. As this falls under normal use, I and other maintainer of this library shall not enforce it. Only changes to the library itself stand under GPLv3. Generally fair use is the goal, due to the modularity of the lib, there is a gray line between components build for it or component build with it for a website...