Skip to content

Architecture

Module Graph

graph TD
    catalog["`:catalog`"] --> compose["`:compose`"]
    compose --> shape["`:shape`"]
    compose --> motion["`:motion`"]
    compose --> foundation["`:foundation`"]
    shape --> foundation
    motion --> foundation

:catalog depends on :compose (and transitively :shape, :motion, and :foundation). :compose depends on :shape, :motion, and :foundation. All three are exposed via api(...) so consumers do not need separate dependencies. :shape depends on :foundation and on compose.runtime + compose.ui. It can be consumed standalone by Compose-based apps that only need the shape system. :motion depends on :foundation and on compose.runtime + compose.animation.core. It can be consumed standalone by Compose-based apps that want to adopt Cortena's motion language without pulling the rest of the framework. :foundation has zero external dependencies — pure Kotlin.

Modules

:foundation

Zero external dependencies. Pure Kotlin.

The single source of truth for all design tokens and for framework-agnostic shape geometry. Values are primitive types (Long for colors, Float for sizes), and shape geometry is emitted through a CornerPathReceiver SAM interface so this module can be consumed by Compose, the Android View system, and AOSP build system (Android.bp) without modification.

foundation/src/commonMain/kotlin/framework/cortena/ui/
├── color/
│   ├── ColorTokens.kt        # raw ARGB Long values (internal)
│   ├── AdaptiveColor.kt      # light + dark color pair
│   ├── ColorToken.kt         # public adaptive color palette
│   └── Palette.kt            # semantic roles (background, primary, error...)
├── geometry/
│   └── Orientation.kt        # Horizontal / Vertical
├── shape/
│   ├── ContinuousCurvature.kt   # pure-Kotlin squircle path emitter
│   ├── CornerPathReceiver.kt    # sink for path commands
│   ├── CornerStyle.kt           # Circular vs Continuous
│   └── internal/
│       └── CornerBuilder.kt     # squircle bezier solver (kotlin.math only)
├── motion/
│   ├── DurationTokens.kt     # raw ms Long values: Fast=150, Medium=250, Slow=450
│   └── EasingTokens.kt       # cubic-bezier coefficients as Float quartets
├── size/
│   ├── GoldenRatio.kt        # φ, √φ, and ⁴√φ single-source constants
│   ├── SizeToken.kt          # Small / Medium / Large
│   └── SizeScale.kt          # raw dp Float values per tier (Small/Large derived from Medium via √φ)
├── typography/
│   ├── TextWeight.kt         # Default / Medium / Bold
│   ├── TypeScale.kt          # golden-ratio derived sp values (anchor BodyMedium = 16)
│   └── Typography.kt         # semantic roles (bodyMedium, titleLarge...)
└── spacing/
    └── Spacing.kt            # 4dp grid (Xs=4, Sm=8, Md=16...)

The shape APIs in :foundation are deliberately framework-agnostic. Non-Compose consumers — for example a system-level dynamic-island overlay rendered with android.graphics.Canvas — implement CornerPathReceiver and call ContinuousCurvature.emit(...) to obtain the same squircle path math used by the Compose components.

:shape

Compose-aware shape system. Depends on :foundation and on compose.runtime + compose.ui. Bridges the framework-agnostic squircle math from :foundation to the Compose Shape API and provides the ComponentShape hierarchy used throughout :compose. Publishable as a standalone AAR for Compose apps that only want the shape system.

shape/src/commonMain/kotlin/framework/cortena/ui/shape/
├── CapsuleShape.kt           # stadium / pill — radius = minDimension / 2
├── ComponentShape.kt         # sealed Shape contract, exposes Corners
├── CornerRadii.kt            # per-corner Dp record + lerp
├── RectangleShape.kt         # sharp rectangle data object
├── RoundedShape.kt           # uniform Dp rounded rectangle
├── ShapeCopy.kt              # ComponentShape.copy(...) overloads
├── ShapeLerp.kt              # lerp(start, stop, fraction[, style])
├── UnevenShape.kt            # per-corner CornerRadii rounded rectangle
└── internal/
    └── ShapeOutline.kt       # bridge from ContinuousCurvature → Compose Outline / Path

:motion

Centralized motion language. Depends on :foundation and on compose.runtime + compose.animation.core. Holds the shared spring presets, duration tiers, and easing curves consumed by every interactive component. Components must read motion specs through LocalMotion.current rather than constructing spring(...) or tween(...) calls inline.

The split mirrors :shape: pure Kotlin tokens at the foundation level (raw Long ms, Float cubic-bezier coefficients), Compose adapters in :motion that turn those tokens into SpringSpec, Easing, and AnimationSpec instances.

motion/src/commonMain/kotlin/framework/cortena/ui/motion/
├── SpringPresets.kt          # Snappy / Smooth / Gentle as SpringSpec<Float>
├── MotionDuration.kt         # DurationTokens narrowed to Int for tween()
├── MotionEasing.kt           # EasingTokens lifted to Compose Easing
├── Motion.kt                 # aggregate carrier consumed via LocalMotion
└── LocalMotion.kt            # staticCompositionLocalOf<Motion> with default DefaultMotion

:compose

Compose wrappers and theme layer. Depends on :foundation, :shape, and :motion. Converts foundation tokens (Long/Float) to Compose types (Color, TextUnit, Dp). Provides Theme { } entry point via CompositionLocalProvider, which also injects LocalMotion for the spring / duration / easing language.

The module is split into commonMain (multiplatform-ready) and androidMain (Android-specific platform code).

commonMain — Shared Compose Layer

compose/src/commonMain/kotlin/framework/cortena/ui/
├── annotation/
│   └── ExperimentalComponentsApi.kt   # opt-in annotation for unstable APIs
├── components/
│   ├── Button.kt            # interactive button with 5 styles, 2 variants
│   ├── Icon.kt              # tinted ImageVector renderer that follows LocalIconSize
│   ├── Separator.kt         # horizontal / vertical divider line
│   ├── Slider.kt            # continuous + discrete value slider
│   ├── Text.kt              # semantic text with 12 TextRole variants × TextWeight
│   └── Toggle.kt            # spring-animated switch with drag + tap
├── graphics/
│   └── shadow/
│       ├── Shadow.kt                  # shadow data class (radius, offset, color)
│       ├── ShadowModifier.kt          # Modifier.componentShadow
│       ├── InnerShadow.kt             # inner shadow rendering
│       └── InnerShadowModifier.kt     # Modifier.innerShadow
├── interaction/
│   ├── DampedAnimation.kt            # spring-physics animation driver
│   ├── InteractiveAnimation.kt       # graphicsLayer press/drag transforms
│   ├── InteractiveHighlight.kt       # highlight composable (commonMain)
│   ├── InteractiveHighlightColor.kt  # highlight color utilities
│   └── PressGesture.kt               # inspectDragGestures helper
├── layout/
│   ├── AppBar.kt            # top app bar slot
│   ├── Body.kt              # edge-to-edge root wrapper
│   ├── GridColumns.kt       # sealed class — Fixed / Adaptive
│   ├── GridView.kt          # eager 2D grid
│   ├── LazyGridView.kt      # lazy 2D grid (LazyVerticalGrid / LazyHorizontalGrid wrapper)
│   ├── LazyScrollView.kt    # lazy 1D list (LazyColumn / LazyRow wrapper)
│   ├── SafeArea.kt          # system insets padding
│   └── ScrollView.kt        # eager scrollable container with bounce overscroll
└── theme/
    ├── ColorExtensions.kt         # ColorToken.value() helpers
    ├── LocalProviders.kt          # CompositionLocal definitions
    ├── StatusBarIconMode.kt       # Light / Dark / Auto enum
    ├── Theme.kt                   # Theme() composable entry point
    └── ThemeMode.kt               # Light / Dark / Auto enum

Shape source files moved to :shape. Component code still imports them via the same package path (framework.cortena.ui.shape.CapsuleShape, etc.) — only the module boundary changed.

androidMain — Android Platform Code

compose/src/androidMain/kotlin/framework/cortena/ui/
├── graphics/
│   └── shadow/              # platform shadow rendering (RenderNode)
├── interaction/
│   ├── InteractiveHighlight.kt       # Android-specific highlight with shader
│   └── InteractiveHighlightShader.kt # AGSL shader for glow effects
└── layout/
    └── ContentView.kt       # ComponentActivity.ContentView() entry point

:catalog

Showcase app. Depends on :compose (and transitively :shape, :motion, :foundation). Used to develop and visually verify all components in a live environment.

catalog/src/main/java/app/cortena/ui/catalog/
├── MainActivity.kt          # main activity with theme toggle
└── demo/
    ├── Button.kt            # ButtonDemo()
    ├── Color.kt             # ColorDemo() — responsive palette grid
    ├── ScrollView.kt        # ScrollViewDemo()
    ├── Slider.kt            # SliderDemo()
    ├── Toggle.kt            # ToggleDemo()
    └── Typography.kt        # TypographyDemo() — all 15 TextRoles + advanced features

Theme System

CompositionLocal Providers

The theme layer exposes the following CompositionLocal keys, each provided by the Theme() composable:

Key Type Default Purpose
LocalIsDark Boolean false Whether the current theme is dark mode
LocalColors Palette LightPalette Semantic color roles for the active theme
LocalContentColor Color? null Inherited foreground color (scoped by parent)
LocalTypography Typography DefaultTypography Semantic text style scales
LocalSpacing Spacing Spacing 4dp-grid spacing tokens
LocalSizeToken SizeToken SizeToken.Medium Global component size tier
LocalMotion Motion DefaultMotion Spring presets, duration tiers, easing curves
LocalTextRole TextRole? null Implicit text role propagated by sized parents (e.g. Button)
LocalTextWeight TextWeight? null Implicit text weight propagated by sized parents
LocalIconSize Dp 24.dp Implicit icon size propagated by sized parents

ThemeMode Resolution

ThemeMode.Auto delegates to isSystemInDarkTheme() inside the Theme() composable, ensuring consistent behavior without manual system-theme checks in outer layouts.

ContentView Entry Point (Android)

ComponentActivity.ContentView() is the recommended Android entry point. It:

  1. Calls enableEdgeToEdge() for transparent system bars.
  2. Wraps content inside Theme() with the provided themeMode, palette, and typography.
  3. Manages status bar icon contrast (StatusBarIconMode) reactively via SideEffect.
  4. Renders an optional status bar color overlay and appBar slot above the main content.

Component Design Patterns

Interaction Model

All interactive components (Button, Slider, Toggle) share a common physics-based interaction system:

  1. DampedAnimation — Central animation driver with spring-physics for position, press feedback (scale), and velocity tracking. Uses Job-tracked coroutines to prevent stale animation conflicts on rapid successive interactions.
  2. inspectDragGestures — Low-level gesture detector that emits onDragStart, onDrag, onDragEnd, and onDragCancel callbacks without consuming pointer events (allowing parent scroll to still function).
  3. applyInteractiveAnimationgraphicsLayer extension that applies press-scale, translation, and feedback transforms in a single pass.
  4. InteractiveHighlight — Platform-specific glow/highlight effect (AGSL shader on Android).

Color Resolution

Components follow a consistent pattern for resolving colors:

val resolvedColor = if (customColor.isSpecified) customColor else Color(colors.semanticRole)

This allows per-instance color overrides while defaulting to theme-aware semantic roles.

Motion

All animation specs originate from LocalMotion.current. Components do not hardcode spring(...) or tween(...) numbers — they pick a preset:

val motion = LocalMotion.current

animateFloatAsState(target, animationSpec = motion.snappy)             // tight feedback
animateDpAsState(targetDp, animationSpec = motion.smooth)              // content shift
tween<Color>(motion.medium, easing = motion.standardEasing)            // deterministic fade

The presets carry the design language. Spring tier covers snappy (tight feedback), smooth (general motion), and gentle (large overlays). Duration tier covers fast (150ms), medium (250ms), and slow (450ms). Easings cover standardEasing, emphasizedEasing, and linearEasing.

The single documented exception is raw gesture-physics primitives — DampedAnimation and InteractiveHighlight — where each track (position, velocity, press progress, scale-x, scale-y) needs its own bespoke dampingRatio / stiffness / visibilityThreshold tuple to feel right under continuous pointer input. Any new bespoke spec inside one of these primitives must include an inline comment explaining why a preset cannot be used.

Shadow System

The componentShadow modifier renders drop shadows that remain visible during scale animations (unlike standard Modifier.shadow which clips at the original bounds). This is critical for interactive components like Toggle and Slider indicators.

Documentation

Component documentation is maintained in docs/components/ following a standardized format:

Document Component
components/AppBar.md Top app bar
components/Button.md Interactive button
components/Icon.md Tinted vector icon
components/Separator.md Visual divider line
components/Slider.md Value adjustment slider
components/Text.md Semantic text component
components/Toggle.md Switch / toggle
layout/Body.md Edge-to-edge root wrapper
layout/ContentView.md Android activity entry point
layout/GridView.md Eager 2D grid
layout/LazyGridView.md Lazy 2D grid
layout/LazyScrollView.md Lazy 1D scrollable list
layout/SafeArea.md System insets padding
layout/ScrollView.md Eager scrollable container
layout/Theme.md Theme composable
extra/Motion.md Motion language reference
extra/Shape.md Shape system + squircle math

Each document follows the structure: Concept → API Reference → Parameters Table → Examples.

Design Decisions

Why are colors stored as Long? androidx.compose.ui.graphics.Color is a Compose type. Storing raw Long in :foundation means the token layer has zero dependency on Compose — it can be referenced from Android.bp builds for ROM integration without pulling in the entire Compose runtime.

Why a separate :shape module? The squircle math itself is pure Kotlin and lives in :foundation. The Compose-binding layer (ComponentShape, RoundedShape, etc.) sits in :shape so that consumers who want only the shape system — for example a CortenaOS dynamic-island overlay or a third-party Compose app — can depend on :shape without pulling the rest of CortenaUI. This also keeps the boundary between framework-agnostic geometry and Compose types explicit and enforceable.

Why a separate :motion module? Same reasoning as :shape. The raw duration ms and cubic-bezier coefficients live in :foundation (pure Kotlin, framework-agnostic). The Compose adapters that turn those into SpringSpec, Easing, and AnimationSpec instances live in :motion so consumers — for example a CortenaOS system surface or a third-party Compose app adopting Cortena's motion language — can depend on :motion without the rest of the framework. The split also keeps :foundation free of any compose.animation.core types.

Why a separate :compose module? When ROM integration comes, :foundation goes into the system image. Compose components live in apps. Keeping them in separate modules makes that boundary explicit and enforceable by the build system.

Why commonMain / androidMain split in :compose? The component APIs, shapes, and theme logic are multiplatform-ready in commonMain. Platform-specific code (edge-to-edge window management, AGSL shaders, RenderNode shadows) lives in androidMain. This separation enables future targets (Desktop, iOS) with minimal refactoring.

Why DampedAnimation instead of Animatable directly? DampedAnimation encapsulates the full interactive lifecycle (press → drag → release) with coordinated spring animations for position, scale, and velocity. It manages coroutine job tracking internally so components don't need to handle animation cancellation logic, preventing thumb-stuck bugs during rapid interactions.