An explainable, deterministic adaptive layout engine that transforms a single content specification into optimized layouts for multiple surfaces — mobile, social, kiosks, digital billboards, and broadcast displays.
Modern content (broadcast lower-thirds, kiosks, mobile apps) must present the same logical information across radically different physical surfaces — different aspect ratios, resolutions, orientations, and safe-area constraints. Traditional responsive design solves this with hardcoded breakpoints and CSS media queries: a fixed layout per known screen size. This does not generalize to unseen surfaces, and it hides the actual decision logic inside CSS, making layout choices unexplainable.
- 🎯 Surface-agnostic resolution — One layout spec resolves correctly across mobile portrait, mobile landscape, broadcast lower-third, and square kiosk out of the box
- 🔍 Explainable — Every layout decision (position, size, hide/show, degradation) carries a human-readable trace explaining exactly why it occurred
- 🧩 Graceful degradation — Lower-priority elements shrink, reflow, or hide before higher-priority elements are affected
- 📐 Framework-agnostic core — Pure TypeScript resolver with zero DOM/React dependency; React only for painting
- 🚀 Unseen surface support — Any new surface profile (smartwatch, print, etc.) flows through the identical code path without code changes
Not just a CSS media query system. Fladapt uses a priority-ordered greedy allocator over a general constraint solver — preferring full explainability over mathematical generality. Every outcome can be traced to one priority comparison and one space check.
| Feature | Description |
|---|---|
| 🎯 Priority-Ordered Allocation | Elements processed from highest to lowest priority; each gets the best size it can given what's left — a single linear pass |
| 📐 Geometry-Driven Classification | Orientation and size tier inferred from width, height, orientation alone — no if (surfaceId === "kiosk") branches |
| 📉 Degradation Ladder | Each element has an authored fallback sequence: shrink → reflow → truncate → hide — first step that fits wins |
| 🔗 Effective Canvas Computation | Surface bounds minus safeArea/bleed insets computed before any allocation — both compose additively |
| 📝 Inline Trace Generation | Every decision appends a trace entry at the point of making it — the explanation can never drift from actual behavior |
| 🖼️ React + CSS Grid Renderer | Pure projection layer — takes ResolvedLayout and paints it with CSS Grid/Flexbox, no layout logic |
| ⚡ Sub-5ms Resolution | Full resolution of a ~20-element spec across a surface in under 5ms — no async/network calls |
| 🧪 4 Built-in Surfaces | Mobile portrait, mobile landscape, broadcast lower-third, square kiosk — all working out of the box |
| Layer | Technologies |
|---|---|
| Backend (Core) | TypeScript (strict mode), zero DOM/React dependency |
| Frontend (Demo) | React 18, TypeScript, CSS Grid/Flexbox |
| Build Tooling | Vite (fast dev server, TS + React support) |
| Testing | Vitest (unit tests for the resolver) |
| Deployment | Vercel / Netlify / GitHub Pages (static build) |
| Containerization | Docker Compose (optional) |
Fladapt is split into two cleanly separated layers:
Resolution Core — Pure TypeScript, no rendering concerns. Converts (LayoutSpec, SurfaceProfile) → ResolvedLayout.
Presentation Layer — React components that take a ResolvedLayout and paint it using CSS Grid/Flexbox, plus a demo harness for switching surfaces and injecting new ones.
fladapt/
├── backend/
│ ├── src/
│ │ ├── model.ts # Type definitions (LayoutSpec, SurfaceProfile, ElementSpec, etc.)
│ │ ├── classify.ts # Surface classification (effective canvas, orientation, size tier)
│ │ ├── resolver.ts # The engine: resolve(spec, surface): ResolvedLayout
│ │ ├── trace.ts # Trace string generation helper
│ │ ├── textMeasure.ts # Text measurement engine (canvas 2D + typographic fallback)
│ │ ├── surfaces/
│ │ │ └── builtin.ts # 4 built-in SurfaceProfile instances
│ │ └── specs/
│ │ └── example.ts # Example layout spec (logo, headline, CTA)
│ ├── package.json
│ └── tsconfig.json
├── frontend/
│ ├── src/
│ │ ├── components/
│ │ │ ├── App.tsx # Demo application (surface picker, trace panel)
│ │ │ ├── Renderer.tsx # CSS Grid/Flexbox renderer
│ │ │ ├── CanvasRenderer.tsx # Canvas-based rendering
│ │ │ └── styles.css # Component styles with design tokens
│ │ ├── main.tsx # React entry point
│ │ └── vite-env.d.ts # Vite type declarations
│ ├── public/
│ │ └── favicon.png # Site favicon
│ ├── index.html # HTML entry point
│ ├── package.json
│ ├── package-lock.json
│ ├── tsconfig.json
│ └── vite.config.ts
├── docs/ # Architecture, Design, PRD, TRD documentation
├── assets/ # Icons, UI samples, product card images
├── vercel.json # Vercel deployment configuration
├── package.json # Root workspace package.json
└── .gitignore
The resolution engine follows a deterministic, priority-ordered algorithm:
Step 1 — Compute effective canvas
effectiveCanvas = {
width: surface.width - (insets.left + insets.right),
height: surface.height - (insets.top + insets.bottom)
}
where insets merges safeArea and bleed if both are present (they compose additively).
Step 2 — Classify surface (generic, id-agnostic) Derive from geometry alone:
orientation: explicit if provided, elsewidth > height ? "landscape" : width < height ? "portrait" : "square"sizeTier: bucket by effective canvas area against fixed thresholds
Step 3 — Sort elements by priority
Ascending priority value = processed first = most protected. Stable sort so tie-break order is deterministic.
Step 4 — Allocate per element (loop) For each element, in priority order:
- Try
preferredSize. If it fits → place it there - Else walk the
degradationladder in order authored on the spec:shrink → to: check if the shrink target fitsreflow → stack|inline: change internal arrangementtruncate → maxLines: reduce content footprinthide: element is dropped
- The first ladder step that fits wins; later steps are not attempted
Step 5 — Position placed elements
Higher-priority elements get first claim on the anchor region for their kind; remaining elements fill leftover space top-to-bottom / left-to-right.
Step 6 — Emit trace + ResolvedLayout
Every element carries its ordered trace: string[], generated inline during steps 4–5, not reconstructed afterward.
logo: preferred size 200×80 fits in 640×140; placed at preferred size.
headline: preferred size 480×120 does not fit in 640×140; attempting degradation ladder.
headline: shrink to 320×80 fits; placed at that size.
cta: preferred size 160×48 does not fit in remaining 120×140; no further degradation steps besides hide → hidden.
Because Steps 1–2 only consume width, height, orientation, and insets, a brand-new profile (e.g., a smartwatch face, 1:1 at 200×200, with a small circular safe inset) flows through the identical code path. No special-casing is required or permitted — this is the core proof point for the live demo.
Fladapt resolves the same layout spec correctly across all four built-in surface types out of the box:
| Surface | Dimensions | Category | Safe Area |
|---|---|---|---|
| 📱 Mobile Portrait | 360 × 640 | mobile | 40px all sides |
| 📱 Mobile Landscape | 640 × 360 | mobile | 40px all sides |
| 📺 Broadcast Lower-Third | 640 × 140 | broadcast | 0px |
| 🖥️ Square Kiosk | 800 × 800 | kiosk | 50px all sides |
| Tool | Version | Purpose |
|---|---|---|
| 18+ | Frontend runtime | |
| 5.x | Core engine language | |
| 5.x | Build tool & dev server |
The fastest way to see the demo:
# 1. Navigate to the frontend directory
cd frontend
# 2. Install dependencies
npm install
# 3. Start the dev server
npm run dev| Service | URL |
|---|---|
| 🌐 Demo App | http://localhost:5173 |
| 📖 Backend API | http://localhost:5173 |
# Build the frontend
cd frontend && npm run build
# Preview the production build
npm run preview# Build and run all services
docker compose up --build# Deploy with the vercel.json configuration
vercel deployA video demonstration of the system resolving layouts across surfaces can be accessed here:
- Demo Video Link: click the icon for Video
Fladapt was built around explainability and generalizability.
-
Stack Selection (TypeScript + React + Vite): We chose TypeScript for the core engine to ensure framework-agnostic resolution with strict type safety. The resolver has zero DOM/React dependency — it is a pure function that can run in any environment. React is used only in the presentation layer to paint already-resolved layouts.
-
Greedy Priority Allocator Over Constraint Solver: We deliberately chose a greedy, priority-ordered allocator over a general constraint solver (LP-style). Elements are processed from highest to lowest priority; each element gets the best size it can given what's left. This trades mathematical generality for full explainability — every outcome can be traced to one priority comparison and one space check.
-
Geometry-Driven Classification: Surfaces are classified by their dimensions and orientation alone — never by an ID lookup. This means a brand-new surface (smartwatch, print, kiosk) flows through the identical code path without any code changes. This is the architectural answer to "what happens when the interviewer gives you a 5th surface live?"
-
Inline Trace Generation: Every decision appends a trace entry at the point of making it. The trace array is never reconstructed after the fact from the final output — this guarantees the explanation can never diverge from actual behavior.
-
Inset Merging:
safeAreaandbleedcompose additively — they shrink/expand usable space from the outer edge by the same amount. This means future constraint types (print-bleed, broadcast-safe-area) are modeled uniformly asInsetfields, with no new control flow needed.
| Tool | Purpose |
|---|---|
| Claude Code | AI pair-programming for development, debugging, and testing |
| Vite | Build tooling and dev server |
| React | Presentation layer rendering |
fladapt/
├── backend/
│ ├── src/
│ │ ├── model.ts # Type definitions (LayoutSpec, SurfaceProfile, etc.)
│ │ ├── classify.ts # Surface classification (effective canvas, orientation, size tier)
│ │ ├── resolver.ts # The engine: resolve(spec, surface): ResolvedLayout
│ │ ├── trace.ts # Trace string generation helper
│ │ ├── textMeasure.ts # Text measurement engine (canvas 2D + typographic fallback)
│ │ ├── surfaces/
│ │ │ └── builtin.ts # 4 built-in SurfaceProfile instances
│ │ └── specs/
│ │ └── example.ts # Example layout spec (logo, headline, CTA)
│ ├── package.json
│ └── tsconfig.json
├── frontend/
│ ├── src/
│ │ ├── components/
│ │ │ ├── App.tsx # Demo application (surface picker, trace panel)
│ │ │ ├── Renderer.tsx # CSS Grid/Flexbox renderer
│ │ │ ├── CanvasRenderer.tsx # Canvas-based rendering
│ │ │ └── styles.css # Component styles with design tokens
│ │ ├── main.tsx # React entry point
│ │ └── vite-env.d.ts # Vite type declarations
│ ├── public/
│ │ └── favicon.png # Site favicon
│ ├── index.html # HTML entry point
│ ├── package.json
│ ├── package-lock.json
│ ├── tsconfig.json
│ └── vite.config.ts
├── docs/ # Architecture, Design, PRD, TRD documentation
├── assets/ # Icons, UI samples, product card images
├── vercel.json # Vercel deployment configuration
├── package.json # Root workspace package.json
└── .gitignore
| Method | Endpoint | Description |
|---|---|---|
function |
resolve(spec, surface) |
Resolve a LayoutSpec against a SurfaceProfile → ResolvedLayout |
function |
classifySurface(surface) |
Derive orientation and size tier from geometry alone |
function |
computeEffectiveCanvas(surface, insets) |
Compute canvas bounds minus safeArea/bleed insets |
function |
measureRenderedText(text, options) |
Measure text width/height using canvas 2D or typographic fallback |
function |
computeContrastRatio(fg, bg) |
Calculate WCAG 2.1 contrast ratio between two hex colors |
| Type | Description |
|---|---|
LayoutSpec |
Surface-independent layout description with elements and their priorities |
ElementSpec |
Content element with preferred size, min size, degradation ladder, a11y constraints |
SurfaceProfile |
Target physical surface with dimensions, orientation, category, and optional insets |
ResolvedLayout |
Output: concrete layout with position, size, visibility, and trace per element |
ResolvedElement |
Single resolved element with applied degradation steps and trace entries |
DegradationStep |
Fallback behavior: shrink, reflow, truncate, or hide |
Inset |
Rectangular inset (safeArea or bleed) with top/right/bottom/left |
| Component | Description |
|---|---|
App.tsx |
Demo application with surface picker, new-surface form, and trace panel |
Renderer.tsx |
Pure CSS projection — takes ResolvedLayout and paints with CSS Grid/Flexbox |
CanvasRenderer.tsx |
Canvas-based rendering for advanced visualization |
cd backend
# Run the test suite
npm testTest Coverage:
| Test File | Covers |
|---|---|
resolver.test.ts |
Element fits without degradation, single degradation step, full ladder exhausted → hidden, safe-area/bleed canvas, unseen surface |
classify.test.ts |
Orientation inference, size tier bucketing, effective canvas computation |
| Trace tests | Assert trace text references actual reason (priority, remaining space) |
vercel deployThis launches the frontend with the Vite build pipeline configured in vercel.json.
docker compose up --buildThis launches all services: Frontend + Backend.
| Service | Deployment Target | Notes |
|---|---|---|
| Frontend | Vercel / Netlify | Set VITE_API_URL to your backend URL |
| Backend | Render / Railway | Set NODE_ENV and TS_NODE |
| Database | Supabase / Neon | Optional, for persisted specs |
Built with ❤️ by M Harish Gautham
⭐ If you find this project impressive, give it a star! ⭐



