Headless engines
Both render layers run on the same framework-free code. You can use it without the components: query rows on a server, expand a recurrence rule in a worker, render a BPMN diagram to SVG in a build step, or put your own markup over the grid's data pipeline.
The three layers
| Package | Contains | Licence |
|---|---|---|
@oge-ui/core | Data: query pipeline, data sources, grouping and summaries, pivot aggregation, tree utilities, CSV, state validators | MIT |
@oge-ui/behavior | Interaction: grid, list, overlay, editor and input machines; sanitizers; config defaults and messages; Excel / PDF builders | MIT |
@oge-ui/<family>-engine | Charts, scheduler, Gantt, kanban, BPMN and pivot layout logic | Commercial |
None of them imports Angular or React (a lint rule enforces it), and their public API is snapshot-checked: @oge-ui/core and @oge-ui/behavior fail CI when a signature changes without an updated API report.
Data processing in core
| Function | Does |
|---|---|
runLoadOptions(rows, options, config?) | Filter → search → sort → group and summaries, or page — the in-memory grid query |
applyFilter , applySort , applyPaging , groupRows , createFilterPredicate | The pipeline steps on their own |
ArrayDataSource , CustomDataSource , CursorDataSource , ODataDataSource , buildODataQuery | The DataSource implementations the grid and tree list read |
computePivot(input) , PivotEngine | Pivot aggregation (MIT — the pivot grid UI is the commercial part) |
buildCsv(rows, columns, options?) , guardCsvFormula , escapeCsvCell | RFC 4180 CSV with the spreadsheet formula guard |
parseStateJson , sanitizeGridStateSnapshot , sanitizeTreeListStateSnapshot , sanitizePivotGridStateSnapshot | Never-throwing restore of untrusted stored state |
import { ChangeDetectionStrategy, Component, computed } from '@angular/core';
import { runLoadOptions } from '@oge-ui/core';
interface Order {
id: number;
customer: string;
total: number;
}
const ORDERS: Order[] = [
{ id: 1, customer: 'Ada', total: 120 },
{ id: 2, customer: 'Grace', total: 80 },
{ id: 3, customer: 'Linus', total: 310 },
];
@Component({
selector: 'demo-root',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- your own markup over the grid's query engine -->
<ul>
@for (order of rows(); track order.id) {
<li>{{ order.customer }} — {{ order.total }}</li>
}
</ul>
<p>{{ page().totalCount }} orders of 100 or more</p>
`,
})
export class Demo {
// the same filter → search → sort → page pipeline the data grid runs in memory
protected readonly page = computed(() =>
runLoadOptions(ORDERS, {
filter: { type: 'binary', field: 'total', op: 'ge', value: 100 },
sort: [{ field: 'total', dir: 'desc' }],
skip: 0,
take: 20,
requireTotalCount: true,
}),
);
// no `group` in the query, so the result is rows, not group items
protected readonly rows = computed(() => this.page().data as readonly Order[]);
}import { buildCsv, parseStateJson, sanitizeGridStateSnapshot } from '@oge-ui/core';
// CSV with the formula guard on (=cmd|… is written as text, not evaluated)
const csv = buildCsv(orders, [
{ caption: 'Customer', accessor: (row) => row.customer },
{ caption: 'Total', accessor: (row) => row.total },
]);
// restore a stored grid state without trusting it
const state = sanitizeGridStateSnapshot(parseStateJson(localStorage.getItem('orders') ?? ''));Interaction cores in behavior
@oge-ui/behavior holds the machines both render layers share — keyboard models, selection, virtualization windows, the grid's state and data cores, masks, the editor's document model — plus the message catalogs and config defaults. Machines that hold state take an OgeReactivityAdapter, which is how one implementation runs on Angular signals and on React:
// @oge-ui/behavior — how a framework-free machine holds reactive state
export interface OgeReactiveCell<T> {
(): T;
set(value: T): void;
}
export interface OgeReactivityAdapter {
cell<T>(initial: T): OgeReactiveCell<T>;
derived<T>(compute: () => T): () => T;
}
// Angular backs it with signal() / computed(); React with a store plus
// change notification; a test can use plain closures.| Export | Shape |
|---|---|
sanitizeUrl(url, options?) , sanitizeResourceUrl(url) | Scheme allowlist for href / src |
ogeSanitizeEditorHtml(html, options?) | The rich-text editor allowlist as a string → string function (browser or Node) |
new OgeMaskCore({ mask, rules?, maskChar? }) | Input mask engine (not reactive) |
new OgeGridStateCore(rx) , new OgeGridDataCore(deps, rx) | The grid state slices and its loading machine |
new OgeSelectListCore(deps, rx) , OgeListVirtualizerCore | Select-family list model and its virtualizer |
prefersReducedMotion() , ogeResolveDirection() , observeDirection() | Motion and direction helpers |
These are the layer the components are built from, and their constructors take the dependencies a render layer wires up. For an app, the stand-alone functions (sanitizers, the editor HTML model, motion and direction helpers) are the practical entry; the stateful cores are for building a render layer of your own.
Family engines
Each commercial family keeps its engine in its own package; the Angular and the React package of the family both depend on it, so installing either brings it along.
import { expandRecurrence, parseRecurrenceRule } from '@oge-ui/scheduler-engine';
// RFC 5545 subset: FREQ, INTERVAL, COUNT / UNTIL, BYDAY, BYMONTHDAY, BYMONTH, BYSETPOS, …
const rule = parseRecurrenceRule('FREQ=WEEKLY;BYDAY=MO,WE;COUNT=6');
const dates = rule
? expandRecurrence(rule, new Date(2026, 9, 5, 9), new Date(2026, 9, 1), new Date(2026, 10, 1))
: [];import { readBpmnXml, renderDiagramSvg, writeBpmnXml } from '@oge-ui/bpmn-engine';
// on a server too: import, validate, render a static SVG, write normalized XML
const { model, warnings } = readBpmnXml(xml);
if (model) {
const svg = renderDiagramSvg(model);
const normalized = writeBpmnXml(model); // byte-stable: import → export → import is equal
}import { createLinearScale, niceTicks } from '@oge-ui/charts-engine';
niceTicks(0, 87, 5); // round tick values for an axis
const y = createLinearScale({ min: 0, max: 100, rangePx: 320, inverted: true });
y.toPx(25); // domain value → pixel offset; y.ticks holds the tick values| Package | Headline exports |
|---|---|
@oge-ui/scheduler-engine | parseRecurrenceRule , serializeRecurrenceRule , expandRecurrence , OgeSchedulerCore ; iCalendar export entry |
@oge-ui/gantt-engine | scheduleGanttProject , criticalPathKeys , autoScheduleForward , buildGanttScale ; Excel, PDF, image and MS Project export entries |
@oge-ui/bpmn-engine | readBpmnXml , writeBpmnXml , renderDiagramSvg , toBpmnJson / fromBpmnJson , OgeBpmnEditorCore |
@oge-ui/charts-engine | niceTicks , createLinearScale , buildCartesianData , buildCartesianScene , buildPieScene ; image and PDF export entries |
@oge-ui/kanban-engine | normalizeCards , groupBoard , kanbanColumnWip , computeColumnWindow |
@oge-ui/pivot-engine | toChartSeries , OgePivotGridCore , OgePivotStateCore ; Excel and PDF export entries |
Licensing
@oge-ui/coreand@oge-ui/behaviorare MIT, and will stay MIT.- The six
@oge-ui/<family>-enginepackages carry their family's commercial licence, exactly like the family's Angular and React packages: free for evaluation, development and testing; production use needs a paid licence — using the engine without the components is still use of the family. - There is no licence key, runtime check, watermark or network call in any package; the licence is enforced by its text (ADR 0003). Terms: Licensing.