Native bridge contracts (Capacitor adapters) live in:
docs/contracts/native-lifecycle.mddocs/contracts/native-back.mddocs/contracts/native-keyboard.mddocs/contracts/native-deeplink.mddocs/contracts/native-statusbar.md
@fstage/env detects the runtime environment and provides a layered policy system for platform-adaptive behaviour.
import { getEnv } from '@fstage/env';
const env = getEnv({ preset: null }); // preset: 'ios' | 'android' | '' for testingReturns a cached environment object per UA/preset combination.
Returns a snapshot of detected environment facts:
const facts = env.getFacts();
// {
// os: 'ios' | 'android' | 'windows' | 'mac' | '',
// deviceClass: 'mobile' | 'desktop',
// isBrowser: bool,
// isNative: bool, // Capacitor native app
// isNode: bool,
// isWorker: bool,
// isStandalone: bool, // display-mode: standalone or navigator.standalone
// isPwa: bool, // isStandalone && !isNative
// touch: bool,
// notifications: bool,
// serviceWorker: bool,
// host: string, // protocol + hostname
// basePath: string,
// nativeEngine: 'capacitor' | '',
// preset: string,
// userAgent: string,
// }Policy is a structured object of platform-adaptive values — motion timing, gesture thresholds, transition keyframes. Built-in defaults exist for default, ios, and android platforms.
// Register app-level overrides (lower priority number = applied first)
env.registerPolicy({
gestures: { edgePan: { enabled: true } },
motion: { duration: { normalMs: 300 } },
}, 100);
const policy = env.getPolicy();
env.getPolicy('motion.duration.normalMs'); // 300Applies platform data attributes and policy CSS variables to the document element:
env.applyToDoc(); // call once in afterLoadLibsAdds data-platform, data-hybrid, data-standalone, tracks virtual keyboard height as --keyboard-height, and writes all policy scalars as --policy-* CSS variables.
@fstage/animator is a WAAPI-based animation engine driven entirely by policy values.
import { createAnimator } from '@fstage/animator';
const animator = createAnimator({
motion: policy.motion, // { duration: { normalMs }, easing, reduced? }
});Animate a single element using a named preset or inline keyframes:
animator.animate(el, 'slideUp');
animator.animate(el, 'fadeIn', { durationFactor: 0.5, delay: 100 });
animator.animate(el, { from: [{ opacity: 0 }], to: [{ opacity: 1 }] });
// returns { finished: Promise, cancel: fn }Available built-in presets: fadeIn, fadeOut, slideUp, slideDown, slideInLeft, slideInRight, slideOutLeft, slideOutRight, scaleIn, scaleOut, pop, slideUpSheet, slideDownSheet, tabBounce, tabPillIn, taskComplete.
Add custom presets by extending ANIMATION_PRESETS:
import { ANIMATION_PRESETS } from '@fstage/animator';
ANIMATION_PRESETS.myEffect = {
from: [{ transform: 'scale(0.8)', opacity: 0 }],
to: [{ transform: 'scale(1)', opacity: 1 }],
};Screen-to-screen transition animation (used internally by transitions):
const handle = animator.start({
from: fromEl,
to: toEl,
direction: 'forward', // 'forward' | 'back'
transition: policy.transitions.pageNavigation,
interactive: false, // true = returns progress/commit/cancel
});
// handle: { finished, destroy, [progress, commit, cancel] }animator.createToggle(spec) — boolean-state show/hide controller, used internally by the animate.toggle component block.
animator.flip(mutationFn, targets, opts?) — FLIP animation around a DOM mutation.
animator.stagger(els, preset, opts?) — staggered collection animation.
animator.collapse(el, opts?) — animate height/opacity to zero.
@fstage/transitions orchestrates page-level view transitions on top of the animator.
Manages the concept of a "screen" — the currently active page region. Used by the component runtime for activated/deactivated hooks.
import { createScreenHost } from '@fstage/transitions';
const screenHost = createScreenHost({ name: config.name });
screenHost.start(rootEl);
const off = screenHost.on('activate', (e) => { /* e.target = screen el */ });
const off = screenHost.on('deactivate', (e) => { /* e.target = screen el */ });import { createTransitionEngine } from '@fstage/transitions';
const transitions = createTransitionEngine({ animator, screenHost });
// Run a transition to a route
transitions.run(route, {
transition: policy.transitions.pageNavigation,
interactive: false,
});screenHostInteraction and accompanyInteraction are named exports from @fstage/transitions. Wire them into the interactions manager so components can use 'screen.*' and 'transition.*' interaction keys:
import {
screenHostInteraction,
accompanyInteraction,
} from '@fstage/transitions';
interactionsManager.extend('screen', screenHostInteraction(screenHost));
interactionsManager.extend('transition', accompanyInteraction(screenHost));This enables interaction keys like 'screen.activate' and 'screen.deactivate' in component interaction maps. accompanyInteraction additionally manages visibility, aria-hidden, and inert for elements that accompany a screen transition.
@fstage/gestures provides unified touch/pointer gesture detection.
import { createGestureManager } from '@fstage/gestures';
const gestureManager = createGestureManager({
policy: policy.gestures,
});
gestureManager.start(appEl);
gestureManager.stop();Register a gesture listener. Returns an unregister function.
gestureManager.on('swipeLeft', {
target: el,
handler: function(e) { /* e.direction, e.velocity, e.distance */ },
});
gestureManager.on('edgePan', {
target: appEl,
edge: 'left',
shouldStart: function(e) { return !document.querySelector('.sheet.is-open'); },
onStart: async function(e) {
e.ctl = await transitions.run(prevRoute, { interactive: true });
},
onProgress: function(e) { e.ctl.progress(e.progress); },
onCommit: async function(e) { await e.ctl.commit(); router.go(-1); },
onCancel: function(e) { e.ctl.cancel(); },
});Supported gesture types: swipe (with directions option), longPress, tap, edgePan.
Wires gestures as a component interaction extension:
import { gestureInteraction } from '@fstage/gestures';
interactionsManager.extend('gesture', gestureInteraction(gestureManager));Enables 'gesture.swipeLeft(.selector)' and similar keys in component interaction maps.
@fstage/interactions provides delegated DOM event handling with debounce/throttle, and an extension system for gesture and transition integrations.
import { createInteractionsManager } from '@fstage/interactions';
import { gestureInteraction } from '@fstage/gestures';
import { screenHostInteraction, accompanyInteraction } from '@fstage/transitions';
const interactionsManager = createInteractionsManager();
interactionsManager.extend('gesture', gestureInteraction(gestureManager));
interactionsManager.extend('screen', screenHostInteraction(screenHost));
interactionsManager.extend('transition', accompanyInteraction(screenHost));Pass to createRuntime — the component runtime calls interactionsManager.activate(def.interactions, ctx) on first render for each component instance.
interactions: {
'click(.btn)': fn, // DOM delegation
'input(.search)': { handler: fn, debounce: 300 },
'keydown(.field)': { handler: fn, keys: ['Enter', 'Escape'], prevent: true },
'click(document)': fn, // document event
'resize(window)': { handler: fn, throttle: 100 },
'gesture.swipeLeft(.row)': fn, // gesture extension
'screen.activate': fn, // screen host extension
}debounce and throttle are mutually exclusive on a single entry.