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:
- Calls
enableEdgeToEdge()for transparent system bars. - Wraps content inside
Theme()with the providedthemeMode,palette, andtypography. - Manages status bar icon contrast (
StatusBarIconMode) reactively viaSideEffect. - Renders an optional status bar color overlay and
appBarslot above the main content.
Component Design Patterns¶
Interaction Model¶
All interactive components (Button, Slider, Toggle) share a common physics-based interaction system:
DampedAnimation— Central animation driver with spring-physics for position, press feedback (scale), and velocity tracking. UsesJob-tracked coroutines to prevent stale animation conflicts on rapid successive interactions.inspectDragGestures— Low-level gesture detector that emitsonDragStart,onDrag,onDragEnd, andonDragCancelcallbacks without consuming pointer events (allowing parent scroll to still function).applyInteractiveAnimation—graphicsLayerextension that applies press-scale, translation, and feedback transforms in a single pass.InteractiveHighlight— Platform-specific glow/highlight effect (AGSL shader on Android).
Color Resolution¶
Components follow a consistent pattern for resolving colors:
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.