From kiln
Extract and structure reusable mobile components with design tokens and preview annotations
npx claudepluginhub moonlightbyte/kiln# Component Extraction & Design System Architecture Extract reusable mobile components from existing code with modern design system patterns, design token organization, and comprehensive preview/testing infrastructure. ## Overview This command helps you transform monolithic UI code into a scalable component library following 2025-2026 mobile design system best practices: - **Slot API Pattern** (Compose): Flexible content slots for composition - **View Modifiers** (SwiftUI): Reusable styling and behavior - **Generic Views**: Type-safe, parameterized component variants - **Design Tokens**...
Extract reusable mobile components from existing code with modern design system patterns, design token organization, and comprehensive preview/testing infrastructure.
This command helps you transform monolithic UI code into a scalable component library following 2025-2026 mobile design system best practices:
Analyze the provided code and generate a component extraction plan with:
{{#if (or (eq strategy "slot-api") (eq strategy "all") (not strategy))}}
{{#if (or (eq strategy "view-modifier") (eq strategy "all") (not strategy))}}
{{#if (or (eq strategy "generic-view") (eq strategy "all") (not strategy))}}
{{#if (or (eq design_tokens) (not design_tokens))}}
{{#if (or (eq include_preview) (not include_preview))}} Jetpack Compose (Showkase):
SwiftUI (native Preview):
{{#if (or (eq include_tests) (not include_tests))}} Snapshot Testing (SwiftUI with SnapshotTesting library):
test_ComponentName_variant_colorSchemeCompose UI Testing (Roborazzi / Paparazzi):
test_ComponentName_variant
{{/if}}/components/ folder organized by feature/theme/tokens/ (Colors, Typography, Spacing, Shapes)/previews/ or alongside component with Preview suffix/tests/ mirroring component structure/modifiers/ or /theme/Use for components with flexible content areas. The component is a "skeleton" - you provide the content.
When to use:
// SLOT API PATTERN
@Composable
fun CustomCard(
modifier: Modifier = Modifier,
shape: Shape = MaterialTheme.shapes.medium,
backgroundColor: Color = MaterialTheme.colorScheme.surface,
content: @Composable (ColumnScope.() -> Unit) // Flexible content slot
) {
Surface(
modifier = modifier,
shape = shape,
color = backgroundColor
) {
Column(
modifier = Modifier.padding(16.dp),
content = content // Caller provides content
)
}
}
// USAGE
CustomCard {
Text("Custom Title")
Spacer(modifier = Modifier.height(8.dp))
Text("Custom body content")
}
Benefits:
Use for extracting reusable styling and behaviors without creating new Views.
When to use:
// VIEW MODIFIER PATTERN
struct CustomCardModifier: ViewModifier {
let backgroundColor: Color
let cornerRadius: CGFloat
let shadowRadius: CGFloat
let elevation: CGFloat
func body(content: Content) -> some View {
content
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(backgroundColor)
.cornerRadius(cornerRadius)
.shadow(radius: shadowRadius)
}
}
extension View {
func customCard(
backgroundColor: Color = .surface,
cornerRadius: CGFloat = 12,
shadowRadius: CGFloat = 4,
elevation: CGFloat = 2
) -> some View {
modifier(
CustomCardModifier(
backgroundColor: backgroundColor,
cornerRadius: cornerRadius,
shadowRadius: shadowRadius,
elevation: elevation
)
)
}
}
// USAGE - Chainable and composable
Text("Hello")
.customCard()
.padding()
Benefits:
Use for parameterized content that varies by type.
// GENERIC VIEW PATTERN (SwiftUI)
struct GenericList<Item: Identifiable, Content: View>: View {
let items: [Item]
let content: (Item) -> Content
var body: some View {
List(items) { item in
content(item)
}
}
}
// USAGE
GenericList(items: users) { user in
HStack {
AsyncImage(url: user.avatarURL)
Text(user.name)
}
}
Benefits:
Use for components with multiple variants sharing core styling.
// BASE COMPONENT PATTERN (Compose)
@Composable
internal fun BaseButton(
modifier: Modifier = Modifier,
shape: Shape = MaterialTheme.shapes.small,
colors: ButtonColors = ButtonDefaults.buttonColors(),
elevation: ButtonElevation = ButtonDefaults.buttonElevation(),
enabled: Boolean = true,
onClick: () -> Unit,
content: @Composable RowScope.() -> Unit
) {
// Core button implementation
Button(
modifier = modifier,
shape = shape,
colors = colors,
elevation = elevation,
enabled = enabled,
onClick = onClick,
content = content
)
}
// VARIANTS use BaseButton with different defaults
@Composable
fun FilledButton(
modifier: Modifier = Modifier,
enabled: Boolean = true,
onClick: () -> Unit,
content: @Composable RowScope.() -> Unit
) {
BaseButton(
modifier = modifier,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
),
enabled = enabled,
onClick = onClick,
content = content
)
}
Benefits:
android/app/src/main/java/com/partyonapp/android/ui/theme/
├── Color.kt # Color tokens
├── Typography.kt # Font scales
├── Spacing.kt # Dimension tokens (4dp grid)
├── Shape.kt # Corner radius tokens
├── Elevation.kt # Shadow tokens
└── Theme.kt # CompositionLocal providers
ios/PartyOn/Design/
├── Colors/
│ ├── ColorTokens.swift
│ └── ColorPalette.swift
├── Typography/
│ ├── TypeScale.swift
│ └── FontTokens.swift
├── Spacing/
│ └── SpacingTokens.swift
├── Shapes/
│ └── ShapeTokens.swift
└── Theme/
└── ThemeProvider.swift
// Kotlin - use with .dp
object Spacing {
val xs4 = 4.dp // Minimal gaps
val xs8 = 8.dp // Tight spacing
val sm12 = 12.dp // Small gaps
val md16 = 16.dp // Standard margin
val lg24 = 24.dp // Large spacing
val xl32 = 32.dp // Extra large
val xxl48 = 48.dp
val xxxl64 = 64.dp
}
// Swift - use as constants
enum Spacing {
static let xs4: CGFloat = 4
static let xs8: CGFloat = 8
static let sm12: CGFloat = 12
static let md16: CGFloat = 16
static let lg24: CGFloat = 24
static let xl32: CGFloat = 32
static let xxl48: CGFloat = 48
static let xxxl64: CGFloat = 64
}
// Bad: Hex-based naming
val colorFF6B6B: Color
val color4ECDC4: Color
// Good: Semantic naming
val colorPrimary: Color = Color(0xFF6200EE)
val colorSecondary: Color = Color(0xFF03DAC6)
val colorError: Color = Color(0xFFB00020)
val colorSurface: Color = Color(0xFFFFFFFF)
val colorOnSurface: Color = Color(0xFF1C1B1F)
// Material Design 3 Typography Scale
val displayLarge = TextStyle(fontSize = 57.sp, fontWeight = FontWeight.Bold, lineHeight = 64.sp)
val displayMedium = TextStyle(fontSize = 45.sp, fontWeight = FontWeight.Bold, lineHeight = 52.sp)
val displaySmall = TextStyle(fontSize = 36.sp, fontWeight = FontWeight.Bold, lineHeight = 44.sp)
val headingLarge = TextStyle(fontSize = 32.sp, fontWeight = FontWeight.Bold, lineHeight = 40.sp)
val headingMedium = TextStyle(fontSize = 28.sp, fontWeight = FontWeight.Bold, lineHeight = 36.sp)
val headingSmall = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold, lineHeight = 32.sp)
val titleLarge = TextStyle(fontSize = 22.sp, fontWeight = FontWeight.Bold, lineHeight = 28.sp)
val titleMedium = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Medium, lineHeight = 24.sp)
val titleSmall = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium, lineHeight = 20.sp)
val bodyLarge = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Normal, lineHeight = 24.sp)
val bodyMedium = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Normal, lineHeight = 20.sp)
val bodySmall = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Normal, lineHeight = 16.sp)
val labelLarge = TextStyle(fontSize = 14.sp, fontWeight = FontWeight.Medium, lineHeight = 20.sp)
val labelMedium = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Medium, lineHeight = 16.sp)
val labelSmall = TextStyle(fontSize = 11.sp, fontWeight = FontWeight.Medium, lineHeight = 16.sp)
// Add to build.gradle.kts
dependencies {
ksp("com.airbnb.android:showkase-processor:1.0.0-beta8")
implementation("com.airbnb.android:showkase:1.0.0-beta8")
}
// Component with preview
@Composable
fun CustomButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true
) {
Button(
onClick = onClick,
modifier = modifier,
enabled = enabled
) {
Text(text)
}
}
// Preview annotation
@Preview(name = "Default State", group = "CustomButton")
@Composable
fun CustomButtonPreview() {
CustomButton(
text = "Click me",
onClick = {}
)
}
// Showkase integration
@ShowkaseComposable(
name = "Custom Button",
group = "Components/Buttons",
description = "A custom button following Material Design 3"
)
@Preview(name = "Default", group = "CustomButton", backgroundColor = 0xFFFFFF, showBackground = true)
@Composable
fun CustomButtonShowkase() {
CustomButton(text = "Click me", onClick = {})
}
// 5 automatic permutations generated:
// - Light mode
// - Dark mode
// - RTL (right-to-left)
// - Font scaled (larger text)
// - Display scaled (larger overall)
Access Preview Browser:
// Component definition
struct CustomButton: View {
let title: String
let action: () -> Void
@Environment(\.isEnabled) var isEnabled
var body: some View {
Button(action: action) {
Text(title)
.frame(maxWidth: .infinity)
.padding()
.background(isEnabled ? Color.primary : Color.gray)
.foregroundColor(.white)
.cornerRadius(8)
}
.disabled(!isEnabled)
}
}
// Swift 6+ Preview macro
#Preview("Default State") {
CustomButton(title: "Click me", action: {})
.padding()
}
#Preview("Dark Mode") {
CustomButton(title: "Click me", action: {})
.padding()
.preferredColorScheme(.dark)
}
#Preview("Disabled") {
CustomButton(title: "Click me", action: {})
.disabled(true)
.padding()
}
#Preview("Large Text (Accessibility)") {
CustomButton(title: "Click me", action: {})
.environment(\.sizeCategory, .extraExtraLarge)
.padding()
}
// Multiple device previews
#Preview("iPhone") {
CustomButton(title: "Click me", action: {})
.previewDevice(PreviewDevice(rawValue: "iPhone 15"))
}
#Preview("iPad") {
CustomButton(title: "Click me", action: {})
.previewDevice(PreviewDevice(rawValue: "iPad (7th generation)"))
}
import SnapshotTesting
import XCTest
class CustomButtonTests: XCTestCase {
func testCustomButton_default() {
let view = CustomButton(title: "Click me", action: {})
assertSnapshot(matching: view, as: .image)
}
func testCustomButton_darkMode() {
let view = CustomButton(title: "Click me", action: {})
.preferredColorScheme(.dark)
assertSnapshot(matching: view, as: .image(colorScheme: .dark))
}
func testCustomButton_disabled() {
let view = CustomButton(title: "Click me", action: {})
.disabled(true)
assertSnapshot(matching: view, as: .image)
}
func testCustomButton_largeText() {
let view = CustomButton(title: "Click me", action: {})
.environment(\.sizeCategory, .extraExtraLarge)
assertSnapshot(matching: view, as: .image)
}
}
Workflow:
XCTAssertSnapshotTesting auto-updates on approval@RunWith(RobolectricTestRunner::class)
class CustomButtonScreenshotTest {
@get:Rule
val rule = RoborazziRule()
@Test
fun testCustomButton_default() {
rule.captureRoboImage(
filePath = "CustomButton/default.png"
) {
CustomButton(
text = "Click me",
onClick = {},
modifier = Modifier.wrapContentSize()
)
}
}
@Test
fun testCustomButton_darkMode() {
rule.captureRoboImage(
filePath = "CustomButton/dark_mode.png"
) {
AppTheme(useDarkTheme = true) {
CustomButton(
text = "Click me",
onClick = {},
modifier = Modifier.wrapContentSize()
)
}
}
}
}
Generate a structured component extraction report:
## Component Extraction Analysis: [File/Screen Name]
### Identified Components
List each component with:
- Component name
- Current location (monolithic)
- Extracted location (proposed)
- Suggested pattern (Slot API, ViewModifier, Generic, Base)
- Scope (size, responsibility)
Example:
- **EventCard**: Extract to `components/EventCard.kt`
- Pattern: Slot API (flexible content)
- Scope: 120 lines → 80 lines (extracted)
- Key props: title, description, image, onTap
### Design Token Extraction
- **Colors found**: [hardcoded hex values and where used]
- Suggested tokens: primary, secondary, surface, error
- **Typography**: [font sizes, weights found]
- Suggested scale: headingLarge, bodyMedium, labelSmall
- **Spacing**: [pixel values that don't follow grid]
- Align to: 4dp grid (4, 8, 12, 16, 24, 32, 48, 64)
- **Shapes**: [corner radii, borders found]
- Create tokens: small (4dp), medium (12dp), large (16dp)
### Component Extraction Plan
For each component, provide:
1. **API Design**
```kotlin
@Composable
fun EventCard(
event: Event,
modifier: Modifier = Modifier,
onTap: (Event) -> Unit,
header: @Composable (Event) -> Unit = { /* default */ },
footer: @Composable (Event) -> Unit = { /* default */ }
) { ... }
Slot Breakdown: What content slots to provide
Design Tokens: Which tokens to use (no hardcoded values)
Preview Code: Complete Showkase/Preview annotations
Testing Strategy: What snapshots to capture
components/
├── EventCard/
│ ├── EventCard.kt
│ ├── EventCardPreview.kt
│ ├── EventCardTests.kt
│ └── README.md
├── CustomButton/
│ ├── CustomButton.kt
│ ├── CustomButtonPreview.kt
│ └── CustomButtonTests.kt
└── ...
theme/
├── Color.kt
├── Typography.kt
├── Spacing.kt
├── Shape.kt
└── Theme.kt
Step-by-step instructions to refactor existing code:
## Example Output
```markdown
## Component Extraction Analysis: EventDetailScreen.kt
### Identified Components
**EventCard** (120 lines)
- Pattern: Slot API
- Scope: Reusable event summary card
- Key slots: header, content, footer
- Found in: EventDetailScreen:45-165
**EventActionButton** (35 lines)
- Pattern: View Modifier + Generic
- Scope: Primary action button for events
- Variations: Join, Leave, Host
- Found in: EventDetailScreen:200-235
**EventMetadata** (55 lines)
- Pattern: Base Component
- Scope: Date, time, location, attendee count
- Variants: Compact, full, upcoming
- Found in: EventDetailScreen:300-355
### Design Token Extraction
**Colors found:**
- `#93DF7C` (join button) → colorSuccess
- `#EEF2F7` (card background) → colorSurface
- `#1C1B1F` (text) → colorOnSurface
- `#B0BCCC` (disabled) → colorOutline
**Typography:**
- 24sp bold → headingMedium
- 16sp regular → bodyMedium
- 14sp medium → labelMedium
**Spacing:**
- 16dp horizontal padding → md16
- 8dp gaps → xs8
- 12dp card spacing → sm12
**Shapes:**
- 18dp radius → medium
- 4dp radius → small
### Component Extraction Plan
#### EventCard Component
```kotlin
@Composable
fun EventCard(
event: Event,
modifier: Modifier = Modifier,
onTap: () -> Unit,
header: @Composable (ColumnScope.(Event) -> Unit)? = null,
content: @Composable (ColumnScope.(Event) -> Unit)? = null,
footer: @Composable (ColumnScope.(Event) -> Unit)? = null,
) {
Surface(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onTap),
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.surface,
tonalElevation = 2.dp
) {
Column(
modifier = Modifier.padding(Spacing.md16)
) {
header?.invoke(this, event)
content?.invoke(this, event)
footer?.invoke(this, event)
}
}
}
Slot breakdown:
header: Event title, image (optional)content: Default shows date/time/locationfooter: Join button, action menuTokens used: MaterialTheme.shapes.medium, colorScheme.surface, Spacing.md16
Previews needed:
android/app/src/main/java/com/partyonapp/android/ui/
├── components/
│ ├── cards/
│ │ ├── EventCard.kt
│ │ ├── EventCardPreview.kt (Showkase)
│ │ ├── EventCardTests.kt (Roborazzi)
│ │ └── README.md
│ ├── buttons/
│ │ ├── EventActionButton.kt
│ │ ├── EventActionButtonPreview.kt
│ │ └── EventActionButtonTests.kt
│ └── metadata/
│ ├── EventMetadata.kt
│ └── EventMetadataPreview.kt
└── theme/
├── Color.kt (new tokens)
├── Spacing.kt (new tokens)
└── Theme.kt (updated)
Create theme tokens (before components)
# Color.kt with colorSuccess, colorSurface, etc.
# Spacing.kt with md16, xs8, sm12, etc.
# Shape.kt with medium, small radius tokens
Extract EventCard component
#93DF7C with MaterialTheme.colorScheme.successSpacing.md16Update EventDetailScreen imports
import com.partyonapp.android.ui.components.cards.EventCardAdd Showkase previews
Add Roborazzi snapshots
Verify visuals
Quick wins (do first):
High-impact:
Dependencies:
## Common Pitfalls & Best Practices
### DO:
- Extract business logic to ViewModels first (components are presentation-only)
- Use slot APIs to maximize flexibility
- Define all tokens in one place
- Add comprehensive previews before refactoring existing screens
- Keep components small (< 150 lines of code)
- Document expected content in slot parameters
- Test snapshots on multiple devices
- Review token usage in design tools (Figma, Material Design 3)
### DON'T:
- Don't hardcode colors, spacing, or typography values in components
- Don't mix state management with component definition
- Don't create generic components that are too abstract
- Don't skip preview annotations (critical for design consistency)
- Don't test snapshots without deterministic data (random content fails)
- Don't create components so small they fragment the codebase
- Don't ignore accessibility (semantics, content descriptions, text contrast)
## References
This command incorporates best practices from:
- [Material Design 3 Jetpack Compose](https://m3.material.io/develop/android/jetpack-compose)
- [Showkase: Android Component Browser](https://github.com/airbnb/Showkase)
- [SwiftUI Component Library Patterns](https://dev.to/sebastienlato/build-a-reusable-swiftui-component-library-15i3)
- [Compose Slot API Guidelines](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.md)
- [SnapshotTesting for SwiftUI](https://github.com/pointfreeco/swift-snapshot-testing)
- [Design Systems in 2026](https://rydarashid.medium.com/design-systems-in-2026-predictions-pitfalls-and-power-moves-f401317f7563)
- [SwiftUI Reusable Components](https://dev.to/swift_pal/master-swiftui-design-systems-from-scattered-colors-to-unified-ui-components-4i9c)