feat(ui-design): add comprehensive UI/UX design plugin v1.0.0

New plugin covering mobile (iOS, Android, React Native) and web
applications with modern design patterns, accessibility, and design systems.

Components:
- 9 skills: design-system-patterns, accessibility-compliance, responsive-design,
  mobile-ios-design, mobile-android-design, react-native-design,
  web-component-design, interaction-design, visual-design-foundations
- 4 commands: design-review, create-component, accessibility-audit, design-system-setup
- 3 agents: ui-designer, accessibility-expert, design-system-architect

Marketplace updated:
- Version bumped to 1.3.4
- 102 agents (+3), 116 skills (+9)
This commit is contained in:
Seth Hobson
2026-01-19 16:22:13 -05:00
parent 8be0e8ac7a
commit 1e54d186fe
47 changed files with 21163 additions and 11 deletions

View File

@@ -0,0 +1,796 @@
# Jetpack Compose Component Library
## Lists and Collections
### Basic LazyColumn
```kotlin
@Composable
fun ItemList(
items: List<Item>,
onItemClick: (Item) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = items,
key = { it.id }
) { item ->
ItemRow(
item = item,
onClick = { onItemClick(item) }
)
}
}
}
```
### Pull to Refresh
```kotlin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RefreshableList(
items: List<Item>,
isRefreshing: Boolean,
onRefresh: () -> Unit
) {
val pullToRefreshState = rememberPullToRefreshState()
PullToRefreshBox(
state = pullToRefreshState,
isRefreshing = isRefreshing,
onRefresh = onRefresh
) {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(items) { item ->
ItemRow(item = item)
}
}
}
}
```
### Swipe to Dismiss
```kotlin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SwipeableItem(
item: Item,
onDelete: () -> Unit
) {
val dismissState = rememberSwipeToDismissBoxState(
confirmValueChange = { value ->
if (value == SwipeToDismissBoxValue.EndToStart) {
onDelete()
true
} else {
false
}
}
)
SwipeToDismissBox(
state = dismissState,
backgroundContent = {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.errorContainer)
.padding(horizontal = 20.dp),
contentAlignment = Alignment.CenterEnd
) {
Icon(
Icons.Default.Delete,
contentDescription = "Delete",
tint = MaterialTheme.colorScheme.onErrorContainer
)
}
}
) {
ItemRow(item = item)
}
}
```
### Sticky Headers
```kotlin
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun GroupedList(
groups: Map<String, List<Item>>
) {
LazyColumn {
groups.forEach { (header, items) ->
stickyHeader {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surfaceVariant
) {
Text(
text = header,
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
items(items, key = { it.id }) { item ->
ItemRow(item = item)
}
}
}
}
```
## Forms and Input
### Text Fields
```kotlin
@Composable
fun LoginForm(
onLogin: (email: String, password: String) -> Unit
) {
var email by rememberSaveable { mutableStateOf("") }
var password by rememberSaveable { mutableStateOf("") }
var passwordVisible by rememberSaveable { mutableStateOf(false) }
var emailError by remember { mutableStateOf<String?>(null) }
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
OutlinedTextField(
value = email,
onValueChange = {
email = it
emailError = if (it.isValidEmail()) null else "Invalid email"
},
label = { Text("Email") },
placeholder = { Text("name@example.com") },
leadingIcon = { Icon(Icons.Default.Email, null) },
isError = emailError != null,
supportingText = emailError?.let { { Text(it) } },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
),
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
leadingIcon = { Icon(Icons.Default.Lock, null) },
trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(
if (passwordVisible) Icons.Default.VisibilityOff
else Icons.Default.Visibility,
contentDescription = "Toggle password visibility"
)
}
},
visualTransformation = if (passwordVisible)
VisualTransformation.None
else
PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
),
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = { onLogin(email, password) },
modifier = Modifier.fillMaxWidth(),
enabled = email.isNotEmpty() && password.isNotEmpty() && emailError == null
) {
Text("Sign In")
}
}
}
```
### Search Bar
```kotlin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchableScreen(
items: List<Item>,
onItemClick: (Item) -> Unit
) {
var query by rememberSaveable { mutableStateOf("") }
var expanded by rememberSaveable { mutableStateOf(false) }
val filteredItems = remember(query, items) {
if (query.isEmpty()) items
else items.filter { it.name.contains(query, ignoreCase = true) }
}
SearchBar(
query = query,
onQueryChange = { query = it },
onSearch = { expanded = false },
active = expanded,
onActiveChange = { expanded = it },
placeholder = { Text("Search items") },
leadingIcon = { Icon(Icons.Default.Search, null) },
trailingIcon = {
if (query.isNotEmpty()) {
IconButton(onClick = { query = "" }) {
Icon(Icons.Default.Clear, "Clear search")
}
}
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = if (expanded) 0.dp else 16.dp)
) {
LazyColumn(
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(16.dp)
) {
items(filteredItems) { item ->
ListItem(
headlineContent = { Text(item.name) },
supportingContent = { Text(item.description) },
modifier = Modifier.clickable {
onItemClick(item)
expanded = false
}
)
}
}
}
}
```
### Selection Controls
```kotlin
@Composable
fun SettingsScreen() {
var notificationsEnabled by rememberSaveable { mutableStateOf(true) }
var selectedOption by rememberSaveable { mutableStateOf(0) }
var expandedDropdown by remember { mutableStateOf(false) }
var selectedLanguage by rememberSaveable { mutableStateOf("English") }
val languages = listOf("English", "Spanish", "French", "German")
Column {
// Switch
ListItem(
headlineContent = { Text("Enable Notifications") },
supportingContent = { Text("Receive push notifications") },
trailingContent = {
Switch(
checked = notificationsEnabled,
onCheckedChange = { notificationsEnabled = it }
)
}
)
HorizontalDivider()
// Radio buttons
Column {
Text(
"Theme",
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.titleSmall
)
listOf("System", "Light", "Dark").forEachIndexed { index, option ->
Row(
modifier = Modifier
.fillMaxWidth()
.selectable(
selected = selectedOption == index,
onClick = { selectedOption = index },
role = Role.RadioButton
)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedOption == index,
onClick = null
)
Spacer(Modifier.width(16.dp))
Text(option)
}
}
}
HorizontalDivider()
// Dropdown
ExposedDropdownMenuBox(
expanded = expandedDropdown,
onExpandedChange = { expandedDropdown = it },
modifier = Modifier.padding(16.dp)
) {
OutlinedTextField(
value = selectedLanguage,
onValueChange = {},
readOnly = true,
label = { Text("Language") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedDropdown) },
modifier = Modifier
.fillMaxWidth()
.menuAnchor()
)
ExposedDropdownMenu(
expanded = expandedDropdown,
onDismissRequest = { expandedDropdown = false }
) {
languages.forEach { language ->
DropdownMenuItem(
text = { Text(language) },
onClick = {
selectedLanguage = language
expandedDropdown = false
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding
)
}
}
}
}
}
```
## Dialogs and Bottom Sheets
### Alert Dialog
```kotlin
@Composable
fun DeleteConfirmationDialog(
itemName: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
icon = {
Icon(
Icons.Default.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error
)
},
title = {
Text("Delete Item?")
},
text = {
Text("Are you sure you want to delete \"$itemName\"? This action cannot be undone.")
},
confirmButton = {
TextButton(
onClick = onConfirm,
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error
)
) {
Text("Delete")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
```
### Modal Bottom Sheet
```kotlin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun OptionsBottomSheet(
onDismiss: () -> Unit,
onOptionSelected: (String) -> Unit
) {
val sheetState = rememberModalBottomSheetState()
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
dragHandle = { BottomSheetDefaults.DragHandle() }
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp)
) {
Text(
"Options",
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
style = MaterialTheme.typography.titleMedium
)
listOf(
Triple(Icons.Default.Share, "Share", "share"),
Triple(Icons.Default.Edit, "Edit", "edit"),
Triple(Icons.Default.FileCopy, "Duplicate", "duplicate"),
Triple(Icons.Default.Delete, "Delete", "delete")
).forEach { (icon, label, action) ->
ListItem(
headlineContent = { Text(label) },
leadingContent = {
Icon(
icon,
contentDescription = null,
tint = if (action == "delete")
MaterialTheme.colorScheme.error
else
MaterialTheme.colorScheme.onSurfaceVariant
)
},
modifier = Modifier.clickable { onOptionSelected(action) }
)
}
}
}
}
```
### Date and Time Pickers
```kotlin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DateTimePickerExample() {
var showDatePicker by remember { mutableStateOf(false) }
var showTimePicker by remember { mutableStateOf(false) }
val datePickerState = rememberDatePickerState()
val timePickerState = rememberTimePickerState()
Column(modifier = Modifier.padding(16.dp)) {
OutlinedButton(onClick = { showDatePicker = true }) {
Icon(Icons.Default.CalendarToday, null)
Spacer(Modifier.width(8.dp))
Text(
datePickerState.selectedDateMillis?.let {
SimpleDateFormat("MMM dd, yyyy", Locale.getDefault())
.format(Date(it))
} ?: "Select Date"
)
}
Spacer(Modifier.height(16.dp))
OutlinedButton(onClick = { showTimePicker = true }) {
Icon(Icons.Default.Schedule, null)
Spacer(Modifier.width(8.dp))
Text(
String.format("%02d:%02d", timePickerState.hour, timePickerState.minute)
)
}
}
if (showDatePicker) {
DatePickerDialog(
onDismissRequest = { showDatePicker = false },
confirmButton = {
TextButton(onClick = { showDatePicker = false }) {
Text("OK")
}
},
dismissButton = {
TextButton(onClick = { showDatePicker = false }) {
Text("Cancel")
}
}
) {
DatePicker(state = datePickerState)
}
}
if (showTimePicker) {
AlertDialog(
onDismissRequest = { showTimePicker = false },
confirmButton = {
TextButton(onClick = { showTimePicker = false }) {
Text("OK")
}
},
dismissButton = {
TextButton(onClick = { showTimePicker = false }) {
Text("Cancel")
}
},
text = {
TimePicker(state = timePickerState)
}
)
}
}
```
## Loading States
### Progress Indicators
```kotlin
@Composable
fun LoadingStates() {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
// Indeterminate circular
CircularProgressIndicator()
// Determinate circular
CircularProgressIndicator(
progress = { 0.7f },
strokeWidth = 4.dp
)
// Indeterminate linear
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
// Determinate linear
LinearProgressIndicator(
progress = { 0.7f },
modifier = Modifier.fillMaxWidth()
)
}
}
```
### Skeleton Loading
```kotlin
@Composable
fun SkeletonLoader(
modifier: Modifier = Modifier
) {
val infiniteTransition = rememberInfiniteTransition(label = "skeleton")
val alpha by infiniteTransition.animateFloat(
initialValue = 0.3f,
targetValue = 0.7f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
label = "alpha"
)
Column(
modifier = modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
repeat(5) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = alpha))
)
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Box(
modifier = Modifier
.height(16.dp)
.fillMaxWidth(0.7f)
.clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = alpha))
)
Box(
modifier = Modifier
.height(12.dp)
.fillMaxWidth(0.5f)
.clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = alpha))
)
}
}
}
}
}
```
### Content Loading Pattern
```kotlin
@Composable
fun <T> AsyncContent(
state: AsyncState<T>,
onRetry: () -> Unit,
content: @Composable (T) -> Unit
) {
when (state) {
is AsyncState.Loading -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
is AsyncState.Success -> {
content(state.data)
}
is AsyncState.Error -> {
Column(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
Icons.Default.Error,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.error
)
Spacer(Modifier.height(16.dp))
Text(
"Something went wrong",
style = MaterialTheme.typography.titleMedium
)
Spacer(Modifier.height(8.dp))
Text(
state.message,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(Modifier.height(24.dp))
Button(onClick = onRetry) {
Text("Try Again")
}
}
}
}
}
sealed class AsyncState<out T> {
object Loading : AsyncState<Nothing>()
data class Success<T>(val data: T) : AsyncState<T>()
data class Error(val message: String) : AsyncState<Nothing>()
}
```
## Animations
### Animated Visibility
```kotlin
@Composable
fun ExpandableCard(
title: String,
content: String
) {
var expanded by rememberSaveable { mutableStateOf(false) }
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier
.clickable { expanded = !expanded }
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(title, style = MaterialTheme.typography.titleMedium)
Icon(
if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
contentDescription = if (expanded) "Collapse" else "Expand"
)
}
AnimatedVisibility(
visible = expanded,
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut()
) {
Text(
text = content,
modifier = Modifier.padding(top = 12.dp),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
```
### Animated Content
```kotlin
@Composable
fun AnimatedCounter(count: Int) {
AnimatedContent(
targetState = count,
transitionSpec = {
if (targetState > initialState) {
slideInVertically { -it } + fadeIn() togetherWith
slideOutVertically { it } + fadeOut()
} else {
slideInVertically { it } + fadeIn() togetherWith
slideOutVertically { -it } + fadeOut()
}.using(SizeTransform(clip = false))
},
label = "counter"
) { targetCount ->
Text(
text = "$targetCount",
style = MaterialTheme.typography.displayMedium
)
}
}
```
### Gesture-Based Animation
```kotlin
@Composable
fun SwipeableCard(
onSwipeLeft: () -> Unit,
onSwipeRight: () -> Unit,
content: @Composable () -> Unit
) {
var offsetX by remember { mutableFloatStateOf(0f) }
val animatedOffset by animateFloatAsState(
targetValue = offsetX,
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
label = "offset"
)
Card(
modifier = Modifier
.fillMaxWidth()
.offset { IntOffset(animatedOffset.roundToInt(), 0) }
.pointerInput(Unit) {
detectHorizontalDragGestures(
onDragEnd = {
when {
offsetX > 200f -> {
onSwipeRight()
offsetX = 0f
}
offsetX < -200f -> {
onSwipeLeft()
offsetX = 0f
}
else -> offsetX = 0f
}
},
onHorizontalDrag = { _, dragAmount ->
offsetX += dragAmount
}
)
}
) {
content()
}
}
```