Overview
Develop
Four packages, plain ES modules and CSS. The core is pure and deterministic. The components are functions from data to DOM, so they sit inside React, Vue, Svelte or a server template equally well.
Packages
| Package | Is | Exports |
|---|---|---|
@lyotic/tokens | Foundation and semantic presentation tokens, and the map from each state value to a role and glyph | foundation, presentation, stateMap; dist/lyotic.css; dist/tokens.json (DTCG) |
@lyotic/core | The EBSRC contract: ontology, Representation Resolver, invariant checks | resolve, validateAtExecution, checkPlan, checkDom, AXES, MODES, authorityState |
@lyotic/morph | The motion layer: one surface across representations | MorphShell, Spring, SPRINGS, enter, exit, stagger, settleLit, flip, pressable, EASE, DUR |
@lyotic/components | Semantic components rendered from a plan | WorkObjectShell, statusMark, sourceRecords, claimBlock, interpretationBlock, alternativeSet, decisionRecord, impactPreview, authorityGate, executionTrace, recoveryPanel, verificationResult, handoffPacket, historyTimeline, <ly-status> |
Division of responsibility
| Part | Owns |
|---|---|
| Your application | Domain objects, the Case, the execution boundary, who approved what |
| Adapters | Declared read, write, version and outcome capabilities per system |
@lyotic/core | Checks that inputs meet representation requirements; resolves the plan |
@lyotic/components | Accessible rendering of the plan, nothing more |
| The harness | That the contract holds across inputs and events, on the plan and on the DOM |
The resolver
import { resolve, validateAtExecution } from '@lyotic/core';
const plan = resolve({
case, objects, role, sources, observations, claims, interpretations,
proposals, alternatives, decision, executions, verifications, preconditions,
adapters, control, policy,
view: { mode: 'compact', request: null }, // what the person is looking at now
});
plan.mode // 'compact' | 'compare' | 'decide' | 'authorize' | 'execute' | 'verify' | 'recover' | 'handoff'
plan.systemMode // what the work calls for, even when the person chose another mode
plan.attention // { level: 'none' | 'requested', reason }
plan.mustShow // disclosures the renderer may not omit
plan.controls // [{ action, allowed, reason, boundVersion }], disallowed ones explained
plan.forbidden // completion words not allowed now
plan.reason // one sentence: why this form, tied to the state change
plan.announce // [{ politeness, text }] for the live region
The resolver has no clock, no randomness and no model call. The same revision, role, policy and view give a deep-equal plan (INV-14). A control in a plan is not a standing right: at the moment of execution, call validateAtExecution(state, actionId, boundVersion) and refuse on any reason it returns.
Rendering
import { WorkObjectShell } from '@lyotic/components';
const shell = new WorkObjectShell(host);
shell.render(state, plan, { initiator: 'system' }); // background update: never moves focus
shell.render(state, plan, { initiator: 'person' }); // the person asked: focus follows them
host.addEventListener('ly-action', (e) => { /* e.detail: { action, boundVersion } */ });
host.addEventListener('ly-explore', (e) => { /* person asked to expand or collapse */ });
host.addEventListener('ly-mode', (e) => { /* { from, to, reason } */ });
Same-mode updates keep in-progress input and focus by key and light the statuses that changed (INV-09, MOT-06). Mode changes morph one surface (MOT-01) and announce the plan’s reason politely.
In React
function WorkObject({ state, onAction }) {
const ref = useRef(null);
const shell = useRef(null);
useEffect(() => { shell.current = new WorkObjectShell(ref.current); return () => shell.current.morph.destroy(); }, []);
useEffect(() => { shell.current.render(state, resolve(state)); }, [state]);
useEffect(() => {
const on = (e) => onAction(e.detail);
ref.current.addEventListener('ly-action', on);
return () => ref.current?.removeEventListener('ly-action', on);
}, [onAction]);
return <div ref={ref} />;
}
The invariant harness
Every invariant has a check. What a check cannot observe is reported as a coverage limit, never as a pass.
import { checkPlan, checkDom } from '@lyotic/core';
const a = checkPlan(state, plan); // semantic: completion, authority, versions, partial outcomes…
const b = checkDom(host, plan); // rendered: operable controls ⊆ plan, forbidden words, glyph + label, disclosures
[...a.violations, ...b.violations] // [{ id: 'INV-03', message }]
[...a.limits, ...b.limits] // what could not be checked
Run npm test for the reference case as a suite. The docs site runs checkDom live on every step of the 5,400 / 1,620 walk.
Tokens
node scripts/build-tokens.mjs # writes packages/tokens/dist/lyotic.css and tokens.json
Edit packages/tokens/src/tokens.js only. The CSS and the DTCG JSON are generated. State colours are never authored by hand in components: a component sets data-ly-<axis>="<value>" and the generated rules give it --ly-fg, --ly-fill, --ly-stroke and --ly-line.
Repository layout
docs/spec/00-definition.ko.md # the canonical definition (wins over everything) docs/spec/01-model.md # the buildable model: IDs every file cites packages/tokens # layer 1 and 2 packages/core # layer 4: resolver, invariants, fixtures, tests packages/morph # motion packages/components # semantic components site/ # this documentation, built from the packages