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

PackageIsExports
@lyotic/tokensFoundation and semantic presentation tokens, and the map from each state value to a role and glyphfoundation, presentation, stateMap; dist/lyotic.css; dist/tokens.json (DTCG)
@lyotic/coreThe EBSRC contract: ontology, Representation Resolver, invariant checksresolve, validateAtExecution, checkPlan, checkDom, AXES, MODES, authorityState
@lyotic/morphThe motion layer: one surface across representationsMorphShell, Spring, SPRINGS, enter, exit, stagger, settleLit, flip, pressable, EASE, DUR
@lyotic/componentsSemantic components rendered from a planWorkObjectShell, statusMark, sourceRecords, claimBlock, interpretationBlock, alternativeSet, decisionRecord, impactPreview, authorityGate, executionTrace, recoveryPanel, verificationResult, handoffPacket, historyTimeline, <ly-status>

Division of responsibility

PartOwns
Your applicationDomain objects, the Case, the execution boundary, who approved what
AdaptersDeclared read, write, version and outcome capabilities per system
@lyotic/coreChecks that inputs meet representation requirements; resolves the plan
@lyotic/componentsAccessible rendering of the plan, nothing more
The harnessThat 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