cygtut
v1.0.4
Published
JSON-driven tutorial overlay engine for web applications
Readme
CygTut
CygTut is a high-level tutorial engine built on top of cyghtml 0.1.2.
It owns tutorial meaning, runtime state, target lookup, anchors, grouping, command flow, and compilation.
It does not own low-level DOM rendering. Final overlay DOM/CSS is rendered by cyghtml.
Status
This README describes the target CygTut specification.
The runtime is currently moving toward this shape. Some existing source files still reflect the earlier command surface, but this document is the reference for where the public authoring model should go next.
Philosophy
- CygTut is a tutorial engine first.
- CygTut should reuse as much of
cyghtmlas possible. - The host page should stay untouched.
- Tutorial UI should render above the host page as overlay DOM.
- Target-based placement is the primary positioning model.
- Final rendering should always compile into ordinary HTML/CSS.
- State is persistent by default.
- Step-like progression should be expressed as named execution flow, not hidden screen replacement.
- Removal and reset should be explicit commands.
- Escape hatches for custom HTML/CSS should exist.
Relationship To cyghtml
The intended architecture is:
cygtut authoring script
-> cygtut runtime state
-> cygtut compile step
-> final cyghtml document
-> cyghtml renders overlay DOMCygTut owns:
- tutorial runtime state
- tutorial variables
- commands
- trigger lifecycle
- target selection
- target anchor and self anchor resolution
- object presets and tutorial object meaning
- grouping rules
- final position and size calculation
- device-aware tutorial branching
- conversion into a final
cyghtmldocument
cyghtml owns:
- overlay root mounting
- final top-level
vars,css, andhtmlrendering - DOM creation from
tagormarkup attrs,style,text, anditems- parent-child composition
- delayed parent resolution
- runtime event hookup
- node patching and final DOM commit
Core Runtime Model
The most important concept in CygTut is the overlay root.
The host page already exists. CygTut does not rewrite it. Instead, it creates overlay objects above that page.
Those overlay objects are placed like this:
- select a target element on the host page
- read the target's current rectangle
- choose a target anchor and self anchor
- compute final absolute overlay coordinates
- compile the result to
cyghtml - let
cyghtmlrender the overlay DOM
Authoring Model
Top-Level Shape
{
"meta": {},
"vars": {},
"boot": [],
"flow": {}
}Top-level keys:
meta: runtime and authoring settingsvars: initial variablesboot: execution nodes that run at startupflow: named execution blocks
Why flow
CygTut no longer needs separate first-class steps and scenes as core language concepts.
Both are just named execution blocks.
This means authors may use names like:
introloginstep-1done
All of them live under flow.
meta and active flow state
meta should not implicitly execute a flow block.
It should only define startup bindings and initial runtime state.
Recommended direction:
{
"meta": {
"bindings": {
"activeStage": {
"path": "global.flow",
"initial": "intro"
}
}
}
}Meaning:
activeStageis a friendly authoring namepathtells CygTut which runtime variable it binds toinitialsets the initial value of that variable at startup- this does not automatically execute
flow.intro
If authors want the initial named flow block to run, that should happen explicitly through boot or through a trigger.
Example using boot:
{
"meta": {
"bindings": {
"activeStage": {
"path": "global.flow",
"initial": "intro"
}
}
},
"boot": [
{
"type": "RUN",
"path": "flow.intro"
}
]
}Example using a trigger command:
{
"boot": [
{
"type": "ADD_TRIGGER",
"id": "activate-intro",
"watch": ["global.flow"],
"if": "global.flow === 'intro'",
"commands": [
{ "type": "RUN", "path": "flow.intro" },
{ "type": "REMOVE_TRIGGER", "id": "activate-intro" }
]
}
]
}This keeps startup state and flow execution separate, which makes the model easier to reason about.
Execution Nodes
CygTut uses two execution node shapes.
1. Action Node
An action node runs one command.
{
"type": "SET_VAR",
"key": "global.step",
"value": 2,
"if": "global.ready === true"
}Fields:
type: requiredif: optional condition- all other keys depend on the command type
2. Block Node
A block node groups commands and may gate them behind one condition.
{
"if": "global.ready === true",
"commands": [
{
"type": "SET_VAR",
"key": "global.step",
"value": 2
}
]
}Fields:
commands: requiredif: optional condition
Common Rules
ifmeans ��execute only if this expression is truthy��.- if a block node
ifis false, all child commands are skipped. - if an action node
ifis false, only that command is skipped. - block nodes may contain action nodes and other block nodes.
- nesting is allowed.
Conditions
CygTut conditions use string expressions.
Recommended operators:
&&||!===!==>>=<<=- parentheses
()
Example:
{
"if": "global.ready === true && global.device === 'mobile'"
}Flow Execution
Named flow blocks are executed through RUN.
{
"type": "RUN",
"path": "flow.login"
}Meaning:
- resolve the named flow block
- execute its nodes in order
Trigger Lifecycle As Commands
Triggers are not special top-level magic. They are runtime objects that may be added, updated, and removed through commands.
ADD_TRIGGER
{
"type": "ADD_TRIGGER",
"id": "login-ready",
"watch": ["global.ready"],
"if": "global.ready === true",
"commands": [
{
"type": "RUN",
"path": "flow.after-login"
}
]
}UPDATE_TRIGGER
{
"type": "UPDATE_TRIGGER",
"id": "login-ready",
"patch": {
"if": "global.ready === true && global.user !== null"
}
}REMOVE_TRIGGER
{
"type": "REMOVE_TRIGGER",
"id": "login-ready"
}Trigger fields:
id: unique trigger idwatch: watched variable pathsif: optional conditioncommands: execution nodes to run when the trigger fires
Command Reference
Object Commands
ADD_OBJECT
{
"type": "ADD_OBJECT",
"object": { ... }
}Adds one tutorial object to runtime state.
UPDATE_OBJECT
{
"type": "UPDATE_OBJECT",
"id": "tip-1",
"patch": { ... }
}Updates part of an existing object.
REMOVE_OBJECT
{
"type": "REMOVE_OBJECT",
"id": "tip-1"
}Removes one object.
Group Commands
CREATE_GROUP
{
"type": "CREATE_GROUP",
"object": {
"id": "group-1",
"type": "group"
}
}ADD_TO_GROUP
{
"type": "ADD_TO_GROUP",
"id": "tip-text",
"groupId": "group-1"
}REMOVE_FROM_GROUP
{
"type": "REMOVE_FROM_GROUP",
"id": "tip-text"
}UNGROUP
{
"type": "UNGROUP",
"id": "group-1"
}Variable Commands
SET_VAR
{
"type": "SET_VAR",
"key": "global.step",
"value": 2
}INC_VAR
{
"type": "INC_VAR",
"key": "global.count",
"value": 1
}TOGGLE_VAR
{
"type": "TOGGLE_VAR",
"key": "global.open"
}Flow Commands
RUN
{
"type": "RUN",
"path": "flow.next"
}EMIT
{
"type": "EMIT",
"name": "openModal",
"payload": {
"id": "login"
}
}Purpose:
- bridge tutorial runtime to host application callbacks
Clear Commands
CLEAR_ALL
{
"type": "CLEAR_ALL"
}CLEAR_GROUP
{
"type": "CLEAR_GROUP",
"id": "group-1"
}Why WAIT_UNTIL Is Not Core
The preferred model is:
- immediate conditional execution through
if - delayed state reaction through trigger commands
Because of that, WAIT_UNTIL is not part of the core spec.
If it ever returns, it should be treated as optional authoring sugar rather than a central control-flow primitive.
Persistent Runtime Model
CygTut is persistent by default.
That means:
- objects stay alive unless explicitly removed
- moving to another named flow block does not automatically clear old objects
- updates should modify existing objects when ids stay stable
- hard reset should happen only through explicit clear or remove commands
This makes it possible to:
- keep a tooltip alive across progression
- update only text or style in the next flow block
- move or restyle an object without recreating everything
Tutorial Object Model
Every tutorial object starts from a common shape.
{
"id": "object-id",
"type": "tooltip",
"target": ".login-button",
"targetAnchor": "bottom",
"selfAnchor": "top",
"offset": { "x": 0, "y": 12 },
"autoPlacement": true,
"x": "0px",
"y": "0px",
"width": "280px",
"height": "120px",
"minWidth": "200px",
"minHeight": "80px",
"maxWidth": "400px",
"maxHeight": "240px",
"className": "custom-class",
"attrs": {},
"style": {},
"vars": {},
"events": {},
"triggers": [],
"animation": {},
"groupId": "group-1"
}Common Fields
Required:
idtype
Placement:
targettargetAnchorselfAnchoroffsetautoPlacementxy
Size:
widthheightminWidthminHeightmaxWidthmaxHeight
Presentation:
classNameattrsstyle
Behavior:
varseventstriggersanimation
Grouping:
groupId
Length Units
The following fields should support multiple length styles:
xywidthheightminWidthminHeightmaxWidthmaxHeight
Allowed forms:
- number
"120px""50%""auto"
Future-safe optional forms:
vwvhremem
Example:
{
"x": "50%",
"y": "24px",
"width": "320px",
"height": "auto"
}Positioning Rules
Target-Based Placement
{
"target": ".login-button",
"targetAnchor": "bottom",
"selfAnchor": "top",
"offset": { "x": 0, "y": 12 },
"autoPlacement": true
}Rules:
targetmeans target-based placement is active- target-following is always on for target-based objects
autoPlacementis optional and only adds fallback placement behavior
Absolute Placement
{
"x": "120px",
"y": "80px"
}Rules:
- if
targetis absent, direct coordinates are used
Object Types
tooltip
{
"id": "tip-1",
"type": "tooltip",
"title": "Login Guide",
"text": "Click the login button to continue."
}highlight
{
"id": "hl-1",
"type": "highlight",
"target": ".login-button",
"padding": 8
}text
{
"id": "text-1",
"type": "text",
"text": "Hello"
}button
{
"id": "btn-1",
"type": "button",
"text": "Next"
}mask
{
"id": "mask-1",
"type": "mask"
}group
{
"id": "group-1",
"type": "group"
}image
{
"id": "img-1",
"type": "image",
"src": "/hero.png",
"alt": "Hero"
}progress
{
"id": "progress-1",
"type": "progress",
"value": 2,
"max": 5
}custom
custom is the escape hatch for author-authored HTML.
{
"id": "custom-1",
"type": "custom",
"markup": "<div class='panel'><strong>Hello</strong><button>Next</button></div>",
"style": {
"left": "120px",
"top": "80px",
"position": "absolute"
}
}Rules:
customshould acceptmarkupmarkupshould contain exactly one root element- common object fields such as
target,style,attrs,events,animation, andgroupIdstill apply - built-in types remain semantic presets, while
customis render-oriented
Events
Object events are command arrays.
{
"events": {
"onClick": [
{
"type": "SET_VAR",
"key": "global.step",
"value": 2
}
]
}
}Rules:
- keys use DOM-style names like
onClick,onMouseEnter,onMouseLeave - values are execution node arrays
- final DOM event hookup is handled by
cyghtml
Object-Local Trigger Rules
Objects may still carry local trigger definitions.
{
"id": "next-btn",
"type": "button",
"vars": {
"clicks": 0
},
"events": {
"onClick": [
{ "type": "INC_VAR", "key": "object.clicks", "value": 1 }
]
},
"triggers": [
{
"watch": ["object.clicks"],
"if": "object.clicks >= 1",
"commands": [
{ "type": "SET_VAR", "key": "global.step", "value": 2 }
]
}
]
}Animation Model
Animation is object-level and CSS-oriented.
Recommended shape:
{
"animation": {
"enter": {
"preset": "fade-in"
},
"loop": {
"custom": {
"animation": "pulse 1.2s ease-in-out infinite"
}
}
}
}Phases
enterloop
Preset examples
fade-inslide-upslide-downslide-leftslide-rightscale-inpulserotate
Custom examples
animationtransitiontransformopacity
The compiler should resolve presets into final CSS values and emit any needed support styles into the final cyghtml document.
Patch Shape
UPDATE_OBJECT.patch is partial.
{
"text": "New text",
"title": "New title",
"width": "320px",
"style": {
"background": "#111827"
},
"animation": {
"enter": { "preset": "fade-in" }
}
}Rules:
- patch only changes provided fields
- unspecified fields remain unchanged
- changing
idortypethrough a patch is discouraged
Trigger Behavior
Triggers are still state-driven reactions.
Normal trigger model:
- a watched variable changes
- matching triggers are re-evaluated
- commands run
- tutorial object state changes
- a new
cyghtmldocument is compiled - the overlay is patched or rerendered
Target Lifecycle
Target handling is central to CygTut.
Current intended behavior:
- if a target exists, compute geometry from its current
getBoundingClientRect() - target-following is always on for target-based objects
- if
autoPlacementis enabled, try fallback anchor combinations when the first placement would leave the viewport - if a target does not exist yet, skip rendering that object for the current pass
- if a target appears later, the next render pass may resolve it
- if the host layout changes, recompute geometry and render again
Current runtime direction:
- rerender on
resize - rerender on
scroll - observe known target elements with
ResizeObserverwhen available - observe host-page DOM changes with
MutationObserverwhen available - schedule repeated render requests to avoid excessive recompilation
Final Render Shape
The compiled result should be a plain cyghtml document.
{
"vars": {
"--tip-left": "120px",
"--tip-top": "240px"
},
"css": {
".cygtut-tooltip": {
"position": "absolute",
"left": "var(--tip-left)",
"top": "var(--tip-top)",
"width": "280px",
"padding": "16px",
"borderRadius": "14px",
"background": "#111827",
"color": "#ffffff"
}
},
"html": [
{
"tag": "div",
"attrs": {
"id": "login-tooltip",
"class": "cygtut-tooltip"
},
"appendTo": "root",
"items": [
{
"tag": "h3",
"text": "Login Guide"
},
{
"tag": "p",
"text": "Click the login button to continue."
}
]
}
]
}Processing Pipeline
authoring script
-> normalize
-> runtime state
-> commands and trigger lifecycle
-> target and anchor resolution
-> compile to cyghtml document
-> cyghtml patches or rerenders DOMPublic Runtime API
The public runtime shape is:
const tut = new CygTut({ script })Current runtime surface should expose:
start()destroy()mount(target?)mountById(id)mountByClass(className)mountByTag(tagName)unmount()run(path)getVar(path)setVar(path, value)updateVars(record)getSnapshot()getRendererDocument()subscribe(listener)
Recommended Usage
Vanilla
import { CygTut } from "cygtut";
const tut = new CygTut({ script });
tut.mountById("app");
await tut.start();
tut.setVar("global.step", 2);
console.log(tut.getVar("global.step"));
tut.destroy();React
import { useRef } from "react";
import { CygTut, type CygTutHandle } from "cygtut/react";
export default function App() {
const tutRef = useRef<CygTutHandle>(null);
return (
<>
<CygTut ref={tutRef} script={script} />
<button onClick={() => tutRef.current?.setVar("global.step", 2)}>
next
</button>
</>
);
}Summary
CygTut should be a tutorial engine above cyghtml.
It should:
- keep tutorial meaning and runtime state
- resolve target-based placement
- compile groups into real parent nodes
- express final animation as CSS
- compile the final result into a
cyghtmldocument - let
cyghtmlrender that document
Low-level overlay rendering belongs in cyghtml.
High-level tutorial meaning belongs in cygtut.
