# LubaUI — AI Agent Reference > For human documentation, see README.md > For Claude Code guidance, see the project's CLAUDE.md > MCP server: `claude mcp add lubaui -- npx lubaui-mcp` (npm: lubaui-mcp) > Website: https://shopofluba.de/lubaui > Repository: https://github.com/ermanakar/LubaUI > Last updated: 2026-08-10 (0.2.0, unreleased) --- ## Philosophy (The Why) LubaUI is built on three pillars: **1. Radical Composability** — Behaviors are extracted into primitives. The press interaction isn't locked in `LubaButton` — it's `.lubaPressable()` so any view can use it. **2. AI-Native Architecture** — Semantic naming, strict tokenization, documented rationale. Every magic number has a name and reason. **3. Low-Friction Adoption** — `LubaButton("Save") { }` just works. Customize when you want, not because you have to. --- ## The Rules (Non-Negotiable) 1. **Never hardcode values** — Use tokens (LubaColors, LubaSpacing, LubaMotion) 2. **Never hardcode animations** — Use LubaMotion for values, LubaAnimations for applied presets 3. **Inside a component, read the environment, not the statics** — `@LubaEnvironment private var luba`, then `luba.colors.*` and `luba.fonts.*`. Reading `LubaColors` / `LubaTypography` directly inside a component body is what stops `.lubaTheme(…)` from reaching the pixels. App code may use the statics. 4. **Route every animation through `luba.motion`** — never `withAnimation` or a bare `.animation(_:value:)` in a component. The policy is what honors Reduce Motion. 5. **Localize built-in strings** — put user-facing/accessibility text in `LubaStrings` plus both `.lproj` files. Never hardcode English in a component. 6. **Preserve backwards compatibility** — Old APIs must continue to work --- ## Architecture ``` Sources/LubaUI/ ├── LubaUI.swift # Entry point, version info ├── Tokens/ # Design tokens (the DNA) │ ├── LubaPrimitives.swift # Tier 1: Raw hex/numbers — NEVER use in components │ ├── LubaColors.swift # Tier 2: Semantic adaptive colors │ ├── LubaSpacing.swift # Tier 2: 4pt grid spacing │ ├── LubaTypography.swift # Tier 2: SF Rounded type scale │ ├── LubaRadius.swift # Tier 2: Corner radii │ ├── LubaMotion.swift # Tier 2: Animation constants ★ │ ├── LubaControlTokens.swift # Tier 3: Form control tokens │ ├── LubaFeedbackTokens.swift # Tier 3: Feedback component tokens │ └── LubaChartTokens.swift # Tier 3: Chart component tokens ├── Primitives/ # Reusable behaviors ★ │ ├── LubaPressable.swift # .lubaPressable() — tap with scale + haptics │ ├── LubaButtonStyling.swift # Button style protocol │ ├── LubaExpandable.swift # .lubaExpandable() — expand/collapse + accordion │ ├── LubaLongPressable.swift # .lubaLongPressable() — long press + progress ring │ ├── LubaShimmerable.swift # .lubaShimmerable() — loading shimmer effect │ ├── LubaSwipeable.swift # .lubaSwipeable() — swipe actions (delete, archive, pin) │ ├── LubaGlass.swift # .lubaGlass() — frosted glass effect (iOS 26 Liquid Glass ready) │ └── LubaChartStyle.swift # .lubaChartStyle() — token-based chart styling ├── Components/ # UI components (26 files) │ ├── LubaAlert.swift # Inline notification banner │ ├── LubaAvatar.swift # Profile image/initials + AvatarGroup │ ├── LubaBadge.swift # Status indicator │ ├── LubaButton.swift # Multi-style button │ ├── LubaCard.swift # Container with elevation │ ├── LubaCheckbox.swift # Binary toggle │ ├── LubaChip.swift # Filter/tag pill │ ├── LubaDivider.swift # Separator with optional label │ ├── LubaIcon.swift # Icon + IconButton + CircledIcon │ ├── LubaLink.swift # Inline text link │ ├── LubaMenu.swift # Context menu │ ├── LubaProgress.swift # Bar + circular progress │ ├── LubaRadio.swift # Radio button + RadioGroup │ ├── LubaRating.swift # Star rating │ ├── LubaSearchBar.swift # Search input │ ├── LubaSheet.swift # Bottom sheet modal │ ├── LubaSkeleton.swift # Shimmer loading placeholders │ ├── LubaSlider.swift # Value slider │ ├── LubaStepper.swift # Numeric +/- adjuster │ ├── LubaTabs.swift # Segmented + underline tabs │ ├── LubaTextArea.swift # Multi-line text editor │ ├── LubaTextField.swift # Text input with states │ ├── LubaToast.swift # Overlay notification │ ├── LubaToggle.swift # Switch control │ ├── LubaTooltip.swift # Contextual help popup │ └── LubaChart.swift # Bar, line, pie, sparkline charts (wraps Swift Charts) ├── Config/ │ └── LubaConfig.swift # Runtime config (haptics, a11y, debug) ├── Theme/ │ └── LubaTheme.swift # Theme configuration + environment └── Utilities/ ├── LubaAccessibility.swift # A11y helpers, WCAG contrast, announcements ├── LubaAnimations.swift # Applied animation presets + transitions └── LubaHaptics.swift # Cross-platform haptic feedback ``` --- ## Token Reference ### Colors (LubaColors) ```swift .textPrimary, .textSecondary, .textTertiary, .textDisabled // Text hierarchy .textOnAccent // Text on accent bg .accent, .accentHover, .accentSubtle // Brand (sage green) .success, .warning, .error // Semantic states .successSubtle, .warningSubtle, .errorSubtle // Subtle semantic bg .surface, .surfaceSecondary, .surfaceTertiary, .background // Surfaces .border, .borderFocused // Borders .gray900, .gray800, .gray600, .gray500, .gray400, .gray200, .gray100, .gray50 // Greyscale ``` ### Spacing (LubaSpacing) — 4pt base ```swift .xxs = 2, .xs = 4, .sm = 8, .md = 12, .lg = 16, .xl = 24, .xxl = 32, .xxxl = 48, .huge = 64 .custom(_ multiplier: Int) -> CGFloat // multiplier * 4 .insets(_ spacing: CGFloat) -> EdgeInsets ``` ### Radius (LubaRadius) ```swift .none = 0, .xs = 4, .sm = 8, .md = 12, .lg = 16, .xl = 24, .full = 9999 ``` ### Motion (LubaMotion) — animation constants ```swift // Press scale hierarchy: smaller elements → more scale, larger elements → less .pressScaleCompact = 0.95 // Icon buttons, chips (5% shrink) .pressScale = 0.97 // Standard press feedback (3% shrink) .pressScaleProminent = 0.98 // Subtle press for large elements (2% shrink) .pressAnimation // spring(0.25, 0.65) — quick bounce .colorAnimation // easeOut(0.12) — color transitions .stateAnimation // spring(0.35, 0.75) — state changes .micro // spring(0.2, 0.7) — ultra-quick .gentle // spring(0.4, 0.8) — soft .bouncy // spring(0.35, 0.5) — playful .disabledOpacity = 0.45 .loadingContentOpacity = 0.7 .iconLabelSpacing = 6 .stagger(index:base:) // Stagger delay for list animations ``` ### Animations (LubaAnimations) — applied animation presets Use `LubaMotion` for raw values/constants. Use `LubaAnimations` for applied `Animation` objects and transitions. ```swift // Spring presets (return Animation) .quick // spring(0.3, 0.7) — button presses, toggles .standard // spring(0.4, 0.75) — cards, panels .gentle // spring(0.5, 0.8) — page transitions .bouncy // spring(0.4, 0.6) — success states // Eased presets .fadeIn // easeOut(0.2) — appearing elements .smooth // easeInOut(0.25) — color/opacity .subtle // easeInOut(0.4) — background changes // Helpers .staggerDelay(for: index) // index * 0.05 // Transitions (on AnyTransition) .lubaSlideUp // Asymmetric slide + fade .lubaScale // Scale 0.9→1.0 + fade .lubaFade // Simple opacity // View modifier .lubaAnimation(.bouncy, value: isActive) ``` ### Component Tokens **Form Controls** (LubaControlTokens.swift): - `LubaSelectionTokens` — controlSize: 20, indicatorSize: 10, checkboxRadius: 5, borderWidth: 1.5, checkmarkSize: 11, labelSpacing: 10, minTouchTarget: 44 - `LubaToggleTokens` — trackWidth: 48, trackHeight: 28, thumbSize: 24, thumbPadding: 2, labelSpacing: 12, minTouchTarget: 44 - `LubaSliderTokens` — trackHeight: 4, thumbSize: 22, thumbBorderWidth: 2, thumbDragScale: 1.1 **Text Fields** (LubaTextField.swift): - `LubaFieldTokens` — minHeight: 48, cornerRadius: 10, horizontalPadding: 14, iconSize: 18, iconFrameWidth: 20, labelFontSize: 13, helperFontSize: 12, borderWidth: 1, borderWidthFocused: 1.5, clearButtonSize: 16 **Cards** (LubaCard.swift): - `LubaCardTokens` — cornerRadius, padding, elevation levels (.flat, .low, .medium, .high) **Feedback** (LubaFeedbackTokens.swift): - `LubaToastTokens` — cornerRadius: 12, shadowBlur: 8, shadowY: 4, shadowOpacity: 0.08, defaultDuration: 3.0 - `LubaProgressTokens` — barHeight: 6, circularSize: 64, circularStrokeWidth: 6 - `LubaSpinnerTokens` — arcTrim: 0.65, arcDuration: 0.75, pulseDuration: 1.0, dotsDuration: 0.4 - `LubaSkeletonTokens` — shimmerDuration: 1.5, cornerRadius: 4, defaultHeight: 14 - `LubaTabsTokens` — tabHeight: 32, underlineHeight: 44, segmentedContainerRadius: 10 - `LubaSheetTokens` — headerPadding: 16, closeButtonSize: 28, titleFontSize: 18 - `LubaIconTokens` — touchTarget: 44, pressScale: 0.95 (refs LubaMotion.pressScaleCompact), circledPaddingMultiplier: 1.8 - `LubaMenuTokens` — minWidth: 200, itemHeight: 44, cornerRadius: 12, iconSize: 16 - `LubaTooltipTokens` — maxWidth: 240, padding: 10, cornerRadius: 8, dismissDuration: 3.0 **Charts** (LubaChartTokens.swift): - `LubaChartTokens` — defaultHeight: 200, compactHeight: 140, expandedHeight: 300, sparklineHeight: 40, barCornerRadius: 4, lineWidth: 2.5, pointSize: 6, areaOpacity: 0.15, gridOpacity: 0.12 - `LubaColors.Chart` — 6-color adaptive palette (sage, slate blue, terracotta, dusty violet, teal, warm sand), grid/axis colors --- ## Component API Reference ### LubaButton ```swift LubaButton( _ title: String, style: LubaButtonStyle = .primary, // .primary | .secondary | .ghost | .destructive | .subtle size: LubaButtonSize = .medium, // .small | .medium | .large isLoading: Bool = false, isDisabled: Bool = false, icon: Image? = nil, iconPosition: LubaIconPosition = .leading, fullWidth: Bool? = nil, action: () -> Void ) // Custom styling via protocol LubaButton("Save", styling: MyCustomStyle()) { } // Glass style LubaButton("Action", style: .glass) { } ``` ### LubaCard ```swift LubaCard( elevation: LubaCardElevation = .low, // .flat | .low | .medium | .high style: LubaCardStyle = .filled, // .filled | .outlined | .ghost | .glass cornerRadius: CGFloat = LubaCardTokens.cornerRadius, padding: CGFloat = LubaCardTokens.padding, content: () -> Content ) ``` ### LubaTextField ```swift LubaTextField( _ label: String, text: Binding, placeholder: String = "", helperText: String? = nil, error: String? = nil, leadingIcon: Image? = nil, trailingIcon: Image? = nil, isSecure: Bool = false, isDisabled: Bool = false, showClearButton: Bool = true ) // Convenience initializers LubaTextField.email(text: $email) LubaTextField.secure("Password", text: $password) ``` ### LubaTextArea ```swift LubaTextArea( _ label: String, text: Binding, placeholder: String = "", characterLimit: Int? = nil, minHeight: CGFloat = 100 ) ``` ### LubaSearchBar ```swift LubaSearchBar( text: Binding, placeholder: String = "Search", showCancelButton: Bool = true, onSubmit: (() -> Void)? = nil, onCancel: (() -> Void)? = nil ) ``` ### LubaStepper ```swift LubaStepper( value: Binding, in range: ClosedRange = 0...99, step: Int = 1, label: String? = nil ) ``` ### LubaRating ```swift LubaRating( value: Binding, maxStars: Int = 5, isReadOnly: Bool = false, label: String? = nil ) ``` ### LubaLink ```swift LubaLink( _ label: String, style: LubaLinkStyle = .default, // .default | .subtle | .external action: () -> Void ) ``` ### LubaAlert ```swift LubaAlert( _ message: String, style: LubaAlertStyle = .info, // .info | .success | .warning | .error title: String? = nil, isDismissible: Bool = false, onDismiss: (() -> Void)? = nil ) ``` ### LubaChip ```swift LubaChip( _ label: String, style: LubaChipStyle = .filled, // .filled | .outlined icon: Image? = nil, isSelected: Bool = false, isDismissible: Bool = false, onDismiss: (() -> Void)? = nil, onTap: (() -> Void)? = nil ) ``` ### LubaMenu ```swift // With custom label LubaMenu(items: [ LubaMenuItem("Edit", icon: Image(systemName: "pencil")) { edit() }, LubaMenuItem("Delete", icon: Image(systemName: "trash"), role: .destructive) { delete() } ]) { Image(systemName: "ellipsis") } // With default ellipsis label LubaMenu(items: [...]) ``` ### LubaTooltip ```swift LubaTooltip("Helpful info", position: .top) { // .top | .bottom Image(systemName: "questionmark.circle") } // Or as modifier Text("Hover me") .lubaTooltip("Helpful info", position: .bottom) ``` ### Form Controls ```swift LubaCheckbox(isChecked: $checked, label: "Accept terms") LubaToggle(isOn: $enabled, label: "Notifications") LubaRadio(isSelected: $selected, label: "Option A") LubaSlider(value: $volume, label: "Volume", showValue: true) ``` ### Tabs ```swift LubaTabs(selection: $tab, tabs: [ (value: 0, label: "Daily"), (value: 1, label: "Weekly") ]) LubaUnderlineTabs(selection: $tab, tabs: [...]) ``` ### Feedback ```swift LubaToast("Message saved", style: .success) .lubaToast(isPresented: $showToast, message: "Saved", style: .success) LubaProgressBar(value: 0.65, showLabel: true) LubaCircularProgress(value: 0.75) LubaSpinner(style: .arc) // .arc | .pulse | .dots | .breathe ``` ### Data Display ```swift LubaAvatar(name: "John Doe", size: .large) LubaBadge("New", style: .accent) LubaIcon("heart.fill", size: .md, color: LubaColors.accent) LubaIconButton("bell") { showNotifications() } LubaSkeleton(height: 14) LubaSkeletonCard() ``` ### Charts ```swift // Requires: import Charts // Bar chart (conforms data to LubaChartData protocol) LubaBarChart(data: items) LubaBarChart(data: items, height: LubaChartTokens.compactHeight, horizontal: true) // Grouped multi-series bar (data conforms to LubaSeriesChartData) LubaGroupedBarChart(data: seriesItems) // Line chart with area fill and points LubaLineChart(data: items, showArea: true, showPoints: true) // Multi-series line LubaMultiLineChart(data: seriesItems) // Pie/donut (iOS 17+) LubaPieChart(data: items) LubaPieChart(data: items, innerRadius: .ratio(0.55)) // donut // Sparkline — minimal inline trend LubaSparkline(values: [4, 7, 5, 9, 6, 8, 12]) .frame(width: 80, height: 40) // Loading skeleton LubaChartSkeleton(style: .bar) LubaChartSkeleton(style: .line) // Style any raw Chart with LubaUI tokens Chart { ... }.lubaChartStyle(height: 200, showAxes: true) ``` ### Layout ```swift LubaDivider() LubaDivider(label: "or continue with") LubaSheetHeader("Settings", subtitle: "Customize") { dismiss() } .lubaSheet(isPresented: $show, size: .medium) { content } ``` --- ## Key Patterns ### Pattern 1: Environment-Aware Components ```swift struct MyRow: View { @LubaEnvironment private var luba // theme + config + Reduce Motion, resolved var body: some View { Text("Hello") .font(luba.fonts.body) // theme typography + Dynamic Type .foregroundStyle(luba.colors.textPrimary) // theme colors .padding(luba.spacing.md) } private func tap() { if luba.hapticsEnabled { LubaHaptics.light() } // haptics stay independent of motion } } ``` `@LubaEnvironment` works in `View`, `ViewModifier`, and `ButtonStyle`. ### Pattern 2: Motion Through the Policy ```swift // ✅ CORRECT — the policy sees LubaConfig *and* system Reduce Motion .animation(luba.motion.animation(LubaMotion.stateAnimation), value: state) // essential .animation(luba.motion.decorative(LubaMotion.pressAnimation), value: isPressed) // decorative .scaleEffect(luba.motion.pressScale(isPressed ? LubaMotion.pressScale : 1.0)) .transition(luba.motion.transition(.move(edge: .top))) luba.motion.run(LubaMotion.micro) { isOpen.toggle() } // ❌ WRONG — blind to Reduce Motion .animation(LubaMotion.pressAnimation, value: isPressed) .animation(config.animationsEnabled ? LubaMotion.stateAnimation : nil, value: state) withAnimation(.spring(response: 0.25)) { isOpen.toggle() } ``` Policy methods: `animation` (essential → cross-fade), `decorative` (→ nil), `interaction`, `repeating` (→ nil), `repeatingOpacity` (survives), `continuous` (survives — progress that carries information), `pressScale` / `motionAmount` (→ neutral), `stagger` (delay → 0), `transition` (→ `.opacity`). ### Pattern 3: Composable Press Behavior ```swift // Any view can be pressable LubaCard { content } .lubaPressable(scale: LubaMotion.pressScaleProminent) { navigate() } Image(systemName: "heart") .lubaPressable(haptic: .medium) { toggleFavorite() } ``` ### Pattern 4: Custom Button Styling ```swift struct BrandStyle: LubaButtonStyling { // Legacy members stay required; forward them to the theme-aware ones. func backgroundColor(isPressed: Bool, colorScheme: ColorScheme) -> Color { backgroundColor(in: LubaButtonStyleContext(isPressed: isPressed, colorScheme: colorScheme)) } func foregroundColor(isPressed: Bool, colorScheme: ColorScheme) -> Color { .white } func borderColor(isPressed: Bool, colorScheme: ColorScheme) -> Color? { nil } // Theme-aware members receive the active LubaThemeColors. func backgroundColor(in c: LubaButtonStyleContext) -> Color { c.isPressed ? c.colors.accentHover : c.colors.accent } func foregroundColor(in c: LubaButtonStyleContext) -> Color { c.colors.textOnAccent } func borderColor(in c: LubaButtonStyleContext) -> Color? { nil } var borderWidth: CGFloat { 0 } var defaultsToFullWidth: Bool { false } var haptic: LubaHapticStyle { .medium } } LubaButton("Custom", styling: BrandStyle()) { } ``` A style that implements only the legacy members still works — the context-based members default to forwarding to them. ### Pattern 4b: Theming an App ```swift // One brand color derives the accent ramp, focus border, info color, chart lead. RootView().lubaTheme(LubaThemeConfiguration(colors: .accented(Color(hex: 0x2F5FD0)))) // Palette-only override for a subtree; typography/spacing/radius are inherited. PromoBanner().lubaTheme(colors: .accented(.orange)) ``` Semantic roles: `accent`/`accentHover`/`accentSubtle`/`textOnAccent`, `background`/`surface`/`surfaceSecondary`/`surfaceTertiary`/`surfaceHover`, `textPrimary`/`textSecondary`/`textTertiary`/`textDisabled`, `border`/`borderStrong`/`borderFocused`/`divider`/`fill`, `success`/`warning`/`error`/`info` (+ `…Subtle`), `chartPalette`/`chartGrid`/`chartAxisLabel`, `glassBorder`/`glassShadow`. ### Pattern 5: Semantic Token Usage ```swift // ✅ CORRECT — app code Text("Hello").foregroundStyle(LubaColors.textPrimary) VStack(spacing: LubaSpacing.lg) { } // ✅ CORRECT — inside a LubaUI component (theme-aware) Text("Hello").foregroundStyle(luba.colors.textPrimary) VStack(spacing: luba.spacing.lg) { } // ❌ WRONG Text("Hello").foregroundStyle(Color(hex: 0x1A1A1A)) VStack(spacing: 16) { } ``` ### Pattern 6: Using Animation Presets ```swift // Applied animations (LubaAnimations) for withAnimation and .animation withAnimation(LubaAnimations.quick) { toggle() } .animation(LubaAnimations.standard, value: isExpanded) // Staggered list animations ForEach(items.indices) { i in row.animation(LubaAnimations.standard.delay(LubaAnimations.staggerDelay(for: i)), value: isVisible) } // Transitions .transition(.lubaSlideUp) .transition(.lubaScale) ``` --- ## New Component Checklist When creating a component: - [ ] Create `LubaFooTokens` if component has magic numbers - [ ] Add `@LubaEnvironment private var luba` - [ ] Take colors from `luba.colors`, fonts from `luba.fonts`, spacing from `luba.spacing` - [ ] Route every animation through `luba.motion` — never bare `withAnimation` - [ ] Use `LubaMotion` / `LubaAnimations` for the animation *values* you hand the policy - [ ] Check `luba.hapticsEnabled` before haptics - [ ] Put user-facing and accessibility strings in `LubaStrings` + `en.lproj` + `de.lproj` - [ ] Use `minHeight`, not `height`, on anything containing text - [ ] Honor `luba.minimumTouchTarget` on interactive elements - [ ] Add #Preview with multiple states - [ ] Preserve backwards compatibility --- ## Build Commands ```bash swift build # Build package swift test # Run tests # Library-only iOS Simulator build (what CI runs) xcodebuild build -scheme LubaUI -destination 'generic/platform=iOS Simulator' ``` --- ## Agent Instructions When generating LubaUI code: 1. Import LubaUI 2. Use semantic tokens — never raw values 3. Use LubaMotion for animation values — never hardcode springs/durations 4. Use LubaAnimations for applied animation presets and transitions 5. Use `.lubaPressable()` to make custom views tappable 6. Use `.lubaSwipeable()` for swipe actions, `.lubaLongPressable()` for long press 7. Use `.lubaShimmerable()` for loading shimmer on any view 8. Wrap content in LubaCard for elevation 9. Use LubaButton for buttons — never custom Button styling 10. In custom components, use `@LubaEnvironment` for colors/fonts/motion/haptics 11. Apply `.lubaTheme(…)` to rebrand — do not pass colors into every component 12. Prefer composition (`.lubaPressable()`, `.lubaToast()`) over inheritance --- ## Glass Primitive ```swift // Apply glass to any view — three intensity levels .lubaGlass(.subtle) // Toolbars, FABs (ultraThinMaterial) .lubaGlass(.regular) // Cards, tab bars (thinMaterial) .lubaGlass(.prominent) // Panels, modals (regularMaterial) .lubaGlass(.regular, tint: LubaColors.accent) // With color tint // Components with glass support LubaButton("Action", style: .glass) { } LubaCard(style: .glass) { content } LubaToast("Saved", useGlass: true) LubaAlert("Info", useGlass: true) LubaSheetHeader("Title", useGlass: true) { dismiss() } LubaTabs(selection: $tab, tabs: tabs, useGlass: true) ``` iOS 16-25: SwiftUI materials. iOS 26+: native Liquid Glass (ready, commented). Solid fallback for reduceTransparency/highContrastMode. --- ## MCP Server LubaUI has an MCP server (`lubaui-mcp` on npm) that makes the design system queryable: ```bash claude mcp add lubaui -- npx lubaui-mcp ``` **10 tools:** lookup_token, lookup_component, lookup_primitive, lookup_components, lookup_tokens, validate_spacing, validate_radius, get_color_palette, suggest_tokens, plan_migration **4 resources:** lubaui://reference/full, lubaui://tokens/all, lubaui://architecture, lubaui://components --- End of AI agent reference.