@lumen-mirror/core
v1.0.2
Published
Signal-driven runtime for **smart mirror** apps: TypeScript modules, HTML templates, shadow DOM, and shared features (typed state + events). Built for Electron + modern Chromium — small surface, no Zone.js, no inputs/outputs, no SPA router.
Readme
@lumen-mirror/core
Signal-driven runtime for smart mirror apps: TypeScript modules, HTML templates, shadow DOM, and shared features (typed state + events). Built for Electron + modern Chromium — small surface, no Zone.js, no inputs/outputs, no SPA router.
npm install @lumen-mirror/coreScaffold with @lumen-mirror/cli (lumen new).
What you get
| | |
| --------------------- | -------------------------------------------------------------------------------------------------------------- |
| Own signals | signal / computed / createEffect / fromHttp via @chocosd/node-signals |
| Features | Shared state + typed dispatch / on events (createFeature + injectFeature) |
| Modules | @Module classes → custom elements with shadow DOM + scoped styles |
| Templates | {{ }}, @if / @else, @for / @in, (click)="…", nested <ChildModule /> |
| Attribute binding | [attr.*] + host.setAttribute with a deny-by-default allowlist |
| Security first | Closed expression grammar, textContent interpolations, method-only handlers, URL scheme checks on href/src |
| Groups | <GroupModule> projects members into one grid slot (auto-persisted selection) |
| Focus + layout | Full-screen focus mode; typed CSS grid via defineLayout |
| Timers | trackInterval / trackTimeout paused on focus hide / group deactivate |
| Error isolation | One widget throw does not abort the mirror |
| Small surface | ESM-only public API; renderer/binder/bus stay internal |
| No config tax | CLI owns Vite — apps do not ship a vite.config.ts |
Intentionally not included: Angular-style inputs/outputs, Zone.js, full DI, virtual DOM, SPA routing, plugin marketplace.
Features — state and events
A feature is a single shared instance: signal state for ongoing values, typed events for one-shots.
import {
createFeature,
emptyProps,
props,
signal,
computed,
injectFeature,
} from '@lumen-mirror/core';
export const clockFeature = createFeature({
source: 'clock',
events: {
tick: props<{ time: string }>(),
opened: emptyProps(),
},
state: () => {
const format = signal<'12' | '24'>('12');
return {
format,
label: computed(() => `fmt-${format()}`),
};
},
});
// In any module:
private readonly clock = injectFeature(clockFeature);
onInit() {
this.clock.state.format.set('24'); // shared state
this.clock.dispatch.opened(); // typed event
this.clock.on.tick(({ payload }) => { … }); // subscribe (auto-disposed)
}Prefer feature.state.* for fields siblings share. Use events for notifications (alarm fired, place selected) — not to mirror state into other modules.
Quick start
import { bootstrap } from '@lumen-mirror/core';
import appConfig from './lumen.config.js';
import { ClockModule, WeatherModule } from './modules/index.js';
await bootstrap({
root: '#app',
modules: [ClockModule, WeatherModule],
config: appConfig,
});import { LumenModule, Module, signal } from '@lumen-mirror/core';
@Module({
templateUrl: './clock.lumen.html',
styleUrls: ['./clock.lumen.scss'],
host: { classes: ['clock'] },
})
export class ClockModule extends LumenModule {
time = signal('12:00');
onInit() {
this.trackInterval(() => {
this.time.set(new Date().toLocaleTimeString());
}, 1000);
}
}<section>
<h1>{{ time }}</h1>
</section>Modules
| Hook / field | When |
| ------------------------------------- | -------------------------------------------------- |
| onInit | After host + shadow root exist, before first paint |
| onMount | Connected to the document |
| onPause / onResume | Focus hide / group deactivate |
| onDestroy | Disconnected |
| this.host | Classes, attributes, styles on the light-DOM host |
| this.trackInterval / trackTimeout | Timers that pause/clear with the module |
Nest children with imports + tags. Share a grid cell with projection:
<GroupModule>
<WelcomeHomeModule label="Welcome" />
<QuotesModule label="Quotes" />
</GroupModule>Templates
<p>{{ weather().current.time }}</p>
@if (showDetails) {
<p>{{ details }}</p>
} @else {
<p>Hidden</p>
} @for (todo of todos; track todo.id) {
<li>{{ todo.label }}</li>
}
<button (click)="openSettings()">Settings</button>
<img [attr.src]="iconUrl" alt="" />- Interpolations unwrap signals; no arbitrary method calls in
{{ }}. - Handlers may call methods and pass
$event, literals, or scope paths. - The CLI validates templates at transform time (
file:line:column).
Signals
import { signal, computed, fromHttp, batch } from '@lumen-mirror/core';
temp = signal(72);
label = computed(() => `${this.temp()}°F`);
forecast = fromHttp<Forecast>(url, { params: this.coords });Also: createEffect, untracked, from, operators (distinctUntilChanged, debounceTime, …), subscribeSignal, isShallowEqual for list view equality.
Layout, focus, security
import { defineLayout, defineLumenConfig } from '@lumen-mirror/core';
export default defineLumenConfig({
layout: defineLayout(4, 4, [
['clock', 'clock', '.', 'weather'],
['welcome', 'welcome', 'welcome', 'welcome'],
['welcome', 'welcome', 'welcome', 'welcome'],
['news-feed', 'news-feed', 'news-feed', 'news-feed'],
] as const),
// optional — replaces the default [attr.*] allowlist
security: {
attributeAllowlist: ['href', 'src', 'aria-*', 'data-*', 'viewBox', 'd'],
},
});import {
requestFocusMode,
exitFocusMode,
getLayout,
getDefaultLayout,
} from '@lumen-mirror/core';
requestFocusMode('clock');
exitFocusMode();
getLayout().set(getDefaultLayout());Dangerous attribute names (on*, srcdoc, …) are always blocked. javascript: / vbscript: / data:text/html are rejected on URL-ish attrs.
Public API surface
Import from @lumen-mirror/core only. Deep paths into the package are not part of the contract.
| Entry | For |
| -------------------------------------------- | ----------------------------------------------------------- |
| @lumen-mirror/core | Apps — bootstrap, modules, features, signals, layout, focus |
| @lumen-mirror/core/template | CLI — template validation |
| @lumen-mirror/core/project | CLI — lumen.json helpers |
| @lumen-mirror/core/platform/electron/shell | Electron dev shell |
Package details
- ESM only
- Peer environment — browser DOM (
document,fetch) - Dependency —
@chocosd/node-signals - Ship size — published
distis on the order of a few hundred KB unminified (tree-shake what you use)
npm install -g @lumen-mirror/cli
lumen new my-mirror