Universal interface intelligence engine unifying real environment signals into a single immutable snapshot for explicit, predictable layout decisions across every screen.
npm install @trap_stevo/axisresolve() and resolveString()
window remains unavailable |
visualViewport when available; graceful fallback otherwise |
env(safe-area-inset-*) when supported |
ts
axis.orientation; // "portrait" | "landscape"
`
$3
`ts
axis.input; // "touch" | "pointer" | "hybrid"
`
$3
`ts
axis.tier; // "compact" | "comfortable" | "expanded" | "cinema"
axis.tierRaw; // raw tier before normalization
`
$3
`ts
axis.aspect.ratio;
axis.aspect.shape;
`
$3
`ts
axis.profile.id; // TouchCompactTall
`
---
π Axis Type Reference
$3
| Member | Type | Description |
|---|---|---|
| snapshot() | () => AxisSnapshot | Returns current immutable snapshot |
| subscribe(handler) | (axis:AxisSnapshot) => () => void | Subscribes to snapshot updates |
| setProfileOverride(id) | (AxisProfileID \| null) => void | Forces a profile identity |
| getProfileOverride() | () => AxisProfileID \| null | Reads override state |
| setResolver(source) | (AxisResolveSource \| null) => void | Installs a resolver source |
| getResolver() | () => AxisResolveSource \| null | Reads resolver source |
| destroy() | () => void | Removes listeners and subscriptions |
---
AxisSnapshot
| Field | Type | Description |
|---|---|---|
| caps | AxisCaps | Capability signals |
| orientation | "portrait" \| "landscape" | Screen orientation |
| viewportWidth | number | Layout viewport width |
| viewportHeight | number | Layout viewport height |
| shortSide | number | Shortest viewport edge |
| input | "touch" \| "pointer" \| "hybrid" | Derived input mode |
| tierRaw | "compact" \| "comfortable" \| "expanded" \| "cinema" | Raw tier (pre-normalization) |
| tier | "compact" \| "comfortable" \| "expanded" \| "cinema" | Normalized tier |
| aspect | AxisAspect | Ratio + shape helpers |
| layoutMode | "stack" \| "split" | Primary layout mode |
| profile | AxisProfile | Canonical identity |
| scale | AxisScale | Measurement helpers |
| viewport | AxisViewport | Layout + visual viewport reality |
| safeArea | AxisSafeArea | Safe-area insets |
| insets | AxisInsets | Content padding and gutters |
| controls | AxisControls | Ergonomic sizing helpers |
| density | AxisDensity | Density factor + override |
| container | AxisContainer | Container sizing helpers |
| grid | AxisGrid | Grid helpers |
| type | AxisType | Typography helpers |
| ui | AxisUI | UI runtime helpers derived from the snapshot |
| resolve(key) | (key:string) => AxisResolveValue | Resolver-driven value |
| resolveString(key,fallback) | (key:string,fallback:string)=>string | Resolver string helper |
| isProfile(profiles) | (AxisProfileID \| AxisProfileID[]) => boolean | Profile identity check |
| only(profiles,fn) | (AxisProfileID \| AxisProfileID[], fn:()=>void)=>void | Execute for matching profiles |
| unless(profiles,fn) | (AxisProfileID \| AxisProfileID[], fn:()=>void)=>void | Execute for non-matching profiles |
| onlyIf(predicate,fn) | (predicate:(axis)=>boolean, fn:()=>void)=>void | Predicate-based execution |
| match(cases,fallback) | (Array<[AxisProfileID\|AxisProfileID[],T]>,T)=>T | Declarative profile matching |
| isGroup(groups) | (AxisProfileGroupID \| AxisProfileGroupID[]) => boolean | Group membership check |
| onlyGroup(groups,fn) | (AxisProfileGroupID \| AxisProfileGroupID[], fn:()=>void)=>void | Execute for matching groups |
| unlessGroup(groups,fn) | (AxisProfileGroupID \| AxisProfileGroupID[], fn:()=>void)=>void | Execute for non-matching groups |
| matchGroup(cases,fallback) | (Array<[AxisProfileGroupID\|AxisProfileGroupID[],T]>,T)=>T | Declarative group matching |
| isTierRaw(tiers) | (AxisTier \| AxisTier[]) => boolean | Raw tier check |
| isTier(tiers) | (AxisTier \| AxisTier[]) => boolean | Normalized tier check |
| applyGlobalScaleVar(name?) | (cssVarName?:string)=>void | Writes scale factor to a CSS variable |
---
AxisCaps
| Field | Type | Meaning |
|---|---|---|
| touch | boolean | Touch-capable input detected |
| hover | boolean | Hover-capable pointer detected |
| anyPointer | "none" \| "coarse" \| "fine" | Highest fidelity pointer |
| anyHover | "none" \| "hover" | Hover support |
| maxTouchPoints | number | Touch contact capacity |
| isTouchLaptop | boolean | Touch + hover + fine pointer |
| isHybrid | boolean | Hybrid input classification |
---
AxisAspect
| Field | Type | Description |
|---|---|---|
| ratio | number | Width / height |
| shape | AxisShape | Shape classification |
| isPortraitLike | boolean | Portrait-biased ratio |
| isLandscapeLike | boolean | Landscape-biased ratio |
| isSquareish | boolean | Near-square ratio |
| isTall | boolean | Tall layout |
| isWide | boolean | Wide layout |
| isUltraWide | boolean | Ultra-wide layout |
| pick(map) | (map) => number | Shape-based selector |
---
AxisProfile
| Field | Type | Description |
|---|---|---|
| id | AxisProfileID | Canonical profile string |
| input | AxisInput | Input classification |
| tier | AxisTier | Layout tier |
| shape | AxisShape | Aspect shape |
| orientation | AxisOrientation | Orientation |
| isTouch | boolean | Touch input |
| isPointer | boolean | Pointer input |
| isHybrid | boolean | Hybrid input |
| isCompact | boolean | Compact tier |
| isComfortable | boolean | Comfortable tier |
| isExpanded | boolean | Expanded tier |
| isCinema | boolean | Cinema tier |
| isTall | boolean | Tall shape |
| isStandard | boolean | Standard shape |
| isWide | boolean | Wide shape |
| isUltraWide | boolean | Ultra-wide shape |
| isSquareish | boolean | Squareish shape |
| isPortrait | boolean | Portrait orientation |
| isLandscape | boolean | Landscape orientation |
---
π§ Profile Matrix (Canonical Identities)
Profile IDs exist for identity and groupingβnot conditional logic by default.
Profiles follow:
`text
`
Total combinations: 60
Normalization rule: pointer + compact short-side windows normalize to comfortable.
| Input | Tier | Shape | Profile ID |
|---|---|---|---|
| Touch | Compact | Tall | TouchCompactTall |
| Touch | Compact | Standard | TouchCompactStandard |
| Touch | Compact | Wide | TouchCompactWide |
| Touch | Compact | UltraWide | TouchCompactUltraWide |
| Touch | Compact | Squareish | TouchCompactSquareish |
| Touch | Comfortable | Tall | TouchComfortableTall |
| Touch | Comfortable | Standard | TouchComfortableStandard |
| Touch | Comfortable | Wide | TouchComfortableWide |
| Touch | Comfortable | UltraWide | TouchComfortableUltraWide |
| Touch | Comfortable | Squareish | TouchComfortableSquareish |
| Touch | Expanded | Tall | TouchExpandedTall |
| Touch | Expanded | Standard | TouchExpandedStandard |
| Touch | Expanded | Wide | TouchExpandedWide |
| Touch | Expanded | UltraWide | TouchExpandedUltraWide |
| Touch | Expanded | Squareish | TouchExpandedSquareish |
| Touch | Cinema | Tall | TouchCinemaTall |
| Touch | Cinema | Standard | TouchCinemaStandard |
| Touch | Cinema | Wide | TouchCinemaWide |
| Touch | Cinema | UltraWide | TouchCinemaUltraWide |
| Touch | Cinema | Squareish | TouchCinemaSquareish |
| Pointer | Comfortable | Tall | PointerComfortableTall |
| Pointer | Comfortable | Standard | PointerComfortableStandard |
| Pointer | Comfortable | Wide | PointerComfortableWide |
| Pointer | Comfortable | UltraWide | PointerComfortableUltraWide |
| Pointer | Comfortable | Squareish | PointerComfortableSquareish |
| Pointer | Expanded | Tall | PointerExpandedTall |
| Pointer | Expanded | Standard | PointerExpandedStandard |
| Pointer | Expanded | Wide | PointerExpandedWide |
| Pointer | Expanded | UltraWide | PointerExpandedUltraWide |
| Pointer | Expanded | Squareish | PointerExpandedSquareish |
| Pointer | Cinema | Tall | PointerCinemaTall |
| Pointer | Cinema | Standard | PointerCinemaStandard |
| Pointer | Cinema | Wide | PointerCinemaWide |
| Pointer | Cinema | UltraWide | PointerCinemaUltraWide |
| Pointer | Cinema | Squareish | PointerCinemaSquareish |
| Hybrid | Compact | Tall | HybridCompactTall |
| Hybrid | Compact | Standard | HybridCompactStandard |
| Hybrid | Compact | Wide | HybridCompactWide |
| Hybrid | Compact | UltraWide | HybridCompactUltraWide |
| Hybrid | Compact | Squareish | HybridCompactSquareish |
| Hybrid | Comfortable | Tall | HybridComfortableTall |
| Hybrid | Comfortable | Standard | HybridComfortableStandard |
| Hybrid | Comfortable | Wide | HybridComfortableWide |
| Hybrid | Comfortable | UltraWide | HybridComfortableUltraWide |
| Hybrid | Comfortable | Squareish | HybridComfortableSquareish |
| Hybrid | Expanded | Tall | HybridExpandedTall |
| Hybrid | Expanded | Standard | HybridExpandedStandard |
| Hybrid | Expanded | Wide | HybridExpandedWide |
| Hybrid | Expanded | UltraWide | HybridExpandedUltraWide |
| Hybrid | Expanded | Squareish | HybridExpandedSquareish |
| Hybrid | Cinema | Tall | HybridCinemaTall |
| Hybrid | Cinema | Standard | HybridCinemaStandard |
| Hybrid | Cinema | Wide | HybridCinemaWide |
| Hybrid | Cinema | UltraWide | HybridCinemaUltraWide |
| Hybrid | Cinema | Squareish | HybridCinemaSquareish |
---
π AxisScale Reference
Axis exposes scaled numeric values and CSS-ready strings only.
No additional unit systems exist.
uiRem and uiPx act as semantic aliases.
Unit conversion stays explicit by design to prevent silent layout drift.
| Helper | Params | Returns | Purpose |
|---|---:|---:|---|
| factor | β | number | Global scale multiplier |
| rem | (n:number) | string | Scaled rem |
| px | (n:number) | number | Scaled pixels |
| remRaw | (n:number) | number | Scaled rem (raw number) |
| pxRaw | (n:number) | number | Scaled px (raw number) |
| pxToRemRaw | (px:number) | number | Convert px to rem (raw) |
| pxToRem | (px:number) | string | Convert px to rem |
| remToPxRaw | (rem:number) | number | Convert rem to px (raw) |
| remToPx | (rem:number) | number | Convert rem to px |
| uiRem | (n:number) | string | Alias of rem |
| uiPx | (n:number) | number | Alias of px |
| clampRem | (min,ideal,max) | string | CSS clamp |
| space | (step:number) | string | Spacing rhythm |
| size | (step:number) | string | General sizing |
| inset | (t,r,b,l) | string | Padding shorthand |
| insetXY | (x,y) | string | Axis padding |
| radius | (key) | string | Border radius |
| border | (key) | string | Border width |
| stroke | (key) | number | Stroke width |
| shadow | (key) | string | Elevation |
| blur | (key) | string | Blur radius |
| font | (key) | string | Font size |
| line | (key) | string | Line height |
| tracking | (key) | string | Letter spacing |
| weight | (key) | number | Font weight |
| icon | (key) | number | Icon size |
| controlHeight | (key) | number | Control height |
| width | (key) | string | Width presets |
---
π§© Essentials Reference
Essentials extend the snapshot surface area without changing core behavior. Axis core stays identical; Essentials add derived convenience fields.
Included fields:
- viewport
- safeArea
- insets
- controls
- density
- container
- grid
- type
---
π Axis UI Runtime Reference
Axis UI derives from the snapshot and exposes on axis.ui.
$3
| Member | Type | Description |
|---|---|---|
| mode | "auto" \| "compact" \| "comfortable" \| "large" | Active UI mode |
| factor | number | UI scale factor |
| setMode(mode) | (mode:AxisUIMode \| null) => void | Overrides UI mode |
| getMode() | () => AxisUIMode | Reads active UI mode |
| rem(value) | (value:number)=>string | UI rem helper |
| px(value) | (value:number)=>number | UI px helper |
| font(key) | (key:string)=>string | UI font helper |
| space(step) | (step:number)=>string | UI spacing helper |
| radius(key) | (key:string)=>string | UI radius helper |
| hit() | ()=>number | UI minimum hit target |
| hitRem() | ()=>string | UI minimum hit target (rem) |
---
π Resolver Reference
Axis supports an optional resolver source installed on the engine.
$3
`ts
const engine = createAxisEngine();
engine.setResolver((axis, key) => {
if (key === "theme.primary")
{
return "#1A73E8";
}
return null;
});
`
$3
`ts
axis.resolve("theme.primary"); // string | number | boolean | null | object | array
axis.resolveString("theme.primary", "#000000"); // string
`
---
π§ Profile Groups Reference
Axis exposes ergonomic profile groups for higher-level matching.
$3
`ts
axis.isGroup("mobileLike");
axis.isGroup(["touch", "compact"]);
axis.isGroup("keyboardOpen");
`
$3
`ts
axis.onlyGroup("mobileLike", () => {
// touch-first stacked UI
});
axis.unlessGroup("keyboardOpen", () => {
// normal bottom bar behavior
});
`
$3
`ts
const columns = axis.matchGroup([
[["mobileLike"], 4],
[["desktopLike"], 12]
], 8);
`
$3
touch, pointer, hybrid
compact, comfortable, expanded, cinema
tall, standard, wide, ultraWide, squareish
portrait, landscape
compactTouch, comfortableTouch, expandedTouch, cinemaTouch
compactPointer, comfortablePointer, expandedPointer, cinemaPointer
compactHybrid, comfortableHybrid, expandedHybrid, cinemaHybrid
mobileLike
desktopLike
keyboardOpen
---
π Comparisons & Branching
Axis replaces conditional chaos with explicit, snapshot-driven branching.
Instead of guessing screen types or stacking breakpoints, logic reads directly from measured reality.
$3
| Intent | Axis Signal | Use Instead of |
|---|---|---|
| Input class | axis.profile.isTouch | if (isMobile) |
| Pointer precision | axis.caps.anyPointer | hover media queries |
| Hybrid devices | axis.profile.isHybrid | device detection |
| Density tier | axis.profile.isCompact | width breakpoints |
| Layout structure | axis.layoutMode | grid breakpoint forks |
| Aspect shape | axis.profile.isUltraWide | ratio math |
| Orientation | axis.profile.isPortrait | orientation media queries |
| Exact identity | axis.isProfile(...) | hard-coded layouts |
| Keyboard open | axis.isGroup("keyboardOpen") | brittle keyboard listeners |
---
Profile Identity Comparison
Test one or more canonical profile IDs.
`ts
axis.isProfile("TouchCompactTall");
`
Multiple profiles:
`ts
axis.isProfile([
"TouchCompactTall",
"HybridComfortableWide"
]);
`
---
Input Class Checks (Touch / Pointer / Hybrid)
Axis exposes class-level booleans directly on the profile.
`ts
axis.profile.isTouch;
axis.profile.isPointer;
axis.profile.isHybrid;
`
Typical usage:
`ts
if (axis.profile.isTouch)
{
// touch-first behavior
}
`
---
Tier-Based Branching
Tier flags replace breakpoint math entirely.
`ts
axis.profile.isCompact;
axis.profile.isComfortable;
axis.profile.isExpanded;
axis.profile.isCinema;
`
Example:
`ts
if (axis.profile.isCompact)
{
// dense, stacked UI
}
`
---
Shape-Based Branching
Shape flags replace aspect-ratio thresholds.
`ts
axis.profile.isTall;
axis.profile.isStandard;
axis.profile.isWide;
axis.profile.isUltraWide;
axis.profile.isSquareish;
`
Example:
`ts
if (axis.profile.isUltraWide)
{
// multi-column canvas layout
}
`
---
Orientation Checks
`ts
axis.profile.isPortrait;
axis.profile.isLandscape;
`
---
only β Execute for Matching Profiles
`ts
axis.only("TouchCompactTall", () => {
// executes only for this profile
});
`
Multiple profiles:
`ts
axis.only([
"TouchCompactTall",
"HybridComfortableStandard"
], () => {
// executes for any matching profile
});
`
---
unless β Execute for Non-Matching Profiles
`ts
axis.unless("PointerExpandedWide", () => {
// executes for all except this profile
});
`
---
Predicate-Based Branching (onlyIf)
Use when logic depends on capability, not identity.
`ts
axis.onlyIf(a => a.caps.isTouchLaptop, () => {
// hybrid laptop ergonomics
});
`
---
Declarative Matching (match)
Select values without branching trees.
`ts
const columns = axis.match([
[["TouchCompactTall"], 4],
[["TouchComfortableStandard"], 6],
[["PointerExpandedWide"], 12]
], 8);
`
- first match wins
- fallback always required
- no cascading if logic
---
Capability-Based Branching
`ts
if (axis.caps.hover && axis.caps.anyPointer === "fine")
{
// hover-precise interactions
}
`
`ts
if (axis.caps.isHybrid)
{
// dual-input ergonomics
}
`
---
βοΈ Snapshot Helper Methods
| Helper | Purpose |
|---|---|
| isProfile | Profile identity check |
| only | Execute for matching profiles |
| unless | Execute for non-matching profiles |
| onlyIf | Predicate-based execution |
| match | Declarative value selection |
| isGroup | Group membership check |
| onlyGroup | Execute for matching groups |
| unlessGroup | Execute for non-matching groups |
| matchGroup | Declarative group selection |
| isTierRaw | Raw tier comparison |
| isTier | Normalized tier comparison |
| applyGlobalScaleVar | Write scale factor to CSS variable |
---
π§Ύ Usage Patterns
$3
`ts
const axis = createAxisEngine().snapshot();
`
$3
`ts
const engine = createAxisEngine();
const unsubscribe = engine.subscribe(axis => {
document.body.dataset.profile = axis.profile.id;
});
`
$3
`ts
axis.only(["TouchCompactTall"], () => {});
axis.unless("PointerExpandedWide", () => {});
`
$3
`ts
axis.onlyIf(a => a.caps.isTouchLaptop, () => {});
`
$3
`ts
const density = axis.match([
[["TouchCompactTall"], 1.1],
[["PointerExpandedWide"], 0.95]
], 1.0);
`
$3
`ts
const layout = axis.matchGroup([
[["mobileLike"], "stack"],
[["desktopLike"], "split"]
], "split");
`
$3
`ts
axis.applyGlobalScaleVar("--axis-scale");
`
$3
`ts
engine.setProfileOverride("TouchCompactTall");
engine.setProfileOverride(null);
`
$3
`ts
engine.setResolver((axis, key) => {
(void axis);
if (key === "layout.headerHeight")
{
return "64px";
}
return null;
});
const h = axis.resolveString("layout.headerHeight", "56px");
`
$3
`ts
const axis = createAxisEngine().snapshot();
`
---
Performance Notes
- RAF-coalesced updates
- minimal recomputation
- deterministic snapshot rebuild
- zero polling
- safe-area measurer reused
---
π¦ Installation
`bash
npm install @trap_stevo/axis
or
yarn add @trap_stevo/axis
`
- ESM
- TypeScript included
- framework-agnostic
- no config
- no flags
- no CLI
---
β‘ Quick Start
Each Quick Start below renders the same UI, uses the same Axis signals, and makes the same layout decisions.
- Shared intent across all examples
- one Axis engine
- one subscription
- snapshot-driven layout
- no breakpoints
- no device detection
- Axis β decision β CSS
---
Quick Start β Teaser
`js
import createAxisEngine from "@trap_stevo/axis";
const engine = createAxisEngine();
const axis = engine.snapshot();
console.log(axis.input);
console.log(axis.tier);
console.log(axis.aspect.shape);
`
---
Shared Layout Decisions (All Examples)
Every Quick Start below uses these exact rules:
| Decision | Axis Signal |
|---|---|
| Layout structure | axis.layoutMode |
| Grid columns | axis.grid.pickColumns(...) |
| Page padding | axis.insets.contentPadding |
| Typography | axis.type.size, axis.type.leading |
| Controls | axis.controls.minHit() |
| Identity | axis.profile.id |
| UI sizing | axis.ui.space, axis.ui.hit |
---
Quick Start β React
`jsx
import React, { useEffect, useMemo, useState } from "react";
import createAxisEngine from "@trap_stevo/axis";
export default function App()
{
const engine = useMemo(() => createAxisEngine(), []);
const [axis, setAxis] = useState(() => engine.snapshot());
useEffect(() => {
const unsubscribe = engine.subscribe(next => setAxis(next));
return () => {
unsubscribe();
engine.destroy();
};
}, [engine]);
const columns = axis.grid.pickColumns({
compact : 4,
comfortable : 8,
expanded : 12,
cinema : 12,
default : 8
});
return (
style={{
minHeight : axis.viewport.h(100, "dynamic"),
background : axis.caps.touch ? "rgb(10,20,40)" : "rgb(12,12,14)",
color : "white",
display : "flex",
justifyContent : "center"
}}
>
style={{
width : "100%",
maxWidth : axis.container.maxWidth,
padding : axis.insets.contentPadding,
boxSizing : "border-box"
}}
>
style={{
display : "flex",
justifyContent : "space-between",
alignItems : "center",
marginBottom : axis.ui.space(8)
}}
>
style={{
fontSize : axis.type.size("2xl"),
fontWeight : 700,
lineHeight : axis.type.leading("tight")
}}
>
Axis Quick Start
profile : {axis.profile.id}
,
gap : axis.grid.gutter
}}
>
{Array.from({ length : columns }).map((_, i) => (
key={String(i)}
style={{
padding : axis.container.cardPadding,
borderRadius : axis.scale.radius("2xl"),
background : "rgba(255,255,255,0.06)"
}}
>
Card {i + 1}
---
Quick Start β HTML / Vanilla DOM (Canonical Reference)
`html
Axis Quick Start
`
---
Quick Start β HTML / Vanilla DOM (Axis UI Reference)
`html
Axis Demo
`
---
Quick Start β Vue (Vue 3, Composition API)
`vue
:style="{
minHeight : axis.viewport.h(100, 'dynamic'),
background : axis.caps.touch ? 'rgb(10,20,40)' : 'rgb(12,12,14)',
color : 'white',
display : 'flex',
justifyContent : 'center'
}"
>
:style="{
width : '100%',
maxWidth : axis.container.maxWidth,
padding : axis.insets.contentPadding,
boxSizing : 'border-box'
}"
>
:style="{
display : 'flex',
justifyContent : 'space-between',
alignItems : 'center',
marginBottom : axis.ui.space(8)
}"
>
:style="{
fontSize : axis.type.size('2xl'),
fontWeight : 700
}"
>
Axis Quick Start
profile : {{ axis.profile.id }}
,
gap : axis.grid.gutter
}"
>
v-for="n in columns"
:key="n"
:style="{
padding : axis.container.cardPadding,
borderRadius : axis.scale.radius('2xl'),
background : 'rgba(255,255,255,0.06)'
}"
>
Card {{ n }}